{"id":19165563,"url":"https://github.com/lablup/etcd-client-py","last_synced_at":"2026-01-12T09:15:01.636Z","repository":{"id":170571934,"uuid":"611674616","full_name":"lablup/etcd-client-py","owner":"lablup","description":"Python binding to etcd-client crate in Rust","archived":false,"fork":false,"pushed_at":"2025-12-15T00:23:36.000Z","size":132,"stargazers_count":7,"open_issues_count":3,"forks_count":1,"subscribers_count":12,"default_branch":"main","last_synced_at":"2025-12-20T12:38:57.262Z","etag":null,"topics":["etcd-client","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lablup.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-03-09T10:08:14.000Z","updated_at":"2025-12-15T00:23:39.000Z","dependencies_parsed_at":"2024-01-29T04:24:45.159Z","dependency_job_id":"6f560d39-7590-4b49-85ee-49cff28bbe6f","html_url":"https://github.com/lablup/etcd-client-py","commit_stats":null,"previous_names":["lablup/etcd-client-py"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/lablup/etcd-client-py","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lablup%2Fetcd-client-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lablup%2Fetcd-client-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lablup%2Fetcd-client-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lablup%2Fetcd-client-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lablup","download_url":"https://codeload.github.com/lablup/etcd-client-py/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lablup%2Fetcd-client-py/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28337658,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T06:09:07.588Z","status":"ssl_error","status_checked_at":"2026-01-12T06:05:18.301Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["etcd-client","python"],"created_at":"2024-11-09T09:28:14.994Z","updated_at":"2026-01-12T09:15:01.630Z","avatar_url":"https://github.com/lablup.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# etcd-client-py\n\n[![PyPI release version](https://badge.fury.io/py/etcd-client-py.svg)](https://pypi.org/project/etcd-client-py/)\n![Wheels](https://img.shields.io/pypi/wheel/etcd-client-py.svg)\n\nPython wrapper of [etcd_client](https://github.com/etcdv3/etcd-client) built with [PyO3](https://github.com/PyO3/pyo3).\n\n## Installation\n\n```bash\npip install etcd_client\n```\n\n## Basic usage\n\n```python\nfrom etcd_client import EtcdClient\netcd = EtcdClient(['http://127.0.0.1:2379'])\n```\n\nActual connection establishment with Etcd's gRPC channel will be done when you call `EtcdClient.connect()`.\n\n```python\nasync def main():\n    async with etcd.connect() as communicator:\n        await communicator.put('testkey'.encode(), 'testvalue'.encode())\n        value = await communicator.get('testkey'.encode())\n        print(bytes(value).decode())  # testvalue\n```\n\n### Working with key prefixes\n\n`EtcdCommunicator.get_prefix(prefix)` returns a list of key-value pairs matching the given prefix.\n\n```python\nasync def main():\n    async with etcd.connect() as communicator:\n        await communicator.put('/testdir'.encode(), 'root'.encode())\n        await communicator.put('/testdir/1'.encode(), '1'.encode())\n        await communicator.put('/testdir/2'.encode(), '2'.encode())\n        await communicator.put('/testdir/2/3'.encode(), '3'.encode())\n\n        test_dir = await communicator.get_prefix('/testdir'.encode())\n\n        for resp in test_dir:\n            # ['/testdir', 'root']\n            # ['/testdir/1', '1']\n            # ['/testdir/2', '2']\n            # ['/testdir/2/3', '3']\n            print([bytes(v).decode() for v in resp])\n```\n\n## Automatic runtime cleanup\n\nThe tokio runtime is automatically cleaned up when the last client context exits. In most cases, no explicit cleanup is needed:\n\n```python\nimport asyncio\nfrom etcd_client import EtcdClient\n\nasync def main():\n    etcd = EtcdClient(['http://127.0.0.1:2379'])\n    async with etcd.connect() as communicator:\n        await communicator.put('testkey'.encode(), 'testvalue'.encode())\n        value = await communicator.get('testkey'.encode())\n        print(bytes(value).decode())\n    # Runtime automatically cleaned up when context exits\n\nasyncio.run(main())\n```\n\nThe library uses reference counting to track active client contexts. When the last context exits, the tokio runtime is gracefully shut down, waiting up to 5 seconds for pending tasks to complete. If you create new clients after this, the runtime is automatically re-initialized.\n\nFor advanced use cases requiring explicit control, `cleanup_runtime()` is available:\n\n```python\nfrom etcd_client import cleanup_runtime\n\n# Force cleanup at a specific point (usually not needed)\ncleanup_runtime()\n```\n\n## Operating with Etcd lock\n\nJust like `EtcdClient.connect()`, you can easily use etcd lock by calling `EtcdClient.with_lock(lock_opts)`.\n\n```python\nasync def first():\n    async with etcd.with_lock(\n        EtcdLockOption(\n            lock_name='foolock'.encode(),\n        )\n    ) as communicator:\n        value = await communicator.get('testkey'.encode())\n        print('first:', bytes(value).decode(), end=' | ')\n\nasync def second():\n    await asyncio.sleep(0.1)\n    async with etcd.with_lock(\n        EtcdLockOption(\n            lock_name='foolock'.encode(),\n        )\n    ) as communicator:\n        value = await communicator.get('testkey'.encode())\n        print('second:', bytes(value).decode())\n\nasync with etcd.connect() as communicator:\n    await communicator.put('testkey'.encode(), 'testvalue'.encode())\nawait asyncio.gather(first(), second())  # first: testvalue | second: testvalue\n```\n\n### Lock timeout\n\nAdding `timeout` parameter to `EtcdLockOption` will add a timeout to the lock acquiring process.\n\n```python\nasync def second():\n    await asyncio.sleep(0.1)\n    async with etcd.with_lock(\n        EtcdLockOption(\n            lock_name='foolock'.encode(),\n            timeout=5.0,\n        )\n    ) as communicator:\n        value = await communicator.get('testkey'.encode())\n        print('second:', bytes(value).decode())\n```\n\n### Lock TTL\n\nAdding `ttl` parameter to `EtcdLockOption` will force the lock to be released after the given seconds.\n\n```python\nasync def first():\n    async with etcd.with_lock(\n        EtcdLockOption(lock_name=\"foolock\".encode(), ttl=5)\n    ) as communicator:\n        await asyncio.sleep(10)\n\nasync def second():\n    start = time.time()\n    async with etcd.with_lock(\n        EtcdLockOption(lock_name=\"foolock\".encode(), ttl=5)\n    ) as communicator:\n        print(f\"acquired lock after {time.time() - start} seconds\")\n\n# 'second' acquired lock after 5.247947931289673 seconds\ndone, _ = await asyncio.wait([\n    asyncio.create_task(first()),\n    asyncio.create_task(second())\n], return_when=asyncio.FIRST_COMPLETED)\n\nfor task in done:\n    print(task.result())\n```\n\n## Watch\n\nYou can watch changes on a key with `EtcdCommunicator.watch(key)`.\n\n```python\nasync def watch():\n    async with etcd.connect() as communicator:\n        async for event in communicator.watch('testkey'.encode()):\n            print(event.event, bytes(event.value).decode())\n\nasync def update():\n    await asyncio.sleep(0.1)\n    async with etcd.connect() as communicator:\n        await communicator.put('testkey'.encode(), '1'.encode())\n        await communicator.put('testkey'.encode(), '2'.encode())\n        await communicator.put('testkey'.encode(), '3'.encode())\n        await communicator.put('testkey'.encode(), '4'.encode())\n        await communicator.put('testkey'.encode(), '5'.encode())\n\nawait asyncio.gather(watch(), update())\n# WatchEventType.PUT 1\n# WatchEventType.PUT 2\n# WatchEventType.PUT 3\n# WatchEventType.PUT 4\n# WatchEventType.PUT 5\n```\n\n### Watch with prefix\n\nWatching changes on keys with a specific prefix can be done with `EtcdCommunicator.watch_prefix(key_prefix)`.\n\n```python\nasync def watch():\n    async with etcd.connect() as communicator:\n        async for event in communicator.watch_prefix('/testdir'.encode()):\n            print(event.event, bytes(event.key).decode(), bytes(event.value).decode())\n\nasync def update():\n    await asyncio.sleep(0.1)\n    async with etcd.connect() as communicator:\n        await communicator.put('/testdir'.encode(), '1'.encode())\n        await communicator.put('/testdir/foo'.encode(), '2'.encode())\n        await communicator.put('/testdir/bar'.encode(), '3'.encode())\n        await communicator.put('/testdir/foo/baz'.encode(), '4'.encode())\n\nawait asyncio.gather(watch(), update())\n# WatchEventType.PUT /testdir 1\n# WatchEventType.PUT /testdir/foo 2\n# WatchEventType.PUT /testdir/bar 3\n# WatchEventType.PUT /testdir/foo/baz 4\n```\n\n## Transaction\n\nYou can run etcd transactions by calling `EtcdCommunicator.txn(txn)`.\n\n### Constructing compares\n\nConstructing compare operations can be done using the `Compare` class.\n\n```python\nfrom etcd_client import Compare, CompareOp\ncompares = [\n    Compare.value('cmpkey1'.encode(), CompareOp.EQUAL, 'foo'.encode()),\n    Compare.value('cmpkey2'.encode(), CompareOp.GREATER, 'bar'.encode()),\n]\n```\n\n### Executing transactions\n\n```python\nasync with etcd.connect() as communicator:\n    await communicator.put('cmpkey1'.encode(), 'foo'.encode())\n    await communicator.put('cmpkey2'.encode(), 'baz'.encode())\n    await communicator.put('successkey'.encode(), 'asdf'.encode())\n\n    compares = [\n        Compare.value('cmpkey1'.encode(), CompareOp.EQUAL, 'foo'.encode()),\n        Compare.value('cmpkey2'.encode(), CompareOp.GREATER, 'bar'.encode()),\n    ]\n\n    res = await communicator.txn(Txn().when(compares).and_then([TxnOp.get('successkey'.encode())]))\n    print(res)  # TODO: Need to write response type bindings.\n```\n\n## How to build\n\n### Prerequisites\n\n* The Rust development environment (2021 edition or later) using [`rustup`](https://rustup.rs/) or your package manager\n* The Python development environment (3.10 or later) using [`pyenv`](https://github.com/pyenv/pyenv#installation) or your package manager\n\n### Build instructions\n\nFirst, create a virtualenv (using the standard venv package, pyenv, or your preferred tool). Then, install the PEP-517 build toolchain and run it.\n\n```shell\npip install -U pip build setuptools\npython -m build --sdist --wheel\n```\n\nThis will automatically install build dependencies like [`maturin`](https://github.com/PyO3/maturin) and build the wheel and source distributions under the `dist/` directory.\n\n## How to develop and test\n\n### Setup development environment\n\nThis project uses [uv](https://docs.astral.sh/uv/) for fast Python package management.\n\n```bash\n# Install uv (if not already installed)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Install all dependencies and build the package\nmake install\n\n# Or manually:\nuv sync --all-extras  # Installs all dependencies from pyproject.toml\nuv run maturin develop  # Builds and installs the Rust extension\n```\n\n### Code quality checks\n\nThis project uses ruff for linting/formatting and mypy for type checking:\n\n```bash\nmake fmt       # Format all code (Python + Rust)\nmake lint      # Lint all code (Python + Rust)\nmake fix       # Auto-fix all issues (Python + Rust)\nmake typecheck # Type check Python code\nmake check     # Run all checks\n```\n\n### Running tests\n\n```bash\nmake test      # Run tests using uv\nuv run pytest  # Or directly with uv\n\n# Tests use testcontainers to automatically spin up etcd\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flablup%2Fetcd-client-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flablup%2Fetcd-client-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flablup%2Fetcd-client-py/lists"}