python-etcd3-async 0.1.6__tar.gz → 0.2.0__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.
Files changed (158) hide show
  1. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/.coverage +0 -0
  2. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/CLAUDE.md +10 -3
  3. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/PKG-INFO +1 -1
  4. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/advanced_queries.md +23 -0
  5. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/leases_locks.md +2 -0
  6. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/sync_client.md +9 -2
  7. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/operations/auto_reconnection.md +24 -0
  8. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/operations/error_handling.md +10 -7
  9. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/operations/failover.md +5 -1
  10. python_etcd3_async-0.2.0/docs/en/resources/history.md +72 -0
  11. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/advanced_queries.md +22 -0
  12. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/leases_locks.md +2 -0
  13. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/sync_client.md +8 -2
  14. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/operations/auto_reconnection.md +19 -0
  15. python_etcd3_async-0.2.0/docs/zh/resources/history.md +63 -0
  16. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/__init__.py +3 -1
  17. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_client.py +21 -0
  18. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_client_extensions.py +132 -58
  19. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_endpoint.py +17 -3
  20. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_managers.py +30 -5
  21. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_multi_client.py +229 -101
  22. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_single_client.py +18 -3
  23. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_watch.py +125 -113
  24. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/client.py +20 -0
  25. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/endpoint.py +16 -0
  26. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/errors.py +14 -0
  27. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/locks.py +49 -13
  28. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/multi_client.py +236 -47
  29. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/shared.py +1 -55
  30. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/sync_kv_operations.py +53 -30
  31. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/utils.py +0 -35
  32. python_etcd3_async-0.2.0/tests/test_async_health_check.py +178 -0
  33. python_etcd3_async-0.2.0/tests/test_async_watch.py +87 -0
  34. python_etcd3_async-0.2.0/tests/test_async_watch_cancel.py +90 -0
  35. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_batch_operations.py +90 -8
  36. python_etcd3_async-0.2.0/tests/test_endpoint_selection.py +201 -0
  37. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_locks.py +41 -0
  38. python_etcd3_async-0.2.0/tests/test_return_semantics.py +170 -0
  39. python_etcd3_async-0.2.0/tests/test_sync_health_check.py +245 -0
  40. python_etcd3_async-0.1.6/PHASE2_PLAN.md +0 -164
  41. python_etcd3_async-0.1.6/docs/en/resources/history.md +0 -39
  42. python_etcd3_async-0.1.6/docs/zh/resources/history.md +0 -38
  43. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/.cnb.yml +0 -0
  44. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/.gitignore +0 -0
  45. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/.pylintrc +0 -0
  46. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/.python-version +0 -0
  47. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/AGENTS.md +0 -0
  48. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/README.md +0 -0
  49. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/README_CN.md +0 -0
  50. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/SKILL.md +0 -0
  51. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/assets/stylesheets/extra.css +0 -0
  52. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/getting-started/installation.md +0 -0
  53. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/authentication.md +0 -0
  54. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/cluster_maintenance.md +0 -0
  55. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/configuration.md +0 -0
  56. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/leader_election.md +0 -0
  57. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/service_discovery.md +0 -0
  58. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/service_registration.md +0 -0
  59. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/transactions.md +0 -0
  60. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/usage.md +0 -0
  61. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/guide/watching.md +0 -0
  62. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/index.md +0 -0
  63. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/operations/async_error_handling.md +0 -0
  64. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/operations/monitoring.md +0 -0
  65. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/operations/observability.md +0 -0
  66. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/resources/authors.md +0 -0
  67. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/resources/contributing.md +0 -0
  68. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/en/resources/releasing.md +0 -0
  69. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/superpowers/plans/2026-04-03-high-cohesion-low-coupling-plan.md +0 -0
  70. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/superpowers/specs/2026-04-03-high-cohesion-low-coupling-design.md +0 -0
  71. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/getting-started/installation.md +0 -0
  72. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/authentication.md +0 -0
  73. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/cluster_maintenance.md +0 -0
  74. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/configuration.md +0 -0
  75. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/leader_election.md +0 -0
  76. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/service_discovery.md +0 -0
  77. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/service_registration.md +0 -0
  78. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/transactions.md +0 -0
  79. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/usage.md +0 -0
  80. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/guide/watching.md +0 -0
  81. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/index.md +0 -0
  82. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/operations/async_error_handling.md +0 -0
  83. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/operations/error_handling.md +0 -0
  84. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/operations/failover.md +0 -0
  85. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/operations/monitoring.md +0 -0
  86. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/operations/observability.md +0 -0
  87. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/resources/authors.md +0 -0
  88. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/resources/contributing.md +0 -0
  89. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/docs/zh/resources/releasing.md +0 -0
  90. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_config.py +0 -0
  91. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/async_monitor.py +0 -0
  92. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/auth.py +0 -0
  93. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/config.py +0 -0
  94. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/events.py +0 -0
  95. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/exceptions.py +0 -0
  96. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/exporters/__init__.py +0 -0
  97. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/exporters/prometheus.py +0 -0
  98. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/leases.py +0 -0
  99. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/log.py +0 -0
  100. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/log_formatters.py +0 -0
  101. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/log_sampling.py +0 -0
  102. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/members.py +0 -0
  103. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/observability.py +0 -0
  104. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/single_client.py +0 -0
  105. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/sync_cluster_operations.py +0 -0
  106. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/sync_lock_operations.py +0 -0
  107. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/sync_maintenance_operations.py +0 -0
  108. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/trace.py +0 -0
  109. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/trace_attributes.py +0 -0
  110. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/transactions.py +0 -0
  111. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcd3/watch.py +0 -0
  112. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcdrpc/__init__.py +0 -0
  113. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcdrpc/auth.proto +0 -0
  114. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcdrpc/kv.proto +0 -0
  115. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/etcdrpc/rpc.proto +0 -0
  116. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/leader_election.py +0 -0
  117. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/leader_election_sync.py +0 -0
  118. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/service_discovery.py +0 -0
  119. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/service_discovery_sync.py +0 -0
  120. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/service_discovery_v2.py +0 -0
  121. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/service_registration.py +0 -0
  122. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/service_registration_sync.py +0 -0
  123. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/examples/service_registry_v2.py +0 -0
  124. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/gen_protos.py +0 -0
  125. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/mkdocs.yml +0 -0
  126. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/pyproject.toml +0 -0
  127. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/scripts/etcd-test.sh +0 -0
  128. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/__init__.py +0 -0
  129. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/conftest.py +0 -0
  130. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_async_client.py +0 -0
  131. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_async_client_integration.py +0 -0
  132. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_async_error_handling.py +0 -0
  133. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_async_kv_operations_integration.py +0 -0
  134. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_async_managers.py +0 -0
  135. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_auth.py +0 -0
  136. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_auth_integration.py +0 -0
  137. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_client.py +0 -0
  138. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_cluster_integration.py +0 -0
  139. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_events.py +0 -0
  140. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_kv_operations.py +0 -0
  141. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_lease_keepalive_retry.py +0 -0
  142. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_leases.py +0 -0
  143. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_leases_integration.py +0 -0
  144. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_locks_integration.py +0 -0
  145. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_log_formatters.py +0 -0
  146. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_log_sampling.py +0 -0
  147. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_maintenance_integration.py +0 -0
  148. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_members.py +0 -0
  149. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_observability_integration.py +0 -0
  150. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_tls_integration.py +0 -0
  151. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_trace_attributes.py +0 -0
  152. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_transactions.py +0 -0
  153. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_transactions_integration.py +0 -0
  154. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_utils.py +0 -0
  155. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_watch.py +0 -0
  156. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/tests/test_watch_integration.py +0 -0
  157. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/usage_guide.md +0 -0
  158. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.0}/uv.lock +0 -0
@@ -266,7 +266,11 @@ auto_init() # Auto-initialize at import time
266
266
  - Error handlers are created via `create_sync_error_handlers()` and `create_async_error_handlers()` factories from `errors.py`
267
267
  - `_sync_handle_keepalive_errors` / `_async_handle_keepalive_errors` - Lease keepalive with `propagate_errors=True`
268
268
 
269
- 3. **Endpoint failover:** `MultiEndpointEtcd3Client` tracks failed endpoints with retry on `failover=True`
269
+ 3. **Endpoint failover:** Both `MultiEndpointEtcd3Client` and
270
+ `AsyncMultiEndpointEtcd3Client` track failed endpoints. With
271
+ `failover=True` a failed endpoint is bypassed and another healthy
272
+ endpoint is picked; with `failover=False` the client stays on the
273
+ endpoint it holds and rebuilds that endpoint's channel in place.
270
274
 
271
275
  4. **Load balancing:** Multiple strategies supported via `LoadBalancerStrategy`:
272
276
  - `PRIORITY` (default): Keep current endpoint if healthy, failover on failure
@@ -274,11 +278,14 @@ auto_init() # Auto-initialize at import time
274
278
  - `RANDOM`: Random endpoint selection
275
279
  - `STICKY`: Maintain connection to the same endpoint
276
280
 
277
- 5. **Auto-reconnection:** Async has `_get_channel()`, sync has matching implementation with health check
281
+ 5. **Auto-reconnection:** Async has `_get_channel()` / `_acquire_channel()`,
282
+ sync has a matching `_get_channel()`, both with a background health probe
278
283
 
279
284
  6. **Lazy authentication:** Async uses `_init_auth()` with `asyncio.Lock()`, sync initializes in `__init__`
280
285
 
281
- 7. **Watch pattern:** Iterator + cancel function returns for both sync and async
286
+ 7. **Watch pattern:** Iterator + cancel function returns for both sync and
287
+ async. Cancelling sends a real cancel request to the server and unblocks
288
+ a pending iteration.
282
289
 
283
290
  8. **Request context tracking:** Use `client.context()` to track operations across async tasks:
284
291
  ```python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-etcd3-async
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: etcd3 async sdk
5
5
  Requires-Python: <4,>=3.12
6
6
  Requires-Dist: grpcio==1.76.0
@@ -49,6 +49,29 @@ async for value, meta in client.get_range('/start', '/end'):
49
49
  print(f'{meta.key}: {value}')
50
50
  ```
51
51
 
52
+ ## Batch Operations
53
+
54
+ The three batch entry points share one concurrency and failure policy:
55
+ items run in parallel, the concurrency ceiling is controlled by
56
+ `concurrency`, and a failing item never aborts the whole batch.
57
+
58
+ ```python
59
+ # Atomic transaction: all succeed or none do
60
+ count = await client.kv.batch_put([('/a', '1'), ('/b', '2')])
61
+
62
+ # Parallel execution: at most 10 at a time, failures only affect the count
63
+ count = await client.kv.batch_put(items, atomic=False, concurrency=10)
64
+
65
+ # Returns (success_count, [(failed_key, error), ...])
66
+ success, failed = await client.kv.put_many(items, concurrency=10)
67
+ ```
68
+
69
+ `batch_put` and `put_many` accept either an iterable of `(key, value)`
70
+ tuples or a mapping; `batch_delete` accepts an iterable of keys.
71
+
72
+ The effective ceiling for `concurrency` is the client's `max_concurrent`
73
+ (default 100): callers may lower it, never raise it.
74
+
52
75
  ## See Also
53
76
 
54
77
  - [API Usage](usage.md)
@@ -148,6 +148,8 @@ await lock.acquire()
148
148
  async with lock.auto_refresh(interval=10): # default: ttl / 3
149
149
  await do_long_running_work()
150
150
  # background refresh stops on exit (the lock itself is NOT released here)
151
+ # if the keep-alive loop fails, the error is raised on exit — otherwise the
152
+ # lease may already be gone while the caller still believes it holds the lock
151
153
  ```
152
154
 
153
155
  For sync clients, iterate the generator yourself:
@@ -38,8 +38,8 @@ from etcd3 import MultiEndpointEtcd3Client, Endpoint, LoadBalancerStrategy
38
38
 
39
39
  etcd = MultiEndpointEtcd3Client(
40
40
  endpoints=[
41
- Endpoint('10.0.0.1', 2379),
42
- Endpoint('10.0.0.2', 2379),
41
+ Endpoint('10.0.0.1', 2379, secure=False),
42
+ Endpoint('10.0.0.2', 2379, secure=False),
43
43
  ],
44
44
  failover=True,
45
45
  max_retries=3,
@@ -49,6 +49,10 @@ etcd = MultiEndpointEtcd3Client(
49
49
  )
50
50
  ```
51
51
 
52
+ `Endpoint` defaults to `secure=True`, which requires TLS credentials. Pass
53
+ `secure=False` for a plaintext connection (or supply `ca_cert` /
54
+ `cert_cert` / `cert_key` for TLS).
55
+
52
56
  ## KV Operations
