python-etcd3-async 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -67,32 +67,32 @@ def _manage_client_errors(self, exc):
67
67
 
68
68
  def _set_span_attributes_from_response(response, endpoint: str = None):
69
69
  """Set span attributes from etcd response.
70
-
70
+
71
71
  :param response: etcd response object
72
72
  :param endpoint: Optional endpoint string
73
73
  """
74
74
  if not is_tracing_available():
75
75
  return
76
-
76
+
77
77
  span = get_current_span()
78
78
  if span is None:
79
79
  return
80
-
80
+
81
81
  # Build attributes from response
82
- attrs = build_response_attributes(header=getattr(response, 'header', None))
83
-
82
+ attrs = build_response_attributes(header=getattr(response, "header", None))
83
+
84
84
  # Add count if available
85
- if hasattr(response, 'count'):
85
+ if hasattr(response, "count"):
86
86
  attrs[TraceAttributes.ETCD_COUNT] = response.count
87
-
87
+
88
88
  # Add deleted if available
89
- if hasattr(response, 'deleted'):
89
+ if hasattr(response, "deleted"):
90
90
  attrs[TraceAttributes.ETCD_DELETED] = response.deleted
91
-
91
+
92
92
  # Add endpoint if provided
93
93
  if endpoint:
94
94
  attrs[TraceAttributes.ETCD_ENDPOINT] = endpoint
95
-
95
+
96
96
  # Set attributes on span
97
97
  for key, value in attrs.items():
98
98
  if value is not None:
@@ -147,11 +147,13 @@ class AsyncKVOperations:
147
147
  credentials=self.client.call_credentials,
148
148
  metadata=self.client.metadata,
149
149
  )
150
-
150
+
151
151
  # Set trace attributes from response
152
- endpoint = str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
152
+ endpoint = (
153
+ str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
154
+ )
153
155
  _set_span_attributes_from_response(response, endpoint)
154
-
156
+
155
157
  return response
156
158
 
157
159
  async def get(self, key, **kwargs):
@@ -259,9 +261,11 @@ class AsyncKVOperations:
259
261
  credentials=self.client.call_credentials,
260
262
  metadata=self.client.metadata,
261
263
  )
262
-
264
+
263
265
  # Set trace attributes from response
264
- endpoint = str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
266
+ endpoint = (
267
+ str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
268
+ )
265
269
  _set_span_attributes_from_response(put_response, endpoint)
266
270
 
267
271
  if prev_kv:
@@ -317,9 +321,11 @@ class AsyncKVOperations:
317
321
  credentials=self.client.call_credentials,
318
322
  metadata=self.client.metadata,
319
323
  )
320
-
324
+
321
325
  # Set trace attributes from response
322
- endpoint = str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
326
+ endpoint = (
327
+ str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
328
+ )
323
329
  _set_span_attributes_from_response(delete_response, endpoint)
324
330
 
325
331
  if return_response:
@@ -391,7 +397,9 @@ class AsyncKVOperations:
391
397
  except Exception as exc:
392
398
  if isinstance(exc, exceptions.BatchOperationError):
393
399
  raise
394
- raise exceptions.BatchOperationError(f"Batch put failed: {exc}") from exc
400
+ raise exceptions.BatchOperationError(
401
+ f"Batch put failed: {exc}"
402
+ ) from exc
395
403
  else:
396
404
  # Non-atomic: parallel execution with concurrency control
397
405
  semaphore = asyncio.Semaphore(concurrency)
@@ -456,7 +464,9 @@ class AsyncKVOperations:
456
464
  except Exception as exc:
457
465
  if isinstance(exc, exceptions.BatchOperationError):
458
466
  raise
459
- raise exceptions.BatchOperationError(f"Batch delete failed: {exc}") from exc
467
+ raise exceptions.BatchOperationError(
468
+ f"Batch delete failed: {exc}"
469
+ ) from exc
460
470
  else:
461
471
  # Non-atomic: parallel execution with concurrency control
462
472
  semaphore = asyncio.Semaphore(concurrency)
@@ -537,21 +547,24 @@ class AsyncLeaseOperations:
537
547
  credentials=self.client.call_credentials,
538
548
  metadata=self.client.metadata,
539
549
  )
540
-
550
+
541
551
  # Set trace attributes for lease
542
552
  if is_tracing_available():
543
553
  span = get_current_span()
544
554
  if span:
545
555
  attrs = build_lease_attributes(
546
- lease_id=lease_grant_response.ID,
547
- ttl=lease_grant_response.TTL
556
+ lease_id=lease_grant_response.ID, ttl=lease_grant_response.TTL
557
+ )
558
+ endpoint = (
559
+ str(self.client.endpoint_in_use)
560
+ if self.client.endpoint_in_use
561
+ else None
548
562
  )
549
- endpoint = str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
550
563
  if endpoint:
551
564
  attrs[TraceAttributes.ETCD_ENDPOINT] = endpoint
552
565
  for key, value in attrs.items():
553
566
  span.set_attribute(key, value)
554
-
567
+
555
568
  return AsyncLease(
556
569
  lease_id=lease_grant_response.ID,
557
570
  ttl=lease_grant_response.TTL,
@@ -569,13 +582,17 @@ class AsyncLeaseOperations:
569
582
  credentials=self.client.call_credentials,
570
583
  metadata=self.client.metadata,
571
584
  )
572
-
585
+
573
586
  # Set trace attributes for lease revocation
574
587
  if is_tracing_available():
575
588
  span = get_current_span()
576
589
  if span:
577
590
  attrs = build_lease_attributes(lease_id=lease_id)
578
- endpoint = str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
591
+ endpoint = (
592
+ str(self.client.endpoint_in_use)
593
+ if self.client.endpoint_in_use
594
+ else None
595
+ )
579
596
  if endpoint:
580
597
  attrs[TraceAttributes.ETCD_ENDPOINT] = endpoint
581
598
  for key, value in attrs.items():
@@ -646,7 +663,7 @@ class AsyncLeaseOperations:
646
663
  # Exponential backoff with jitter
647
664
  retry_delay = min(
648
665
  base_retry_delay * (2 ** (consecutive_errors - 1)),
649
- max_retry_delay
666
+ max_retry_delay,
650
667
  )
651
668
  jitter = random.uniform(0, retry_delay * 0.1)
652
669
  actual_delay = retry_delay + jitter
@@ -677,7 +694,7 @@ class AsyncLeaseOperations:
677
694
  credentials=self.client.call_credentials,
678
695
  metadata=self.client.metadata,
679
696
  )
680
-
697
+
681
698
  # Set trace attributes for lease info
682
699
  if is_tracing_available():
683
700
  span = get_current_span()
@@ -685,15 +702,19 @@ class AsyncLeaseOperations:
685
702
  attrs = build_lease_attributes(
686
703
  lease_id=lease_id,
687
704
  ttl=lease_timing_response.TTL,
688
- remaining_ttl=getattr(lease_timing_response, 'TTL', None)
705
+ remaining_ttl=getattr(lease_timing_response, "TTL", None),
706
+ )
707
+ endpoint = (
708
+ str(self.client.endpoint_in_use)
709
+ if self.client.endpoint_in_use
710
+ else None
689
711
  )
690
- endpoint = str(self.client.endpoint_in_use) if self.client.endpoint_in_use else None
691
712
  if endpoint:
692
713
  attrs[TraceAttributes.ETCD_ENDPOINT] = endpoint
693
714
  for key, value in attrs.items():
694
715
  if value is not None:
695
716
  span.set_attribute(key, value)
696
-
717
+
697
718
  return lease_timing_response
698
719
 
699
720
 
etcd3/exceptions.py CHANGED
@@ -39,9 +39,11 @@ class NoServerAvailableError(Etcd3Exception):
39
39
 
40
40
  class LockReleaseError(Etcd3Exception):
41
41
  """Raised when a lock release operation fails."""
42
+
42
43
  pass
43
44
 
44
45
 
45
46
  class BatchOperationError(Etcd3Exception):
46
47
  """Raised when a batch operation fails."""
48
+
47
49
  pass
etcd3/locks.py CHANGED
@@ -88,7 +88,9 @@ class Lock(object):
88
88
  success, metadata = self.etcd_client.transaction(
89
89
  compare=[self.etcd_client.transactions.create(self.key) == 0],
90
90
  success=[
91
- self.etcd_client.transactions.put(self.key, self.uuid, lease=self.lease)
91
+ self.etcd_client.transactions.put(
92
+ self.key, self.uuid, lease=self.lease
93
+ )
92
94
  ],
93
95
  failure=[self.etcd_client.transactions.get(self.key)],
94
96
  )
@@ -189,7 +191,7 @@ class Lock(object):
189
191
  )
190
192
 
191
193
  if success:
192
- return 'released'
194
+ return "released"
193
195
 
194
196
  # Transaction succeeded but comparison failed
195
197
  # Check if key exists and who owns it
@@ -200,26 +202,28 @@ class Lock(object):
200
202
  raise exceptions.LockReleaseError(
201
203
  "Lock key does not exist - may have expired or been released"
202
204
  )
203
- return 'not_found'
205
+ return "not_found"
204
206
  if current_value != self.uuid:
205
207
  if raise_error:
206
208
  raise exceptions.LockReleaseError(
207
209
  "Lock is held by another owner - cannot release"
208
210
  )
209
- return 'not_owner'
211
+ return "not_owner"
210
212
 
211
213
  if raise_error:
212
214
  raise exceptions.LockReleaseError("Unknown lock release failure")
213
- return 'error'
215
+ return "error"
214
216
 
215
217
  except exceptions.Etcd3Exception:
216
218
  if raise_error:
217
219
  raise
218
- return 'error'
220
+ return "error"
219
221
  except Exception as exc:
220
222
  if raise_error:
221
- raise exceptions.LockReleaseError(f"Lock release failed: {exc}") from exc
222
- return 'error'
223
+ raise exceptions.LockReleaseError(
224
+ f"Lock release failed: {exc}"
225
+ ) from exc
226
+ return "error"
223
227
 
224
228
  def refresh(self):
225
229
  """Refresh the time to live on this lock."""
@@ -315,22 +319,24 @@ class AsyncLock(object):
315
319
 
316
320
  def _set_lock_span_attributes(self, acquired: bool = None):
317
321
  """Set lock-related attributes on current span.
318
-
322
+
319
323
  :param acquired: Whether the lock was acquired (optional)
320
324
  """
321
325
  if not is_tracing_available():
322
326
  return
323
-
327
+
324
328
  span = get_current_span()
325
329
  if span is None:
326
330
  return
327
-
331
+
328
332
  attrs = build_lock_attributes(
329
- name=self.key if isinstance(self.key, str) else self.key.decode('utf-8', errors='replace'),
333
+ name=self.key
334
+ if isinstance(self.key, str)
335
+ else self.key.decode("utf-8", errors="replace"),
330
336
  uuid=self.uuid,
331
- acquired=acquired
337
+ acquired=acquired,
332
338
  )
333
-
339
+
334
340
  for key, value in attrs.items():
335
341
  if value is not None:
336
342
  span.set_attribute(key, value)
@@ -345,7 +351,9 @@ class AsyncLock(object):
345
351
  success, metadata = await self.etcd_client.transaction(
346
352
  compare=[self.etcd_client.transactions.create(self.key) == 0],
347
353
  success=[
348
- self.etcd_client.transactions.put(self.key, self.uuid, lease=self.lease)
354
+ self.etcd_client.transactions.put(
355
+ self.key, self.uuid, lease=self.lease
356
+ )
349
357
  ],
350
358
  failure=[self.etcd_client.transactions.get(self.key)],
351
359
  )
@@ -450,7 +458,7 @@ class AsyncLock(object):
450
458
 
451
459
  if success:
452
460
  self._set_lock_span_attributes(acquired=False)
453
- return 'released'
461
+ return "released"
454
462
 
455
463
  # Transaction succeeded but comparison failed
456
464
  # Check if key exists and who owns it
@@ -462,29 +470,31 @@ class AsyncLock(object):
462
470
  raise exceptions.LockReleaseError(
463
471
  "Lock key does not exist - may have expired or been released"
464
472
  )
465
- return 'not_found'
473
+ return "not_found"
466
474
  if current_value != self.uuid:
467
475
  self._set_lock_span_attributes(acquired=None) # Owned by another
468
476
  if raise_error:
469
477
  raise exceptions.LockReleaseError(
470
478
  "Lock is held by another owner - cannot release"
471
479
  )
472
- return 'not_owner'
480
+ return "not_owner"
473
481
 
474
482
  self._set_lock_span_attributes(acquired=None)
475
483
  if raise_error:
476
484
  raise exceptions.LockReleaseError("Unknown lock release failure")
477
- return 'error'
485
+ return "error"
478
486
 
479
487
  except exceptions.Etcd3Exception:
480
488
  self._set_lock_span_attributes(acquired=None)
481
489
  if raise_error:
482
490
  raise
483
- return 'error'
491
+ return "error"
484
492
  except Exception as exc:
485
493
  if raise_error:
486
- raise exceptions.LockReleaseError(f"Lock release failed: {exc}") from exc
487
- return 'error'
494
+ raise exceptions.LockReleaseError(
495
+ f"Lock release failed: {exc}"
496
+ ) from exc
497
+ return "error"
488
498
 
489
499
  def refresh(self):
490
500
  """Refresh the time to lease on this lock.
@@ -540,9 +550,7 @@ class AsyncLock(object):
540
550
  refresh_gen = self.refresh()
541
551
  # Get one response from the generator to trigger keepalive
542
552
  try:
543
- await asyncio.wait_for(
544
- refresh_gen.__anext__(), timeout=5.0
545
- )
553
+ await asyncio.wait_for(refresh_gen.__anext__(), timeout=5.0)
546
554
  # Successfully refreshed
547
555
  except StopAsyncIteration:
548
556
  # Generator ended unexpectedly
etcd3/trace_attributes.py CHANGED
@@ -12,35 +12,35 @@ from typing import Any, Dict, Optional
12
12
 
13
13
  class EtcdOperationType(Enum):
14
14
  """etcd operation types for tracing."""
15
-
15
+
16
16
  # KV operations
17
17
  KV_GET = "kv.get"
18
18
  KV_PUT = "kv.put"
19
19
  KV_DELETE = "kv.delete"
20
20
  KV_RANGE = "kv.range"
21
21
  KV_TXN = "kv.transaction"
22
-
22
+
23
23
  # Lease operations
24
24
  LEASE_GRANT = "lease.grant"
25
25
  LEASE_REVOKE = "lease.revoke"
26
26
  LEASE_KEEPALIVE = "lease.keepalive"
27
27
  LEASE_TTL = "lease.ttl"
28
-
28
+
29
29
  # Lock operations
30
30
  LOCK_ACQUIRE = "lock.acquire"
31
31
  LOCK_RELEASE = "lock.release"
32
32
  LOCK_REFRESH = "lock.refresh"
33
-
33
+
34
34
  # Watch operations
35
35
  WATCH_CREATE = "watch.create"
36
36
  WATCH_CANCEL = "watch.cancel"
37
-
37
+
38
38
  # Cluster operations
39
39
  CLUSTER_MEMBER_ADD = "cluster.member.add"
40
40
  CLUSTER_MEMBER_REMOVE = "cluster.member.remove"
41
41
  CLUSTER_MEMBER_UPDATE = "cluster.member.update"
42
42
  CLUSTER_MEMBER_LIST = "cluster.member.list"
43
-
43
+
44
44
  # Maintenance operations
45
45
  MAINTENANCE_STATUS = "maintenance.status"
46
46
  MAINTENANCE_COMPACT = "maintenance.compact"
@@ -50,126 +50,126 @@ class EtcdOperationType(Enum):
50
50
  @dataclass(frozen=True)
51
51
  class TraceAttributes:
52
52
  """Standard attribute names for etcd OpenTelemetry traces.
53
-
53
+
54
54
  These attributes follow OpenTelemetry semantic conventions where applicable
55
55
  and define etcd-specific attributes for comprehensive observability.
56
56
  """
57
-
57
+
58
58
  # === OpenTelemetry Standard Database Attributes ===
59
59
  DB_SYSTEM = "db.system"
60
60
  """Database system: always 'etcd' for this client."""
61
-
61
+
62
62
  DB_OPERATION = "db.operation"
63
63
  """Database operation name (e.g., 'kv.get', 'lease.grant')."""
64
-
64
+
65
65
  DB_STATEMENT = "db.statement"
66
66
  """Database statement (for complex queries, optional)."""
67
-
67
+
68
68
  # === etcd Key-Value Attributes ===
69
69
  ETCD_KEY = "etcd.key"
70
70
  """The key being operated on."""
71
-
71
+
72
72
  ETCD_RANGE_END = "etcd.range_end"
73
73
  """End of the key range for range operations."""
74
-
74
+
75
75
  ETCD_LIMIT = "etcd.limit"
76
76
  """Maximum number of keys to return."""
77
-
77
+
78
78
  ETCD_REVISION = "etcd.revision"
79
79
  """Current revision of the operation result."""
80
-
80
+
81
81
  ETCD_PREV_REVISION = "etcd.prev_revision"
82
82
  """Previous revision (for compare operations)."""
83
-
83
+
84
84
  ETCD_COUNT = "etcd.count"
85
85
  """Number of keys returned or affected."""
86
-
86
+
87
87
  ETCD_DELETED = "etcd.deleted"
88
88
  """Number of keys deleted."""
89
-
89
+
90
90
  # === etcd Cluster Attributes ===
91
91
  ETCD_ENDPOINT = "etcd.endpoint"
92
92
  """The etcd endpoint being accessed."""
93
-
93
+
94
94
  ETCD_CLUSTER_ID = "etcd.cluster_id"
95
95
  """Cluster ID from response header."""
96
-
96
+
97
97
  ETCD_MEMBER_ID = "etcd.member_id"
98
98
  """Member ID from response header."""
99
-
99
+
100
100
  ETCD_RAFT_TERM = "etcd.raft_term"
101
101
  """Raft term from response header."""
102
-
102
+
103
103
  # === etcd Lease Attributes ===
104
104
  ETCD_LEASE_ID = "etcd.lease.id"
105
105
  """Lease ID."""
106
-
106
+
107
107
  ETCD_LEASE_TTL = "etcd.lease.ttl"
108
108
  """Lease TTL in seconds."""
109
-
109
+
110
110
  ETCD_LEASE_REMAINING_TTL = "etcd.lease.remaining_ttl"
111
111
  """Remaining TTL for the lease."""
112
-
112
+
113
113
  # === etcd Lock Attributes ===
114
114
  ETCD_LOCK_NAME = "etcd.lock.name"
115
115
  """Lock name (key path)."""
116
-
116
+
117
117
  ETCD_LOCK_UUID = "etcd.lock.uuid"
118
118
  """Lock UUID (first 8 bytes)."""
119
-
119
+
120
120
  ETCD_LOCK_ACQUIRED = "etcd.lock.acquired"
121
121
  """Whether the lock was successfully acquired."""
122
-
122
+
123
123
  # === etcd Transaction Attributes ===
124
124
  ETCD_TXN_COMPARE_COUNT = "etcd.txn.compare_count"
125
125
  """Number of compare conditions."""
126
-
126
+
127
127
  ETCD_TXN_SUCCESS_COUNT = "etcd.txn.success_count"
128
128
  """Number of success operations."""
129
-
129
+
130
130
  ETCD_TXN_FAILURE_COUNT = "etcd.txn.failure_count"
131
131
  """Number of failure operations."""
132
-
132
+
133
133
  ETCD_TXN_SUCCEEDED = "etcd.txn.succeeded"
134
134
  """Whether the transaction succeeded."""
135
-
135
+
136
136
  # === etcd Batch Attributes ===
137
137
  ETCD_BATCH_SIZE = "etcd.batch.size"
138
138
  """Number of operations in the batch."""
139
-
139
+
140
140
  ETCD_BATCH_ATOMIC = "etcd.batch.atomic"
141
141
  """Whether the batch is atomic."""
142
-
142
+
143
143
  # === etcd Error Attributes ===
144
144
  ETCD_ERROR_CODE = "etcd.error_code"
145
145
  """Error code if operation failed."""
146
-
146
+
147
147
  ETCD_ERROR_MESSAGE = "etcd.error_message"
148
148
  """Error message if operation failed."""
149
-
149
+
150
150
  # === Retry Attributes ===
151
151
  ETCD_RETRY_COUNT = "etcd.retry.count"
152
152
  """Number of retries for this operation."""
153
-
153
+
154
154
  ETCD_RETRY_ENDPOINT = "etcd.retry.endpoint"
155
155
  """Endpoint used after retry."""
156
156
 
157
157
 
158
158
  def _safe_decode(value: Optional[bytes], max_length: int = 256) -> Optional[str]:
159
159
  """Safely decode bytes to string for attribute values.
160
-
160
+
161
161
  Args:
162
162
  value: Bytes value to decode
163
163
  max_length: Maximum length for the decoded string
164
-
164
+
165
165
  Returns:
166
166
  Decoded string or None if input is None
167
167
  """
168
168
  if value is None:
169
169
  return None
170
-
170
+
171
171
  try:
172
- decoded = value.decode('utf-8', errors='replace')
172
+ decoded = value.decode("utf-8", errors="replace")
173
173
  if len(decoded) > max_length:
174
174
  decoded = decoded[:max_length] + "..."
175
175
  return decoded
@@ -182,17 +182,17 @@ def build_kv_attributes(
182
182
  key: Optional[bytes] = None,
183
183
  range_end: Optional[bytes] = None,
184
184
  limit: Optional[int] = None,
185
- **kwargs
185
+ **kwargs,
186
186
  ) -> Dict[str, Any]:
187
187
  """Build trace attributes for KV operations.
188
-
188
+
189
189
  Args:
190
190
  operation: Operation type (e.g., 'kv.get', 'kv.put')
191
191
  key: The key being operated on
192
192
  range_end: End of key range for range operations
193
193
  limit: Maximum number of keys to return
194
194
  **kwargs: Additional attributes
195
-
195
+
196
196
  Returns:
197
197
  Dictionary of trace attributes
198
198
  """
@@ -200,51 +200,48 @@ def build_kv_attributes(
200
200
  TraceAttributes.DB_SYSTEM: "etcd",
201
201
  TraceAttributes.DB_OPERATION: operation,
202
202
  }
203
-
203
+
204
204
  if key is not None:
205
205
  attrs[TraceAttributes.ETCD_KEY] = _safe_decode(key)
206
-
206
+
207
207
  if range_end is not None:
208
208
  attrs[TraceAttributes.ETCD_RANGE_END] = _safe_decode(range_end)
209
-
209
+
210
210
  if limit is not None:
211
211
  attrs[TraceAttributes.ETCD_LIMIT] = limit
212
-
212
+
213
213
  attrs.update(kwargs)
214
214
  return attrs
215
215
 
216
216
 
217
217
  def build_response_attributes(
218
- header=None,
219
- count: Optional[int] = None,
220
- deleted: Optional[int] = None,
221
- **kwargs
218
+ header=None, count: Optional[int] = None, deleted: Optional[int] = None, **kwargs
222
219
  ) -> Dict[str, Any]:
223
220
  """Build trace attributes from etcd response.
224
-
221
+
225
222
  Args:
226
223
  header: Response header from etcd
227
224
  count: Number of keys returned/affected
228
225
  deleted: Number of keys deleted
229
226
  **kwargs: Additional attributes
230
-
227
+
231
228
  Returns:
232
229
  Dictionary of trace attributes
233
230
  """
234
231
  attrs = {}
235
-
232
+
236
233
  if header:
237
234
  attrs[TraceAttributes.ETCD_REVISION] = header.revision
238
235
  attrs[TraceAttributes.ETCD_CLUSTER_ID] = header.cluster_id
239
236
  attrs[TraceAttributes.ETCD_MEMBER_ID] = header.member_id
240
237
  attrs[TraceAttributes.ETCD_RAFT_TERM] = header.raft_term
241
-
238
+
242
239
  if count is not None:
243
240
  attrs[TraceAttributes.ETCD_COUNT] = count
244
-
241
+
245
242
  if deleted is not None:
246
243
  attrs[TraceAttributes.ETCD_DELETED] = deleted
247
-
244
+
248
245
  attrs.update(kwargs)
249
246
  return attrs
250
247
 
@@ -253,32 +250,32 @@ def build_lease_attributes(
253
250
  lease_id: Optional[int] = None,
254
251
  ttl: Optional[int] = None,
255
252
  remaining_ttl: Optional[int] = None,
256
- **kwargs
253
+ **kwargs,
257
254
  ) -> Dict[str, Any]:
258
255
  """Build trace attributes for lease operations.
259
-
256
+
260
257
  Args:
261
258
  lease_id: Lease ID
262
259
  ttl: Lease TTL in seconds
263
260
  remaining_ttl: Remaining TTL
264
261
  **kwargs: Additional attributes
265
-
262
+
266
263
  Returns:
267
264
  Dictionary of trace attributes
268
265
  """
269
266
  attrs = {
270
267
  TraceAttributes.DB_SYSTEM: "etcd",
271
268
  }
272
-
269
+
273
270
  if lease_id is not None:
274
271
  attrs[TraceAttributes.ETCD_LEASE_ID] = lease_id
275
-
272
+
276
273
  if ttl is not None:
277
274
  attrs[TraceAttributes.ETCD_LEASE_TTL] = ttl
278
-
275
+
279
276
  if remaining_ttl is not None:
280
277
  attrs[TraceAttributes.ETCD_LEASE_REMAINING_TTL] = remaining_ttl
281
-
278
+
282
279
  attrs.update(kwargs)
283
280
  return attrs
284
281
 
@@ -287,34 +284,34 @@ def build_lock_attributes(
287
284
  name: Optional[str] = None,
288
285
  uuid: Optional[bytes] = None,
289
286
  acquired: Optional[bool] = None,
290
- **kwargs
287
+ **kwargs,
291
288
  ) -> Dict[str, Any]:
292
289
  """Build trace attributes for lock operations.
293
-
290
+
294
291
  Args:
295
292
  name: Lock name (key path)
296
293
  uuid: Lock UUID (will use first 8 bytes)
297
294
  acquired: Whether the lock was acquired
298
295
  **kwargs: Additional attributes
299
-
296
+
300
297
  Returns:
301
298
  Dictionary of trace attributes
302
299
  """
303
300
  attrs = {
304
301
  TraceAttributes.DB_SYSTEM: "etcd",
305
302
  }
306
-
303
+
307
304
  if name is not None:
308
305
  attrs[TraceAttributes.ETCD_LOCK_NAME] = name
309
-
306
+
310
307
  if uuid is not None:
311
308
  # Use first 8 bytes of UUID for readability
312
309
  uuid_str = uuid[:8].hex() if isinstance(uuid, bytes) else str(uuid)[:16]
313
310
  attrs[TraceAttributes.ETCD_LOCK_UUID] = uuid_str
314
-
311
+
315
312
  if acquired is not None:
316
313
  attrs[TraceAttributes.ETCD_LOCK_ACQUIRED] = acquired
317
-
314
+
318
315
  attrs.update(kwargs)
319
316
  return attrs
320
317
 
@@ -324,52 +321,48 @@ def build_txn_attributes(
324
321
  success_count: Optional[int] = None,
325
322
  failure_count: Optional[int] = None,
326
323
  succeeded: Optional[bool] = None,
327
- **kwargs
324
+ **kwargs,
328
325
  ) -> Dict[str, Any]:
329
326
  """Build trace attributes for transaction operations.
