python-etcd3-async 0.1.6__tar.gz → 9.9.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of python-etcd3-async might be problematic. Click here for more details.
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/PKG-INFO +1 -1
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/__init__.py +3 -1
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_client.py +21 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_client_extensions.py +132 -58
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_endpoint.py +17 -3
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_managers.py +30 -5
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_multi_client.py +229 -101
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_single_client.py +18 -3
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_watch.py +125 -113
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/client.py +20 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/endpoint.py +16 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/errors.py +14 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/locks.py +49 -13
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/multi_client.py +236 -47
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/shared.py +1 -55
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/sync_kv_operations.py +53 -30
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/utils.py +0 -35
- python_etcd3_async-9.9.9/etcdrpc/auth_pb2.py +43 -0
- python_etcd3_async-9.9.9/etcdrpc/auth_pb2.pyi +44 -0
- python_etcd3_async-9.9.9/etcdrpc/auth_pb2_grpc.py +24 -0
- python_etcd3_async-9.9.9/etcdrpc/kv_pb2.py +40 -0
- python_etcd3_async-9.9.9/etcdrpc/kv_pb2.pyi +39 -0
- python_etcd3_async-9.9.9/etcdrpc/kv_pb2_grpc.py +24 -0
- python_etcd3_async-9.9.9/etcdrpc/rpc_pb2.py +237 -0
- python_etcd3_async-9.9.9/etcdrpc/rpc_pb2.pyi +811 -0
- python_etcd3_async-9.9.9/etcdrpc/rpc_pb2_grpc.py +1893 -0
- python_etcd3_async-9.9.9/scripts/publish.sh +223 -0
- python_etcd3_async-0.1.6/.cnb.yml +0 -127
- python_etcd3_async-0.1.6/.coverage +0 -0
- python_etcd3_async-0.1.6/.gitignore +0 -18
- python_etcd3_async-0.1.6/.pylintrc +0 -29
- python_etcd3_async-0.1.6/.python-version +0 -1
- python_etcd3_async-0.1.6/AGENTS.md +0 -240
- python_etcd3_async-0.1.6/CLAUDE.md +0 -334
- python_etcd3_async-0.1.6/PHASE2_PLAN.md +0 -164
- python_etcd3_async-0.1.6/SKILL.md +0 -105
- python_etcd3_async-0.1.6/docs/assets/stylesheets/extra.css +0 -53
- python_etcd3_async-0.1.6/docs/en/getting-started/installation.md +0 -73
- python_etcd3_async-0.1.6/docs/en/guide/advanced_queries.md +0 -55
- python_etcd3_async-0.1.6/docs/en/guide/authentication.md +0 -116
- python_etcd3_async-0.1.6/docs/en/guide/cluster_maintenance.md +0 -130
- python_etcd3_async-0.1.6/docs/en/guide/configuration.md +0 -140
- python_etcd3_async-0.1.6/docs/en/guide/leader_election.md +0 -129
- python_etcd3_async-0.1.6/docs/en/guide/leases_locks.md +0 -187
- python_etcd3_async-0.1.6/docs/en/guide/service_discovery.md +0 -167
- python_etcd3_async-0.1.6/docs/en/guide/service_registration.md +0 -159
- python_etcd3_async-0.1.6/docs/en/guide/sync_client.md +0 -138
- python_etcd3_async-0.1.6/docs/en/guide/transactions.md +0 -136
- python_etcd3_async-0.1.6/docs/en/guide/usage.md +0 -151
- python_etcd3_async-0.1.6/docs/en/guide/watching.md +0 -145
- python_etcd3_async-0.1.6/docs/en/index.md +0 -78
- python_etcd3_async-0.1.6/docs/en/operations/async_error_handling.md +0 -468
- python_etcd3_async-0.1.6/docs/en/operations/auto_reconnection.md +0 -84
- python_etcd3_async-0.1.6/docs/en/operations/error_handling.md +0 -85
- python_etcd3_async-0.1.6/docs/en/operations/failover.md +0 -74
- python_etcd3_async-0.1.6/docs/en/operations/monitoring.md +0 -74
- python_etcd3_async-0.1.6/docs/en/operations/observability.md +0 -225
- python_etcd3_async-0.1.6/docs/en/resources/authors.md +0 -13
- python_etcd3_async-0.1.6/docs/en/resources/contributing.md +0 -46
- python_etcd3_async-0.1.6/docs/en/resources/history.md +0 -39
- python_etcd3_async-0.1.6/docs/en/resources/releasing.md +0 -65
- python_etcd3_async-0.1.6/docs/superpowers/plans/2026-04-03-high-cohesion-low-coupling-plan.md +0 -1093
- python_etcd3_async-0.1.6/docs/superpowers/specs/2026-04-03-high-cohesion-low-coupling-design.md +0 -255
- python_etcd3_async-0.1.6/docs/zh/getting-started/installation.md +0 -73
- python_etcd3_async-0.1.6/docs/zh/guide/advanced_queries.md +0 -55
- python_etcd3_async-0.1.6/docs/zh/guide/authentication.md +0 -112
- python_etcd3_async-0.1.6/docs/zh/guide/cluster_maintenance.md +0 -129
- python_etcd3_async-0.1.6/docs/zh/guide/configuration.md +0 -138
- python_etcd3_async-0.1.6/docs/zh/guide/leader_election.md +0 -129
- python_etcd3_async-0.1.6/docs/zh/guide/leases_locks.md +0 -180
- python_etcd3_async-0.1.6/docs/zh/guide/service_discovery.md +0 -167
- python_etcd3_async-0.1.6/docs/zh/guide/service_registration.md +0 -159
- python_etcd3_async-0.1.6/docs/zh/guide/sync_client.md +0 -136
- python_etcd3_async-0.1.6/docs/zh/guide/transactions.md +0 -133
- python_etcd3_async-0.1.6/docs/zh/guide/usage.md +0 -151
- python_etcd3_async-0.1.6/docs/zh/guide/watching.md +0 -140
- python_etcd3_async-0.1.6/docs/zh/index.md +0 -78
- python_etcd3_async-0.1.6/docs/zh/operations/async_error_handling.md +0 -458
- python_etcd3_async-0.1.6/docs/zh/operations/auto_reconnection.md +0 -77
- python_etcd3_async-0.1.6/docs/zh/operations/error_handling.md +0 -80
- python_etcd3_async-0.1.6/docs/zh/operations/failover.md +0 -70
- python_etcd3_async-0.1.6/docs/zh/operations/monitoring.md +0 -74
- python_etcd3_async-0.1.6/docs/zh/operations/observability.md +0 -225
- python_etcd3_async-0.1.6/docs/zh/resources/authors.md +0 -13
- python_etcd3_async-0.1.6/docs/zh/resources/contributing.md +0 -45
- python_etcd3_async-0.1.6/docs/zh/resources/history.md +0 -38
- python_etcd3_async-0.1.6/docs/zh/resources/releasing.md +0 -63
- python_etcd3_async-0.1.6/examples/leader_election.py +0 -287
- python_etcd3_async-0.1.6/examples/leader_election_sync.py +0 -301
- python_etcd3_async-0.1.6/examples/service_discovery.py +0 -204
- python_etcd3_async-0.1.6/examples/service_discovery_sync.py +0 -213
- python_etcd3_async-0.1.6/examples/service_discovery_v2.py +0 -235
- python_etcd3_async-0.1.6/examples/service_registration.py +0 -226
- python_etcd3_async-0.1.6/examples/service_registration_sync.py +0 -197
- python_etcd3_async-0.1.6/examples/service_registry_v2.py +0 -237
- python_etcd3_async-0.1.6/mkdocs.yml +0 -95
- python_etcd3_async-0.1.6/tests/__init__.py +0 -0
- python_etcd3_async-0.1.6/tests/conftest.py +0 -244
- python_etcd3_async-0.1.6/tests/test_async_client.py +0 -374
- python_etcd3_async-0.1.6/tests/test_async_client_integration.py +0 -157
- python_etcd3_async-0.1.6/tests/test_async_error_handling.py +0 -192
- python_etcd3_async-0.1.6/tests/test_async_kv_operations_integration.py +0 -102
- python_etcd3_async-0.1.6/tests/test_async_managers.py +0 -88
- python_etcd3_async-0.1.6/tests/test_auth.py +0 -186
- python_etcd3_async-0.1.6/tests/test_auth_integration.py +0 -427
- python_etcd3_async-0.1.6/tests/test_batch_operations.py +0 -447
- python_etcd3_async-0.1.6/tests/test_client.py +0 -117
- python_etcd3_async-0.1.6/tests/test_cluster_integration.py +0 -69
- python_etcd3_async-0.1.6/tests/test_events.py +0 -252
- python_etcd3_async-0.1.6/tests/test_kv_operations.py +0 -524
- python_etcd3_async-0.1.6/tests/test_lease_keepalive_retry.py +0 -478
- python_etcd3_async-0.1.6/tests/test_leases.py +0 -242
- python_etcd3_async-0.1.6/tests/test_leases_integration.py +0 -207
- python_etcd3_async-0.1.6/tests/test_locks.py +0 -356
- python_etcd3_async-0.1.6/tests/test_locks_integration.py +0 -100
- python_etcd3_async-0.1.6/tests/test_log_formatters.py +0 -234
- python_etcd3_async-0.1.6/tests/test_log_sampling.py +0 -180
- python_etcd3_async-0.1.6/tests/test_maintenance_integration.py +0 -40
- python_etcd3_async-0.1.6/tests/test_members.py +0 -200
- python_etcd3_async-0.1.6/tests/test_observability_integration.py +0 -205
- python_etcd3_async-0.1.6/tests/test_tls_integration.py +0 -58
- python_etcd3_async-0.1.6/tests/test_trace_attributes.py +0 -311
- python_etcd3_async-0.1.6/tests/test_transactions.py +0 -240
- python_etcd3_async-0.1.6/tests/test_transactions_integration.py +0 -270
- python_etcd3_async-0.1.6/tests/test_utils.py +0 -280
- python_etcd3_async-0.1.6/tests/test_watch.py +0 -142
- python_etcd3_async-0.1.6/tests/test_watch_integration.py +0 -117
- python_etcd3_async-0.1.6/usage_guide.md +0 -223
- python_etcd3_async-0.1.6/uv.lock +0 -910
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/README.md +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/README_CN.md +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_config.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/async_monitor.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/auth.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/config.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/events.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/exceptions.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/exporters/__init__.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/exporters/prometheus.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/leases.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/log.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/log_formatters.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/log_sampling.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/members.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/observability.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/single_client.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/sync_cluster_operations.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/sync_lock_operations.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/sync_maintenance_operations.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/trace.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/trace_attributes.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/transactions.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcd3/watch.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcdrpc/__init__.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcdrpc/auth.proto +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcdrpc/kv.proto +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/etcdrpc/rpc.proto +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/gen_protos.py +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/pyproject.toml +0 -0
- {python_etcd3_async-0.1.6 → python_etcd3_async-9.9.9}/scripts/etcd-test.sh +0 -0
|
@@ -8,6 +8,7 @@ from etcd3.shared import Transactions
|
|
|
8
8
|
from etcd3.async_single_client import AsyncEtcd3Client
|
|
9
9
|
from etcd3.async_multi_client import AsyncMultiEndpointEtcd3Client
|
|
10
10
|
from etcd3.async_endpoint import AsyncEndpoint
|
|
11
|
+
from etcd3.async_endpoint import AsyncEndpointConfig
|
|
11
12
|
from etcd3.async_client_extensions import get_request_context, set_request_context
|
|
12
13
|
from etcd3.exceptions import Etcd3Exception
|
|
13
14
|
from etcd3.observability import ObservabilityConfig
|
|
@@ -50,7 +51,7 @@ from etcd3.async_single_client import async_client
|
|
|
50
51
|
|
|
51
52
|
__author__ = "owasd"
|
|
52
53
|
__email__ = ""
|
|
53
|
-
__version__ = "
|
|
54
|
+
__version__ = "9.9.9"
|
|
54
55
|
|
|
55
56
|
__all__ = (
|
|
56
57
|
"etcdrpc",
|
|
@@ -71,6 +72,7 @@ __all__ = (
|
|
|
71
72
|
"Permission",
|
|
72
73
|
"MultiEndpointEtcd3Client",
|
|
73
74
|
"AsyncEndpoint",
|
|
75
|
+
"AsyncEndpointConfig",
|
|
74
76
|
"AsyncEtcd3Client",
|
|
75
77
|
"AsyncMultiEndpointEtcd3Client",
|
|
76
78
|
"async_client",
|
|
@@ -8,6 +8,9 @@ For new code, prefer importing from:
|
|
|
8
8
|
- etcd3.async_endpoint: AsyncEndpoint
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import sys
|
|
12
|
+
import types
|
|
13
|
+
|
|
11
14
|
# Re-export config classes
|
|
12
15
|
from etcd3.async_config import AsyncClientConfig, AsyncMultiEndpointConfig
|
|
13
16
|
from etcd3.config import RangeRequestConfig
|
|
@@ -48,3 +51,21 @@ __all__ = [
|
|
|
48
51
|
"_async_handle_keepalive_errors",
|
|
49
52
|
"async_client",
|
|
50
53
|
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _CallableModule(types.ModuleType):
|
|
57
|
+
"""Module that can also be awaited like the factory function it re-exports.
|
|
58
|
+
|
|
59
|
+
``import etcd3.async_client`` makes Python rebind the
|
|
60
|
+
``etcd3.async_client`` attribute from the factory function to this
|
|
61
|
+
module. Without this, the documented ``await etcd3.async_client(...)``
|
|
62
|
+
call would break for anyone who imported the compatibility shim.
|
|
63
|
+
Calling the module delegates to the factory function, so both
|
|
64
|
+
spellings keep working.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __call__(self, *args, **kwargs):
|
|
68
|
+
return async_client(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
sys.modules[__name__].__class__ = _CallableModule
|
|
@@ -117,7 +117,55 @@ class AsyncKVOperations:
|
|
|
117
117
|
|
|
118
118
|
def __init__(self, client, max_concurrent=100):
|
|
119
119
|
self.client = client
|
|
120
|
-
|
|
120
|
+
# Client-wide ceiling for concurrent key-value operations. A batch
|
|
121
|
+
# call may ask for less concurrency, never for more.
|
|
122
|
+
self._max_concurrent = max_concurrent
|
|
123
|
+
|
|
124
|
+
def _limit(self, concurrency):
|
|
125
|
+
"""Resolve the concurrency limit for one batch call."""
|
|
126
|
+
if concurrency is None:
|
|
127
|
+
concurrency = self._max_concurrent
|
|
128
|
+
return max(1, min(int(concurrency), self._max_concurrent))
|
|
129
|
+
|
|
130
|
+
async def _run_bounded(self, items, operation, concurrency=None):
|
|
131
|
+
"""Run ``operation`` over ``items`` with bounded concurrency.
|
|
132
|
+
|
|
133
|
+
Shared by ``batch_put``, ``batch_delete`` and ``put_many`` so that
|
|
134
|
+
all three have one concurrency policy and one failure policy:
|
|
135
|
+
per-item errors are captured and never abort the whole batch.
|
|
136
|
+
|
|
137
|
+
:param items: Items to process
|
|
138
|
+
:type items: list
|
|
139
|
+
:param operation: Async callable applied to a single item
|
|
140
|
+
:type operation: callable
|
|
141
|
+
:param concurrency: Maximum number of items processed at the same time
|
|
142
|
+
:type concurrency: int, optional
|
|
143
|
+
:returns: List of ``(item, value, error)`` tuples in input order
|
|
144
|
+
:rtype: list[tuple]
|
|
145
|
+
"""
|
|
146
|
+
limit = self._limit(concurrency)
|
|
147
|
+
# The semaphore is created per call so it always belongs to the
|
|
148
|
+
# running event loop (asyncio.Semaphore binds itself on first use).
|
|
149
|
+
semaphore = asyncio.Semaphore(limit)
|
|
150
|
+
results = [(item, None, None) for item in items]
|
|
151
|
+
|
|
152
|
+
async def run(index, item):
|
|
153
|
+
async with semaphore:
|
|
154
|
+
try:
|
|
155
|
+
value = await operation(item)
|
|
156
|
+
except Exception as exc: # noqa: BLE001 - per-item isolation
|
|
157
|
+
results[index] = (item, None, exc)
|
|
158
|
+
else:
|
|
159
|
+
results[index] = (item, value, None)
|
|
160
|
+
|
|
161
|
+
# asyncio.TaskGroup keeps every task inside this scope: no task can
|
|
162
|
+
# outlive the call unobserved, and one failing item cannot cancel
|
|
163
|
+
# the others (errors are captured inside ``run``).
|
|
164
|
+
async with asyncio.TaskGroup() as tg:
|
|
165
|
+
for index, item in enumerate(items):
|
|
166
|
+
tg.create_task(run(index, item))
|
|
167
|
+
|
|
168
|
+
return results
|
|
121
169
|
|
|
122
170
|
async def _get_kv_stub(self):
|
|
123
171
|
"""Get the KV stub using the current channel with auto-reconnection.
|
|
@@ -139,13 +187,19 @@ class AsyncKVOperations:
|
|
|
139
187
|
"""
|
|
140
188
|
range_request = self.client._build_get_range_request(key, **kwargs)
|
|
141
189
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
async def _range():
|
|
191
|
+
# The stub is created per attempt so that a retry picks up a
|
|
192
|
+
# freshly built channel.
|
|
193
|
+
kv_stub = await self._get_kv_stub()
|
|
194
|
+
return await kv_stub.Range(
|
|
195
|
+
range_request,
|
|
196
|
+
timeout=self.client.timeout,
|
|
197
|
+
credentials=self.client.call_credentials,
|
|
198
|
+
metadata=self.client.metadata,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Reads are idempotent, so a transient failure can be retried.
|
|
202
|
+
response = await self.client._retry_with_backoff(_range)
|
|
149
203
|
|
|
150
204
|
# Set trace attributes from response
|
|
151
205
|
endpoint = (
|
|
@@ -329,8 +383,18 @@ class AsyncKVOperations:
|
|
|
329
383
|
return delete_response.deleted
|
|
330
384
|
|
|
331
385
|
@_async_handle_errors
|
|
332
|
-
async def delete_prefix(self, prefix):
|
|
333
|
-
"""
|
|
386
|
+
async def delete_prefix(self, prefix, return_response=False):
|
|
387
|
+
"""
|
|
388
|
+
Delete a range of keys with a prefix.
|
|
389
|
+
|
|
390
|
+
:param prefix: prefix of the keys to delete
|
|
391
|
+
:type prefix: str or bytes
|
|
392
|
+
:param return_response: return the full response
|
|
393
|
+
:type return_response: bool
|
|
394
|
+
:returns: the number of deleted keys when ``return_response`` is
|
|
395
|
+
False, and the full response when ``return_response`` is True
|
|
396
|
+
:rtype: int or :class:`.rpc_pb2.DeleteRangeResponse`
|
|
397
|
+
"""
|
|
334
398
|
delete_request = RequestBuilder.build_delete_request(
|
|
335
399
|
key=prefix,
|
|
336
400
|
range_end=utils.prefix_range_end(utils.to_bytes(prefix)),
|
|
@@ -345,8 +409,24 @@ class AsyncKVOperations:
|
|
|
345
409
|
metadata=self.client.metadata,
|
|
346
410
|
)
|
|
347
411
|
|
|
412
|
+
if return_response:
|
|
413
|
+
return delete_response
|
|
348
414
|
return delete_response.deleted
|
|
349
415
|
|
|
416
|
+
async def transaction(self, compare, success=None, failure=None):
|
|
417
|
+
"""Perform a transaction.
|
|
418
|
+
|
|
419
|
+
:param compare: A list of comparisons to make
|
|
420
|
+
:param success: A list of operations to perform if all the comparisons
|
|
421
|
+
are true
|
|
422
|
+
:param failure: A list of operations to perform if any of the
|
|
423
|
+
comparisons are false
|
|
424
|
+
:return: A tuple of (operation status, responses)
|
|
425
|
+
"""
|
|
426
|
+
return await self.client.transaction(
|
|
427
|
+
compare=compare, success=success, failure=failure
|
|
428
|
+
)
|
|
429
|
+
|
|
350
430
|
async def batch_put(self, items, concurrency=10, atomic=True):
|
|
351
431
|
"""Batch put multiple key-value pairs.
|
|
352
432
|
|
|
@@ -354,8 +434,8 @@ class AsyncKVOperations:
|
|
|
354
434
|
ensuring either all operations succeed or none do. If atomic=False,
|
|
355
435
|
operations are executed in parallel with concurrency control.
|
|
356
436
|
|
|
357
|
-
:param items: Iterable of (key, value) tuples to put
|
|
358
|
-
:type items: Iterable[tuple[str | bytes, str | bytes]]
|
|
437
|
+
:param items: Iterable of (key, value) tuples or a mapping to put
|
|
438
|
+
:type items: Iterable[tuple[str | bytes, str | bytes]] or dict
|
|
359
439
|
:param concurrency: Maximum number of concurrent put operations (only used if atomic=False)
|
|
360
440
|
:type concurrency: int
|
|
361
441
|
:param atomic: If True (default), execute as atomic transaction. If False, parallel execution.
|
|
@@ -364,6 +444,9 @@ class AsyncKVOperations:
|
|
|
364
444
|
:rtype: int
|
|
365
445
|
:raises BatchOperationError: If atomic=True and some operations fail
|
|
366
446
|
"""
|
|
447
|
+
# Accept a mapping as well as an iterable of pairs
|
|
448
|
+
if isinstance(items, dict):
|
|
449
|
+
items = list(items.items())
|
|
367
450
|
# Convert to list to allow multiple iterations
|
|
368
451
|
items_list = list(items)
|
|
369
452
|
if not items_list:
|
|
@@ -397,23 +480,14 @@ class AsyncKVOperations:
|
|
|
397
480
|
) from exc
|
|
398
481
|
else:
|
|
399
482
|
# Non-atomic: parallel execution with concurrency control
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
async def put_with_semaphore(key, value):
|
|
404
|
-
async with semaphore:
|
|
405
|
-
try:
|
|
406
|
-
await self.put(key, value)
|
|
407
|
-
results.append((key, 1))
|
|
408
|
-
except Exception:
|
|
409
|
-
results.append((key, 0))
|
|
483
|
+
async def put_item(item):
|
|
484
|
+
key, value = item
|
|
485
|
+
return await self.put(key, value)
|
|
410
486
|
|
|
411
|
-
|
|
412
|
-
for k, v in items_list:
|
|
413
|
-
tg.create_task(put_with_semaphore(k, v))
|
|
487
|
+
results = await self._run_bounded(items_list, put_item, concurrency)
|
|
414
488
|
|
|
415
489
|
# Count successful operations
|
|
416
|
-
return sum(1 for _,
|
|
490
|
+
return sum(1 for _, _, error in results if error is None)
|
|
417
491
|
|
|
418
492
|
async def batch_delete(self, keys, concurrency=10, atomic=True):
|
|
419
493
|
"""Batch delete multiple keys.
|
|
@@ -464,54 +538,54 @@ class AsyncKVOperations:
|
|
|
464
538
|
) from exc
|
|
465
539
|
else:
|
|
466
540
|
# Non-atomic: parallel execution with concurrency control
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
try:
|
|
473
|
-
result = await self.delete(key)
|
|
474
|
-
# delete returns True/False or deleted count
|
|
475
|
-
if isinstance(result, bool):
|
|
476
|
-
results.append((key, 1 if result else 0))
|
|
477
|
-
else:
|
|
478
|
-
results.append((key, 1 if result > 0 else 0))
|
|
479
|
-
except Exception:
|
|
480
|
-
results.append((key, 0))
|
|
481
|
-
|
|
482
|
-
async with asyncio.TaskGroup() as tg:
|
|
483
|
-
for k in keys_list:
|
|
484
|
-
tg.create_task(delete_with_semaphore(k))
|
|
541
|
+
async def delete_item(key):
|
|
542
|
+
# delete returns the number of deleted keys
|
|
543
|
+
return await self.delete(key)
|
|
544
|
+
|
|
545
|
+
results = await self._run_bounded(keys_list, delete_item, concurrency)
|
|
485
546
|
|
|
486
547
|
# Count successful operations
|
|
487
|
-
return sum(
|
|
548
|
+
return sum(count for _, count, error in results if error is None)
|
|
488
549
|
|
|
489
|
-
async def put_many(self, items, timeout=None):
|
|
550
|
+
async def put_many(self, items, timeout=None, concurrency=10):
|
|
490
551
|
"""Put multiple key-value pairs with controlled concurrency.
|
|
491
552
|
|
|
492
553
|
This is a simpler interface for batch puts that handles all
|
|
493
|
-
the asyncio complexity internally.
|
|
554
|
+
the asyncio complexity internally. It shares the concurrency and
|
|
555
|
+
failure policy of :meth:`batch_put`: items are processed in
|
|
556
|
+
parallel up to ``concurrency`` at a time, and a failing item never
|
|
557
|
+
aborts the rest of the batch.
|
|
494
558
|
|
|
495
559
|
:param items: Dict of {key: value} pairs or iterable of (key, value) tuples
|
|
496
560
|
:type items: dict or Iterable[tuple]
|
|
497
561
|
:param timeout: Optional timeout for each individual put operation
|
|
498
562
|
:type timeout: float
|
|
563
|
+
:param concurrency: Maximum number of concurrent put operations
|
|
564
|
+
:type concurrency: int
|
|
499
565
|
:returns: Tuple of (success_count, failed_keys)
|
|
500
566
|
:rtype: tuple[int, list]
|
|
501
567
|
"""
|
|
502
568
|
if isinstance(items, dict):
|
|
503
569
|
items = list(items.items())
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
570
|
+
items_list = list(items)
|
|
571
|
+
if not items_list:
|
|
572
|
+
return 0, []
|
|
573
|
+
|
|
574
|
+
async def put_item(item):
|
|
575
|
+
key, value = item
|
|
576
|
+
if timeout is None:
|
|
577
|
+
return await self.put(key, value)
|
|
578
|
+
# asyncio.timeout applies the deadline to the current task, so
|
|
579
|
+
# it composes with an enclosing timeout.
|
|
580
|
+
async with asyncio.timeout(timeout):
|
|
581
|
+
return await self.put(key, value)
|
|
582
|
+
|
|
583
|
+
results = await self._run_bounded(items_list, put_item, concurrency)
|
|
584
|
+
|
|
585
|
+
success_count = sum(1 for _, _, error in results if error is None)
|
|
586
|
+
failed_keys = [
|
|
587
|
+
(item[0], error) for item, _, error in results if error is not None
|
|
588
|
+
]
|
|
515
589
|
|
|
516
590
|
return success_count, failed_keys
|
|
517
591
|
|
|
@@ -41,6 +41,12 @@ class AsyncEndpointConfig:
|
|
|
41
41
|
class AsyncEndpoint(BaseEndpoint):
|
|
42
42
|
"""Represents an etcd cluster endpoint for async operations.
|
|
43
43
|
|
|
44
|
+
The signature mirrors the synchronous ``Endpoint`` class: ``host`` and
|
|
45
|
+
``port`` come first, so both ``AsyncEndpoint('localhost', 2379)`` and
|
|
46
|
+
``AsyncEndpoint(host='localhost', port=2379)`` work. An
|
|
47
|
+
``AsyncEndpointConfig`` can be passed as the keyword-only ``config``
|
|
48
|
+
argument instead.
|
|
49
|
+
|
|
44
50
|
:param config: AsyncEndpointConfig object with endpoint configuration
|
|
45
51
|
:param str host: Endpoint host (ignored if config is provided)
|
|
46
52
|
:param int port: Endpoint port (ignored if config is provided)
|
|
@@ -57,13 +63,14 @@ class AsyncEndpoint(BaseEndpoint):
|
|
|
57
63
|
|
|
58
64
|
def __init__(
|
|
59
65
|
self,
|
|
60
|
-
config=None,
|
|
61
66
|
host=None,
|
|
62
67
|
port=None,
|
|
63
68
|
secure=True,
|
|
64
69
|
creds=None,
|
|
65
70
|
time_retry=300.0,
|
|
66
71
|
opts=None,
|
|
72
|
+
*,
|
|
73
|
+
config=None,
|
|
67
74
|
):
|
|
68
75
|
# Use config object if provided, otherwise use individual parameters
|
|
69
76
|
if config is not None:
|
|
@@ -96,17 +103,24 @@ class AsyncEndpoint(BaseEndpoint):
|
|
|
96
103
|
except Exception:
|
|
97
104
|
return False
|
|
98
105
|
|
|
99
|
-
async def health_check_async(self):
|
|
106
|
+
async def health_check_async(self, timeout=2.0):
|
|
100
107
|
"""Perform an asynchronous health check using gRPC status check.
|
|
101
108
|
|
|
102
109
|
This method uses the channel's channel_ready() for a more robust
|
|
103
110
|
connectivity check.
|
|
104
111
|
|
|
112
|
+
:param timeout: Maximum time to wait for the channel to become ready
|
|
113
|
+
:type timeout: float, optional
|
|
105
114
|
:returns: True if the endpoint is healthy
|
|
106
115
|
:rtype: bool
|
|
107
116
|
"""
|
|
108
117
|
try:
|
|
109
|
-
|
|
118
|
+
# asyncio.timeout (3.11+) is used instead of asyncio.wait_for:
|
|
119
|
+
# it applies a deadline to the current task, so it composes
|
|
120
|
+
# correctly with an enclosing timeout instead of cancelling a
|
|
121
|
+
# nested task.
|
|
122
|
+
async with asyncio.timeout(timeout):
|
|
123
|
+
await self.channel.channel_ready()
|
|
110
124
|
return True
|
|
111
125
|
except asyncio.TimeoutError:
|
|
112
126
|
return False
|
|
@@ -39,6 +39,12 @@ class AsyncWatchManager:
|
|
|
39
39
|
)
|
|
40
40
|
return self._watcher
|
|
41
41
|
|
|
42
|
+
async def close(self):
|
|
43
|
+
"""Close the watcher and release its background task."""
|
|
44
|
+
if self._watcher is not None:
|
|
45
|
+
await self._watcher.close()
|
|
46
|
+
self._watcher = None
|
|
47
|
+
|
|
42
48
|
async def add_watch_callback(self, *args, **kwargs):
|
|
43
49
|
"""
|
|
44
50
|
Watch a key or range of keys and call a callback on every response.
|
|
@@ -90,23 +96,42 @@ class AsyncWatchManager:
|
|
|
90
96
|
Use ``cancel`` to cancel the watch request.
|
|
91
97
|
"""
|
|
92
98
|
watcher = await self._get_watcher()
|
|
93
|
-
|
|
99
|
+
|
|
100
|
+
# The watch runs on the shared watcher (one background task and one
|
|
101
|
+
# gRPC stream for the whole client), so the real watch id is known
|
|
102
|
+
# and the watch can actually be cancelled server-side.
|
|
103
|
+
response_queue = asyncio.Queue()
|
|
104
|
+
|
|
105
|
+
async def callback(response):
|
|
106
|
+
await response_queue.put(response)
|
|
107
|
+
|
|
108
|
+
watch_id = await watcher.add_callback(key, callback, **kwargs)
|
|
94
109
|
|
|
95
110
|
cancel_event = asyncio.Event()
|
|
96
111
|
|
|
97
112
|
async def cancel():
|
|
113
|
+
if cancel_event.is_set():
|
|
114
|
+
return
|
|
98
115
|
cancel_event.set()
|
|
99
|
-
|
|
116
|
+
# Wake up a pending iteration so it can finish.
|
|
117
|
+
await response_queue.put(None)
|
|
118
|
+
await watcher.cancel(watch_id)
|
|
100
119
|
|
|
101
120
|
async def event_iterator():
|
|
102
121
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
while not cancel_event.is_set():
|
|
123
|
+
response = await response_queue.get()
|
|
124
|
+
if response is None:
|
|
105
125
|
break
|
|
106
|
-
|
|
126
|
+
if isinstance(response, Exception):
|
|
127
|
+
cancel_event.set()
|
|
128
|
+
raise response
|
|
129
|
+
yield response
|
|
107
130
|
except grpc.RpcError as exc:
|
|
108
131
|
if not cancel_event.is_set():
|
|
109
132
|
self.etcd_client._manage_grpc_errors(exc)
|
|
133
|
+
finally:
|
|
134
|
+
await cancel()
|
|
110
135
|
|
|
111
136
|
return event_iterator(), cancel
|
|
112
137
|
|