python-etcd3-async 0.2.0__tar.gz → 0.2.1__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 (170) hide show
  1. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/.cnb.yml +7 -0
  2. python_etcd3_async-0.2.1/.gitignore +18 -0
  3. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/AGENTS.md +16 -2
  4. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/CLAUDE.md +9 -4
  5. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/PKG-INFO +29 -6
  6. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/README.md +9 -3
  7. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/README_CN.md +9 -3
  8. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/getting-started/installation.md +6 -0
  9. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/advanced_queries.md +23 -0
  10. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/configuration.md +7 -1
  11. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/index.md +1 -1
  12. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/operations/async_error_handling.md +12 -2
  13. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/operations/auto_reconnection.md +9 -0
  14. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/resources/history.md +38 -0
  15. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/resources/releasing.md +24 -3
  16. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/getting-started/installation.md +5 -0
  17. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/advanced_queries.md +23 -0
  18. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/configuration.md +5 -1
  19. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/index.md +1 -1
  20. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/operations/async_error_handling.md +9 -2
  21. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/operations/auto_reconnection.md +7 -0
  22. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/operations/error_handling.md +9 -5
  23. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/operations/failover.md +5 -1
  24. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/resources/history.md +31 -0
  25. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/resources/releasing.md +19 -2
  26. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/__init__.py +1 -1
  27. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_client_extensions.py +27 -13
  28. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_multi_client.py +7 -6
  29. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/sync_kv_operations.py +26 -39
  30. python_etcd3_async-0.2.1/etcdrpc/auth_pb2.py +43 -0
  31. python_etcd3_async-0.2.1/etcdrpc/auth_pb2.pyi +44 -0
  32. python_etcd3_async-0.2.1/etcdrpc/auth_pb2_grpc.py +24 -0
  33. python_etcd3_async-0.2.1/etcdrpc/kv_pb2.py +40 -0
  34. python_etcd3_async-0.2.1/etcdrpc/kv_pb2.pyi +39 -0
  35. python_etcd3_async-0.2.1/etcdrpc/kv_pb2_grpc.py +24 -0
  36. python_etcd3_async-0.2.1/etcdrpc/rpc_pb2.py +237 -0
  37. python_etcd3_async-0.2.1/etcdrpc/rpc_pb2.pyi +811 -0
  38. python_etcd3_async-0.2.1/etcdrpc/rpc_pb2_grpc.py +1893 -0
  39. python_etcd3_async-0.2.1/pyproject.toml +113 -0
  40. python_etcd3_async-0.2.1/scripts/gen-protos.sh +90 -0
  41. python_etcd3_async-0.2.1/scripts/publish.sh +281 -0
  42. python_etcd3_async-0.2.1/tests/test_grpc_compat.py +160 -0
  43. python_etcd3_async-0.2.1/tests/test_retry_behavior.py +218 -0
  44. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/uv.lock +11 -5
  45. python_etcd3_async-0.2.0/.gitignore +0 -18
  46. python_etcd3_async-0.2.0/gen_protos.py +0 -69
  47. python_etcd3_async-0.2.0/pyproject.toml +0 -72
  48. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/.coverage +0 -0
  49. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/.pylintrc +0 -0
  50. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/.python-version +0 -0
  51. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/SKILL.md +0 -0
  52. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/assets/stylesheets/extra.css +0 -0
  53. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/authentication.md +0 -0
  54. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/cluster_maintenance.md +0 -0
  55. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/leader_election.md +0 -0
  56. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/leases_locks.md +0 -0
  57. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/service_discovery.md +0 -0
  58. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/service_registration.md +0 -0
  59. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/sync_client.md +0 -0
  60. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/transactions.md +0 -0
  61. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/usage.md +0 -0
  62. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/guide/watching.md +0 -0
  63. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/operations/error_handling.md +0 -0
  64. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/operations/failover.md +0 -0
  65. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/operations/monitoring.md +0 -0
  66. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/operations/observability.md +0 -0
  67. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/resources/authors.md +0 -0
  68. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/en/resources/contributing.md +0 -0
  69. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/superpowers/plans/2026-04-03-high-cohesion-low-coupling-plan.md +0 -0
  70. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/superpowers/specs/2026-04-03-high-cohesion-low-coupling-design.md +0 -0
  71. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/authentication.md +0 -0
  72. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/cluster_maintenance.md +0 -0
  73. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/leader_election.md +0 -0
  74. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/leases_locks.md +0 -0
  75. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/service_discovery.md +0 -0
  76. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/service_registration.md +0 -0
  77. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/sync_client.md +0 -0
  78. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/transactions.md +0 -0
  79. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/usage.md +0 -0
  80. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/guide/watching.md +0 -0
  81. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/operations/monitoring.md +0 -0
  82. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/operations/observability.md +0 -0
  83. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/resources/authors.md +0 -0
  84. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/docs/zh/resources/contributing.md +0 -0
  85. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_client.py +0 -0
  86. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_config.py +0 -0
  87. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_endpoint.py +0 -0
  88. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_managers.py +0 -0
  89. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_monitor.py +0 -0
  90. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_single_client.py +0 -0
  91. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/async_watch.py +0 -0
  92. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/auth.py +0 -0
  93. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/client.py +0 -0
  94. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/config.py +0 -0
  95. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/endpoint.py +0 -0
  96. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/errors.py +0 -0
  97. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/events.py +0 -0
  98. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/exceptions.py +0 -0
  99. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/exporters/__init__.py +0 -0
  100. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/exporters/prometheus.py +0 -0
  101. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/leases.py +0 -0
  102. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/locks.py +0 -0
  103. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/log.py +0 -0
  104. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/log_formatters.py +0 -0
  105. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/log_sampling.py +0 -0
  106. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/members.py +0 -0
  107. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/multi_client.py +0 -0
  108. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/observability.py +0 -0
  109. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/shared.py +0 -0
  110. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/single_client.py +0 -0
  111. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/sync_cluster_operations.py +0 -0
  112. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/sync_lock_operations.py +0 -0
  113. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/sync_maintenance_operations.py +0 -0
  114. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/trace.py +0 -0
  115. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/trace_attributes.py +0 -0
  116. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/transactions.py +0 -0
  117. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/utils.py +0 -0
  118. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcd3/watch.py +0 -0
  119. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcdrpc/__init__.py +0 -0
  120. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcdrpc/auth.proto +0 -0
  121. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcdrpc/kv.proto +0 -0
  122. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/etcdrpc/rpc.proto +0 -0
  123. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/leader_election.py +0 -0
  124. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/leader_election_sync.py +0 -0
  125. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/service_discovery.py +0 -0
  126. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/service_discovery_sync.py +0 -0
  127. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/service_discovery_v2.py +0 -0
  128. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/service_registration.py +0 -0
  129. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/service_registration_sync.py +0 -0
  130. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/examples/service_registry_v2.py +0 -0
  131. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/mkdocs.yml +0 -0
  132. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/scripts/etcd-test.sh +0 -0
  133. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/__init__.py +0 -0
  134. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/conftest.py +0 -0
  135. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_client.py +0 -0
  136. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_client_integration.py +0 -0
  137. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_error_handling.py +0 -0
  138. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_health_check.py +0 -0
  139. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_kv_operations_integration.py +0 -0
  140. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_managers.py +0 -0
  141. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_watch.py +0 -0
  142. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_async_watch_cancel.py +0 -0
  143. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_auth.py +0 -0
  144. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_auth_integration.py +0 -0
  145. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_batch_operations.py +0 -0
  146. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_client.py +0 -0
  147. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_cluster_integration.py +0 -0
  148. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_endpoint_selection.py +0 -0
  149. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_events.py +0 -0
  150. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_kv_operations.py +0 -0
  151. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_lease_keepalive_retry.py +0 -0
  152. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_leases.py +0 -0
  153. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_leases_integration.py +0 -0
  154. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_locks.py +0 -0
  155. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_locks_integration.py +0 -0
  156. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_log_formatters.py +0 -0
  157. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_log_sampling.py +0 -0
  158. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_maintenance_integration.py +0 -0
  159. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_members.py +0 -0
  160. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_observability_integration.py +0 -0
  161. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_return_semantics.py +0 -0
  162. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_sync_health_check.py +0 -0
  163. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_tls_integration.py +0 -0
  164. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_trace_attributes.py +0 -0
  165. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_transactions.py +0 -0
  166. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_transactions_integration.py +0 -0
  167. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_utils.py +0 -0
  168. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_watch.py +0 -0
  169. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/tests/test_watch_integration.py +0 -0
  170. {python_etcd3_async-0.2.0 → python_etcd3_async-0.2.1}/usage_guide.md +0 -0
