python-etcd3-async 0.1.5__py3-none-any.whl → 0.1.6__py3-none-any.whl

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.
etcd3/__init__.py CHANGED
@@ -50,7 +50,7 @@ from etcd3.async_single_client import async_client
50
50
 
51
51
  __author__ = "owasd"
52
52
  __email__ = ""
53
- __version__ = "0.1.0"
53
+ __version__ = "0.1.6"
54
54
 
55
55
  __all__ = (
56
56
  "etcdrpc",
@@ -730,9 +730,9 @@ class AsyncClusterOperations:
730
730
  def __init__(self, client):
731
731
  self.client = client
732
732
 
733
- async def add_member(self, urls):
733
+ async def add_member(self, urls, is_learner=False):
734
734
  """Add a member into the cluster."""
735
- return await self.client.cluster_manager.add_member(urls)
735
+ return await self.client.cluster_manager.add_member(urls, is_learner)
736
736
 
737
737
  async def remove_member(self, member_id):
738
738
  """Remove an existing member from the cluster."""
etcd3/async_managers.py CHANGED
@@ -224,14 +224,19 @@ class AsyncClusterManager:
224
224
  def __init__(self, etcd_client):
225
225
  self.etcd_client = etcd_client
226
226
 
227
- async def add_member(self, urls):
227
+ async def add_member(self, urls, is_learner=False):
228
228
  """
229
229
  Add a member into the cluster.
230
230
 
231
+ :param urls: peer URLs the member will advertise to the cluster
232
+ :param is_learner: add as a non-voting learner, which does not
233
+ affect quorum on an existing cluster
231
234
  :returns: new member
232
235
  :rtype: :class:`.Member`
233
236
  """
234
- member_add_request = etcdrpc.MemberAddRequest(peerURLs=urls)
237
+ member_add_request = etcdrpc.MemberAddRequest(
238
+ peerURLs=urls, isLearner=is_learner
239
+ )
235
240
 
236
241
  # Create cluster stub directly
237
242
  channel = await self.etcd_client._get_channel()
@@ -19,7 +19,6 @@ from etcd3.shared import (
19
19
  EXCEPTIONS_BY_CODE,
20
20
  FAILED_EP_CODES,
21
21
  Transactions,
22
- EtcdTokenCallCredentials,
23
22
  RequestBuilder,
24
23
  )
25
24
  from etcd3.async_managers import (
@@ -161,8 +160,14 @@ class AsyncMultiEndpointEtcd3Client:
161
160
  self.transactions = Transactions()
162
161
  self.auth = AsyncAuthOperations(self)
163
162
 
164
- async def _init_auth(self):
165
- """Initialize authentication asynchronously."""
163
+ async def _init_auth(self, channel=None):
164
+ """Initialize authentication asynchronously.
165
+
166
+ :param channel: Channel to run Authenticate on. Callers from
167
+ ``_get_channel`` pass the channel they are about to return —
168
+ re-acquiring here could rebuild (close) that channel.
169
+ :type channel: grpc.aio.Channel, optional
170
+ """
166
171
  if self._auth_done:
167
172
  return
168
173
 
@@ -172,7 +177,8 @@ class AsyncMultiEndpointEtcd3Client:
172
177
 
173
178
  if self._user and self._password:
174
179
  # Create auth stub with auto-reconnection
175
- channel = await self._get_channel()
180
+ if channel is None:
181
+ channel = await self._acquire_channel()
176
182
  auth_stub = etcdrpc.AuthStub(channel)
177
183
 
178
184
  auth_request = etcdrpc.AuthenticateRequest(
@@ -180,10 +186,11 @@ class AsyncMultiEndpointEtcd3Client:
180
186
  )
181
187
 
182
188
  resp = await auth_stub.Authenticate(auth_request, timeout=self.timeout)
189
+ # Token is attached via self.metadata on every call. Do not
190
+ # create grpc call credentials here: grpc.aio rejects them on
191
+ # insecure channels ("call credentials are only valid on
192
+ # secure channels") and the metadata token works everywhere.
183
193
  self.metadata = (("token", resp.token),)
184
- self.call_credentials = grpc.metadata_call_credentials(
185
- EtcdTokenCallCredentials(resp.token)
186
- )
187
194
 
188
195
  self._auth_done = True
189
196
 
@@ -280,11 +287,28 @@ class AsyncMultiEndpointEtcd3Client:
280
287
  """Try to rebuild the channel, return True on success, False on failure."""
281
288
  try:
282
289
  await endpoint.rebuild_channel()
290
+ # A rebuilt channel may talk to a restarted server whose
291
+ # token seed changed; force re-authentication on next use.
292
+ self._auth_done = False
283
293
  return True
284
294
  except Exception:
285
295
  return False
286
296
 
287
297
  async def _get_channel(self):
298
+ """Get an available channel, authenticating first when credentials exist.
299
+
300
+ Lazy auth: when the client was created with user/password and no
301
+ token is held yet (initial use or after a channel rebuild), the
302
+ Authenticate RPC runs before the channel is handed out. All call
303
+ sites read ``self.metadata`` after ``_get_channel()``, so the
304
+ token is always attached.
305
+ """
306
+ channel = await self._acquire_channel()
307
+ if self._user and self._password and not self._auth_done:
308
+ await self._init_auth(channel)
309
+ return channel
310
+
311
+ async def _acquire_channel(self):
288
312
  """Get an available channel with automatic reconnection support.
289
313
 
290
314
  Uses two-phase locking strategy:
@@ -969,14 +993,17 @@ class AsyncMultiEndpointEtcd3Client:
969
993
  """
970
994
  return await self.lock_ops.lock(name, ttl=ttl)
971
995
 
972
- async def add_member(self, urls):
996
+ async def add_member(self, urls, is_learner=False):
973
997
  """
974
998
  Add a member into the cluster.
975
999
 
1000
+ :param urls: peer URLs the member will advertise to the cluster
1001
+ :param is_learner: add as a non-voting learner, which does not
1002
+ affect quorum on an existing cluster
976
1003
  :returns: new member
977
1004
  :rtype: :class:`.Member`
978
1005
  """
979
- return await self.cluster_ops.add_member(urls)
1006
+ return await self.cluster_ops.add_member(urls, is_learner)
980
1007
 
981
1008
  async def remove_member(self, member_id):
982
1009
  """
etcd3/errors.py CHANGED
@@ -26,6 +26,79 @@ _FAILED_EP_CODES = [
26
26
  ]
27
27
 
28
28
 
29
+ def _is_invalid_auth_token(exc):
30
+ """Check if an RPC error means the server rejected our auth token."""
31
+ try:
32
+ return exc.code() == grpc.StatusCode.UNAUTHENTICATED
33
+ except Exception:
34
+ return False
35
+
36
+
37
+ async def _try_reauth(client_or_ops):
38
+ """Re-authenticate after the server rejected our token.
39
+
40
+ etcd rotates its token seed on restart, so an existing channel can be
41
+ fully healthy while the stored token is dead. Drop the token and let
42
+ the lazy-auth path in ``_get_channel`` mint a fresh one.
43
+
44
+ :return: True when a new token was obtained, False when re-auth does
45
+ not apply (no credentials or non-authenticating client object)
46
+ """
47
+ client = getattr(client_or_ops, "client", client_or_ops)
48
+ if not (
49
+ getattr(client, "_user", None)
50
+ and getattr(client, "_password", None)
51
+ and hasattr(client, "_get_channel")
52
+ ):
53
+ return False
54
+ client._auth_done = False
55
+ await client._get_channel()
56
+ return True
57
+
58
+
59
+ async def _call_with_auth_retry(payload, self, args, kwargs, error_handler_func):
60
+ """Run payload; on an invalid-token error re-authenticate and retry once.
61
+
62
+ The retry attempt is safe: an UNAUTHENTICATED call never reached the
63
+ state machine, so the operation was not applied server-side.
64
+ """
65
+ try:
66
+ return await payload(self, *args, **kwargs)
67
+ except grpc.RpcError as exc:
68
+ if _is_invalid_auth_token(exc) and await _try_reauth(self):
69
+ try:
70
+ return await payload(self, *args, **kwargs)
71
+ except grpc.RpcError as retry_exc:
72
+ exc = retry_exc
73
+ error_handler_func(self, exc)
74
+
75
+
76
+ def _try_reauth_sync(client_or_ops):
77
+ """Sync counterpart of :func:`_try_reauth` (see above)."""
78
+ client = getattr(client_or_ops, "client", client_or_ops)
79
+ if not (
80
+ getattr(client, "_user", None)
81
+ and getattr(client, "_password", None)
82
+ and hasattr(client, "_authenticate")
83
+ ):
84
+ return False
85
+ client._authenticate()
86
+ return True
87
+
88
+
89
+ def _call_with_auth_retry_sync(payload, self, args, kwargs, error_handler_func):
90
+ """Sync counterpart of :func:`_call_with_auth_retry` (see above)."""
91
+ try:
92
+ return payload(self, *args, **kwargs)
93
+ except grpc.RpcError as exc:
94
+ if _is_invalid_auth_token(exc) and _try_reauth_sync(self):
95
+ try:
96
+ return payload(self, *args, **kwargs)
97
+ except grpc.RpcError as retry_exc:
98
+ exc = retry_exc
99
+ error_handler_func(self, exc)
100
+
101
+
29
102
  def _make_async_handle_errors_with_tracing(error_handler_func, operation_name: str):
30
103
  """Factory to create async error handlers with tracing."""
31
104
 
@@ -34,15 +107,13 @@ def _make_async_handle_errors_with_tracing(error_handler_func, operation_name: s
34
107
  async def handler(self, *args, **kwargs):
35
108
  tracer = get_tracer()
36
109
  if tracer is None:
37
- try:
38
- return await payload(self, *args, **kwargs)
39
- except grpc.RpcError as exc:
40
- error_handler_func(self, exc)
110
+ return await _call_with_auth_retry(
111
+ payload, self, args, kwargs, error_handler_func
112
+ )
41
113
  with create_span(operation_name, {"operation": operation_name}):
42
- try:
43
- return await payload(self, *args, **kwargs)
44
- except grpc.RpcError as exc:
45
- error_handler_func(self, exc)
114
+ return await _call_with_auth_retry(
115
+ payload, self, args, kwargs, error_handler_func
116
+ )
46
117
 
47
118
  return handler
48
119
 
@@ -100,17 +171,13 @@ def _make_sync_handle_errors_with_tracing(error_handler_func, operation_name: st
100
171
  def handler(self, *args, **kwargs):
101
172
  tracer = get_tracer()
102
173
  if tracer is None:
103
- try:
104
- return payload(self, *args, **kwargs)
105
- except grpc.RpcError as exc:
106
- error_handler_func(self, exc)
107
- return
174
+ return _call_with_auth_retry_sync(
175
+ payload, self, args, kwargs, error_handler_func
176
+ )
108
177
  with create_span(operation_name, {"operation": operation_name}):
109
- try:
110
- return payload(self, *args, **kwargs)
111
- except grpc.RpcError as exc:
112
- error_handler_func(self, exc)
113
- return
178
+ return _call_with_auth_retry_sync(
179
+ payload, self, args, kwargs, error_handler_func
180
+ )
114
181
 
115
182
  return handler
116
183
 
etcd3/multi_client.py CHANGED
@@ -164,21 +164,17 @@ class MultiEndpointEtcd3Client:
164
164
  cred_params = [c is not None for c in (user, password)]
165
165
 
166
166
  if all(cred_params):
167
- auth_request = etcdrpc.AuthenticateRequest(name=user, password=password)
168
-
169
- # Create auth stub directly (no caching needed)
170
- auth_stub = etcdrpc.AuthStub(self.channel)
171
- resp = auth_stub.Authenticate(auth_request, self.timeout)
172
- self.metadata = (("token", resp.token),)
173
- self.call_credentials = grpc.metadata_call_credentials(
174
- self._get_etcd_token_call_credentials(resp.token)
175
- )
176
-
167
+ self._user = user
168
+ self._password = password
169
+ self._authenticate()
177
170
  elif any(cred_params):
178
- raise Exception(
171
+ raise ValueError(
179
172
  "if using authentication credentials both user and password "
180
173
  "must be specified."
181
174
  )
175
+ else:
176
+ self._user = None
177
+ self._password = None
182
178
 
183
179
  # Import here to avoid circular dependency
184
180
  from etcd3.shared import Transactions
@@ -206,11 +202,19 @@ class MultiEndpointEtcd3Client:
206
202
 
207
203
  self.maintenance = SyncMaintenanceOperations(self)
208
204
 
209
- def _get_etcd_token_call_credentials(self, token):
210
- """Get token call credentials for authentication."""
211
- from etcd3.shared import EtcdTokenCallCredentials
205
+ def _authenticate(self):
206
+ """Obtain an auth token on the current channel (metadata-only).
212
207
 
213
- return EtcdTokenCallCredentials(token)
208
+ The token rides in ``self.metadata`` on every call. Do not create
209
+ grpc call credentials here: they are rejected on insecure channels
210
+ and the metadata token works everywhere, matching the async client.
211
+ """
212
+ auth_request = etcdrpc.AuthenticateRequest(
213
+ name=self._user, password=self._password
214
+ )
215
+ auth_stub = etcdrpc.AuthStub(self.channel)
216
+ resp = auth_stub.Authenticate(auth_request, self.timeout)
217
+ self.metadata = (("token", resp.token),)
214
218
 
215
219
  @property
216
220
  def _current_endpoint_label(self):
@@ -1159,14 +1163,17 @@ class MultiEndpointEtcd3Client:
1159
1163
  """
1160
1164
  return self.lock_ops.lock(name, ttl=ttl)
1161
1165
 
1162
- def add_member(self, urls):
1166
+ def add_member(self, urls, is_learner=False):
1163
1167
  """
1164
1168
  Add a member into the cluster.
1165
1169
 
1170
+ :param urls: peer URLs the member will advertise to the cluster
1171
+ :param is_learner: add as a non-voting learner, which does not
1172
+ affect quorum on an existing cluster
1166
1173
  :returns: new member
1167
1174
  :rtype: :class:`.Member`
1168
1175
  """
1169
- return self.cluster.add_member(urls)
1176
+ return self.cluster.add_member(urls, is_learner)
1170
1177
 
1171
1178
  def remove_member(self, member_id):
1172
1179
  """
etcd3/shared.py CHANGED
@@ -9,7 +9,16 @@ import functools
9
9
  import grpc
10
10
  import etcdrpc
11
11
 
12
- import etcd3.exceptions as exceptions
12
+ # For backward compatibility, expose error handler factories
13
+ from etcd3.errors import (
14
+ _make_sync_handle_errors_with_tracing,
15
+ _make_sync_handle_generator_errors_with_tracing,
16
+ _make_async_handle_errors_with_tracing as _make_async_handle_errors_with_tracing,
17
+ _make_async_handle_generator_errors_with_tracing as _make_async_handle_generator_errors_with_tracing,
18
+ _EXCEPTIONS_BY_CODE,
19
+ _FAILED_EP_CODES,
20
+ )
21
+
13
22
  import etcd3.transactions as transactions
14
23
  import etcd3.utils as utils
15
24
 
@@ -299,16 +308,6 @@ class RequestBuilder:
299
308
  return delete_request
300
309
 
301
310
 
302
- # For backward compatibility, expose error handler factories
303
- from etcd3.errors import (
304
- _make_sync_handle_errors_with_tracing,
305
- _make_sync_handle_generator_errors_with_tracing,
306
- _make_async_handle_errors_with_tracing,
307
- _make_async_handle_generator_errors_with_tracing,
308
- _EXCEPTIONS_BY_CODE,
309
- _FAILED_EP_CODES,
310
- )
311
-
312
311
  # Re-export for compatibility
313
312
  EXCEPTIONS_BY_CODE = _EXCEPTIONS_BY_CODE
314
313
  FAILED_EP_CODES = _FAILED_EP_CODES
@@ -324,7 +323,10 @@ def _init_sync_error_handlers(error_handler_func):
324
323
 
325
324
  This must be called after client module is fully loaded.
326
325
  """
327
- global _sync_handle_errors, _sync_handle_generator_errors, _sync_handle_keepalive_errors
326
+ global \
327
+ _sync_handle_errors, \
328
+ _sync_handle_generator_errors, \
329
+ _sync_handle_keepalive_errors
328
330
  _sync_handle_errors = _make_sync_handle_errors_with_tracing(
329
331
  error_handler_func, "etcd3.operation"
330
332
  )
@@ -43,14 +43,19 @@ class SyncClusterOperations:
43
43
  return self.client.metadata
44
44
 
45
45
  @_sync_handle_errors
46
- def add_member(self, urls):
46
+ def add_member(self, urls, is_learner=False):
47
47
  """
48
48
  Add a member into the cluster.
49
49
 
50
+ :param urls: peer URLs the member will advertise to the cluster
51
+ :param is_learner: add as a non-voting learner, which does not
52
+ affect quorum on an existing cluster
50
53
  :returns: new member
51
54
  :rtype: :class:`.Member`
52
55
  """
53
- member_add_request = etcdrpc.MemberAddRequest(peerURLs=urls)
56
+ member_add_request = etcdrpc.MemberAddRequest(
57
+ peerURLs=urls, isLearner=is_learner
58
+ )
54
59
 
55
60
  # Create cluster stub directly
56
61
  cluster_stub = etcdrpc.ClusterStub(self.channel)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-etcd3-async
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: etcd3 async sdk
5
5
  Requires-Python: <4,>=3.12
6
6
  Requires-Dist: grpcio==1.76.0
@@ -1,18 +1,18 @@
1
- etcd3/__init__.py,sha256=gK79kTpBAMmbuVRuXc3kxxBjbjhNPTpqdaphqUwSals,3353
1
+ etcd3/__init__.py,sha256=SB6RBboBlNB86OdfisuMJIAgdpq87uez2JrUGfhLL4E,3353
2
2
  etcd3/async_client.py,sha256=r10DHsJP6rWD55N5iq28ONRcU3VgWQozTJiLbFnKUEs,1499
3
- etcd3/async_client_extensions.py,sha256=umle1miB5KgsKWrShdhw1nI-hXADOGju1_l2rXf2N1E,34184
3
+ etcd3/async_client_extensions.py,sha256=-L6FYftLfwzxWNKMZZCgu0SILBLLtSwsLUZ8Np8ixOY,34214
4
4
  etcd3/async_config.py,sha256=tjGR_SP25uYpC0wI9LoHxKgPuL7yaSHWtUQrcPrx0uQ,3530
5
5
  etcd3/async_endpoint.py,sha256=pXlMaV7acNQPFohYV3eCWfVTDSgvZlbwy42z0tx3rm4,4595
6
- etcd3/async_managers.py,sha256=WJkLaD38N60b0eqWQTlUQ-jX1NDyMqSrYxHO6NtItV8,18122
6
+ etcd3/async_managers.py,sha256=qEDED9SbYy8xrMn180fK3FdlVzJnqV_Rs_5wvDLYnYA,18391
7
7
  etcd3/async_monitor.py,sha256=iZPac3Pqn5bI0ONguJJctQsi4b2ft4hWPknug6jj5RY,453
8
- etcd3/async_multi_client.py,sha256=urqRbf-lQ2AAljiAFP5Y6PzsJLHWXF9UTCZBiD2l2rU,45205
8
+ etcd3/async_multi_client.py,sha256=N_FbfIHxCIKltdcJx7P0n14bLVwtdvZnpYl8jWk31yQ,46709
9
9
  etcd3/async_single_client.py,sha256=VO-ct-x6jPnqf2tPUlMYeitftxvgJTrM5KzyV7WaRhI,4506
10
10
  etcd3/async_watch.py,sha256=m6I2BAoI54J-ZPVVMxao6XJQXvVyQ88Hp2QeauD9z20,15962
11
11
  etcd3/auth.py,sha256=gWd6Cl2CMe4P6i4SsLQ_9UMv9bNSCs_QAMqyOVfKQ3k,21896
12
12
  etcd3/client.py,sha256=KXC7IwVOQlgky5GBg2Qmf-pNBwHlgR27rzKe-7tBSD4,1332
13
13
  etcd3/config.py,sha256=3Jm6Nbpdp-hiToR_FtM6cgktV5vB_5Da3S-aENtAtYU,6069
14
14
  etcd3/endpoint.py,sha256=aSMBmEitAADW0ABAZhc8L7jPYsQo8amEWKv6LJbsmC4,2589
15
- etcd3/errors.py,sha256=abSlnMmq5pG_7tJZK9hK1odR9O-7KPPjv1YE2OGccYM,6760
15
+ etcd3/errors.py,sha256=kCy_vXTC8pInVv4UZqyGgc6LLcEevw1pWMlapWZCr4I,9084
16
16
  etcd3/events.py,sha256=TBWIiw6c_PcdAaBv_-jEqbOZNWTzTkZfDbHrYCkR9n0,903
17
17
  etcd3/exceptions.py,sha256=mkCdJAyWIJiVwHnT7PvDQ8aJTN3d8mW4KXs_V91Lr74,1093
18
18
  etcd3/leases.py,sha256=pkJVfYWCZ_uBXSjla5TXFfNf_D6yoTJnkntefOaPRrk,2788
@@ -21,11 +21,11 @@ etcd3/log.py,sha256=sMo6av48x9Nf_YDIWdSv8R-VUeVXi1n17wGDAAaFnlM,4282
21
21
  etcd3/log_formatters.py,sha256=weWgMQjuI_cZFBEUORGCgkFJQIPvDUaiCLJN64t7s9o,7304
22
22
  etcd3/log_sampling.py,sha256=7PhNz-HOZ6sHdBiXS29YJ3baHY_L1VwJaq4NZt2yX78,6692
23
23
  etcd3/members.py,sha256=4t71YSzPfoEzyNMIRGFuJQXoydfHUwlk_JDxTM8moeg,1325
24
- etcd3/multi_client.py,sha256=Y6JoWZeUfn8JyezFtI0eqJ9Zr0kMw9BZVbzFy8AEizw,44208
24
+ etcd3/multi_client.py,sha256=FiI85nMgc2Atni6GgEPW67kXVtjllS6c6W6uRf8HRck,44550
25
25
  etcd3/observability.py,sha256=BLOUMs1H2icF-tEWB_RoZFKoCNoXZt1ZZERGfckesW4,14215
26
- etcd3/shared.py,sha256=tGCM8NTGQoTtOUBk6w4ve3bymcsn9VSr0YsBiBERXKA,10913
26
+ etcd3/shared.py,sha256=RGzbc7SNprvC-QFODVfP9HR2hD1oAc9tVaNwVQ6GGmI,10999
27
27
  etcd3/single_client.py,sha256=sG3tna7i_1y5u_XuwvsH0BeMQIj6uaKL5XgOYRpratI,5014
28
- etcd3/sync_cluster_operations.py,sha256=sCI1ntT4LXKhq-lMw_HX0zdpAyaaLzSFjwZMcM9OSjo,4050
28
+ etcd3/sync_cluster_operations.py,sha256=YgJGjqdSEhzp-0A1QIwXJtZNw-IbjDnxTDCiZKJydIE,4319
29
29
  etcd3/sync_kv_operations.py,sha256=yMeq6sLvcY-DbMIZGyzDxOyU_4mT5FEse7rGaJJUqA8,22150
30
30
  etcd3/sync_lock_operations.py,sha256=XkezEM0lDGKhj3BPiMtmfFteWatm-XbZewAtjcreuwM,1091
31
31
  etcd3/sync_maintenance_operations.py,sha256=oDXRfrMsTYvpMKbvD0K1k1nwgzyYcnjmqO8jZ3QZfUI,8397
@@ -49,6 +49,6 @@ etcdrpc/rpc.proto,sha256=UD4vXbdPUNHSLSx7K_UltWqJjWswHBKSs0t9aNMA4qg,31351
49
49
  etcdrpc/rpc_pb2.py,sha256=8qhnULNr8jym0qyivDVqJB86H_u1Rb_K8iY8FNT9e0s,30252
50
50
  etcdrpc/rpc_pb2.pyi,sha256=TfPtHXIluO_D1rchVMVfoSgZTPQLk-YNhmcJBIt9XIk,35473
51
51
  etcdrpc/rpc_pb2_grpc.py,sha256=HOifyKL0X3BmLkS1r0tWTMfyfChBiy76O6Ys12kT5n8,73810
52
- python_etcd3_async-0.1.5.dist-info/METADATA,sha256=JvloI7KUXRtpZrxkwbDvIHnYd-gpvGovMnf8s9JwYjE,8723
53
- python_etcd3_async-0.1.5.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
54
- python_etcd3_async-0.1.5.dist-info/RECORD,,
52
+ python_etcd3_async-0.1.6.dist-info/METADATA,sha256=WBABRd9BOnhf7XzvTGViH3mGsym5srfDLgXo5cN9aWg,8723
53
+ python_etcd3_async-0.1.6.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
54
+ python_etcd3_async-0.1.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any