53
57
 
54
58
  ```python
@@ -119,6 +123,9 @@ success, responses = etcd.transaction(
119
123
  | Lock keep-alive | iterate `lock.refresh()` | `async with lock.auto_refresh()` |
120
124
  | Batch ops | top-level `batch_put`/`batch_delete` | `client.kv.batch_put/concurrency=...` |
121
125
  | Maintenance | via `etcd.maintenance.*` | top-level `status/compact/...` |
126
+ | Members | `etcd.members` (property) | `async for m in etcd.members()` |
127
+ | Delete result | number of deleted keys | number of deleted keys |
128
+ | Put result | response header (or `KVMetadata` with `prev_kv=True`) | response header (or `KVMetadata` with `prev_kv=True`) |
122
129
  | Close | `etcd.close()` | `await client.close()` |
123
130
 
124
131
  ## Blocking in Async Code
@@ -25,6 +25,30 @@ required.
25
25
  - Phase 3: Update endpoint state (under lock)
26
26
  5. Once rebuilt successfully, the endpoint is marked as healthy again
27
27
 
28
+ ## Health Probe Task
29
+
30
+ `health_check_interval` drives a background probe task:
31
+
32
+ - It starts lazily on the client's first request — a client that is never
33
+ used owns no task at all;
34
+ - Each cycle probes only the endpoints that are currently marked as failed,
35
+ so healthy endpoints cause no extra round trips;
36
+ - An endpoint that answers again is marked as healthy immediately, without
37
+ waiting for the `time_retry` window to expire;
38
+ - `close()` cancels the task, so its lifetime matches the client's exactly.
39
+
40
+ The sync client behaves the same way, with a daemon thread
41
+ (`etcd3-health-check`) as the carrier instead: it starts lazily when a
42
+ channel is first acquired, and `close()` signals it to exit and waits for
43
+ it to finish. The thread is a daemon, so it never keeps the process alive
44
+ even if `close()` is never called.
45
+
46
+ ```python
47
+ # Inspect endpoint state: marked-failed / ok
48
+ for label, endpoint in client.endpoints.items():
49
+ print(label, 'marked-failed' if endpoint.is_failed() else 'ok')
50
+ ```
51
+
28
52
  ## Tuning Reconnection
29
53
 
30
54
  Recovery attempts are configured on the multi-endpoint clients:
@@ -60,8 +60,7 @@ current endpoint as failed, which drives [Failover](failover.md) and
60
60
  ## Handler Layers
61
61
 
62
62
  - **Unary calls**: wrapped by generated error handlers that map
63
- `grpc.RpcError` to the exceptions above and (for the multi-endpoint
64
- clients) trigger retry with backoff.
63
+ `grpc.RpcError` to the exceptions above.
65
64
  - **Generators** (watch iterators, range streams): errors are mapped as
66
65
  they surface during iteration; generator handlers may end the stream
67
66
  quietly when the watch is cancelled normally.
@@ -71,11 +70,15 @@ current endpoint as failed, which drives [Failover](failover.md) and
71
70
 
72
71
  ## Retry Guidance
73
72
 
74
- The client already retries transient failures when `failover=True`
75
- (`max_retries`, `retry_backoff`, `retry_max_backoff`). Retry at the
76
- application level only for operations that are safe to repeat (idempotent
77
- puts, reads) and treat `RevisionCompactedError` as a signal to resync
78
- state rather than to retry blindly.
73
+ Read operations are retried automatically on transient failures
74
+ (`UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`), bounded by
75
+ `max_retries`, `retry_backoff` and `retry_max_backoff`.
76
+
77
+ Writes are **not** retried automatically: the request may already have been
78
+ applied server-side when the error surfaced, so repeating it could duplicate
79
+ the effect. Retry writes at the application level only where they are safe
80
+ to repeat (idempotent puts), and treat `RevisionCompactedError` as a signal
81
+ to resync state rather than to retry blindly.
79
82
 
80
83
  ## See Also
81
84
 
@@ -44,10 +44,14 @@ see [Configuration](../guide/configuration.md).
44
44
  the endpoint is marked failed
45
45
  3. On the next operation the client picks a healthy endpoint (per the load
46
46
  balancer strategy) and rebuilds the failed endpoint's channel in the
47
- background
47
+ background. With `failover=False` the client instead stays on the
48
+ endpoint it holds and rebuilds that endpoint's channel in place.
48
49
  4. Periodic health checks (`health_check_interval`) recover endpoints once
49
50
  the server answers again
50
51
 
52
+ Both clients behave identically here: the sync and the async multi-endpoint
53
+ client apply the same strategy and the same failover rules.
54
+
51
55
  ## Load Balancer Strategies
52
56
 
53
57
  | Strategy | Description |
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: History
3
+ ---
4
+
5
+ # History
6
+
7
+ ## v0.2.0 (2026-11)
8
+
9
+ Sync and async clients aligned on behaviour that had drifted apart:
10
+
11
+ - **Endpoint selection**: the async client now applies the load balancer
12
+ strategy and fails over to a healthy endpoint, like the sync client.
13
+ The `failover` flag is honoured by both clients: with failover enabled a
14
+ failed endpoint is bypassed, with failover disabled the client stays on
15
+ its endpoint and rebuilds that endpoint's channel in place
16
+ - **Watch cancellation**: the async client cancels the real watch instead
17
+ of watch id 0, so cancelling removes the server-side watch and unblocks
18
+ a pending iteration. The async watcher no longer races on a shared
19
+ event, and closing the client closes the watcher
20
+ - **Return values**: `delete()` / `delete_prefix()` return the number of
21
+ deleted keys on both clients, `put()` returns the response header (or
22
+ the previous key-value metadata with `prev_kv=True`), and the async
23
+ client gained `kv.transaction()`
24
+ - **Instrumentation**: every operation is recorded exactly once (the sync
25
+ client recorded it twice)
26
+ - **Retries**: `max_retries` / `retry_backoff` now take effect on the
27
+ idempotent read paths. Writes are not retried automatically, because the
28
+ request may already have been applied when the error surfaced
29
+ - **Compatibility shims**: `etcd3.client(...)` and
30
+ `await etcd3.async_client(...)` keep working after
31
+ `import etcd3.client` / `import etcd3.async_client`
32
+ - Sync `put_many()` honours its `timeout` argument; a transaction may
33
+ omit `success` / `failure` on both clients
34
+
35
+ ## v0.1.6 (2026-10)
36
+
37
+ - Sync client: health probe for `health_check_interval`; batch operation
38
+ semantics unified
39
+
40
+ ## v0.1.5 (2026-09)
41
+
42
+ - Sync client: gRPC error handling imported consistently across operation
43
+ modules; reconnection and watch cancel now reach the server
44
+ (`cancel_watch` no longer leaks threads/streams)
45
+ - Lock API: `Lock`/`AsyncLock.release()` return explicit status strings
46
+ (`released` / `not_owner` / `not_found` / `error`); sync `lock` restored
47
+ as a method with the operations namespace renamed to `lock_ops`
48
+ - Watch: `Watch.iterator()` renamed to `get_iterator()`; iterator cancel
49
+ is server-side on timeout
50
+ - Async: parallel endpoint health checks; error-mapping regression fixed
51
+ - Documentation site restructured (en + zh)
52
+
53
+ ## v0.1.1 – v0.1.4 (2026-02 – 2026-04)
54
+
55
+ - High-cohesion module split: `single_client` / `multi_client` /
56
+ `async_single_client` / `async_multi_client`, `sync_*_operations`
57
+ mixins, `config` / `async_config`, `errors`, `auth`, `leases`, `locks`,
58
+ `members`
59
+ - `client.py` / `async_client.py` converted to compatibility shims
60
+ - Auth (user/role/permission) API added
61
+ - Async managers use the async channel for auto-reconnection
62
+ - Service examples improved (dataclass + v2 variants)
63
+
64
+ ## v0.1.0
65
+
66
+ - Initial release
67
+ - Async client support (`grpc.aio`)
68
+ - Sync client support
69
+ - Lease, lock, watch and transaction operations
70
+ - Multi-endpoint support with failover
71
+ - Auto reconnection
72
+ - Monitoring metrics
@@ -49,6 +49,28 @@ async for value, meta in client.get_range('/start', '/end'):
49
49
  print(f'{meta.key}: {value}')
50
50
  ```
51
51
 
52
+ ## 批量操作
53
+
54
+ 三个批量入口共享同一套并发与失败语义:条目并行执行,并发上限由
55
+ `concurrency` 控制,单个条目失败不会中断整批。
56
+
57
+ ```python
58
+ # 原子事务:全部成功或全部失败
59
+ count = await client.kv.batch_put([('/a', '1'), ('/b', '2')])
60
+
61
+ # 并行执行:最多 10 个并发,单条失败只计入失败数
62
+ count = await client.kv.batch_put(items, atomic=False, concurrency=10)
63
+
64
+ # 返回 (成功数, [(失败的键, 异常), ...])
65
+ success, failed = await client.kv.put_many(items, concurrency=10)
66
+ ```
67
+
68
+ `batch_put` 与 `put_many` 都接受 `(key, value)` 元组序列或字典;
69
+ `batch_delete` 接受键序列。
70
+
71
+ `concurrency` 的实际上限为客户端创建时的 `max_concurrent`(默认 100),
72
+ 调用方只能调低、不能调高。
73
+
52
74
  ## 另见
53
75
 
54
76
  - [API 使用](usage.md)
@@ -142,6 +142,8 @@ await lock.acquire()
142
142
  async with lock.auto_refresh(interval=10): # 默认 ttl / 3
143
143
  await do_long_running_work()
144
144
  # 退出上下文时停止后台刷新(注意:并不会释放锁)
145
+ # 若保活循环中途失败,退出上下文时会抛出该异常 ——
146
+ # 否则租约可能已失效,而调用方仍以为持锁
145
147
  ```
146
148
 
147
149
  同步客户端需自行迭代生成器:
@@ -36,8 +36,8 @@ from etcd3 import MultiEndpointEtcd3Client, Endpoint, LoadBalancerStrategy
36
36
 
37
37
  etcd = MultiEndpointEtcd3Client(
38
38
  endpoints=[
39
- Endpoint('10.0.0.1', 2379),
40
- Endpoint('10.0.0.2', 2379),
39
+ Endpoint('10.0.0.1', 2379, secure=False),
40
+ Endpoint('10.0.0.2', 2379, secure=False),
41
41
  ],
42
42
  failover=True,
43
43
  max_retries=3,
@@ -47,6 +47,9 @@ etcd = MultiEndpointEtcd3Client(
47
47
  )
48
48
  ```
49
49
 
50
+ `Endpoint` 默认为 `secure=True`,此时必须提供 TLS 凭据。纯文本连接请传
51
+ `secure=False`(或提供 `ca_cert` / `cert_cert` / `cert_key` 启用 TLS)。
52
+
50
53
  ## 键值操作
51
54
 
52
55
  ```python
@@ -117,6 +120,9 @@ success, responses = etcd.transaction(
117
120
  | 锁保活 | 迭代 `lock.refresh()` | `async with lock.auto_refresh()` |
118
121
  | 批量操作 | 顶层 `batch_put`/`batch_delete` | `client.kv.batch_put(concurrency=...)` |
119
122
  | 运维 | 经 `etcd.maintenance.*` | 顶层 `status/compact/...` |
123
+ | 成员列表 | `etcd.members`(属性) | `async for m in etcd.members()` |
124
+ | 删除结果 | 删除的键数量 | 删除的键数量 |
125
+ | 写入结果 | 响应头(`prev_kv=True` 时为 `KVMetadata`) | 响应头(`prev_kv=True` 时为 `KVMetadata`) |
120
126
  | 关闭 | `etcd.close()` | `await client.close()` |
121
127
 
122
128
  ## 在异步代码中调用同步客户端
@@ -20,6 +20,25 @@ title: 自动重连
20
20
  - 阶段3:更新端点状态(在锁内)
21
21
  5. 重建成功后,端点被重新标记为健康
22
22
 
23
+ ## 健康探测任务
24
+
25
+ `health_check_interval` 对应一个后台探测任务:
26
+
27
+ - 客户端首次发起请求时惰性启动 —— 从未使用的客户端不占用任何任务;
28
+ - 每个周期只探测被标记为失败的端点,健康端点不做无谓往返;
29
+ - 端点恢复应答后立即被标记为健康,不必等到 `time_retry` 窗口结束;
30
+ - `close()` 会取消该任务,任务生命周期与客户端严格一致。
31
+
32
+ 同步客户端行为一致,只是载体换成守护线程(`etcd3-health-check`):
33
+ 首次取通道时惰性启动,`close()` 通知其退出并等待收尾;
34
+ 线程为守护线程,即使未调用 `close()` 也不会阻止进程退出。
35
+
36
+ ```python
37
+ # 观察端点状态:marked-failed / ok
38
+ for label, endpoint in client.endpoints.items():
39
+ print(label, 'marked-failed' if endpoint.is_failed() else 'ok')
40
+ ```
41
+
23
42
  ## 重连调优
24
43
 
25
44
  恢复相关参数在多端点客户端上配置:
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: 更新历史
3
+ ---
4
+
5
+ # 更新历史
6
+
7
+ ## v0.2.0 (2026-11)
8
+
9
+ 同步与异步客户端在行为差异上完成对齐:
10
+
11
+ - **端点选择**:异步客户端现在与同步客户端一样应用负载均衡策略,并在
12
+ 端点失败时切换到健康端点。两个客户端都真正尊重 `failover` 参数:
13
+ 开启时绕过失败端点,关闭时留在当前端点并原地重建其 channel
14
+ - **Watch 取消**:异步客户端取消的是真实的 watch(此前取消的是并不存在
15
+ 的 watch id 0),因此取消会真正移除服务端 watch 并唤醒阻塞中的迭代
16
+ 器。异步 watcher 不再共享事件产生竞态,关闭客户端也会关闭 watcher
17
+ - **返回值**:两个客户端的 `delete()` / `delete_prefix()` 均返回删除的键
18
+ 数量,`put()` 均返回响应头(`prev_kv=True` 时返回旧值元数据),异步
19
+ 客户端新增 `kv.transaction()`
20
+ - **埋点**:每个操作只会被记录一次(同步客户端此前重复记录)
21
+ - **重试**:`max_retries` / `retry_backoff` 现在对幂等读路径生效。写操作
22
+ 不自动重试,因为报错时请求可能已在服务端生效
23
+ - **兼容层**:`import etcd3.client` / `import etcd3.async_client` 之后,
24
+ `etcd3.client(...)` 与 `await etcd3.async_client(...)` 仍可正常调用
25
+ - 同步端 `put_many()` 现在真正使用其 `timeout` 参数;两个客户端的事务都
26
+ 可以省略 `success` / `failure`
27
+
28
+ ## v0.1.6 (2026-10)
29
+
30
+ - 同步客户端:实现 `health_check_interval` 健康探测;统一批量操作语义
31
+
32
+ ## v0.1.5 (2026-09)
33
+
34
+ - 同步客户端:各操作模块统一复用 gRPC 错误处理;重连与 watch 取消
35
+ 现在真正送达服务端(`cancel_watch` 不再泄漏线程/流)
36
+ - 锁 API:`Lock`/`AsyncLock.release()` 返回明确的语义状态
37
+ (`released` / `not_owner` / `not_found` / `error`);同步 `lock`
38
+ 恢复为方法,操作命名空间更名为 `lock_ops`
39
+ - Watch:`Watch.iterator()` 更名为 `get_iterator()`;迭代器超时即
40
+ 取消服务端 watch
41
+ - 异步:端点健康检查并行化;修复错误映射回归
42
+ - 文档站重构(中英双语)
43
+
44
+ ## v0.1.1 – v0.1.4 (2026-02 – 2026-04)
45
+
46
+ - 高内聚模块拆分:`single_client` / `multi_client` /
47
+ `async_single_client` / `async_multi_client`、`sync_*_operations`
48
+ 混入、`config` / `async_config`、`errors`、`auth`、`leases`、`locks`、
49
+ `members`
50
+ - `client.py` / `async_client.py` 转为兼容性 shim
51
+ - 新增 Auth(用户/角色/权限)API
52
+ - 异步 managers 使用异步 channel 实现自动重连
53
+ - 服务示例改进(dataclass + v2 版本)
54
+
55
+ ## v0.1.0
56
+
57
+ - 初始发布
58
+ - 支持异步客户端(`grpc.aio`)
59
+ - 支持同步客户端
60
+ - 租约、锁、观察与事务操作
61
+ - 多端点支持与故障转移
62
+ - 自动重连
63
+ - 监控指标
@@ -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__ = "0.1.6"
54
+ __version__ = "0.2.0"
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