330
-
327
+
331
328
  Args:
332
329
  compare_count: Number of compare conditions
333
330
  success_count: Number of success operations
334
331
  failure_count: Number of failure operations
335
332
  succeeded: Whether the transaction succeeded
336
333
  **kwargs: Additional attributes
337
-
334
+
338
335
  Returns:
339
336
  Dictionary of trace attributes
340
337
  """
341
338
  attrs = {
342
339
  TraceAttributes.DB_SYSTEM: "etcd",
343
340
  }
344
-
341
+
345
342
  if compare_count is not None:
346
343
  attrs[TraceAttributes.ETCD_TXN_COMPARE_COUNT] = compare_count
347
-
344
+
348
345
  if success_count is not None:
349
346
  attrs[TraceAttributes.ETCD_TXN_SUCCESS_COUNT] = success_count
350
-
347
+
351
348
  if failure_count is not None:
352
349
  attrs[TraceAttributes.ETCD_TXN_FAILURE_COUNT] = failure_count
353
-
350
+
354
351
  if succeeded is not None:
355
352
  attrs[TraceAttributes.ETCD_TXN_SUCCEEDED] = succeeded
356
-
353
+
357
354
  attrs.update(kwargs)
358
355
  return attrs
359
356
 
360
357
 
361
- def build_batch_attributes(
362
- size: int,
363
- atomic: bool = False,
364
- **kwargs
365
- ) -> Dict[str, Any]:
358
+ def build_batch_attributes(size: int, atomic: bool = False, **kwargs) -> Dict[str, Any]:
366
359
  """Build trace attributes for batch operations.
367
-
360
+
368
361
  Args:
369
362
  size: Number of operations in the batch
370
363
  atomic: Whether the batch is atomic
371
364
  **kwargs: Additional attributes
372
-
365
+
373
366
  Returns:
374
367
  Dictionary of trace attributes
375
368
  """
@@ -378,57 +371,52 @@ def build_batch_attributes(
378
371
  TraceAttributes.ETCD_BATCH_SIZE: size,
379
372
  TraceAttributes.ETCD_BATCH_ATOMIC: atomic,
380
373
  }
381
-
374
+
382
375
  attrs.update(kwargs)
383
376
  return attrs
384
377
 
385
378
 
386
- def build_endpoint_attributes(
387
- endpoint: str,
388
- **kwargs
389
- ) -> Dict[str, Any]:
379
+ def build_endpoint_attributes(endpoint: str, **kwargs) -> Dict[str, Any]:
390
380
  """Build trace attributes for endpoint-related operations.
391
-
381
+
392
382
  Args:
393
383
  endpoint: The etcd endpoint
394
384
  **kwargs: Additional attributes
395
-
385
+
396
386
  Returns:
397
387
  Dictionary of trace attributes
398
388
  """
399
389
  attrs = {
400
390
  TraceAttributes.ETCD_ENDPOINT: endpoint,
401
391
  }
402
-
392
+
403
393
  attrs.update(kwargs)
404
394
  return attrs
405
395
 
406
396
 
407
397
  def build_error_attributes(
408
- error_code: Optional[str] = None,
409
- error_message: Optional[str] = None,
410
- **kwargs
398
+ error_code: Optional[str] = None, error_message: Optional[str] = None, **kwargs
411
399
  ) -> Dict[str, Any]:
412
400
  """Build trace attributes for error cases.
413
-
401
+
414
402
  Args:
415
403
  error_code: Error code
416
404
  error_message: Error message
417
405
  **kwargs: Additional attributes
418
-
406
+
419
407
  Returns:
420
408
  Dictionary of trace attributes
421
409
  """
422
410
  attrs = {}
423
-
411
+
424
412
  if error_code is not None:
425
413
  attrs[TraceAttributes.ETCD_ERROR_CODE] = error_code
426
-
414
+
427
415
  if error_message is not None:
428
416
  # Truncate long error messages
429
417
  if len(error_message) > 256:
430
418
  error_message = error_message[:256] + "..."
431
419
  attrs[TraceAttributes.ETCD_ERROR_MESSAGE] = error_message
432
-
420
+
433
421
  attrs.update(kwargs)
434
422
  return attrs
etcd3/utils.py CHANGED
@@ -13,9 +13,9 @@ import grpc
13
13
  # events, cancel = await etcd.watch('/key', filters=[FILTER_DELETE])
14
14
  # # Only PUT events will be received, DELETE events are filtered out
15
15
  #
16
- FILTER_PUT = 0 # Filter out PUT events (only DELETE events will be received)
17
- FILTER_DELETE = 1 # Filter out DELETE events (only PUT events will be received)
18
- FILTER_COMMIT = 2 # Filter out all events (no events will be received)
16
+ FILTER_PUT = 0 # Filter out PUT events (only DELETE events will be received)
17
+ FILTER_DELETE = 1 # Filter out DELETE events (only PUT events will be received)
18
+ FILTER_COMMIT = 2 # Filter out all events (no events will be received)
19
19
 
20
20
 
21
21
  class LoadBalancerStrategy(Enum):
@@ -27,6 +27,7 @@ class LoadBalancerStrategy(Enum):
27
27
  RANDOM: Random - select random healthy endpoint
28
28
  STICKY: Sticky - maintain connection to the same endpoint
29
29
  """
30
+
30
31
  PRIORITY = "priority"
31
32
  ROUND_ROBIN = "round_robin"
32
33
  RANDOM = "random"
etcdrpc/__init__.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  # 简单的检查,如果文件不存在则报错提示(防御性编程)
4
4
  from pathlib import Path
5
+
5
6
  _current_dir = Path(__file__).parent
6
7
  if not (_current_dir / "rpc_pb2.py").exists():
7
8
  raise ImportError(
@@ -13,4 +14,4 @@ from etcdrpc.rpc_pb2 import *
13
14
  from etcdrpc.rpc_pb2_grpc import *
14
15
 
15
16
  # 获取所有非私有成员
16
- __all__ = [name for name in globals().keys() if not name.startswith('_')]
17
+ __all__ = [name for name in globals().keys() if not name.startswith("_")]
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-etcd3-async
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: etcd3 async sdk
5
5
  Requires-Python: <4,>=3.12
6
- Requires-Dist: grpcio==1.76.0
6
+ Requires-Dist: grpcio==1.66.2
7
7
  Requires-Dist: prometheus-client>=0.24.1
8
- Requires-Dist: protobuf==6.31.1
9
8
  Description-Content-Type: text/markdown
10
9
 
11
10
  # python-etcd3-async
@@ -25,6 +24,7 @@ Python async client for the etcd API v3, supporting both synchronous and asynchr
25
24
  - **Keepalive error propagation**: Lease keepalive errors are properly propagated for connection loss detection
26
25
  - **Endpoint failover**: Built-in support for multiple endpoints with automatic failover
27
26
  - **Context manager**: Clean resource management for both sync and async
27
+ - **Observability**: Structured logging, OpenTelemetry tracing, and Prometheus metrics
28
28
 
29
29
  ## Quick Start
30
30
 
@@ -294,14 +294,53 @@ client = await async_client(
294
294
  )
295
295
  ```
296
296
 
297
- ## Environment Variables
297
+ ## Observability
298
298
 
299
- | Variable | Description |
300
- |----------|-------------|
301
- | `ETCD3_LOG_LEVEL` | Set logging level (DEBUG, INFO, WARNING, ERROR) |
299
+ ### Configure via Environment
300
+
301
+ ```bash
302
+ export ETCD_OBS_LOG_LEVEL=DEBUG
303
+ export ETCD_OBS_LOG_FORMAT=json
304
+ export ETCD_OBS_TRACING_ENABLED=true
305
+ export ETCD_OBS_PROMETHEUS_ENABLED=true
306
+ ```
307
+
308
+ ### Configure Programmatically
309
+
310
+ ```python
311
+ from etcd3 import ObservabilityConfig
312
+
313
+ config = ObservabilityConfig(
314
+ tracing_enabled=True,
315
+ log_format="json",
316
+ log_level="INFO",
317
+ prometheus_enabled=True,
318
+ )
319
+ config.apply()
320
+ ```
321
+
322
+ ### Access Metrics
323
+
324
+ ```python
325
+ from etcd3.exporters.prometheus import get_exporter
326
+
327
+ exporter = get_exporter()
328
+ if exporter:
329
+ print(exporter.metrics().decode())
330
+ ```
302
331
 
303
332
  ## API Reference
304
333
 
334
+ ### Environment Variables
335
+
336
+ | Variable | Default | Description |
337
+ |----------|---------|-------------|
338
+ | `ETCD_OBS_LOG_LEVEL` | INFO | Logging level (DEBUG, INFO, WARNING, ERROR) |
339
+ | `ETCD_OBS_LOG_FORMAT` | text | Log format (text, json, colored) |
340
+ | `ETCD_OBS_TRACING_ENABLED` | true | Enable OpenTelemetry tracing |
341
+ | `ETCD_OBS_TRACING_SERVICE_NAME` | etcd3-client | Service name for tracing |
342
+ | `ETCD_OBS_PROMETHEUS_ENABLED` | false | Enable Prometheus metrics export |
343
+
305
344
  ### Client Factory
306
345
 
307
346
  | Function | Description |
@@ -1,6 +1,6 @@
1
1
  etcd3/__init__.py,sha256=_G81-O1yZsDNpMp_VvWDS8YNE9EgHEmgAhXdZ5VokP8,2811
2
2
  etcd3/async_client.py,sha256=wnzRgPzmFN2Xe0TPxSL-bHTo_lnL01l_I2L6arMGUaw,54229
3
- etcd3/async_client_extensions.py,sha256=IgW_2l0_h0AtFUFe6uPHYD-7UlBRMdMrwoWEsfDgI0U,34213
3
+ etcd3/async_client_extensions.py,sha256=VWXLhmjBNL1UNhMynZmx5P2sPn_mgTpVXvvU98GDY90,34483
4
4
  etcd3/async_endpoint.py,sha256=nDQtXCze1H8SAHIFJ0HYc1dBY2Nc0gw_t3SVT5d1Zmg,4587
5
5
  etcd3/async_managers.py,sha256=bUbrqkeq1Rv_qhDGOkIqgSoqA-Lyo10Vbxc60KahtXc,17620
6
6
  etcd3/async_monitor.py,sha256=iZPac3Pqn5bI0ONguJJctQsi4b2ft4hWPknug6jj5RY,453
@@ -8,9 +8,9 @@ etcd3/async_watch.py,sha256=m6I2BAoI54J-ZPVVMxao6XJQXvVyQ88Hp2QeauD9z20,15962
8
8
  etcd3/client.py,sha256=43OjwOGlxgA3pKTkMZoL4Aem0_Axk7TDrb6-TB6veos,72347
9
9
  etcd3/endpoint.py,sha256=aSMBmEitAADW0ABAZhc8L7jPYsQo8amEWKv6LJbsmC4,2589
10
10
  etcd3/events.py,sha256=e4gTiUE4SRon8R-if9SKORIM_jr8XtVye-GcV-owKdw,972
11
- etcd3/exceptions.py,sha256=mCsOaL6Pw1C72JZ8XqvTPiFsklk5CSjyDmMtdqv5Wv4,996
11
+ etcd3/exceptions.py,sha256=W73_Brq7p25clQLW4sC0zKx_EZG7x2JToInwZ9D-gAc,998
12
12
  etcd3/leases.py,sha256=7eVl0rA2IGIOBTHbBn0Te9SQ-TKBr4v20X7iUzWYcSo,2804
13
- etcd3/locks.py,sha256=KBtMS33FLGS13lcvJkIQPelnRLyTUXhqhJRs4De-uts,22533
13
+ etcd3/locks.py,sha256=NrLqqrloZ1HksmQRYb_NEa96Nn-tASS0EgzoTPwG6Oo,22640
14
14
  etcd3/log.py,sha256=sMo6av48x9Nf_YDIWdSv8R-VUeVXi1n17wGDAAaFnlM,4282
15
15
  etcd3/log_formatters.py,sha256=weWgMQjuI_cZFBEUORGCgkFJQIPvDUaiCLJN64t7s9o,7304
16
16
  etcd3/log_sampling.py,sha256=7PhNz-HOZ6sHdBiXS29YJ3baHY_L1VwJaq4NZt2yX78,6692
@@ -18,13 +18,13 @@ etcd3/members.py,sha256=LLg6sAWDaaW0wzl_yBF3ialMQuaRKLFpFDEcmWcbqzY,1414
18
18
  etcd3/observability.py,sha256=BLOUMs1H2icF-tEWB_RoZFKoCNoXZt1ZZERGfckesW4,14215
19
19
  etcd3/shared.py,sha256=6-mhHNvGD6TPwq79EQa6oGxoZoGKR5oZSZkJeSbZpc8,16195
20
20
  etcd3/trace.py,sha256=OtOmAZ5uz7v7dqPoAaEM8d94prK5f5tSEJq7Qi2w_0c,6650
21
- etcd3/trace_attributes.py,sha256=oE_r3OMvbqm_BWlwKsTWjJucJEQ4osWu0HvwlyI9uc4,12025
21
+ etcd3/trace_attributes.py,sha256=-wk0ZFqjzQT4g7_HErWhJMsVk8ocfPcMDNLJzwDMEjc,11621
22
22
  etcd3/transactions.py,sha256=FJe-J5QBm-pV_J9dGIMor35PHAXvb61bjSELkg5Bw4U,7587
23
- etcd3/utils.py,sha256=M1Rl9LwEvDUlkFBcaK6YPYLAPXjGmgt11d4WWqaNJHc,4811
23
+ etcd3/utils.py,sha256=j10jFnxzZKy5-ImvrWVs18QPbQMvsFmnibPe0Mctm9g,4806
24
24
  etcd3/watch.py,sha256=rPEvOm1nQOPzj00OV6GFObZ2uSQqNl2pEcT7ut7xO44,8838
25
25
  etcd3/exporters/__init__.py,sha256=9nMfrCsQ-pN5WUIY2ZHBsY4N3xmdD2Z24mDCi19hJE8,362
26
26
  etcd3/exporters/prometheus.py,sha256=goA42MAwuR-FI4QH5K4jMOJt51tfC2V9BTeAf9dL5hg,10211
27
- etcdrpc/__init__.py,sha256=msPVuBwxhRwvr3kn1r6RkUMalHpbXRfrg6A3bNmVu8s,547
27
+ etcdrpc/__init__.py,sha256=-ykqbdDaroJTTyUip4RaZDewcnPPJcOwI9bXYxxXF6Q,549
28
28
  etcdrpc/auth.proto,sha256=Py2ysZTy4i89vxRHBdXK-KcPCkojcEfWXDNbDajLRWk,1170
29
29
  etcdrpc/auth_pb2.py,sha256=H-HBNZ64Mi0aqMIgjqvgdDcm5DBl9GIhqipRBCt1LlA,2120
30
30
  etcdrpc/auth_pb2.pyi,sha256=vEV6IzZnKObqDI6fJPtXAqRbnKZC3vXy4UAem_XZVvY,1985
@@ -37,6 +37,6 @@ etcdrpc/rpc.proto,sha256=UD4vXbdPUNHSLSx7K_UltWqJjWswHBKSs0t9aNMA4qg,31351
37
37
  etcdrpc/rpc_pb2.py,sha256=8qhnULNr8jym0qyivDVqJB86H_u1Rb_K8iY8FNT9e0s,30252
38
38
  etcdrpc/rpc_pb2.pyi,sha256=TfPtHXIluO_D1rchVMVfoSgZTPQLk-YNhmcJBIt9XIk,35473
39
39
  etcdrpc/rpc_pb2_grpc.py,sha256=HOifyKL0X3BmLkS1r0tWTMfyfChBiy76O6Ys12kT5n8,73810
40
- python_etcd3_async-0.1.0.dist-info/METADATA,sha256=OSgYlmcjLRkK0MJYJrKBb84luLROrXAPYeYe7H-N854,7724
41
- python_etcd3_async-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
- python_etcd3_async-0.1.0.dist-info/RECORD,,
40
+ python_etcd3_async-0.1.2.dist-info/METADATA,sha256=JHiGpX_FT-5svHnzKLIfiev9t9hxWqVZug3Gz_YerF4,8700
41
+ python_etcd3_async-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
42
+ python_etcd3_async-0.1.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any