python-etcd3-async 0.1.0__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.
@@ -0,0 +1,140 @@
1
+ """Async endpoint implementation for etcd3 client.
2
+
3
+ This module provides the AsyncEndpoint class for managing etcd cluster endpoints
4
+ in async operations.
5
+ """
6
+
7
+ import asyncio
8
+
9
+ import grpc
10
+
11
+ from etcd3.endpoint import BaseEndpoint
12
+
13
+
14
+ class AsyncEndpointConfig:
15
+ """Configuration for AsyncEndpoint creation.
16
+
17
+ :param str host: Endpoint host
18
+ :param int port: Endpoint port
19
+ :param bool secure: Use secure channel, default True
20
+ :param creds: Credentials to use for secure channel, required if
21
+ secure=True
22
+ :type creds: grpc.ChannelCredentials, optional
23
+ :param time_retry: Seconds to wait before retrying this endpoint after
24
+ failure, default 300.0
25
+ :type time_retry: int or float
26
+ :param opts: Additional gRPC options
27
+ :type opts: dict, optional
28
+ """
29
+
30
+ def __init__(
31
+ self, host, port, secure=True, creds=None, time_retry=300.0, opts=None
32
+ ):
33
+ self.host = host
34
+ self.port = port
35
+ self.secure = secure
36
+ self.creds = creds
37
+ self.time_retry = time_retry
38
+ self.opts = opts
39
+
40
+
41
+ class AsyncEndpoint(BaseEndpoint):
42
+ """Represents an etcd cluster endpoint for async operations.
43
+
44
+ :param config: AsyncEndpointConfig object with endpoint configuration
45
+ :param str host: Endpoint host (ignored if config is provided)
46
+ :param int port: Endpoint port (ignored if config is provided)
47
+ :param bool secure: Use secure channel, default True (ignored if config is provided)
48
+ :param creds: Credentials to use for secure channel, required if
49
+ secure=True (ignored if config is provided)
50
+ :type creds: grpc.ChannelCredentials, optional
51
+ :param time_retry: Seconds to wait before retrying this endpoint after
52
+ failure, default 300.0 (ignored if config is provided)
53
+ :type time_retry: int or float
54
+ :param opts: Additional gRPC options (ignored if config is provided)
55
+ :type opts: dict, optional
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ config=None,
61
+ host=None,
62
+ port=None,
63
+ secure=True,
64
+ creds=None,
65
+ time_retry=300.0,
66
+ opts=None,
67
+ ):
68
+ # Use config object if provided, otherwise use individual parameters
69
+ if config is not None:
70
+ host = config.host
71
+ port = config.port
72
+ secure = config.secure
73
+ creds = config.creds
74
+ time_retry = config.time_retry
75
+ opts = config.opts
76
+
77
+ if host is None or port is None:
78
+ raise ValueError("Host and port must be provided")
79
+
80
+ super().__init__(host, port, secure, creds, time_retry, opts)
81
+ self.channel = self._mkchannel(opts)
82
+
83
+ def is_healthy(self):
84
+ """Check if the endpoint is healthy.
85
+
86
+ Only READY state is considered healthy. CONNECTING state is excluded
87
+ to ensure the connection is fully established before use.
88
+
89
+ :returns: True if the endpoint is healthy (READY state)
90
+ :rtype: bool
91
+ """
92
+ try:
93
+ state = self.channel.get_state()
94
+ # Only READY is considered healthy, not CONNECTING
95
+ return state == grpc.ChannelConnectivity.READY
96
+ except Exception:
97
+ return False
98
+
99
+ async def health_check_async(self):
100
+ """Perform an asynchronous health check using gRPC status check.
101
+
102
+ This method uses the channel's await_ready() for a more robust
103
+ connectivity check.
104
+
105
+ :returns: True if the endpoint is healthy
106
+ :rtype: bool
107
+ """
108
+ try:
109
+ await asyncio.wait_for(self.channel.healthy(), timeout=2.0)
110
+ return True
111
+ except asyncio.TimeoutError:
112
+ return False
113
+ except Exception:
114
+ return False
115
+
116
+ async def rebuild_channel(self):
117
+ """Rebuild the gRPC channel.
118
+
119
+ This method closes the existing channel and creates a new one,
120
+ which is useful when the connection has become stale (e.g., after
121
+ etcd server restart).
122
+ """
123
+ try:
124
+ await self.channel.close()
125
+ except Exception:
126
+ pass # Ignore close errors
127
+
128
+ self.channel = self._mkchannel(self.opts)
129
+
130
+ async def close(self):
131
+ """Close the gRPC channel."""
132
+ try:
133
+ await self.channel.close()
134
+ except Exception:
135
+ pass # Ignore close errors
136
+
137
+ def _mkchannel(self, opts):
138
+ if self.secure:
139
+ return grpc.aio.secure_channel(self.netloc, self.credentials, options=opts)
140
+ return grpc.aio.insecure_channel(self.netloc, options=opts)
@@ -0,0 +1,498 @@
1
+ """Manager classes for etcd3 async client.
2
+
3
+ This module provides manager classes to handle specific functionality
4
+ for the etcd3 async client, reducing the size of the main client class.
5
+ """
6
+
7
+ import asyncio
8
+
9
+ import grpc
10
+ import etcdrpc
11
+
12
+ from etcd3 import utils
13
+ from etcd3.members import Member
14
+ from etcd3.client import Status, Alarm
15
+ from etcd3.async_watch import AsyncWatcher
16
+
17
+
18
+ class AsyncWatchManager:
19
+ """Manager for watch-related operations.
20
+
21
+ :param etcd_client: The etcd client instance
22
+ :type etcd_client: AsyncMultiEndpointEtcd3Client
23
+ """
24
+
25
+ def __init__(self, etcd_client):
26
+ self.etcd_client = etcd_client
27
+ self._watcher = None
28
+
29
+ def _get_watcher(self):
30
+ """Get or create a watcher instance."""
31
+ if self._watcher is None:
32
+ watchstub = etcdrpc.WatchStub(self.etcd_client.channel)
33
+ self._watcher = AsyncWatcher(
34
+ watchstub,
35
+ timeout=self.etcd_client.timeout,
36
+ call_credentials=self.etcd_client.call_credentials,
37
+ metadata=self.etcd_client.metadata,
38
+ )
39
+ return self._watcher
40
+
41
+ async def add_watch_callback(self, *args, **kwargs):
42
+ """
43
+ Watch a key or range of keys and call a callback on every response.
44
+
45
+ If timeout was declared during the client initialization and
46
+ the watch cannot be created during that time the method raises
47
+ a ``WatchTimedOut`` exception.
48
+
49
+ :param key: key to watch
50
+ :param callback: callback function
51
+
52
+ :returns: watch_id. Later it could be used for cancelling watch.
53
+ """
54
+ watcher = self._get_watcher()
55
+ return await watcher.add_callback(*args, **kwargs)
56
+
57
+ async def add_watch_prefix_callback(self, key_prefix, callback, **kwargs):
58
+ """
59
+ Watch a prefix and call a callback on every response.
60
+
61
+ If timeout was declared during the client initialization and
62
+ the watch cannot be created during that time the method raises
63
+ a ``WatchTimedOut`` exception.
64
+
65
+ :param key_prefix: prefix to watch
66
+ :param callback: callback function
67
+
68
+ :returns: watch_id. Later it could be used for cancelling watch.
69
+ """
70
+ kwargs["range_end"] = utils.prefix_range_end(utils.to_bytes(key_prefix))
71
+ return await self.add_watch_callback(key_prefix, callback, **kwargs)
72
+
73
+ async def watch_response(self, key, **kwargs):
74
+ """
75
+ Watch a key.
76
+
77
+ Example usage:
78
+
79
+ .. code-block:: python
80
+ responses_iterator, cancel = await etcd.watch_response('/doot/key')
81
+ async for response in responses_iterator:
82
+ print(response)
83
+
84
+ :param key: key to watch
85
+
86
+ :returns: tuple of ``responses_iterator`` and ``cancel``.
87
+ Use ``responses_iterator`` to get the watch responses,
88
+ each of which contains a header and a list of events.
89
+ Use ``cancel`` to cancel the watch request.
90
+ """
91
+ watcher = self._get_watcher()
92
+ event_iter = watcher.watch(key, **kwargs)
93
+
94
+ cancel_event = asyncio.Event()
95
+
96
+ async def cancel():
97
+ cancel_event.set()
98
+ await watcher.cancel(0)
99
+
100
+ async def event_iterator():
101
+ try:
102
+ async for event in event_iter:
103
+ if cancel_event.is_set():
104
+ break
105
+ yield event
106
+ except grpc.RpcError as exc:
107
+ if not cancel_event.is_set():
108
+ self.etcd_client._manage_grpc_errors(exc)
109
+
110
+ return event_iterator(), cancel
111
+
112
+ async def watch(self, key, **kwargs):
113
+ """
114
+ Watch a key.
115
+
116
+ Example usage:
117
+
118
+ .. code-block:: python
119
+ events_iterator, cancel = await etcd.watch('/doot/key')
120
+ async for event in events_iterator:
121
+ print(event)
122
+
123
+ :param key: key to watch
124
+
125
+ :returns: tuple of ``events_iterator`` and ``cancel``.
126
+ Use ``events_iterator`` to get the events of key changes
127
+ and ``cancel`` to cancel the watch request.
128
+ """
129
+ response_iterator, cancel = await self.watch_response(key, **kwargs)
130
+ return utils.async_response_to_event_iterator(response_iterator), cancel
131
+
132
+ async def watch_prefix_response(self, key_prefix, **kwargs):
133
+ """
134
+ Watch a range of keys with a prefix.
135
+
136
+ :param key_prefix: prefix to watch
137
+
138
+ :returns: tuple of ``responses_iterator`` and ``cancel``.
139
+ """
140
+ kwargs["range_end"] = utils.prefix_range_end(utils.to_bytes(key_prefix))
141
+ return await self.watch_response(key_prefix, **kwargs)
142
+
143
+ async def watch_prefix(self, key_prefix, **kwargs):
144
+ """
145
+ Watch a range of keys with a prefix.
146
+
147
+ :param key_prefix: prefix to watch
148
+
149
+ :returns: tuple of ``events_iterator`` and ``cancel``.
150
+ """
151
+ kwargs["range_end"] = utils.prefix_range_end(utils.to_bytes(key_prefix))
152
+ return await self.watch(key_prefix, **kwargs)
153
+
154
+ async def watch_once_response(self, key, timeout=None, **kwargs):
155
+ """
156
+ Watch a key and stop after the first response.
157
+
158
+ If the timeout was specified and response didn't arrive method
159
+ will raise ``WatchTimedOut`` exception.
160
+
161
+ :param key: key to watch
162
+ :param timeout: (optional) timeout in seconds.
163
+
164
+ :returns: ``WatchResponse``
165
+ """
166
+ watcher = self._get_watcher()
167
+ return await watcher.watch_once(key, timeout=timeout, **kwargs)
168
+
169
+ async def watch_once(self, key, timeout=None, **kwargs):
170
+ """
171
+ Watch a key and stop after the first event.
172
+
173
+ If the timeout was specified and event didn't arrive method
174
+ will raise ``WatchTimedOut`` exception.
175
+
176
+ :param key: key to watch
177
+ :param timeout: (optional) timeout in seconds.
178
+
179
+ :returns: ``Event``
180
+ """
181
+ response = await self.watch_once_response(key, timeout=timeout, **kwargs)
182
+ if not response.events:
183
+ raise IndexError("No events in watch response")
184
+ return response.events[0]
185
+
186
+ async def watch_prefix_once_response(self, key_prefix, timeout=None, **kwargs):
187
+ """
188
+ Watch a range of keys with a prefix and stop after the first response.
189
+
190
+ If the timeout was specified and response didn't arrive method
191
+ will raise ``WatchTimedOut`` exception.
192
+ """
193
+ kwargs["range_end"] = utils.prefix_range_end(utils.to_bytes(key_prefix))
194
+ return await self.watch_once_response(key_prefix, timeout=timeout, **kwargs)
195
+
196
+ async def watch_prefix_once(self, key_prefix, timeout=None, **kwargs):
197
+ """
198
+ Watch a range of keys with a prefix and stop after the first event.
199
+
200
+ If the timeout was specified and event didn't arrive method
201
+ will raise ``WatchTimedOut`` exception.
202
+ """
203
+ kwargs["range_end"] = utils.prefix_range_end(utils.to_bytes(key_prefix))
204
+ return await self.watch_once(key_prefix, timeout=timeout, **kwargs)
205
+
206
+ async def cancel_watch(self, watch_id):
207
+ """
208
+ Stop watching a key or range of keys.
209
+
210
+ :param watch_id: watch_id returned by ``add_watch_callback`` method
211
+ """
212
+ watcher = self._get_watcher()
213
+ await watcher.cancel(watch_id)
214
+
215
+
216
+ class AsyncClusterManager:
217
+ """Manager for cluster-related operations.
218
+
219
+ :param etcd_client: The etcd client instance
220
+ :type etcd_client: AsyncMultiEndpointEtcd3Client
221
+ """
222
+
223
+ def __init__(self, etcd_client):
224
+ self.etcd_client = etcd_client
225
+
226
+ async def add_member(self, urls):
227
+ """
228
+ Add a member into the cluster.
229
+
230
+ :returns: new member
231
+ :rtype: :class:`.Member`
232
+ """
233
+ member_add_request = etcdrpc.MemberAddRequest(peerURLs=urls)
234
+
235
+ # Create cluster stub directly
236
+ cluster_stub = etcdrpc.ClusterStub(self.etcd_client.channel)
237
+ member_add_response = await cluster_stub.MemberAdd(
238
+ member_add_request,
239
+ timeout=self.etcd_client.timeout,
240
+ credentials=self.etcd_client.call_credentials,
241
+ metadata=self.etcd_client.metadata,
242
+ )
243
+
244
+ member = member_add_response.member
245
+ return Member(
246
+ member.ID,
247
+ member.name,
248
+ member.peerURLs,
249
+ member.clientURLs,
250
+ etcd_client=self.etcd_client,
251
+ )
252
+
253
+ async def remove_member(self, member_id):
254
+ """
255
+ Remove an existing member from the cluster.
256
+
257
+ :param member_id: ID of the member to remove
258
+ """
259
+ member_rm_request = etcdrpc.MemberRemoveRequest(ID=member_id)
260
+ # Create cluster stub directly
261
+ cluster_stub = etcdrpc.ClusterStub(self.etcd_client.channel)
262
+ await cluster_stub.MemberRemove(
263
+ member_rm_request,
264
+ timeout=self.etcd_client.timeout,
265
+ credentials=self.etcd_client.call_credentials,
266
+ metadata=self.etcd_client.metadata,
267
+ )
268
+
269
+ async def update_member(self, member_id, peer_urls):
270
+ """
271
+ Update the configuration of an existing member in the cluster.
272
+
273
+ :param member_id: ID of the member to update
274
+ :param peer_urls: new list of peer urls the member will use to
275
+ communicate with the cluster
276
+ """
277
+ member_update_request = etcdrpc.MemberUpdateRequest(
278
+ ID=member_id, peerURLs=peer_urls
279
+ )
280
+ # Create cluster stub directly
281
+ cluster_stub = etcdrpc.ClusterStub(self.etcd_client.channel)
282
+ await cluster_stub.MemberUpdate(
283
+ member_update_request,
284
+ timeout=self.etcd_client.timeout,
285
+ credentials=self.etcd_client.call_credentials,
286
+ metadata=self.etcd_client.metadata,
287
+ )
288
+
289
+ async def members(self):
290
+ """
291
+ List of all members associated with the cluster.
292
+
293
+ :type: sequence of :class:`.Member`
294
+
295
+ """
296
+ member_list_request = etcdrpc.MemberListRequest()
297
+ # Create cluster stub directly
298
+ cluster_stub = etcdrpc.ClusterStub(self.etcd_client.channel)
299
+ member_list_response = await cluster_stub.MemberList(
300
+ member_list_request,
301
+ timeout=self.etcd_client.timeout,
302
+ credentials=self.etcd_client.call_credentials,
303
+ metadata=self.etcd_client.metadata,
304
+ )
305
+
306
+ for member in member_list_response.members:
307
+ yield Member(
308
+ member.ID,
309
+ member.name,
310
+ member.peerURLs,
311
+ member.clientURLs,
312
+ etcd_client=self.etcd_client,
313
+ )
314
+
315
+
316
+ class AsyncMaintenanceManager:
317
+ """Manager for maintenance-related operations.
318
+
319
+ :param etcd_client: The etcd client instance
320
+ :type etcd_client: AsyncMultiEndpointEtcd3Client
321
+ """
322
+
323
+ def __init__(self, etcd_client):
324
+ self.etcd_client = etcd_client
325
+
326
+ async def status(self):
327
+ """Get the status of the responding member."""
328
+ status_request = etcdrpc.StatusRequest()
329
+ # Create maintenance stub directly
330
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
331
+ status_response = await maintenance_stub.Status(
332
+ status_request,
333
+ timeout=self.etcd_client.timeout,
334
+ credentials=self.etcd_client.call_credentials,
335
+ metadata=self.etcd_client.metadata,
336
+ )
337
+
338
+ members_list = []
339
+ async for member in self.etcd_client.cluster_manager.members():
340
+ members_list.append(member)
341
+
342
+ for m in members_list:
343
+ if m.id == status_response.leader:
344
+ leader = m
345
+ break
346
+ else:
347
+ leader = None
348
+
349
+ return Status(
350
+ status_response.version,
351
+ status_response.dbSize,
352
+ leader,
353
+ status_response.raftIndex,
354
+ status_response.raftTerm,
355
+ )
356
+
357
+ async def defragment(self):
358
+ """Defragment a member's backend database to recover storage space."""
359
+ defrag_request = etcdrpc.DefragmentRequest()
360
+ # Create maintenance stub directly
361
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
362
+ await maintenance_stub.Defragment(
363
+ defrag_request,
364
+ timeout=self.etcd_client.timeout,
365
+ credentials=self.etcd_client.call_credentials,
366
+ metadata=self.etcd_client.metadata,
367
+ )
368
+
369
+ async def hash(self):
370
+ """
371
+ Return the hash of the local KV state.
372
+
373
+ :returns: kv state hash
374
+ :rtype: int
375
+ """
376
+ hash_request = etcdrpc.HashRequest()
377
+ # Create maintenance stub directly
378
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
379
+ hash_response = await maintenance_stub.Hash(
380
+ hash_request,
381
+ timeout=self.etcd_client.timeout,
382
+ credentials=self.etcd_client.call_credentials,
383
+ metadata=self.etcd_client.metadata,
384
+ )
385
+ return hash_response.hash
386
+
387
+ async def create_alarm(self, member_id=0):
388
+ """Create an alarm.
389
+
390
+ If no member id is given, the alarm is activated for all the
391
+ members of the cluster. Only the `no space` alarm can be raised.
392
+
393
+ :param member_id: The cluster member id to create an alarm to.
394
+ If 0, the alarm is created for all the members
395
+ of the cluster.
396
+ :returns: list of :class:`.Alarm`
397
+ """
398
+ alarm_request = self._build_alarm_request("activate", member_id, "no space")
399
+ # Create maintenance stub directly
400
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
401
+ alarm_response = await maintenance_stub.Alarm(
402
+ alarm_request,
403
+ timeout=self.etcd_client.timeout,
404
+ credentials=self.etcd_client.call_credentials,
405
+ metadata=self.etcd_client.metadata,
406
+ )
407
+
408
+ return [Alarm(alarm.alarm, alarm.memberID) for alarm in alarm_response.alarms]
409
+
410
+ async def list_alarms(self, member_id=0, alarm_type="none"):
411
+ """List the activated alarms.
412
+
413
+ :param member_id:
414
+ :param alarm_type: The cluster member id to create an alarm to.
415
+ If 0, the alarm is created for all the members
416
+ of the cluster.
417
+ :returns: sequence of :class:`.Alarm`
418
+ """
419
+ alarm_request = self._build_alarm_request("get", member_id, alarm_type)
420
+ # Create maintenance stub directly
421
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
422
+ alarm_response = await maintenance_stub.Alarm(
423
+ alarm_request,
424
+ timeout=self.etcd_client.timeout,
425
+ credentials=self.etcd_client.call_credentials,
426
+ metadata=self.etcd_client.metadata,
427
+ )
428
+
429
+ for alarm in alarm_response.alarms:
430
+ yield Alarm(alarm.alarm, alarm.memberID)
431
+
432
+ def _build_alarm_request(self, alarm_action, member_id, alarm_type):
433
+ """Build an alarm request.
434
+
435
+ :param alarm_action: The alarm action ('get', 'activate', 'deactivate')
436
+ :param member_id: The member ID
437
+ :param alarm_type: The alarm type
438
+ :return: AlarmRequest instance
439
+ """
440
+ alarm_request = etcdrpc.AlarmRequest()
441
+
442
+ if alarm_action == "get":
443
+ alarm_request.action = etcdrpc.AlarmRequest.GET
444
+ elif alarm_action == "activate":
445
+ alarm_request.action = etcdrpc.AlarmRequest.ACTIVATE
446
+ elif alarm_action == "deactivate":
447
+ alarm_request.action = etcdrpc.AlarmRequest.DEACTIVATE
448
+ else:
449
+ raise ValueError(f"Unknown alarm action: {alarm_action}")
450
+
451
+ alarm_request.memberID = member_id
452
+
453
+ if alarm_type == "none":
454
+ alarm_request.alarm = etcdrpc.NONE
455
+ elif alarm_type == "no space":
456
+ alarm_request.alarm = etcdrpc.NOSPACE
457
+ else:
458
+ raise ValueError(f"Unknown alarm type: {alarm_type}")
459
+
460
+ return alarm_request
461
+
462
+ async def disarm_alarm(self, member_id=0):
463
+ """Cancel an alarm.
464
+
465
+ :param member_id: The cluster member id to cancel an alarm.
466
+ If 0, the alarm is canceled for all the members
467
+ of the cluster.
468
+ :returns: List of :class:`.Alarm`
469
+ """
470
+ alarm_request = self._build_alarm_request("deactivate", member_id, "no space")
471
+ # Create maintenance stub directly
472
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
473
+ alarm_response = await maintenance_stub.Alarm(
474
+ alarm_request,
475
+ timeout=self.etcd_client.timeout,
476
+ credentials=self.etcd_client.call_credentials,
477
+ metadata=self.etcd_client.metadata,
478
+ )
479
+
480
+ return [Alarm(alarm.alarm, alarm.memberID) for alarm in alarm_response.alarms]
481
+
482
+ async def snapshot(self, file_obj):
483
+ """Take a snapshot of the database.
484
+
485
+ :param file_obj: A file-like object to write the database contents in.
486
+ """
487
+ snapshot_request = etcdrpc.SnapshotRequest()
488
+ # Create maintenance stub directly
489
+ maintenance_stub = etcdrpc.MaintenanceStub(self.etcd_client.channel)
490
+ snapshot_response = maintenance_stub.Snapshot(
491
+ snapshot_request,
492
+ timeout=self.etcd_client.timeout,
493
+ credentials=self.etcd_client.call_credentials,
494
+ metadata=self.etcd_client.metadata,
495
+ )
496
+
497
+ async for response in snapshot_response:
498
+ await file_obj.write(response.blob)
etcd3/async_monitor.py ADDED
@@ -0,0 +1,16 @@
1
+ """Monitoring utilities for etcd3 async client.
2
+
3
+ .. deprecated::
4
+ Use etcd3.exporters.prometheus instead for metrics collection.
5
+
6
+ This module provides monitoring functionality for the etcd3 async client,
7
+ including metrics collection and logging utilities.
8
+ """
9
+
10
+ # Re-export from prometheus exporter
11
+ from etcd3.exporters.prometheus import (
12
+ get_metrics_interface as get_monitor,
13
+ monitor_request,
14
+ )
15
+
16
+ __all__ = ["get_monitor", "monitor_request"]