altcodepro-polydb-python 2.5.3__py3-none-any.whl → 2.5.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.
@@ -23,10 +23,12 @@ class DynamoDBAdapter(NoSQLKVAdapter):
23
23
  Production-grade DynamoDB adapter with optional S3 overflow.
24
24
 
25
25
  Goals (matches your adapter test style)
26
- - stored row keeps "id" == pk
27
- - query({"id": ...}) works
26
+ - stored row keeps "id" == rk (the row key -- see NoSQLKVAdapter._get_pk_rk,
27
+ which derives rk from data.get(rk_field, ...) with rk_field defaulting
28
+ to "id"); put() returns the full stored record, not just {"id": ...}
29
+ - query({"id": ...}) works (matches the record's own "id"/rk, not PK)
28
30
  - patch() merges (handled by NoSQLKVAdapter.patch)
29
- - delete() returns {"id": <pk>} and raises sqlite3.DatabaseError on missing
31
+ - delete() returns {"id": <rk>} and raises sqlite3.DatabaseError on missing
30
32
  - query_page() uses DynamoDB LastEvaluatedKey token (stable)
31
33
  - LocalStack support (endpoint_url) + auto create table/bucket in test/dev
32
34
  """
@@ -273,16 +275,24 @@ class DynamoDBAdapter(NoSQLKVAdapter):
273
275
  payload["PK"] = pk
274
276
  payload["SK"] = rk
275
277
 
276
- # Ensure "id" field exists for tests/querying
277
- payload.setdefault("id", pk)
278
+ # "id" is the row key (NoSQLKVAdapter._get_pk_rk derives `rk` from
279
+ # data.get(rk_field, ...) with rk_field defaulting to "id"), so it
280
+ # is already present in `payload` via dict(data) above whenever the
281
+ # caller supplied one. Only fall back to `rk` -- never `pk`, which
282
+ # would silently collide every row in the same partition onto the
283
+ # same "id" and break id-addressed lookups (query/get/patch/delete
284
+ # by {"id": ...}), the same corruption previously present here and
285
+ # in VercelKVAdapter.
286
+ payload.setdefault("id", rk)
278
287
  payload.setdefault("_pk", pk)
279
288
  payload.setdefault("_rk", rk)
280
289
 
281
290
  ref = self._maybe_overflow_to_s3(model, pk, rk, payload)
282
291
  table.put_item(Item=ref if ref is not None else payload)
283
292
 
284
- # ✅ match pattern: put returns {"id": pk}
285
- return {"id": pk}
293
+ # Return the full stored record, not just {"id": pk} -- callers
294
+ # (e.g. DatabaseFactory.create()) use this as the created row.
295
+ return payload
286
296
 
287
297
  except Exception as e:
288
298
  raise NoSQLError(f"DynamoDB put failed: {e}")
@@ -297,7 +307,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
297
307
  if not item:
298
308
  return None
299
309
 
300
- item.setdefault("id", pk)
310
+ item.setdefault("id", rk)
301
311
  return self._resolve_overflow(item)
302
312
 
303
313
  except Exception as e:
@@ -318,8 +328,11 @@ class DynamoDBAdapter(NoSQLKVAdapter):
318
328
  table = self._get_table(model)
319
329
  filters = filters or {}
320
330
 
321
- # Treat 'id' as PK by convention (since we store PK=pk=id in NoSQLKVAdapter by default)
322
- pk_value = filters.get("PK") or filters.get("partition_key") or filters.get("id")
331
+ # "id" is the row key by convention (see _put_raw), not the
332
+ # partition key -- only route to Query when the caller names the
333
+ # actual PK explicitly. A bare {"id": ...} filter falls through to
334
+ # Scan below with "id" applied as a normal attribute filter.
335
+ pk_value = filters.get("PK") or filters.get("partition_key")
323
336
 
324
337
  if pk_value is not None:
325
338
  key_cond = Key("PK").eq(str(pk_value))
@@ -330,7 +343,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
330
343
 
331
344
  # Other filters as FilterExpression
332
345
  other = {
333
- k: v for k, v in filters.items() if k not in ("PK", "SK", "partition_key", "id")
346
+ k: v for k, v in filters.items() if k not in ("PK", "SK", "partition_key")
334
347
  }
335
348
  if other:
336
349
  expr = None
@@ -361,7 +374,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
361
374
 
362
375
  out: List[JsonDict] = []
363
376
  for it in items:
364
- it.setdefault("id", it.get("_pk") or it.get("PK"))
377
+ it.setdefault("id", it.get("_rk") or it.get("SK"))
365
378
  out.append(self._resolve_overflow(it))
366
379
  return out
367
380
 
@@ -392,7 +405,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
392
405
  pass
393
406
 
394
407
  table.delete_item(Key={"PK": pk, "SK": rk})
395
- return {"id": pk}
408
+ return {"id": rk}
396
409
 
397
410
  except DatabaseError:
398
411
  raise
@@ -427,7 +440,9 @@ class DynamoDBAdapter(NoSQLKVAdapter):
427
440
 
428
441
  start_key = self._decode_token(continuation_token) if continuation_token else None
429
442
 
430
- pk_value = query.get("PK") or query.get("partition_key") or query.get("id")
443
+ # "id" is the row key by convention, not the partition key --
444
+ # see _query_raw.
445
+ pk_value = query.get("PK") or query.get("partition_key")
431
446
 
432
447
  items: List[JsonDict] = []
433
448
  last_key = start_key
@@ -448,7 +463,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
448
463
  kwargs["ExclusiveStartKey"] = last_key
449
464
 
450
465
  other_filters = {
451
- k: v for k, v in query.items() if k not in ("PK", "SK", "partition_key", "id")
466
+ k: v for k, v in query.items() if k not in ("PK", "SK", "partition_key")
452
467
  }
453
468
 
454
469
  if other_filters:
@@ -501,7 +516,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
501
516
  out: List[JsonDict] = []
502
517
 
503
518
  for it in items:
504
- it.setdefault("id", it.get("_pk") or it.get("PK"))
519
+ it.setdefault("id", it.get("_rk") or it.get("SK"))
505
520
  out.append(self._resolve_overflow(it))
506
521
 
507
522
  next_tok = self._encode_token(last_key) if last_key else None
@@ -25,12 +25,21 @@ class FirestoreAdapter(NoSQLKVAdapter):
25
25
  Production-grade Firestore adapter with optional GCS overflow.
26
26
 
27
27
  Goals (matches your tests)
28
- - Document id == pk (so querying {"id": ...} works)
28
+ - stored row keeps "id" == rk (the row key -- see NoSQLKVAdapter._get_pk_rk,
29
+ which derives rk from data.get(rk_field, ...) with rk_field defaulting
30
+ to "id"), so querying {"id": ...} matches the record's own id
29
31
  - patch() merges (preserves existing fields)
30
- - delete() returns {"id": <pk>} and raises DatabaseError on missing
32
+ - delete() returns {"id": <rk>} and raises DatabaseError on missing
31
33
  - query_page() returns (rows, token) with stable pagination
32
34
  - Emulator support via FIRESTORE_EMULATOR_HOST
33
35
 
36
+ NOTE: the Firestore *document id* is still `pk` alone (see _doc_id) --
37
+ this adapter does not compose pk+rk into the physical document key the
38
+ way VercelKVAdapter/DynamoDBAdapter do. That is a pre-existing, separate
39
+ design constraint (two rows sharing the same pk collide onto one
40
+ document) left as-is here since fixing it is a bigger structural change
41
+ this pass didn't verify against a live Firestore/emulator.
42
+
34
43
  NOTE: google-cloud-firestore / google-cloud-storage are imported lazily
35
44
  inside the methods that use them, so installing them is only required when
36
45
  this adapter is actually used.
@@ -118,7 +127,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
118
127
  return self._client.collection(self._collection_name(model))
119
128
 
120
129
  def _doc_id(self, pk: str) -> str:
121
- # IMPORTANT for tests: doc_id == pk == row["id"]
130
+ # Document id is the partition key alone (not composed with rk).
122
131
  return str(pk)
123
132
 
124
133
  def _blob_key(self, model: type, pk: str, rk: str, checksum: str) -> str:
@@ -148,7 +157,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
148
157
  blob.upload_from_string(data_bytes)
149
158
 
150
159
  ref: JsonDict = {
151
- "id": pk,
160
+ "id": rk,
152
161
  "_pk": pk,
153
162
  "_rk": rk,
154
163
  "_overflow": True,
@@ -203,9 +212,15 @@ class FirestoreAdapter(NoSQLKVAdapter):
203
212
  collection = self._get_collection(model)
204
213
  doc_id = self._doc_id(pk)
205
214
 
206
- # Ensure required identifiers exist for tests and convenience
215
+ # "id" is the row key (see NoSQLKVAdapter._get_pk_rk, which derives
216
+ # rk from data.get(rk_field, ...) with rk_field defaulting to "id").
217
+ # It is already present in `payload` via dict(data) above whenever
218
+ # the caller supplied one -- only fall back to `rk`, never `pk`,
219
+ # which would silently collapse every row's "id" to its partition
220
+ # key and break id-addressed lookups (the same corruption
221
+ # previously present here and in VercelKVAdapter/DynamoDBAdapter).
207
222
  payload: JsonDict = dict(data or {})
208
- payload["id"] = pk
223
+ payload.setdefault("id", rk)
209
224
  payload["_pk"] = pk
210
225
  payload["_rk"] = rk
211
226
 
@@ -215,7 +230,8 @@ class FirestoreAdapter(NoSQLKVAdapter):
215
230
  else:
216
231
  collection.document(doc_id).set(payload)
217
232
 
218
- return {"id": pk}
233
+ # Return the full stored record, not just {"id": pk}.
234
+ return overflow_ref if overflow_ref is not None else payload
219
235
 
220
236
  except Exception as e:
221
237
  raise NoSQLError(f"Firestore put failed: {e}")
@@ -231,7 +247,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
231
247
  return None
232
248
 
233
249
  doc_data = snap.to_dict() or {}
234
- doc_data.setdefault("id", pk)
250
+ doc_data.setdefault("id", rk)
235
251
 
236
252
  return self._resolve_overflow(doc_data)
237
253
 
@@ -275,8 +291,9 @@ class FirestoreAdapter(NoSQLKVAdapter):
275
291
  out: List[JsonDict] = []
276
292
  for d in docs:
277
293
  row = d.to_dict() or {}
278
- # Ensure id present for tests
279
- row.setdefault("id", row.get("_pk") or d.id)
294
+ # Fall back to the row key, not the partition key, when "id"
295
+ # is absent from the stored document (legacy/incomplete data).
296
+ row.setdefault("id", row.get("_rk") or d.id)
280
297
  out.append(self._resolve_overflow(row))
281
298
  return out
282
299
 
@@ -288,7 +305,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
288
305
  """
289
306
  Test expectations:
290
307
  - deleting nonexistent raises sqlite3.DatabaseError
291
- - delete returns {"id": pk}
308
+ - delete returns {"id": rk}
292
309
  - deletes overflow blob if present
293
310
  """
294
311
  try:
@@ -311,7 +328,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
311
328
  pass
312
329
 
313
330
  collection.document(doc_id).delete()
314
- return {"id": pk}
331
+ return {"id": rk}
315
332
 
316
333
  except DatabaseError:
317
334
  raise
@@ -355,7 +372,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
355
372
  rows: List[JsonDict] = []
356
373
  for d in docs:
357
374
  row = d.to_dict() or {}
358
- row.setdefault("id", row.get("_pk") or d.id)
375
+ row.setdefault("id", row.get("_rk") or d.id)
359
376
  rows.append(self._resolve_overflow(row))
360
377
 
361
378
  next_token = None
@@ -237,3 +237,49 @@ class GCPPubSubAdapter(QueueAdapter):
237
237
 
238
238
  except Exception as e:
239
239
  raise QueueError(f"Pub/Sub ack failed: {e}")
240
+
241
+ # ---------------------------------------------------------
242
+ # extend -- real GCP Pub/Sub ModifyAckDeadline. delay()/cancel() are
243
+ # NOT overridden: a published Pub/Sub message is deliverable
244
+ # immediately (no "don't make this visible until N seconds from
245
+ # now" primitive exists), so the base class's NotImplementedError
246
+ # is the honest answer for both.
247
+ # ---------------------------------------------------------
248
+
249
+ # Pub/Sub's own real, documented maximum ack deadline. Google's API
250
+ # rejects anything above this with an invalid-argument error, so
251
+ # enforce it here rather than leaving callers to hit an opaque
252
+ # remote error.
253
+ MAX_ACK_DEADLINE_SECONDS = 600
254
+
255
+ @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
256
+ def extend(
257
+ self, ack_id: str, queue_name: str = "default", *, visibility_timeout: int = 30
258
+ ) -> bool:
259
+ """Real Pub/Sub ModifyAckDeadline -- ack_id is the same ack_id
260
+ receive() returned for this message."""
261
+ if not ack_id:
262
+ raise QueueError("ack_id is required for Pub/Sub extend")
263
+ if visibility_timeout > self.MAX_ACK_DEADLINE_SECONDS:
264
+ raise QueueError(
265
+ f"Pub/Sub ack deadline cannot exceed {self.MAX_ACK_DEADLINE_SECONDS}s "
266
+ f"(GCP's own real maximum), got {visibility_timeout}"
267
+ )
268
+ try:
269
+ if not self._subscriber:
270
+ raise ConnectionError("Pub/Sub subscriber not initialized")
271
+
272
+ _, subscription = self._resolve_names(queue_name)
273
+ sub_path = self._subscription_path(subscription)
274
+
275
+ self._subscriber.modify_ack_deadline(
276
+ request={
277
+ "subscription": sub_path,
278
+ "ack_ids": [ack_id],
279
+ "ack_deadline_seconds": visibility_timeout,
280
+ }
281
+ )
282
+ return True
283
+
284
+ except Exception as e:
285
+ raise QueueError(f"Pub/Sub extend failed: {e}")
@@ -0,0 +1,79 @@
1
+ # src/polydb/adapters/GCPSecretManagerAdapter.py
2
+ import os
3
+ from typing import Optional
4
+
5
+ from ..base.SecretsAdapter import SecretsAdapter
6
+ from ..errors import ConnectionError
7
+ from ..retry import retry
8
+
9
+
10
+ class GCPSecretManagerAdapter(SecretsAdapter):
11
+ def __init__(self, project_id: str = ""):
12
+ super().__init__()
13
+
14
+ self.project_id = project_id or os.getenv("GOOGLE_CLOUD_PROJECT")
15
+ if not self.project_id:
16
+ raise ConnectionError("GOOGLE_CLOUD_PROJECT is not configured")
17
+
18
+ self._client = None
19
+ self._initialize_client()
20
+
21
+ def _initialize_client(self) -> None:
22
+ from google.cloud import secretmanager
23
+
24
+ self._client = secretmanager.SecretManagerServiceClient()
25
+
26
+ def _secret_path(self, key: str) -> str:
27
+ return f"projects/{self.project_id}/secrets/{key}"
28
+
29
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
30
+ def get_secret(self, key: str) -> Optional[str]:
31
+ from google.api_core.exceptions import NotFound
32
+
33
+ try:
34
+ resp = self._client.access_secret_version(
35
+ name=f"{self._secret_path(key)}/versions/latest"
36
+ )
37
+ except NotFound:
38
+ return None
39
+ return resp.payload.data.decode("utf-8")
40
+
41
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
42
+ def set_secret(self, key: str, value: str) -> None:
43
+ from google.api_core.exceptions import AlreadyExists
44
+
45
+ try:
46
+ self._client.create_secret(
47
+ request={
48
+ "parent": f"projects/{self.project_id}",
49
+ "secret_id": key,
50
+ "secret": {"replication": {"automatic": {}}},
51
+ }
52
+ )
53
+ except AlreadyExists:
54
+ pass
55
+ self._client.add_secret_version(
56
+ request={
57
+ "parent": self._secret_path(key),
58
+ "payload": {"data": value.encode("utf-8")},
59
+ }
60
+ )
61
+
62
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
63
+ def delete_secret(self, key: str) -> bool:
64
+ from google.api_core.exceptions import NotFound
65
+
66
+ try:
67
+ self._client.delete_secret(request={"name": self._secret_path(key)})
68
+ return True
69
+ except NotFound:
70
+ return False
71
+
72
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
73
+ def list_secrets(self, prefix: str = "") -> list[str]:
74
+ secrets = self._client.list_secrets(request={"parent": f"projects/{self.project_id}"})
75
+ return [
76
+ s.name.rsplit("/", 1)[-1]
77
+ for s in secrets
78
+ if s.name.rsplit("/", 1)[-1].startswith(prefix)
79
+ ]