Async Python client for etcd v3 (grpc.aio, Python 3.11+)
Project description
etcd3aio
Async Python client for etcd v3 using grpc.aio.
Features
- Thin facade over the etcd v3 gRPC API — no hidden magic
- Async-first: all I/O via
grpc.aio, never blocks the event loop - Strong typing: fully annotated,
py.typedmarker, satisfiespyright - Automatic retry with exponential backoff for transient errors
- Watch streams with automatic reconnection
- Distributed lock and leader election primitives
- Background lease keep-alive and token refresh context managers
Requirements
- Python 3.11+
- etcd v3.5+ tested against v3.6 (see Local cluster to run one locally)
Installation
pip install etcd3aio
Quick Start
import asyncio
from etcd3aio import Etcd3Client, EtcdConnectionError
async def main() -> None:
try:
async with Etcd3Client(['localhost:2379']) as client:
await client.ping()
# Key-value
await client.kv.put('myapp/greeting', 'hello')
resp = await client.kv.get('myapp/greeting')
print(resp.kvs[0].value.decode()) # hello
# Distributed lock
async with client.lock('myapp/resource'):
print('acquired exclusive section')
# Leader election
async with client.election('myapp/leader', value=b'node-1') as e:
leader = await e.leader()
print(f'leader: {leader.kvs[0].value.decode()}')
except EtcdConnectionError:
print('could not connect to etcd')
raise SystemExit(1)
asyncio.run(main())
Error Handling
All etcd errors inherit from EtcdError. The table below maps each exception to the
underlying gRPC status code and the typical recovery action.
from etcd3aio import (
EtcdError,
EtcdConnectionError,
EtcdTransientError,
EtcdUnauthenticatedError,
EtcdPermissionDeniedError,
)
try:
await client.kv.put('key', 'value')
except EtcdConnectionError:
# UNAVAILABLE — endpoint unreachable after all retry attempts
# Action: check cluster health, verify endpoints
...
except EtcdTransientError:
# DEADLINE_EXCEEDED — operation timed out after all retry attempts
# Action: retry with backoff or increase the deadline
...
except EtcdUnauthenticatedError:
# UNAUTHENTICATED — token missing, expired, or invalid
# Action: re-authenticate and set a new token
...
except EtcdPermissionDeniedError:
# PERMISSION_DENIED — authenticated user lacks the required role/permission
# Action: review RBAC grants for this user
...
except EtcdError:
# Catch-all for any other etcd-related error
...
Transient errors (UNAVAILABLE, DEADLINE_EXCEEDED) are retried automatically with
exponential backoff (up to 3 attempts, 0.05 s → 1.0 s). Exceptions are only raised
after all retry attempts are exhausted.
Local cluster (Docker)
docker compose -f docker/compose.yaml up -d
This starts two independent clusters:
| Cluster | Ports | Transport |
|---|---|---|
etcd1 / etcd2 / etcd3 |
2379, 3379, 4379 | Plain TCP (no TLS) |
etcdtls1 / etcdtls2 / etcdtls3 |
5379, 6379, 7379 | Mutual TLS (mTLS) |
Generating TLS certificates
The mTLS cluster requires certificate files in docker/. To regenerate them:
bash docker/gen-certs.sh
This creates server-ca.crt, client-cert.crt, client-key.key, and peer certificate pairs — all with the correct Subject Alternative Names for the TLS cluster nodes.
Connecting to the TLS cluster
Three certificate files are required — obtain them from your etcd administrator or generate them with gen-certs.sh for local development:
| File | Purpose | Etcd3Client parameter |
|---|---|---|
server-ca.crt |
CA that signed the server certificate | ca_cert |
client-cert.crt |
Client certificate presented during the mTLS handshake | cert_chain |
client-key.key |
Private key for the client certificate | cert_key |
Pass the file contents as bytes directly to the client constructor:
from pathlib import Path
from etcd3aio import Etcd3Client
certs = Path('docker') # adjust to your certificate directory
async with Etcd3Client(
['localhost:5379', 'localhost:6379', 'localhost:7379'],
ca_cert= (certs / 'server-ca.crt').read_bytes(),
cert_chain= (certs / 'client-cert.crt').read_bytes(),
cert_key= (certs / 'client-key.key').read_bytes(),
tls_server_name='localhost', # required for multi-endpoint TLS — see MODULES.md
) as client:
await client.ping()
tls_server_nameis required when connecting to multiple endpoints. The gRPCipv4:round-robin scheme encodes all addresses as a comma-separated string and cannot derive a hostname for TLS verification. Set it to a DNS name present in the server certificate's Subject Alternative Names (SANs).
Documentation
- MODULES.md — service API reference with code examples
- EXAMPLES.md — step-by-step walkthrough of all example scripts
- CONTRIBUTING.md — local workflow, design principles and quality checks
- ARCHITECTURE.md — module boundaries and responsibilities
- CHANGELOG.md — version history
- ROADMAP.md — implemented and deferred features
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file etcd3aio-0.3.0.tar.gz.
File metadata
- Download URL: etcd3aio-0.3.0.tar.gz
- Upload date:
- Size: 52.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d4f664a6d2f22e9331d4150e0eabf1dc44b4b2dc85792e6fcab60c0a9ea278b
|
|
| MD5 |
fdc3b613fe8c0430253c6d759872b68b
|
|
| BLAKE2b-256 |
41808aba9731ed9e4b3c0a586d404d722ba02d2c4f36002c7fca88770f1fe77f
|
Provenance
The following attestation bundles were made for etcd3aio-0.3.0.tar.gz:
Publisher:
publish.yml on danielsabatini/etcd3aio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etcd3aio-0.3.0.tar.gz -
Subject digest:
1d4f664a6d2f22e9331d4150e0eabf1dc44b4b2dc85792e6fcab60c0a9ea278b - Sigstore transparency entry: 1042701501
- Sigstore integration time:
-
Permalink:
danielsabatini/etcd3aio@2097414f7b3ac8ce835ab500d6dba45ccb860638 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/danielsabatini
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2097414f7b3ac8ce835ab500d6dba45ccb860638 -
Trigger Event:
release
-
Statement type:
File details
Details for the file etcd3aio-0.3.0-py3-none-any.whl.
File metadata
- Download URL: etcd3aio-0.3.0-py3-none-any.whl
- Upload date:
- Size: 61.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da2221e1a24bc162b54ad4fa60264bbba1fd3f1e3494f5630328d2c8a0c276a6
|
|
| MD5 |
9eb9900ad294b5d1d35b8d6ae4675216
|
|
| BLAKE2b-256 |
0e630d774813e09769a843ef5781a47d80cad53e7b039672110e1df27bdea9d1
|
Provenance
The following attestation bundles were made for etcd3aio-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on danielsabatini/etcd3aio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etcd3aio-0.3.0-py3-none-any.whl -
Subject digest:
da2221e1a24bc162b54ad4fa60264bbba1fd3f1e3494f5630328d2c8a0c276a6 - Sigstore transparency entry: 1042701504
- Sigstore integration time:
-
Permalink:
danielsabatini/etcd3aio@2097414f7b3ac8ce835ab500d6dba45ccb860638 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/danielsabatini
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2097414f7b3ac8ce835ab500d6dba45ccb860638 -
Trigger Event:
release
-
Statement type: