altcodepro-polydb-python 2.2.2__py3-none-any.whl → 2.2.4__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.
Files changed (38) hide show
  1. altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
  2. altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
  3. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
  4. polydb/__init__.py +2 -2
  5. polydb/adapters/AzureBlobStorageAdapter.py +146 -41
  6. polydb/adapters/AzureFileStorageAdapter.py +148 -43
  7. polydb/adapters/AzureQueueAdapter.py +96 -34
  8. polydb/adapters/AzureTableStorageAdapter.py +462 -119
  9. polydb/adapters/BlockchainBlobAdapter.py +111 -0
  10. polydb/adapters/BlockchainKVAdapter.py +152 -0
  11. polydb/adapters/BlockchainQueueAdapter.py +116 -0
  12. polydb/adapters/DynamoDBAdapter.py +463 -176
  13. polydb/adapters/FirestoreAdapter.py +320 -148
  14. polydb/adapters/GCPPubSubAdapter.py +217 -0
  15. polydb/adapters/GCPStorageAdapter.py +184 -39
  16. polydb/adapters/MongoDBAdapter.py +159 -39
  17. polydb/adapters/PostgreSQLAdapter.py +285 -83
  18. polydb/adapters/S3Adapter.py +172 -35
  19. polydb/adapters/S3CompatibleAdapter.py +62 -8
  20. polydb/adapters/SQSAdapter.py +121 -44
  21. polydb/adapters/VercelBlobAdapter.py +196 -0
  22. polydb/adapters/VercelKVAdapter.py +275 -283
  23. polydb/adapters/VercelQueueAdapter.py +61 -0
  24. polydb/audit/AuditStorage.py +1 -1
  25. polydb/base/NoSQLKVAdapter.py +113 -101
  26. polydb/base/ObjectStorageAdapter.py +42 -6
  27. polydb/base/QueueAdapter.py +2 -2
  28. polydb/base/SharedFilesAdapter.py +2 -2
  29. polydb/cloudDatabaseFactory.py +200 -0
  30. polydb/databaseFactory.py +434 -101
  31. polydb/models.py +63 -1
  32. polydb/query.py +111 -42
  33. altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
  34. altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
  35. polydb/adapters/PubSubAdapter.py +0 -85
  36. polydb/factory.py +0 -107
  37. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
  38. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import hashlib
5
5
  import json
6
6
  import threading
7
- from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
7
+ from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING, cast
8
8
 
9
9
  from ..json_safe import json_safe
10
10
 
@@ -19,107 +19,117 @@ if TYPE_CHECKING:
19
19
 
20
20
  class NoSQLKVAdapter:
21
21
  """Base with auto-overflow and LINQ support"""
22
-
22
+
23
23
  def __init__(self, partition_config: Optional[PartitionConfig] = None):
24
24
  from ..utils import setup_logger
25
+
25
26
  self.logger = setup_logger(self.__class__.__name__)
26
27
  self.partition_config = partition_config
27
28
  self.object_storage = None
28
29
  self._lock = threading.Lock()
29
30
  self.max_size = 1024 * 1024 # 1MB
30
-
31
+
31
32
  def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
32
33
  """Extract PK/RK from model metadata"""
33
- meta = getattr(model, '__polydb__', {})
34
- pk_field = meta.get('pk_field', 'id')
35
- rk_field = meta.get('rk_field')
36
-
34
+ meta = getattr(model, "__polydb__", {})
35
+ pk_field = meta.get("pk_field", "id")
36
+ rk_field = meta.get("rk_field")
37
+
37
38
  if self.partition_config:
38
39
  try:
39
40
  pk = self.partition_config.partition_key_template.format(**data)
40
41
  except KeyError:
41
42
  pk = f"default_{data.get(pk_field, hashlib.md5(json.dumps(data, sort_keys=True,default=json_safe).encode()).hexdigest()[:8])}"
42
43
  else:
43
- pk = str(data.get(pk_field, 'default'))
44
-
44
+ pk = str(data.get(pk_field, "default"))
45
+
45
46
  if rk_field and rk_field in data:
46
47
  rk = str(data[rk_field])
47
48
  elif self.partition_config and self.partition_config.row_key_template:
48
49
  try:
49
50
  rk = self.partition_config.row_key_template.format(**data)
50
51
  except KeyError:
51
- rk = hashlib.md5(json.dumps(data, sort_keys=True,default=json_safe).encode()).hexdigest()
52
+ rk = hashlib.md5(
53
+ json.dumps(data, sort_keys=True, default=json_safe).encode()
54
+ ).hexdigest()
52
55
  else:
53
- rk = data.get('id', hashlib.md5(json.dumps(data, sort_keys=True,default=json_safe).encode()).hexdigest())
54
-
56
+ rk = data.get(
57
+ "id",
58
+ hashlib.md5(
59
+ json.dumps(data, sort_keys=True, default=json_safe).encode()
60
+ ).hexdigest(),
61
+ )
62
+
55
63
  return str(pk), str(rk)
56
-
64
+
57
65
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
58
66
  def _check_overflow(self, data: JsonDict) -> Tuple[JsonDict, Optional[str]]:
59
67
  """Check size and store in blob if needed"""
60
- data_bytes = json.dumps(data,default=json_safe).encode()
68
+ data_bytes = json.dumps(data, default=json_safe).encode()
61
69
  data_size = len(data_bytes)
62
-
70
+
63
71
  if data_size > self.max_size:
64
72
  with self._lock:
65
73
  if not self.object_storage:
66
- from ..factory import CloudDatabaseFactory
74
+ from ..cloudDatabaseFactory import CloudDatabaseFactory
75
+
67
76
  factory = CloudDatabaseFactory()
68
77
  self.object_storage = factory.get_object_storage()
69
-
78
+
70
79
  blob_id = hashlib.md5(data_bytes).hexdigest()
71
80
  blob_key = f"overflow/{blob_id}.json"
72
-
81
+
73
82
  try:
74
83
  self.object_storage.put(blob_key, data_bytes)
75
84
  except Exception as e:
76
85
  raise StorageError(f"Overflow storage failed: {str(e)}")
77
-
86
+
78
87
  return {
79
88
  "_overflow": True,
80
89
  "_blob_key": blob_key,
81
90
  "_size": data_size,
82
- "_checksum": blob_id
91
+ "_checksum": blob_id,
83
92
  }, blob_key
84
-
93
+
85
94
  return data, None
86
-
95
+
87
96
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
88
97
  def _retrieve_overflow(self, data: JsonDict) -> JsonDict:
89
98
  """Retrieve from blob if overflow"""
90
99
  if not data.get("_overflow"):
91
100
  return data
92
-
101
+
93
102
  with self._lock:
94
103
  if not self.object_storage:
95
- from ..factory import CloudDatabaseFactory
104
+ from ..cloudDatabaseFactory import CloudDatabaseFactory
105
+
96
106
  factory = CloudDatabaseFactory()
97
107
  self.object_storage = factory.get_object_storage()
98
-
108
+
99
109
  try:
100
110
  blob_data = self.object_storage.get(data["_blob_key"])
101
111
  retrieved = json.loads(blob_data.decode())
102
-
112
+
103
113
  # Verify checksum
104
114
  checksum = hashlib.md5(blob_data).hexdigest()
105
115
  if checksum != data.get("_checksum"):
106
116
  raise StorageError("Checksum mismatch on overflow retrieval")
107
-
117
+
108
118
  return retrieved
109
119
  except Exception as e:
110
120
  raise StorageError(f"Overflow retrieval failed: {str(e)}")
111
-
121
+
112
122
  def _apply_filters(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
113
123
  """Apply filters in-memory for NoSQL"""
114
124
  if not builder.filters:
115
125
  return results
116
-
126
+
117
127
  filtered = []
118
128
  for item in results:
119
129
  match = True
120
130
  for f in builder.filters:
121
131
  value = item.get(f.field)
122
-
132
+
123
133
  if f.operator == Operator.EQ and value != f.value:
124
134
  match = False
125
135
  elif f.operator == Operator.NE and value == f.value:
@@ -138,101 +148,99 @@ class NoSQLKVAdapter:
138
148
  match = False
139
149
  elif f.operator == Operator.CONTAINS and (not value or f.value not in str(value)):
140
150
  match = False
141
- elif f.operator == Operator.STARTS_WITH and (not value or not str(value).startswith(f.value)):
151
+ elif f.operator == Operator.STARTS_WITH and (
152
+ not value or not str(value).startswith(f.value)
153
+ ):
142
154
  match = False
143
- elif f.operator == Operator.ENDS_WITH and (not value or not str(value).endswith(f.value)):
155
+ elif f.operator == Operator.ENDS_WITH and (
156
+ not value or not str(value).endswith(f.value)
157
+ ):
144
158
  match = False
145
-
159
+
146
160
  if not match:
147
161
  break
148
-
162
+
149
163
  if match:
150
164
  filtered.append(item)
151
-
165
+
152
166
  return filtered
153
-
167
+
154
168
  def _apply_ordering(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
155
169
  """Apply ordering"""
156
170
  if not builder.order_by_fields:
157
171
  return results
158
-
172
+
159
173
  for field, desc in reversed(builder.order_by_fields):
160
- results = sorted(
161
- results,
162
- key=lambda x: x.get(field, ''),
163
- reverse=desc
164
- )
165
-
174
+ results = sorted(results, key=lambda x: x.get(field, ""), reverse=desc)
175
+
166
176
  return results
167
-
177
+
168
178
  def _apply_pagination(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
169
179
  """Apply skip/take"""
170
180
  if builder.skip_count:
171
- results = results[builder.skip_count:]
172
-
181
+ results = results[builder.skip_count :]
182
+
173
183
  if builder.take_count:
174
- results = results[:builder.take_count]
175
-
184
+ results = results[: builder.take_count]
185
+
176
186
  return results
177
-
187
+
178
188
  def _apply_projection(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
179
- """Apply field selection"""
180
- if not builder.select_fields:
189
+
190
+ fields = builder.selected_fields
191
+ if not fields:
181
192
  return results
182
-
183
- return [
184
- {k: v for k, v in item.items() if k in builder.select_fields}
185
- for item in results
186
- ]
187
-
193
+
194
+ field_set = set(fields)
195
+
196
+ return [{k: v for k, v in item.items() if k in field_set} for item in results]
197
+
188
198
  # Abstract methods to implement
189
199
  def _put_raw(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
190
200
  raise NotImplementedError
191
-
201
+
192
202
  def _get_raw(self, model: type, pk: str, rk: str) -> Optional[JsonDict]:
193
203
  raise NotImplementedError
194
-
195
- def _query_raw(self, model: type, filters: Dict[str, Any], limit: Optional[int]) -> List[JsonDict]:
204
+
205
+ def _query_raw(
206
+ self, model: type, filters: Dict[str, Any], limit: Optional[int]
207
+ ) -> List[JsonDict]:
196
208
  raise NotImplementedError
197
-
209
+
198
210
  def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
199
211
  raise NotImplementedError
200
-
212
+
201
213
  # Protocol implementation
202
214
  def put(self, model: type, data: JsonDict) -> JsonDict:
203
215
  pk, rk = self._get_pk_rk(model, data)
204
216
  store_data, _ = self._check_overflow(data)
205
217
  return self._put_raw(model, pk, rk, store_data)
206
-
218
+
207
219
  def query(
208
220
  self,
209
221
  model: type,
210
222
  query: Optional[Lookup] = None,
211
223
  limit: Optional[int] = None,
212
224
  no_cache: bool = False,
213
- cache_ttl: Optional[int] = None
225
+ cache_ttl: Optional[int] = None,
214
226
  ) -> List[JsonDict]:
215
227
  results = self._query_raw(model, query or {}, limit)
216
228
  return [self._retrieve_overflow(r) for r in results]
217
-
229
+
218
230
  def query_page(
219
- self,
220
- model: type,
221
- query: Lookup,
222
- page_size: int,
223
- continuation_token: Optional[str] = None
231
+ self, model: type, query: Lookup, page_size: int, continuation_token: Optional[str] = None
224
232
  ) -> Tuple[List[JsonDict], Optional[str]]:
225
233
  # Basic implementation - override per provider
226
234
  offset = int(continuation_token) if continuation_token else 0
227
235
  results = self.query(model, query, limit=page_size + 1)
228
-
236
+
229
237
  has_more = len(results) > page_size
230
238
  if has_more:
231
239
  results = results[:page_size]
232
-
240
+
233
241
  next_token = str(offset + page_size) if has_more else None
234
242
  return results, next_token
235
-
243
+
236
244
  def patch(
237
245
  self,
238
246
  model: type,
@@ -240,64 +248,68 @@ class NoSQLKVAdapter:
240
248
  data: JsonDict,
241
249
  *,
242
250
  etag: Optional[str] = None,
243
- replace: bool = False
251
+ replace: bool = False,
244
252
  ) -> JsonDict:
245
253
  if isinstance(entity_id, dict):
246
- pk = entity_id.get('partition_key') or entity_id.get('pk')
247
- rk = entity_id.get('row_key') or entity_id.get('rk') or entity_id.get('id')
254
+ pk = entity_id.get("partition_key") or entity_id.get("pk")
255
+ rk = entity_id.get("row_key") or entity_id.get("rk") or entity_id.get("id")
248
256
  else:
249
- pk, rk = self._get_pk_rk(model, {'id': entity_id})
250
-
257
+ pk, rk = self._get_pk_rk(model, {"id": entity_id})
258
+
251
259
  if not replace:
252
- existing = self._get_raw(model, pk, rk) # type: ignore
260
+ existing = self._get_raw(model, pk, rk) # type: ignore
253
261
  if existing:
254
262
  existing = self._retrieve_overflow(existing)
255
263
  existing.update(data)
256
264
  data = existing
257
-
265
+
258
266
  store_data, _ = self._check_overflow(data)
259
- return self._put_raw(model, pk, rk, store_data) # type: ignore
260
-
267
+ return self._put_raw(model, pk, rk, store_data) # type: ignore
268
+
261
269
  def upsert(self, model: type, data: JsonDict, *, replace: bool = False) -> JsonDict:
262
270
  return self.put(model, data)
263
-
271
+
264
272
  def delete(
265
- self,
266
- model: type,
267
- entity_id: Union[Any, Lookup],
268
- *,
269
- etag: Optional[str] = None
273
+ self, model: type, entity_id: Union[Any, Lookup], *, etag: Optional[str] = None
270
274
  ) -> JsonDict:
271
275
  if isinstance(entity_id, dict):
272
- pk = entity_id.get('partition_key') or entity_id.get('pk')
273
- rk = entity_id.get('row_key') or entity_id.get('rk') or entity_id.get('id')
276
+ pk = entity_id.get("partition_key") or entity_id.get("pk")
277
+ rk = entity_id.get("row_key") or entity_id.get("rk") or entity_id.get("id")
274
278
  else:
275
- pk, rk = self._get_pk_rk(model, {'id': entity_id})
276
-
277
- return self._delete_raw(model, pk, rk, etag) # type: ignore
278
-
279
+ pk, rk = self._get_pk_rk(model, {"id": entity_id})
280
+
281
+ return self._delete_raw(model, pk, rk, etag) # type: ignore
282
+
283
+ def query_linq_rows(self, model: type, builder: QueryBuilder) -> List[JsonDict]:
284
+ """
285
+ Typed wrapper for queries that return rows.
286
+ Use this when builder.count_only is False.
287
+ """
288
+ result = self.query_linq(model, builder)
289
+ return cast(List[JsonDict], result)
290
+
279
291
  def query_linq(self, model: type, builder: QueryBuilder) -> Union[List[JsonDict], int]:
280
292
  """LINQ-style query"""
281
293
  results = self._query_raw(model, {}, None)
282
294
  results = [self._retrieve_overflow(r) for r in results]
283
-
295
+
284
296
  results = self._apply_filters(results, builder)
285
-
297
+
286
298
  if builder.count_only:
287
299
  return len(results)
288
-
300
+
289
301
  results = self._apply_ordering(results, builder)
290
302
  results = self._apply_pagination(results, builder)
291
303
  results = self._apply_projection(results, builder)
292
-
304
+
293
305
  if builder.distinct:
294
306
  seen = set()
295
307
  unique = []
296
308
  for r in results:
297
- key = json.dumps(r, sort_keys=True,default=json_safe)
309
+ key = json.dumps(r, sort_keys=True, default=json_safe)
298
310
  if key not in seen:
299
311
  seen.add(key)
300
312
  unique.append(r)
301
313
  results = unique
302
-
303
- return results
314
+
315
+ return results
@@ -1,6 +1,7 @@
1
- from polydb.utils import setup_logger
1
+ from ..errors import StorageError
2
+ from ..utils import setup_logger
2
3
  from abc import ABC, abstractmethod
3
- from typing import List, Optional
4
+ from typing import Any, Dict, List, Optional
4
5
 
5
6
 
6
7
  class ObjectStorageAdapter(ABC):
@@ -10,19 +11,34 @@ class ObjectStorageAdapter(ABC):
10
11
  self.logger = setup_logger(self.__class__.__name__)
11
12
 
12
13
  def put(
13
- self, key: str, data: bytes, optimize: bool = True, media_type: Optional[str] = None
14
+ self,
15
+ key: str,
16
+ data: bytes,
17
+ fileName: str = "",
18
+ optimize: bool = True,
19
+ media_type: Optional[str] = None,
20
+ metadata: Dict[str, Any] | None = None,
14
21
  ) -> str:
15
22
  """Store object with optional optimization"""
16
23
  if optimize and media_type:
17
24
  data = self._optimize_media(data, media_type)
18
- return self._put_raw(key, data)
25
+ return self._put_raw(
26
+ key=key, data=data, fileName=fileName, media_type=media_type, metadata=metadata
27
+ )
19
28
 
20
29
  def _optimize_media(self, data: bytes, media_type: str) -> bytes:
21
30
  """Optimize images and videos - placeholder for implementation"""
22
31
  return data
23
32
 
24
33
  @abstractmethod
25
- def _put_raw(self, key: str, data: bytes) -> str:
34
+ def _put_raw(
35
+ self,
36
+ key: str,
37
+ data: bytes,
38
+ fileName: str = "",
39
+ media_type: Optional[str] = None,
40
+ metadata: Dict[str, Any] | None = None,
41
+ ) -> str:
26
42
  """Provider-specific put"""
27
43
  pass
28
44
 
@@ -39,4 +55,24 @@ class ObjectStorageAdapter(ABC):
39
55
  @abstractmethod
40
56
  def list(self, prefix: str = "") -> List[str]:
41
57
  """List objects with prefix"""
42
- pass
58
+ pass
59
+
60
+ def upload(self, key: str, data: bytes, **kwargs) -> str:
61
+ """
62
+ Alias for put().
63
+ Accepts kwargs so callers can pass optimize/media_type without breaking.
64
+ """
65
+ return self.put(key, data, **kwargs)
66
+
67
+ def download(self, key: str) -> bytes:
68
+ """
69
+ Alias for get() but guarantees bytes or raises.
70
+ This matches how your tests expect download() to behave.
71
+ """
72
+ if not key:
73
+ raise StorageError("Key cannot be empty")
74
+
75
+ data = self.get(key)
76
+ if data is None:
77
+ raise StorageError(f"Object not found: {key}")
78
+ return data
@@ -22,6 +22,6 @@ class QueueAdapter(ABC):
22
22
  pass
23
23
 
24
24
  @abstractmethod
25
- def delete(self, message_id: str, queue_name: str = "default") -> bool:
25
+ def delete(self, message_id: str, queue_name: str = "default", pop_receipt: str = "") -> bool:
26
26
  """Delete message from queue"""
27
- pass
27
+ pass
@@ -17,7 +17,7 @@ class SharedFilesAdapter(ABC):
17
17
  pass
18
18
 
19
19
  @abstractmethod
20
- def read(self, path: str) -> bytes:
20
+ def read(self, path: str) -> bytes | None:
21
21
  """Read file"""
22
22
  pass
23
23
 
@@ -29,4 +29,4 @@ class SharedFilesAdapter(ABC):
29
29
  @abstractmethod
30
30
  def list(self, directory: str = "/") -> List[str]:
31
31
  """List files in directory"""
32
- pass
32
+ pass