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.
- etcd3/__init__.py +90 -0
- etcd3/async_client.py +1495 -0
- etcd3/async_client_extensions.py +878 -0
- etcd3/async_endpoint.py +140 -0
- etcd3/async_managers.py +498 -0
- etcd3/async_monitor.py +16 -0
- etcd3/async_watch.py +462 -0
- etcd3/client.py +2021 -0
- etcd3/endpoint.py +79 -0
- etcd3/events.py +40 -0
- etcd3/exceptions.py +47 -0
- etcd3/exporters/__init__.py +18 -0
- etcd3/exporters/prometheus.py +365 -0
- etcd3/leases.py +106 -0
- etcd3/locks.py +593 -0
- etcd3/log.py +146 -0
- etcd3/log_formatters.py +228 -0
- etcd3/log_sampling.py +221 -0
- etcd3/members.py +44 -0
- etcd3/observability.py +431 -0
- etcd3/shared.py +486 -0
- etcd3/trace.py +231 -0
- etcd3/trace_attributes.py +434 -0
- etcd3/transactions.py +280 -0
- etcd3/utils.py +158 -0
- etcd3/watch.py +270 -0
- etcdrpc/__init__.py +16 -0
- etcdrpc/auth.proto +49 -0
- etcdrpc/auth_pb2.py +43 -0
- etcdrpc/auth_pb2.pyi +44 -0
- etcdrpc/auth_pb2_grpc.py +24 -0
- etcdrpc/kv.proto +43 -0
- etcdrpc/kv_pb2.py +40 -0
- etcdrpc/kv_pb2.pyi +39 -0
- etcdrpc/kv_pb2_grpc.py +24 -0
- etcdrpc/rpc.proto +924 -0
- etcdrpc/rpc_pb2.py +237 -0
- etcdrpc/rpc_pb2.pyi +811 -0
- etcdrpc/rpc_pb2_grpc.py +1893 -0
- python_etcd3_async-0.1.0.dist-info/METADATA +328 -0
- python_etcd3_async-0.1.0.dist-info/RECORD +42 -0
- python_etcd3_async-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""etcd semantic attributes for OpenTelemetry tracing.
|
|
2
|
+
|
|
3
|
+
This module defines standardized attributes for etcd operations in OpenTelemetry
|
|
4
|
+
traces, following the OpenTelemetry semantic conventions:
|
|
5
|
+
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EtcdOperationType(Enum):
|
|
14
|
+
"""etcd operation types for tracing."""
|
|
15
|
+
|
|
16
|
+
# KV operations
|
|
17
|
+
KV_GET = "kv.get"
|
|
18
|
+
KV_PUT = "kv.put"
|
|
19
|
+
KV_DELETE = "kv.delete"
|
|
20
|
+
KV_RANGE = "kv.range"
|
|
21
|
+
KV_TXN = "kv.transaction"
|
|
22
|
+
|
|
23
|
+
# Lease operations
|
|
24
|
+
LEASE_GRANT = "lease.grant"
|
|
25
|
+
LEASE_REVOKE = "lease.revoke"
|
|
26
|
+
LEASE_KEEPALIVE = "lease.keepalive"
|
|
27
|
+
LEASE_TTL = "lease.ttl"
|
|
28
|
+
|
|
29
|
+
# Lock operations
|
|
30
|
+
LOCK_ACQUIRE = "lock.acquire"
|
|
31
|
+
LOCK_RELEASE = "lock.release"
|
|
32
|
+
LOCK_REFRESH = "lock.refresh"
|
|
33
|
+
|
|
34
|
+
# Watch operations
|
|
35
|
+
WATCH_CREATE = "watch.create"
|
|
36
|
+
WATCH_CANCEL = "watch.cancel"
|
|
37
|
+
|
|
38
|
+
# Cluster operations
|
|
39
|
+
CLUSTER_MEMBER_ADD = "cluster.member.add"
|
|
40
|
+
CLUSTER_MEMBER_REMOVE = "cluster.member.remove"
|
|
41
|
+
CLUSTER_MEMBER_UPDATE = "cluster.member.update"
|
|
42
|
+
CLUSTER_MEMBER_LIST = "cluster.member.list"
|
|
43
|
+
|
|
44
|
+
# Maintenance operations
|
|
45
|
+
MAINTENANCE_STATUS = "maintenance.status"
|
|
46
|
+
MAINTENANCE_COMPACT = "maintenance.compact"
|
|
47
|
+
MAINTENANCE_DEFRAG = "maintenance.defragment"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class TraceAttributes:
|
|
52
|
+
"""Standard attribute names for etcd OpenTelemetry traces.
|
|
53
|
+
|
|
54
|
+
These attributes follow OpenTelemetry semantic conventions where applicable
|
|
55
|
+
and define etcd-specific attributes for comprehensive observability.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# === OpenTelemetry Standard Database Attributes ===
|
|
59
|
+
DB_SYSTEM = "db.system"
|
|
60
|
+
"""Database system: always 'etcd' for this client."""
|
|
61
|
+
|
|
62
|
+
DB_OPERATION = "db.operation"
|
|
63
|
+
"""Database operation name (e.g., 'kv.get', 'lease.grant')."""
|
|
64
|
+
|
|
65
|
+
DB_STATEMENT = "db.statement"
|
|
66
|
+
"""Database statement (for complex queries, optional)."""
|
|
67
|
+
|
|
68
|
+
# === etcd Key-Value Attributes ===
|
|
69
|
+
ETCD_KEY = "etcd.key"
|
|
70
|
+
"""The key being operated on."""
|
|
71
|
+
|
|
72
|
+
ETCD_RANGE_END = "etcd.range_end"
|
|
73
|
+
"""End of the key range for range operations."""
|
|
74
|
+
|
|
75
|
+
ETCD_LIMIT = "etcd.limit"
|
|
76
|
+
"""Maximum number of keys to return."""
|
|
77
|
+
|
|
78
|
+
ETCD_REVISION = "etcd.revision"
|
|
79
|
+
"""Current revision of the operation result."""
|
|
80
|
+
|
|
81
|
+
ETCD_PREV_REVISION = "etcd.prev_revision"
|
|
82
|
+
"""Previous revision (for compare operations)."""
|
|
83
|
+
|
|
84
|
+
ETCD_COUNT = "etcd.count"
|
|
85
|
+
"""Number of keys returned or affected."""
|
|
86
|
+
|
|
87
|
+
ETCD_DELETED = "etcd.deleted"
|
|
88
|
+
"""Number of keys deleted."""
|
|
89
|
+
|
|
90
|
+
# === etcd Cluster Attributes ===
|
|
91
|
+
ETCD_ENDPOINT = "etcd.endpoint"
|
|
92
|
+
"""The etcd endpoint being accessed."""
|
|
93
|
+
|
|
94
|
+
ETCD_CLUSTER_ID = "etcd.cluster_id"
|
|
95
|
+
"""Cluster ID from response header."""
|
|
96
|
+
|
|
97
|
+
ETCD_MEMBER_ID = "etcd.member_id"
|
|
98
|
+
"""Member ID from response header."""
|
|
99
|
+
|
|
100
|
+
ETCD_RAFT_TERM = "etcd.raft_term"
|
|
101
|
+
"""Raft term from response header."""
|
|
102
|
+
|
|
103
|
+
# === etcd Lease Attributes ===
|
|
104
|
+
ETCD_LEASE_ID = "etcd.lease.id"
|
|
105
|
+
"""Lease ID."""
|
|
106
|
+
|
|
107
|
+
ETCD_LEASE_TTL = "etcd.lease.ttl"
|
|
108
|
+
"""Lease TTL in seconds."""
|
|
109
|
+
|
|
110
|
+
ETCD_LEASE_REMAINING_TTL = "etcd.lease.remaining_ttl"
|
|
111
|
+
"""Remaining TTL for the lease."""
|
|
112
|
+
|
|
113
|
+
# === etcd Lock Attributes ===
|
|
114
|
+
ETCD_LOCK_NAME = "etcd.lock.name"
|
|
115
|
+
"""Lock name (key path)."""
|
|
116
|
+
|
|
117
|
+
ETCD_LOCK_UUID = "etcd.lock.uuid"
|
|
118
|
+
"""Lock UUID (first 8 bytes)."""
|
|
119
|
+
|
|
120
|
+
ETCD_LOCK_ACQUIRED = "etcd.lock.acquired"
|
|
121
|
+
"""Whether the lock was successfully acquired."""
|
|
122
|
+
|
|
123
|
+
# === etcd Transaction Attributes ===
|
|
124
|
+
ETCD_TXN_COMPARE_COUNT = "etcd.txn.compare_count"
|
|
125
|
+
"""Number of compare conditions."""
|
|
126
|
+
|
|
127
|
+
ETCD_TXN_SUCCESS_COUNT = "etcd.txn.success_count"
|
|
128
|
+
"""Number of success operations."""
|
|
129
|
+
|
|
130
|
+
ETCD_TXN_FAILURE_COUNT = "etcd.txn.failure_count"
|
|
131
|
+
"""Number of failure operations."""
|
|
132
|
+
|
|
133
|
+
ETCD_TXN_SUCCEEDED = "etcd.txn.succeeded"
|
|
134
|
+
"""Whether the transaction succeeded."""
|
|
135
|
+
|
|
136
|
+
# === etcd Batch Attributes ===
|
|
137
|
+
ETCD_BATCH_SIZE = "etcd.batch.size"
|
|
138
|
+
"""Number of operations in the batch."""
|
|
139
|
+
|
|
140
|
+
ETCD_BATCH_ATOMIC = "etcd.batch.atomic"
|
|
141
|
+
"""Whether the batch is atomic."""
|
|
142
|
+
|
|
143
|
+
# === etcd Error Attributes ===
|
|
144
|
+
ETCD_ERROR_CODE = "etcd.error_code"
|
|
145
|
+
"""Error code if operation failed."""
|
|
146
|
+
|
|
147
|
+
ETCD_ERROR_MESSAGE = "etcd.error_message"
|
|
148
|
+
"""Error message if operation failed."""
|
|
149
|
+
|
|
150
|
+
# === Retry Attributes ===
|
|
151
|
+
ETCD_RETRY_COUNT = "etcd.retry.count"
|
|
152
|
+
"""Number of retries for this operation."""
|
|
153
|
+
|
|
154
|
+
ETCD_RETRY_ENDPOINT = "etcd.retry.endpoint"
|
|
155
|
+
"""Endpoint used after retry."""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _safe_decode(value: Optional[bytes], max_length: int = 256) -> Optional[str]:
|
|
159
|
+
"""Safely decode bytes to string for attribute values.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
value: Bytes value to decode
|
|
163
|
+
max_length: Maximum length for the decoded string
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Decoded string or None if input is None
|
|
167
|
+
"""
|
|
168
|
+
if value is None:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
decoded = value.decode('utf-8', errors='replace')
|
|
173
|
+
if len(decoded) > max_length:
|
|
174
|
+
decoded = decoded[:max_length] + "..."
|
|
175
|
+
return decoded
|
|
176
|
+
except Exception:
|
|
177
|
+
return repr(value)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def build_kv_attributes(
|
|
181
|
+
operation: str,
|
|
182
|
+
key: Optional[bytes] = None,
|
|
183
|
+
range_end: Optional[bytes] = None,
|
|
184
|
+
limit: Optional[int] = None,
|
|
185
|
+
**kwargs
|
|
186
|
+
) -> Dict[str, Any]:
|
|
187
|
+
"""Build trace attributes for KV operations.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
operation: Operation type (e.g., 'kv.get', 'kv.put')
|
|
191
|
+
key: The key being operated on
|
|
192
|
+
range_end: End of key range for range operations
|
|
193
|
+
limit: Maximum number of keys to return
|
|
194
|
+
**kwargs: Additional attributes
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dictionary of trace attributes
|
|
198
|
+
"""
|
|
199
|
+
attrs = {
|
|
200
|
+
TraceAttributes.DB_SYSTEM: "etcd",
|
|
201
|
+
TraceAttributes.DB_OPERATION: operation,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if key is not None:
|
|
205
|
+
attrs[TraceAttributes.ETCD_KEY] = _safe_decode(key)
|
|
206
|
+
|
|
207
|
+
if range_end is not None:
|
|
208
|
+
attrs[TraceAttributes.ETCD_RANGE_END] = _safe_decode(range_end)
|
|
209
|
+
|
|
210
|
+
if limit is not None:
|
|
211
|
+
attrs[TraceAttributes.ETCD_LIMIT] = limit
|
|
212
|
+
|
|
213
|
+
attrs.update(kwargs)
|
|
214
|
+
return attrs
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def build_response_attributes(
|
|
218
|
+
header=None,
|
|
219
|
+
count: Optional[int] = None,
|
|
220
|
+
deleted: Optional[int] = None,
|
|
221
|
+
**kwargs
|
|
222
|
+
) -> Dict[str, Any]:
|
|
223
|
+
"""Build trace attributes from etcd response.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
header: Response header from etcd
|
|
227
|
+
count: Number of keys returned/affected
|
|
228
|
+
deleted: Number of keys deleted
|
|
229
|
+
**kwargs: Additional attributes
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dictionary of trace attributes
|
|
233
|
+
"""
|
|
234
|
+
attrs = {}
|
|
235
|
+
|
|
236
|
+
if header:
|
|
237
|
+
attrs[TraceAttributes.ETCD_REVISION] = header.revision
|
|
238
|
+
attrs[TraceAttributes.ETCD_CLUSTER_ID] = header.cluster_id
|
|
239
|
+
attrs[TraceAttributes.ETCD_MEMBER_ID] = header.member_id
|
|
240
|
+
attrs[TraceAttributes.ETCD_RAFT_TERM] = header.raft_term
|
|
241
|
+
|
|
242
|
+
if count is not None:
|
|
243
|
+
attrs[TraceAttributes.ETCD_COUNT] = count
|
|
244
|
+
|
|
245
|
+
if deleted is not None:
|
|
246
|
+
attrs[TraceAttributes.ETCD_DELETED] = deleted
|
|
247
|
+
|
|
248
|
+
attrs.update(kwargs)
|
|
249
|
+
return attrs
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def build_lease_attributes(
|
|
253
|
+
lease_id: Optional[int] = None,
|
|
254
|
+
ttl: Optional[int] = None,
|
|
255
|
+
remaining_ttl: Optional[int] = None,
|
|
256
|
+
**kwargs
|
|
257
|
+
) -> Dict[str, Any]:
|
|
258
|
+
"""Build trace attributes for lease operations.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
lease_id: Lease ID
|
|
262
|
+
ttl: Lease TTL in seconds
|
|
263
|
+
remaining_ttl: Remaining TTL
|
|
264
|
+
**kwargs: Additional attributes
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary of trace attributes
|
|
268
|
+
"""
|
|
269
|
+
attrs = {
|
|
270
|
+
TraceAttributes.DB_SYSTEM: "etcd",
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if lease_id is not None:
|
|
274
|
+
attrs[TraceAttributes.ETCD_LEASE_ID] = lease_id
|
|
275
|
+
|
|
276
|
+
if ttl is not None:
|
|
277
|
+
attrs[TraceAttributes.ETCD_LEASE_TTL] = ttl
|
|
278
|
+
|
|
279
|
+
if remaining_ttl is not None:
|
|
280
|
+
attrs[TraceAttributes.ETCD_LEASE_REMAINING_TTL] = remaining_ttl
|
|
281
|
+
|
|
282
|
+
attrs.update(kwargs)
|
|
283
|
+
return attrs
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def build_lock_attributes(
|
|
287
|
+
name: Optional[str] = None,
|
|
288
|
+
uuid: Optional[bytes] = None,
|
|
289
|
+
acquired: Optional[bool] = None,
|
|
290
|
+
**kwargs
|
|
291
|
+
) -> Dict[str, Any]:
|
|
292
|
+
"""Build trace attributes for lock operations.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
name: Lock name (key path)
|
|
296
|
+
uuid: Lock UUID (will use first 8 bytes)
|
|
297
|
+
acquired: Whether the lock was acquired
|
|
298
|
+
**kwargs: Additional attributes
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Dictionary of trace attributes
|
|
302
|
+
"""
|
|
303
|
+
attrs = {
|
|
304
|
+
TraceAttributes.DB_SYSTEM: "etcd",
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if name is not None:
|
|
308
|
+
attrs[TraceAttributes.ETCD_LOCK_NAME] = name
|
|
309
|
+
|
|
310
|
+
if uuid is not None:
|
|
311
|
+
# Use first 8 bytes of UUID for readability
|
|
312
|
+
uuid_str = uuid[:8].hex() if isinstance(uuid, bytes) else str(uuid)[:16]
|
|
313
|
+
attrs[TraceAttributes.ETCD_LOCK_UUID] = uuid_str
|
|
314
|
+
|
|
315
|
+
if acquired is not None:
|
|
316
|
+
attrs[TraceAttributes.ETCD_LOCK_ACQUIRED] = acquired
|
|
317
|
+
|
|
318
|
+
attrs.update(kwargs)
|
|
319
|
+
return attrs
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def build_txn_attributes(
|
|
323
|
+
compare_count: Optional[int] = None,
|
|
324
|
+
success_count: Optional[int] = None,
|
|
325
|
+
failure_count: Optional[int] = None,
|
|
326
|
+
succeeded: Optional[bool] = None,
|
|
327
|
+
**kwargs
|
|
328
|
+
) -> Dict[str, Any]:
|
|
329
|
+
"""Build trace attributes for transaction operations.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
compare_count: Number of compare conditions
|
|
333
|
+
success_count: Number of success operations
|
|
334
|
+
failure_count: Number of failure operations
|
|
335
|
+
succeeded: Whether the transaction succeeded
|
|
336
|
+
**kwargs: Additional attributes
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dictionary of trace attributes
|
|
340
|
+
"""
|
|
341
|
+
attrs = {
|
|
342
|
+
TraceAttributes.DB_SYSTEM: "etcd",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if compare_count is not None:
|
|
346
|
+
attrs[TraceAttributes.ETCD_TXN_COMPARE_COUNT] = compare_count
|
|
347
|
+
|
|
348
|
+
if success_count is not None:
|
|
349
|
+
attrs[TraceAttributes.ETCD_TXN_SUCCESS_COUNT] = success_count
|
|
350
|
+
|
|
351
|
+
if failure_count is not None:
|
|
352
|
+
attrs[TraceAttributes.ETCD_TXN_FAILURE_COUNT] = failure_count
|
|
353
|
+
|
|
354
|
+
if succeeded is not None:
|
|
355
|
+
attrs[TraceAttributes.ETCD_TXN_SUCCEEDED] = succeeded
|
|
356
|
+
|
|
357
|
+
attrs.update(kwargs)
|
|
358
|
+
return attrs
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def build_batch_attributes(
|
|
362
|
+
size: int,
|
|
363
|
+
atomic: bool = False,
|
|
364
|
+
**kwargs
|
|
365
|
+
) -> Dict[str, Any]:
|
|
366
|
+
"""Build trace attributes for batch operations.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
size: Number of operations in the batch
|
|
370
|
+
atomic: Whether the batch is atomic
|
|
371
|
+
**kwargs: Additional attributes
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dictionary of trace attributes
|
|
375
|
+
"""
|
|
376
|
+
attrs = {
|
|
377
|
+
TraceAttributes.DB_SYSTEM: "etcd",
|
|
378
|
+
TraceAttributes.ETCD_BATCH_SIZE: size,
|
|
379
|
+
TraceAttributes.ETCD_BATCH_ATOMIC: atomic,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
attrs.update(kwargs)
|
|
383
|
+
return attrs
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def build_endpoint_attributes(
|
|
387
|
+
endpoint: str,
|
|
388
|
+
**kwargs
|
|
389
|
+
) -> Dict[str, Any]:
|
|
390
|
+
"""Build trace attributes for endpoint-related operations.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
endpoint: The etcd endpoint
|
|
394
|
+
**kwargs: Additional attributes
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dictionary of trace attributes
|
|
398
|
+
"""
|
|
399
|
+
attrs = {
|
|
400
|
+
TraceAttributes.ETCD_ENDPOINT: endpoint,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
attrs.update(kwargs)
|
|
404
|
+
return attrs
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def build_error_attributes(
|
|
408
|
+
error_code: Optional[str] = None,
|
|
409
|
+
error_message: Optional[str] = None,
|
|
410
|
+
**kwargs
|
|
411
|
+
) -> Dict[str, Any]:
|
|
412
|
+
"""Build trace attributes for error cases.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
error_code: Error code
|
|
416
|
+
error_message: Error message
|
|
417
|
+
**kwargs: Additional attributes
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Dictionary of trace attributes
|
|
421
|
+
"""
|
|
422
|
+
attrs = {}
|
|
423
|
+
|
|
424
|
+
if error_code is not None:
|
|
425
|
+
attrs[TraceAttributes.ETCD_ERROR_CODE] = error_code
|
|
426
|
+
|
|
427
|
+
if error_message is not None:
|
|
428
|
+
# Truncate long error messages
|
|
429
|
+
if len(error_message) > 256:
|
|
430
|
+
error_message = error_message[:256] + "..."
|
|
431
|
+
attrs[TraceAttributes.ETCD_ERROR_MESSAGE] = error_message
|
|
432
|
+
|
|
433
|
+
attrs.update(kwargs)
|
|
434
|
+
return attrs
|
etcd3/transactions.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Transaction operations for etcd3.
|
|
2
|
+
|
|
3
|
+
This module defines classes for building etcd transactions with
|
|
4
|
+
comparison operations and conditional mutations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import etcdrpc
|
|
10
|
+
import etcd3.utils as utils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_OPERATORS = {
|
|
14
|
+
etcdrpc.Compare.EQUAL: "==",
|
|
15
|
+
etcdrpc.Compare.NOT_EQUAL: "!=",
|
|
16
|
+
etcdrpc.Compare.LESS: "<",
|
|
17
|
+
etcdrpc.Compare.GREATER: ">",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseCompare:
|
|
22
|
+
"""Base class for transaction comparison operations.
|
|
23
|
+
|
|
24
|
+
:param key: The key to compare
|
|
25
|
+
:type key: str or bytes
|
|
26
|
+
:param range_end: End of key range for range comparisons
|
|
27
|
+
:type range_end: str or bytes, optional
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
key: Any
|
|
31
|
+
range_end: Optional[Any]
|
|
32
|
+
value: Any
|
|
33
|
+
op: Optional[Any]
|
|
34
|
+
|
|
35
|
+
def __init__(self, key: Any, range_end: Optional[Any] = None) -> None:
|
|
36
|
+
self.key = key
|
|
37
|
+
self.range_end = range_end
|
|
38
|
+
self.value = None
|
|
39
|
+
self.op = None
|
|
40
|
+
|
|
41
|
+
def __eq__(self, other: Any) -> "BaseCompare":
|
|
42
|
+
"""Set comparison to equal."""
|
|
43
|
+
self.value = other
|
|
44
|
+
self.op = etcdrpc.Compare.EQUAL
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def __ne__(self, other: Any) -> "BaseCompare":
|
|
48
|
+
"""Set comparison to not equal."""
|
|
49
|
+
self.value = other
|
|
50
|
+
self.op = etcdrpc.Compare.NOT_EQUAL
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def __lt__(self, other: Any) -> "BaseCompare":
|
|
54
|
+
"""Set comparison to less than."""
|
|
55
|
+
self.value = other
|
|
56
|
+
self.op = etcdrpc.Compare.LESS
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __gt__(self, other: Any) -> "BaseCompare":
|
|
60
|
+
"""Set comparison to greater than."""
|
|
61
|
+
self.value = other
|
|
62
|
+
self.op = etcdrpc.Compare.GREATER
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
"""Return string representation of the comparison."""
|
|
67
|
+
if self.range_end is None:
|
|
68
|
+
keys = self.key
|
|
69
|
+
else:
|
|
70
|
+
keys = "[{}, {})".format(self.key, self.range_end)
|
|
71
|
+
return "{}: {} {} '{}'".format(
|
|
72
|
+
self.__class__, keys, _OPERATORS.get(self.op), self.value
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def build_message(self) -> etcdrpc.Compare:
|
|
76
|
+
"""Build the gRPC Compare message.
|
|
77
|
+
|
|
78
|
+
:returns: Compare message for the transaction
|
|
79
|
+
:rtype: etcdrpc.Compare
|
|
80
|
+
"""
|
|
81
|
+
compare = etcdrpc.Compare()
|
|
82
|
+
compare.key = utils.to_bytes(self.key)
|
|
83
|
+
if self.range_end is not None:
|
|
84
|
+
compare.range_end = utils.to_bytes(self.range_end)
|
|
85
|
+
|
|
86
|
+
if self.op is None:
|
|
87
|
+
raise ValueError("op must be one of =, !=, < or >")
|
|
88
|
+
|
|
89
|
+
compare.result = self.op
|
|
90
|
+
|
|
91
|
+
self.build_compare(compare)
|
|
92
|
+
return compare
|
|
93
|
+
|
|
94
|
+
def build_compare(self, compare: etcdrpc.Compare) -> None:
|
|
95
|
+
"""Build the target-specific comparison.
|
|
96
|
+
|
|
97
|
+
Override in subclasses to set the appropriate comparison target.
|
|
98
|
+
|
|
99
|
+
:param compare: The Compare message to populate
|
|
100
|
+
:type compare: etcdrpc.Compare
|
|
101
|
+
"""
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Value(BaseCompare):
|
|
106
|
+
"""Compare the value of a key.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
>>> transactions.Value('/key') == b'value'
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def build_compare(self, compare: etcdrpc.Compare) -> None:
|
|
113
|
+
"""Build a value comparison."""
|
|
114
|
+
compare.target = etcdrpc.Compare.VALUE
|
|
115
|
+
compare.value = utils.to_bytes(self.value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Version(BaseCompare):
|
|
119
|
+
"""Compare the version of a key.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> transactions.Version('/key') > 0
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def build_compare(self, compare: etcdrpc.Compare) -> None:
|
|
126
|
+
"""Build a version comparison."""
|
|
127
|
+
compare.target = etcdrpc.Compare.VERSION
|
|
128
|
+
compare.version = int(self.value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Create(BaseCompare):
|
|
132
|
+
"""Compare the creation revision of a key.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> transactions.Create('/key') == 0
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def build_compare(self, compare: etcdrpc.Compare) -> None:
|
|
139
|
+
"""Build a creation revision comparison."""
|
|
140
|
+
compare.target = etcdrpc.Compare.CREATE
|
|
141
|
+
compare.create_revision = int(self.value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Mod(BaseCompare):
|
|
145
|
+
"""Compare the modification revision of a key.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
>>> transactions.Mod('/key') > 100
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def build_compare(self, compare: etcdrpc.Compare) -> None:
|
|
152
|
+
"""Build a modification revision comparison."""
|
|
153
|
+
compare.target = etcdrpc.Compare.MOD
|
|
154
|
+
compare.mod_revision = int(self.value)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Lease(BaseCompare):
|
|
158
|
+
"""Compare the lease ID attached to a key.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> transactions.Lease('/key') == lease_id
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def build_compare(self, compare: etcdrpc.Compare) -> None:
|
|
165
|
+
"""Build a lease comparison."""
|
|
166
|
+
compare.target = etcdrpc.Compare.LEASE
|
|
167
|
+
compare.lease = int(self.value)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Put:
|
|
171
|
+
"""A put operation for use in transactions.
|
|
172
|
+
|
|
173
|
+
:param key: The key to set
|
|
174
|
+
:type key: str or bytes
|
|
175
|
+
:param value: The value to set
|
|
176
|
+
:type value: str or bytes, optional
|
|
177
|
+
:param lease: Lease ID to associate with the key
|
|
178
|
+
:type lease: int, optional
|
|
179
|
+
:param prev_kv: Whether to return the previous key-value pair
|
|
180
|
+
:type prev_kv: bool
|
|
181
|
+
:param ignore_value: Whether to ignore the value field
|
|
182
|
+
:type ignore_value: bool
|
|
183
|
+
:param ignore_lease: Whether to ignore the lease field
|
|
184
|
+
:type ignore_lease: bool
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
key: Any
|
|
188
|
+
value: Optional[Any]
|
|
189
|
+
lease: Optional[int]
|
|
190
|
+
prev_kv: bool
|
|
191
|
+
ignore_value: bool
|
|
192
|
+
ignore_lease: bool
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
key: Any,
|
|
197
|
+
value: Optional[Any] = None,
|
|
198
|
+
lease: Optional[int] = None,
|
|
199
|
+
prev_kv: bool = False,
|
|
200
|
+
ignore_value: bool = False,
|
|
201
|
+
ignore_lease: bool = False,
|
|
202
|
+
) -> None:
|
|
203
|
+
self.key = key
|
|
204
|
+
self.value = value
|
|
205
|
+
self.lease = lease
|
|
206
|
+
self.prev_kv = prev_kv
|
|
207
|
+
self.ignore_value = ignore_value
|
|
208
|
+
self.ignore_lease = ignore_lease
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class Get:
|
|
212
|
+
"""A get operation for use in transactions.
|
|
213
|
+
|
|
214
|
+
:param key: The key to get
|
|
215
|
+
:type key: str or bytes
|
|
216
|
+
:param range_end: End of key range for prefix queries
|
|
217
|
+
:type range_end: str or bytes, optional
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
key: Any
|
|
221
|
+
range_end: Optional[Any]
|
|
222
|
+
|
|
223
|
+
def __init__(self, key: Any, range_end: Optional[Any] = None) -> None:
|
|
224
|
+
self.key = key
|
|
225
|
+
self.range_end = range_end
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Delete:
|
|
229
|
+
"""A delete operation for use in transactions.
|
|
230
|
+
|
|
231
|
+
:param key: The key to delete
|
|
232
|
+
:type key: str or bytes
|
|
233
|
+
:param range_end: End of key range for range deletes
|
|
234
|
+
:type range_end: str or bytes, optional
|
|
235
|
+
:param prev_kv: Whether to return the deleted key-value pairs
|
|
236
|
+
:type prev_kv: bool
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
key: Any
|
|
240
|
+
range_end: Optional[Any]
|
|
241
|
+
prev_kv: bool
|
|
242
|
+
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
key: Any,
|
|
246
|
+
range_end: Optional[Any] = None,
|
|
247
|
+
prev_kv: bool = False,
|
|
248
|
+
) -> None:
|
|
249
|
+
self.key = key
|
|
250
|
+
self.range_end = range_end
|
|
251
|
+
self.prev_kv = prev_kv
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class Txn:
|
|
255
|
+
"""A transaction with conditional operations.
|
|
256
|
+
|
|
257
|
+
Transactions allow atomic execution of a compare sequence
|
|
258
|
+
followed by success/failure operation lists.
|
|
259
|
+
|
|
260
|
+
:param compare: List of comparison operations
|
|
261
|
+
:type compare: list of BaseCompare
|
|
262
|
+
:param success: List of operations if all comparisons succeed
|
|
263
|
+
:type success: list of Put, Get, Delete, or Txn, optional
|
|
264
|
+
:param failure: List of operations if any comparison fails
|
|
265
|
+
:type failure: list of Put, Get, Delete, or Txn, optional
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
compare: list
|
|
269
|
+
success: Optional[list]
|
|
270
|
+
failure: Optional[list]
|
|
271
|
+
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
compare: list,
|
|
275
|
+
success: Optional[list] = None,
|
|
276
|
+
failure: Optional[list] = None,
|
|
277
|
+
) -> None:
|
|
278
|
+
self.compare = compare
|
|
279
|
+
self.success = success
|
|
280
|
+
self.failure = failure
|