@@ -35,6 +35,13 @@ $:
35
35
  - name: ruff format
36
36
  script:
37
37
  - uv run --no-sync ruff format --check etcd3/ etcdrpc/ tests/
38
+ - name: generated protobuf code is up to date
39
+ # The generated code in etcdrpc/ is checked in and carries the
40
+ # grpcio/protobuf version floors this package supports. Regenerate
41
+ # it with the pinned codegen toolchain and fail if it drifted.
42
+ script:
43
+ - uv sync --group codegen
44
+ - uv run --no-sync ./scripts/gen-protos.sh --check
38
45
 
39
46
  - name: unit
40
47
  docker:
@@ -0,0 +1,18 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ # The generated protobuf/gRPC code in etcdrpc/ is checked in on purpose:
9
+ # it embeds GRPC_GENERATED_VERSION, which is the floor of the grpcio range
10
+ # this package supports. Regenerate it with scripts/gen-protos.sh.
11
+ .trae/documents/*
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ htmlcov/
15
+ # Virtual environments
16
+ .venv
17
+ /site
18
+ .etcd-tls/
@@ -67,10 +67,24 @@ uv run ruff format etcd3/ etcdrpc/ # Format
67
67
 
68
68
  ### Other
69
69
  ```bash
70
- uv run python gen_protos.py # Regenerate protobuf
70
+ ./scripts/gen-protos.sh # Regenerate the protobuf/gRPC code in etcdrpc/
71
71
  uv run mkdocs serve # Serve docs
72
72
  ```
73
73
 
74
+ ### Protobuf / gRPC version floors
75
+ - `etcdrpc/*_pb2*.py` is **generated code that is checked in**. The
76
+ `grpcio` and `protobuf` floors in `pyproject.toml` must equal the
77
+ versions embedded in it (`GRPC_GENERATED_VERSION` and the protobuf
78
+ gencode version).
79
+ - The `codegen` dependency group pins the generator to that floor, so
80
+ regenerating never silently raises it. `scripts/gen-protos.sh` uses it.
81
+ - The runtime dependencies are **ranges** (`grpcio>=...`, `protobuf>=...`):
82
+ the generated code only rejects a runtime *older* than the floor, so
83
+ newer versions are fine and the package can be installed next to other
84
+ packages that need a different grpcio.
85
+ - `tests/test_grpc_compat.py` fails if the floors and the codegen pin
86
+ drift apart.
87
+
74
88
  ### Versioning
75
89
  - Single source of truth: `__version__` in `etcd3/__init__.py`
76
90
  - `pyproject.toml` uses `dynamic = ["version"]` via
@@ -179,7 +193,7 @@ etcd3/
179
193
  log.py / log_formatters.py / log_sampling.py # Logging
180
194
  observability.py # ObservabilityConfig / configure_from_env / auto_init
181
195
  exporters/ # Prometheus exporter
182
- etcdrpc/ # Generated protobuf/gRPC code
196
+ etcdrpc/ # .proto sources + the generated protobuf/gRPC code
183
197
  tests/ # Test files
184
198
  ```
185
199
 
@@ -82,9 +82,10 @@ uv run ruff format etcd3/ etcdrpc/
82
82
 
83
83
  ### Protobuf Generation
84
84
  ```bash
85
- # Protobuf files are auto-generated during build via hatchling hook
86
- # To manually regenerate:
87
- uv run python gen_protos.py
85
+ # The generated code is checked in; regenerate it with:
86
+ ./scripts/gen-protos.sh
87
+ # CI fails if the checked-in code drifts from the .proto files:
88
+ ./scripts/gen-protos.sh --check
88
89
  ```
89
90
 
90
91
  ### Environment Variables
@@ -212,7 +213,11 @@ Generated gRPC/Python bindings from etcd proto files:
212
213
  - `kv_pb2.py`, `kv_pb2_grpc.py` - Key-value service
213
214
  - `auth_pb2.py`, `auth_pb2_grpc.py` - Authentication service
214
215
 
215
- Compiled during build via `gen_protos.py` (hatchling build hook).
216
+ The generated code is checked in (it embeds `GRPC_GENERATED_VERSION`,
217
+ the oldest grpcio this package supports) and regenerated with
218
+ `scripts/gen-protos.sh` using the pinned `codegen` toolchain. The runtime
219
+ dependencies are ranges, so newer grpcio/protobuf versions work and the
220
+ package installs next to other packages with a different grpcio.
216
221
 
217
222
  ### Examples (`examples/`)
218
223
 
@@ -1,11 +1,28 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-etcd3-async
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: etcd3 async sdk
5
+ Project-URL: Repository, https://cnb.cool/owasd/python3-etcd-async
6
+ Project-URL: Source, https://cnb.cool/owasd/python3-etcd-async
7
+ Project-URL: Homepage, https://cnb.cool/owasd/python3-etcd-async
8
+ Project-URL: Issues, https://cnb.cool/owasd/python3-etcd-async/issues
9
+ Project-URL: Changelog, https://cnb.cool/owasd/python3-etcd-async/blob/main/docs/en/resources/history.md
10
+ Author: owasd
11
+ License-Expression: Apache-2.0
12
+ Keywords: async,etcd,etcd3,grpc,kubernetes
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
5
22
  Requires-Python: <4,>=3.12
6
- Requires-Dist: grpcio==1.76.0
23
+ Requires-Dist: grpcio>=1.76.0
7
24
  Requires-Dist: prometheus-client>=0.24.1
8
- Requires-Dist: protobuf==6.31.1
25
+ Requires-Dist: protobuf>=6.31.1
9
26
  Description-Content-Type: text/markdown
10
27
 
11
28
  # python-etcd3-async
@@ -23,7 +40,7 @@ Python async client for the etcd API v3, supporting both synchronous and asynchr
23
40
  - **Async/Sync dual support**: Same API for both async and sync usage with feature parity
24
41
  - **Automatic reconnection**: Auto-rebuild stale connections (e.g., after etcd server restart)
25
42
  - **Keepalive error propagation**: Lease keepalive errors are properly propagated for connection loss detection
26
- - **Endpoint failover**: Built-in support for multiple endpoints with automatic failover
43
+ - **Endpoint failover**: Built-in support for multiple endpoints with opt-in automatic failover (`failover=True`)
27
44
  - **Context manager**: Clean resource management for both sync and async
28
45
  - **Observability**: Structured logging, OpenTelemetry tracing, and Prometheus metrics
29
46
 
@@ -272,8 +289,14 @@ client = await async_client(
272
289
  )
273
290
 
274
291
  # Multiple endpoints with failover
275
- client = await async_client(
276
- endpoints=['localhost:2379', 'localhost:22379'],
292
+ # (the single-endpoint factory does not take endpoints=)
293
+ from etcd3 import AsyncMultiEndpointEtcd3Client, AsyncEndpoint
294
+
295
+ client = AsyncMultiEndpointEtcd3Client(
296
+ endpoints=[
297
+ AsyncEndpoint('localhost', 2379, secure=False),
298
+ AsyncEndpoint('localhost', 22379, secure=False),
299
+ ],
277
300
  failover=True, # Automatically switch to healthy endpoint
278
301
  )
279
302
 
@@ -13,7 +13,7 @@ Python async client for the etcd API v3, supporting both synchronous and asynchr
13
13
  - **Async/Sync dual support**: Same API for both async and sync usage with feature parity
14
14
  - **Automatic reconnection**: Auto-rebuild stale connections (e.g., after etcd server restart)
15
15
  - **Keepalive error propagation**: Lease keepalive errors are properly propagated for connection loss detection
16
- - **Endpoint failover**: Built-in support for multiple endpoints with automatic failover
16
+ - **Endpoint failover**: Built-in support for multiple endpoints with opt-in automatic failover (`failover=True`)
17
17
  - **Context manager**: Clean resource management for both sync and async
18
18
  - **Observability**: Structured logging, OpenTelemetry tracing, and Prometheus metrics
19
19
 
@@ -262,8 +262,14 @@ client = await async_client(
262
262
  )
263
263
 
264
264
  # Multiple endpoints with failover
265
- client = await async_client(
266
- endpoints=['localhost:2379', 'localhost:22379'],
265
+ # (the single-endpoint factory does not take endpoints=)
266
+ from etcd3 import AsyncMultiEndpointEtcd3Client, AsyncEndpoint
267
+
268
+ client = AsyncMultiEndpointEtcd3Client(
269
+ endpoints=[
270
+ AsyncEndpoint('localhost', 2379, secure=False),
271
+ AsyncEndpoint('localhost', 22379, secure=False),
272
+ ],
267
273
  failover=True, # Automatically switch to healthy endpoint
268
274
  )
269
275
 
@@ -13,7 +13,7 @@ Python etcd API v3 异步客户端,支持同步和异步操作。
13
13
  - **同步/异步双模式**: 同一套 API 同时支持 sync 和 async,功能完全对等
14
14
  - **自动重连**: 自动重建失效连接(如 etcd 服务器重启后)
15
15
  - **Keepalive 错误传播**: 租约保活错误正确传播,便于连接断开检测
16
- - **端点故障转移**: 支持多节点自动故障转移
16
+ - **端点故障转移**: 支持多节点,可选开启自动故障转移(`failover=True`)
17
17
  - **上下文管理**: 资源清理更加简洁
18
18
  - **可观测性**: 结构化日志、OpenTelemetry 追踪、Prometheus 指标
19
19
 
@@ -262,8 +262,14 @@ client = await async_client(
262
262
  )
263
263
 
264
264
  # 多节点故障转移
265
- client = await async_client(
266
- endpoints=['localhost:2379', 'localhost:22379'],
265
+ # (单端点工厂函数不接受 endpoints= 参数)
266
+ from etcd3 import AsyncMultiEndpointEtcd3Client, AsyncEndpoint
267
+
268
+ client = AsyncMultiEndpointEtcd3Client(
269
+ endpoints=[
270
+ AsyncEndpoint('localhost', 2379, secure=False),
271
+ AsyncEndpoint('localhost', 22379, secure=False),
272
+ ],
267
273
  failover=True, # 自动切换到健康节点
268
274
  )
269
275
 
@@ -28,6 +28,12 @@ uv pip install -e .
28
28
 
29
29
  - Python 3.12+
30
30
  - etcd v3 API server (v3.1+ recommended)
31
+ - `grpcio>=1.76.0` and `protobuf>=6.31.1`
32
+
33
+ The grpcio and protobuf requirements are **ranges**, not exact pins, so the
34
+ package installs next to other packages that need a different grpcio. The
35
+ generated protobuf code only rejects a runtime that is *older* than the
36
+ version it was generated with, so newer grpcio releases work as well.
31
37
 
32
38
  ## Virtual Environments with uv
33
39
 
@@ -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
+ ## Write Results
53
+
54
+ The sync and the async client return the same shapes for writes:
55
+
56
+ | Call | Result |
57
+ |------|--------|
58
+ | `put(key, value)` | the response header (revision, etc.) |
59
+ | `put(key, value, prev_kv=True)` | `KVMetadata` describing the **previous** value |
60
+ | `delete(key)` | the number of deleted keys |
61
+ | `delete_prefix(prefix)` | the number of deleted keys |
62
+ | `delete(key, return_response=True)` | the full `DeleteRangeResponse` |
63
+
64
+ ```python
65
+ # How many keys did that delete remove?
66
+ removed = await client.delete('/mykey')
67
+
68
+ # What was stored before this write?
69
+ previous = await client.put('/mykey', 'new', prev_kv=True)
70
+ print(previous.value) # b'old'
71
+ ```
72
+
73
+ `delete_prefix` accepts `return_response=True` on both clients as well.
74
+
52
75
  ## Batch Operations
53
76
 
54
77
  The three batch entry points share one concurrency and failure policy:
@@ -75,12 +75,18 @@ client = AsyncMultiEndpointEtcd3Client(
75
75
  | `user` | str | None | Username for authentication |
76
76
  | `password` | str | None | Password for authentication |
77
77
  | `failover` | bool | False | Enable automatic failover between endpoints |
78
- | `max_retries` | int | 3 | Maximum retries for transient errors |
78
+ | `max_retries` | int | 3 | Maximum retries for transient errors (read paths only) |
79
79
  | `retry_backoff` | float | 0.1 | Initial retry backoff in seconds |
80
80
  | `retry_max_backoff` | float | 10.0 | Maximum retry backoff in seconds |
81
81
  | `load_balancer` | LoadBalancerStrategy | PRIORITY | Load balancing strategy |
82
82
  | `health_check_interval` | float | 30.0 | Health check interval in seconds |
83
83
 
84
+ `failover` and `load_balancer` decide which endpoint a request goes to:
85
+ with `failover=True` a failed endpoint is bypassed, with `failover=False`
86
+ the client stays on its endpoint and rebuilds the channel in place. The
87
+ retry settings apply to the idempotent read paths only — writes are
88
+ never retried automatically.
89
+
84
90
  ## Configuration Classes
85
91
 
86
92
  Every keyword above can also be passed as a typed dataclass via `config=`,
@@ -10,7 +10,7 @@ python-etcd3-async is an async Python client library for the etcd v3 API based o
10
10
 
11
11
  - **Async First**: Pure async operations using `grpc.aio`
12
12
  - **Sync Support**: Synchronous client API also available
13
- - **Failover**: Multi-endpoint support with automatic failover
13
+ - **Failover**: Multi-endpoint support with opt-in automatic failover
14
14
  - **Auto Reconnection**: Automatic channel rebuild on connection failure
15
15
  - **Complete API**: Full support for all major etcd v3 features
16
16
 
@@ -22,7 +22,7 @@ This guide provides comprehensive information on the error handling mechanisms o
22
22
  The etcd3 async client provides robust error handling capabilities to ensure reliable operation in production environments. Key features include:
23
23
 
24
24
  - **Comprehensive error mapping**: All gRPC status codes are mapped to appropriate Python exceptions
25
- - **Exponential backoff retry**: Automatically retry transient errors with configurable backoff
25
+ - **Exponential backoff retry**: Transient errors are retried on the idempotent read paths
26
26
  - **Resource management**: Proper cleanup of gRPC channels and other resources
27
27
  - **Monitoring**: Detailed metrics collection for error rates, latencies, and endpoint health
28
28
  - **Failover support**: Automatic switching between endpoints when failures occur
@@ -52,13 +52,23 @@ The etcd3 async client maps gRPC status codes to the following exception types:
52
52
 
53
53
  ## Retry Mechanism
54
54
 
55
- The etcd3 async client implements an exponential backoff retry strategy for transient errors. By default, it will retry the following error types:
55
+ The client retries transient errors on the **idempotent read paths** (`get`,
56
+ `get_prefix`, `get_range`, `get_all` and their `*_response` variants).
57
+ By default, it will retry the following error types:
56
58
 
57
59
  - `UNAVAILABLE` (connection errors)
58
60
  - `DEADLINE_EXCEEDED` (timeouts)
59
61
  - `INTERNAL` (server errors)
60
62
  - `RESOURCE_EXHAUSTED` (resource constraints)
61
63
 
64
+ **Writes are not retried automatically.** A failed request may already have
65
+ been applied server-side, so repeating it could duplicate the effect.
66
+ Retry writes in the application, and only where they are safe to repeat
67
+ (idempotent puts). Treat `RevisionCompactedError` as a signal to resync
68
+ state rather than to retry blindly.
69
+
70
+ The same rules apply to the sync client.
71
+
62
72
  ### Retry Configuration
63
73
 
64
74
  You can configure the retry behavior when creating the client:
@@ -67,6 +67,15 @@ client = AsyncMultiEndpointEtcd3Client(
67
67
  )
68
68
  ```
69
69
 
70
+ `max_retries` and the backoff settings apply to the idempotent read paths.
71
+ Writes are never retried automatically — see
72
+ [Error Handling](error_handling.md#retry-guidance).
73
+
74
+ With `failover=True` a failed endpoint is bypassed and the client picks
75
+ another healthy one. With `failover=False` the client stays on the
76
+ endpoint it holds and rebuilds that endpoint's channel in place. The sync
77
+ and the async client behave the same way.
78
+
70
79
  Each `Endpoint` also carries `time_retry` (default 300 s) — the window in
71
80
  which a failed endpoint stays marked failed before being considered again.
72
81
 
@@ -4,8 +4,46 @@ title: History
4
4
 
5
5
  # History
6
6
 
7
+ ## v0.2.1 (2026-11)
8
+
9
+ ### grpcio / protobuf compatibility
10
+
11
+ - The `grpcio` and `protobuf` requirements are now **ranges**
12
+ (`grpcio>=1.76.0`, `protobuf>=6.31.1`) instead of exact pins, so the
13
+ package installs next to other packages that need a different grpcio
14
+ - The generated protobuf/gRPC code in `etcdrpc/` is checked in, and the
15
+ build no longer needs protoc or grpcio-tools - it works on every
16
+ supported Python
17
+ - The generator is pinned to the supported floor in a new `codegen`
18
+ dependency group, so regenerating can never silently raise the floor
19
+ and break users on older grpcio
20
+ - `scripts/gen-protos.sh` regenerates the code and
21
+ `scripts/gen-protos.sh --check` fails when it has drifted; the check
22
+ runs in CI
23
+ - `tests/test_grpc_compat.py` pins the contract: the declared floors must
24
+ match the versions embedded in the generated code
25
+
26
+ ### Packaging
27
+
28
+ - The package metadata now points at the repository
29
+ (https://cnb.cool/owasd/python3-etcd-async) and carries the license,
30
+ author, keywords and classifiers - the PyPI page of the previous
31
+ releases had no repository link at all
32
+ - `scripts/publish.sh` publishes a release: it reads the token from
33
+ `~/.pypirc` (uv does not read that file itself), refuses placeholder
34
+ versions, only uploads the artifacts of the current version and asks
35
+ for a confirmation before uploading
36
+
37
+ ### Documentation
38
+
39
+ - The guides now describe the 0.2.0 behaviour: the retry policy covers
40
+ the idempotent read paths only, the failover rules for both clients,
41
+ and the shared write results
42
+
7
43
  ## v0.2.0 (2026-11)
8
44
 
45
+ ### Sync and async parity
46
+
9
47
  Sync and async clients aligned on behaviour that had drifted apart:
10
48
 
11
49
  - **Endpoint selection**: the async client now applies the load balancer
@@ -39,8 +39,8 @@ and hatch reads that attribute at build time — do not add a static
39
39
  uv run ruff check tests/ etcd3/ && uv run ruff format --check tests/ etcd3/
40
40
  ```
41
41
 
42
- 5. **Build the distribution** (protobuf code is regenerated by the
43
- hatch build hook).
42
+ 5. **Build the distribution** (the generated protobuf code is checked in,
43
+ so the build needs no protoc).
44
44
 
45
45
  ```bash
46
46
  uv build
@@ -56,10 +56,31 @@ and hatch reads that attribute at build time — do not add a static
56
56
  7. **Publish** to the index and tag the release:
57
57
 
58
58
  ```bash
59
- uv publish # or the project's index workflow
59
+ scripts/publish.sh publish # builds if needed, uploads, confirms first
60
60
  git tag vX.Y.Z
61
61
  git push && git push --tags
62
62
  ```
63
63
 
64
+ `scripts/publish.sh` wraps `uv publish` because **`uv` does not read
65
+ `~/.pypirc`**: the script takes the token from the `[pypi]` section of
66
+ that file and passes it through `UV_PUBLISH_TOKEN`. In CI, where
67
+ trusted publishing (OIDC) is configured on PyPI, no token is needed and
68
+ the script publishes without one.
69
+
70
+ The script refuses to publish placeholder versions (`9.9.9`, `0.0.0`,
71
+ non-`MAJOR.MINOR.PATCH` strings) and warns when the version is already
72
+ on PyPI. A published version cannot be replaced, and it becomes the
73
+ version that installers resolve to, so a scratch version must never
74
+ reach the index - use `scripts/publish.sh dry-run` to test the upload
75
+ path instead.
76
+
77
+ Other commands: `check` (pre-flight checks only), `dry-run` (validates
78
+ the upload without sending anything), `build`, `verify` (waits until the
79
+ release shows up on PyPI).
80
+
81
+ Overrides: `PYPI_TOKEN`, `PYPI_CONFIG`, `PYPI_SECTION`,
82
+ `PYPI_PUBLISH_URL`, `PYPI_CHECK_URL`, `PUBLISH_CONFIRM=yes` (skips the
83
+ confirmation prompt).
84
+
64
85
  8. **Verify the published artifact** resolves from a clean environment
65
86
  (`uv add python-etcd3-async==X.Y.Z` in a scratch project).
@@ -28,6 +28,11 @@ uv pip install -e .
28
28
 
29
29
  - Python 3.12+
30
30
  - etcd v3 API 服务器(推荐 v3.1+)
31
+ - `grpcio>=1.76.0` 与 `protobuf>=6.31.1`
32
+
33
+ grpcio 与 protobuf 的依赖是**区间**而非精确锁定,因此本包可以与需要其它
34
+ grpcio 版本的包共存。生成的 protobuf 代码只会拒绝比生成时**更旧**的运行时
35
+ 版本,因此更新的 grpcio 同样可用。
31
36
 
32
37
  ## 使用 uv 管理虚拟环境
33
38
 
@@ -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
+ ## 写操作返回值
53
+
54
+ 同步与异步客户端的写操作返回形态一致:
55
+
56
+ | 调用 | 返回值 |
57
+ |------|--------|
58
+ | `put(key, value)` | 响应头(含 revision 等信息) |
59
+ | `put(key, value, prev_kv=True)` | 描述**旧值**的 `KVMetadata` |
60
+ | `delete(key)` | 删除的键数量 |
61
+ | `delete_prefix(prefix)` | 删除的键数量 |
62
+ | `delete(key, return_response=True)` | 完整的 `DeleteRangeResponse` |
63
+
64
+ ```python
65
+ # 这次删除移除了多少个键?
66
+ removed = await client.delete('/mykey')
67
+
68
+ # 写入之前的值是什么?
69
+ previous = await client.put('/mykey', 'new', prev_kv=True)
70
+ print(previous.value) # b'old'
71
+ ```
72
+
73
+ 两个客户端的 `delete_prefix` 都支持 `return_response=True`。
74
+
52
75
  ## 批量操作
53
76
 
54
77
  三个批量入口共享同一套并发与失败语义:条目并行执行,并发上限由
@@ -75,12 +75,16 @@ client = AsyncMultiEndpointEtcd3Client(
75
75
  | `user` | str | None | 认证用户名 |
76
76
  | `password` | str | None | 认证密码 |
77
77
  | `failover` | bool | False | 启用端点间自动故障转移 |
78
- | `max_retries` | int | 3 | 瞬态错误的最大重试次数 |
78
+ | `max_retries` | int | 3 | 瞬态错误的最大重试次数(仅读路径) |
79
79
  | `retry_backoff` | float | 0.1 | 初始重试退避时间(秒) |
80
80
  | `retry_max_backoff` | float | 10.0 | 最大重试退避时间(秒) |
81
81
  | `load_balancer` | LoadBalancerStrategy | PRIORITY | 负载均衡策略 |
82
82
  | `health_check_interval` | float | 30.0 | 健康检查间隔(秒) |
83
83
 
84
+ `failover` 与 `load_balancer` 共同决定请求发往哪个端点:`failover=True`
85
+ 时失败端点会被绕过,`failover=False` 时客户端留在当前端点并原地重建
86
+ channel。重试参数只作用于幂等读路径——写操作永远不会自动重试。
87
+
84
88
  ## 配置类
85
89
 
86
90
  上述所有关键字都可以通过 `config=` 以带类型的 dataclass 传入,
@@ -10,7 +10,7 @@ python-etcd3-async 是一个基于 `grpc.aio` 的 etcd v3 API 异步 Python 客
10
10
 
11
11
  - **异步优先**:使用 `grpc.aio` 实现纯异步操作
12
12
  - **同步支持**:同时提供同步客户端 API
13
- - **故障转移**:多端点支持,自动故障转移
13
+ - **故障转移**:多端点支持,可选开启自动故障转移
14
14
  - **自动重连**:连接失效时自动重建
15
15
  - **完整 API**:支持 etcd v3 所有主要功能
16
16
 
@@ -11,7 +11,7 @@ title: 异步错误处理
11
11
  etcd3 异步客户端提供强大的错误处理功能,确保在生产环境中的可靠运行。主要特性包括:
12
12
 
13
13
  - **全面错误映射**:所有 gRPC 状态码都映射到相应的 Python 异常
14
- - **指数退避重试**:自动重试瞬态错误,支持可配置退避
14
+ - **指数退避重试**:在幂等读路径上自动重试瞬态错误,支持可配置退避
15
15
  - **资源管理**:正确清理 gRPC 通道和其他资源
16
16
  - **监控**:详细收集错误率、延迟和端点健康状况指标
17
17
  - **故障转移支持**:发生故障时自动切换端点
@@ -41,13 +41,20 @@ etcd3 异步客户端将 gRPC 状态码映射到以下异常类型:
41
41
 
42
42
  ## 重试机制
43
43
 
44
- etcd3 异步客户端对瞬态错误实现指数退避重试策略。默认情况下,它将重试以下错误类型:
44
+ 客户端在**幂等读路径**(`get`、`get_prefix`、`get_range`、`get_all` 及其
45
+ `*_response` 变体)上对瞬态错误实现指数退避重试。默认情况下,它将重试以下错误类型:
45
46
 
46
47
  - `UNAVAILABLE`(连接错误)
47
48
  - `DEADLINE_EXCEEDED`(超时)
48
49
  - `INTERNAL`(服务器错误)
49
50
  - `RESOURCE_EXHAUSTED`(资源限制)
50
51
 
52
+ **写操作不会自动重试。** 请求报错时可能已在服务端生效,重复执行可能产生
53
+ 重复副作用。写操作请在应用层重试,且仅在可安全重复时重试(幂等的 put)。
54
+ 遇到 `RevisionCompactedError` 应视为需要重新同步状态,而不是盲目重试。
55
+
56
+ 同步客户端遵循同样的规则。
57
+
51
58
  ### 重试配置
52
59
 
53
60
  创建客户端时可以配置重试行为:
@@ -60,6 +60,13 @@ client = AsyncMultiEndpointEtcd3Client(
60
60
  每个 `Endpoint` 还带有 `time_retry`(默认 300 秒)—
61
61
  失败端点在被重新纳入候选之前保持"失败"标记的时间窗口。
62
62
 
63
+ `max_retries` 及退避参数只作用于幂等读路径。写操作永远不会自动重试,
64
+ 详见[错误处理](error_handling.md)。
65
+
66
+ `failover=True` 时,失败端点会被绕过,客户端改用其它健康端点;
67
+ `failover=False` 时,客户端留在当前端点并原地重建其 channel。
68
+ 同步与异步客户端行为一致。
69
+
63
70
  ## 观察恢复过程
64
71
 
65
72
  ```python
@@ -59,7 +59,7 @@ except exceptions.NoServerAvailableError as e:
59
59
  ## 错误处理层
60
60
 
61
61
  - **一元调用**:由生成的错误处理器包装,把 `grpc.RpcError` 映射为
62
- 上述异常,并在多端点客户端上触发带退避的重试。
62
+ 上述异常。
63
63
  - **生成器**(watch 迭代器、range 流):错误在迭代过程中浮现时被映射;
64
64
  watch 被正常取消时,生成器处理器可能会静默结束流。
65
65
  - **保活流**(租约刷新):映射后的错误会传播给消费方,
@@ -67,10 +67,14 @@ except exceptions.NoServerAvailableError as e:
67
67
 
68
68
  ## 重试建议
69
69
 
70
- 启用 `failover=True` 时,客户端已对瞬时失败进行重试
71
- (`max_retries`、`retry_backoff`、`retry_max_backoff`)。
72
- 应用层重试只用于可安全重复的操作(幂等的 put、读),
73
- 并把 `RevisionCompactedError` 视为重新同步状态的信号,而非盲目重试。
70
+ 读操作(`get`、`get_prefix`、`get_range`、`get_all` 及其
71
+ `*_response` 变体)在遇到瞬时失败时会自动重试,次数由
72
+ `max_retries`、`retry_backoff`、`retry_max_backoff` 控制。
73
+
74
+ **写操作不会自动重试。** 请求报错时可能已在服务端生效,重复执行可能
75
+ 产生重复副作用。写操作请在应用层重试,且仅在可安全重复时重试
76
+ (幂等的 put)。把 `RevisionCompactedError` 视为需要重新同步状态的信号,
77
+ 而非盲目重试。
74
78
 
75
79
  ## 另见
76
80
 
@@ -41,10 +41,14 @@ client = AsyncMultiEndpointEtcd3Client(
41
41
  2. 当请求因 UNAVAILABLE、DEADLINE_EXCEEDED 或 INTERNAL 失败时,
42
42
  端点被标记为失败
43
43
  3. 下一次操作时,客户端按负载均衡策略选择健康端点,
44
- 并在后台重建失败端点的通道
44
+ 并在后台重建失败端点的通道。若 `failover=False`,
45
+ 客户端则留在当前端点,并原地重建该端点的通道
45
46
  4. 定期健康检查(`health_check_interval`)在服务器恢复应答后
46
47
  将端点标记为健康
47
48
 
49
+ 两个客户端行为一致:同步与异步多端点客户端使用相同的策略和相同的
50
+ 故障转移规则。
51
+
48
52
  ## 负载均衡策略
49
53
 
50
54
  | 策略 | 描述 |
@@ -4,8 +4,39 @@ title: 更新历史
4
4
 
5
5
  # 更新历史
6
6
 
7
+ ## v0.2.1 (2026-11)
8
+
9
+ ### grpcio / protobuf 兼容性
10
+
11
+ - `grpcio` 与 `protobuf` 的依赖改为**区间**
12
+ (`grpcio>=1.76.0`、`protobuf>=6.31.1`),不再精确锁定,因此本包可以与
13
+ 需要其它 grpcio 版本的包共存
14
+ - `etcdrpc/` 下生成的 protobuf/gRPC 代码改为入库,构建时不再需要 protoc
15
+ 或 grpcio-tools,在所有受支持的 Python 上都能构建
16
+ - 生成器在新增的 `codegen` 依赖组中锁定到所支持的下限,重新生成代码不会
17
+ 悄悄抬高下限、导致使用旧版 grpcio 的用户在导入时失败
18
+ - `scripts/gen-protos.sh` 用于重新生成代码,
19
+ `scripts/gen-protos.sh --check` 在代码发生漂移时报错,该检查已接入 CI
20
+ - `tests/test_grpc_compat.py` 固化此约定:声明的下限必须与生成代码中嵌入的
21
+ 版本一致
22
+
23
+ ### 打包
24
+
25
+ - 包元数据现在指向代码仓库(https://cnb.cool/owasd/python3-etcd-async),
26
+ 并补齐了许可证、作者、关键词与分类器——此前版本的 PyPI 页面完全没有
27
+ 仓库链接
28
+ - `scripts/publish.sh` 用于发布:从 `~/.pypirc` 读取 token(uv 本身不读该
29
+ 文件),拒绝占位版本号,只上传当前版本的产物,并在上传前要求确认
30
+
31
+ ### 文档
32
+
33
+ - 各指南已与 0.2.0 的行为对齐:重试策略只覆盖幂等读路径、两个客户端的
34
+ 故障转移规则,以及统一的写操作返回值
35
+
7
36
  ## v0.2.0 (2026-11)
8
37
 
38
+ ### 同步 / 异步一致性
39
+
9
40
  同步与异步客户端在行为差异上完成对齐:
10
41
 
11
42
  - **端点选择**:异步客户端现在与同步客户端一样应用负载均衡策略,并在