altcodepro-polydb-python 2.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.
Files changed (51) hide show
  1. altcodepro_polydb_python-2.1.0.dist-info/METADATA +378 -0
  2. altcodepro_polydb_python-2.1.0.dist-info/RECORD +51 -0
  3. altcodepro_polydb_python-2.1.0.dist-info/WHEEL +5 -0
  4. altcodepro_polydb_python-2.1.0.dist-info/licenses/LICENSE +21 -0
  5. altcodepro_polydb_python-2.1.0.dist-info/top_level.txt +1 -0
  6. polydb/__init__.py +64 -0
  7. polydb/adapters/AzureBlobStorageAdapter.py +77 -0
  8. polydb/adapters/AzureFileStorageAdapter.py +79 -0
  9. polydb/adapters/AzureQueueAdapter.py +61 -0
  10. polydb/adapters/AzureTableStorageAdapter.py +182 -0
  11. polydb/adapters/DynamoDBAdapter.py +216 -0
  12. polydb/adapters/EFSAdapter.py +50 -0
  13. polydb/adapters/FirestoreAdapter.py +193 -0
  14. polydb/adapters/GCPStorageAdapter.py +81 -0
  15. polydb/adapters/MongoDBAdapter.py +136 -0
  16. polydb/adapters/PostgreSQLAdapter.py +453 -0
  17. polydb/adapters/PubSubAdapter.py +83 -0
  18. polydb/adapters/S3Adapter.py +86 -0
  19. polydb/adapters/S3CompatibleAdapter.py +90 -0
  20. polydb/adapters/SQSAdapter.py +84 -0
  21. polydb/adapters/VercelKVAdapter.py +327 -0
  22. polydb/adapters/__init__.py +0 -0
  23. polydb/advanced_query.py +147 -0
  24. polydb/audit/AuditStorage.py +136 -0
  25. polydb/audit/__init__.py +7 -0
  26. polydb/audit/context.py +53 -0
  27. polydb/audit/manager.py +47 -0
  28. polydb/audit/models.py +86 -0
  29. polydb/base/NoSQLKVAdapter.py +301 -0
  30. polydb/base/ObjectStorageAdapter.py +42 -0
  31. polydb/base/QueueAdapter.py +27 -0
  32. polydb/base/SharedFilesAdapter.py +32 -0
  33. polydb/base/__init__.py +0 -0
  34. polydb/batch.py +163 -0
  35. polydb/cache.py +204 -0
  36. polydb/databaseFactory.py +748 -0
  37. polydb/decorators.py +21 -0
  38. polydb/errors.py +82 -0
  39. polydb/factory.py +107 -0
  40. polydb/models.py +39 -0
  41. polydb/monitoring.py +313 -0
  42. polydb/multitenancy.py +197 -0
  43. polydb/py.typed +0 -0
  44. polydb/query.py +150 -0
  45. polydb/registry.py +71 -0
  46. polydb/retry.py +76 -0
  47. polydb/schema.py +205 -0
  48. polydb/security.py +458 -0
  49. polydb/types.py +127 -0
  50. polydb/utils.py +61 -0
  51. polydb/validation.py +131 -0
@@ -0,0 +1,136 @@
1
+ # src/polydb/audit/AuditStorage.py
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ from typing import Optional, Dict, Any
6
+
7
+ from .models import AuditRecord
8
+ from ..factory import CloudDatabaseFactory
9
+
10
+
11
+ class AuditStorage:
12
+ """Audit log with distributed-safe hash chaining"""
13
+
14
+ _lock = threading.Lock()
15
+
16
+ def __init__(self):
17
+ self.factory = CloudDatabaseFactory()
18
+ self.sql = self.factory.get_sql()
19
+ self._ensure_table()
20
+
21
+ def _ensure_table(self):
22
+ """Create audit table if not exists"""
23
+ try:
24
+ schema = """
25
+ CREATE TABLE IF NOT EXISTS polydb_audit_log (
26
+ audit_id UUID PRIMARY KEY,
27
+ timestamp TIMESTAMP NOT NULL,
28
+ tenant_id VARCHAR(255),
29
+ actor_id VARCHAR(255),
30
+ roles TEXT[],
31
+ action VARCHAR(50) NOT NULL,
32
+ model VARCHAR(255) NOT NULL,
33
+ entity_id VARCHAR(255),
34
+ storage_type VARCHAR(20) NOT NULL,
35
+ provider VARCHAR(50) NOT NULL,
36
+ success BOOLEAN NOT NULL,
37
+ before JSONB,
38
+ after JSONB,
39
+ changed_fields TEXT[],
40
+ trace_id VARCHAR(255),
41
+ request_id VARCHAR(255),
42
+ ip_address VARCHAR(45),
43
+ user_agent TEXT,
44
+ error TEXT,
45
+ hash VARCHAR(64) NOT NULL,
46
+ previous_hash VARCHAR(64),
47
+ created_at TIMESTAMP DEFAULT NOW()
48
+ );
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_audit_tenant_timestamp
51
+ ON polydb_audit_log(tenant_id, timestamp DESC);
52
+ CREATE INDEX IF NOT EXISTS idx_audit_model_entity
53
+ ON polydb_audit_log(model, entity_id);
54
+ CREATE INDEX IF NOT EXISTS idx_audit_actor
55
+ ON polydb_audit_log(actor_id, timestamp DESC);
56
+ CREATE INDEX IF NOT EXISTS idx_audit_hash_chain
57
+ ON polydb_audit_log(tenant_id, timestamp DESC, previous_hash);
58
+ """
59
+
60
+ self.sql.execute(schema)
61
+ except Exception:
62
+ # Table may already exist
63
+ pass
64
+
65
+ def get_last_hash(self, tenant_id: Optional[str]) -> Optional[str]:
66
+ """Get most recent hash with strict ordering (distributed-safe)"""
67
+ with self._lock:
68
+ try:
69
+ from ..query import QueryBuilder, Operator
70
+
71
+ builder = QueryBuilder()
72
+
73
+ if tenant_id is not None:
74
+ builder.where('tenant_id', Operator.EQ, tenant_id)
75
+
76
+ builder.order_by('timestamp', descending=True).take(1)
77
+
78
+ results = self.sql.query_linq('polydb_audit_log', builder)
79
+
80
+ if results and len(results) > 0:
81
+ return results[0].get('hash')
82
+
83
+ return None
84
+ except Exception:
85
+ return None
86
+
87
+ def persist(self, record: AuditRecord) -> None:
88
+ """Persist with lock to ensure chain integrity"""
89
+ with self._lock:
90
+ self.sql.insert('polydb_audit_log', {
91
+ 'audit_id': record.audit_id,
92
+ 'timestamp': record.timestamp,
93
+ 'tenant_id': record.tenant_id,
94
+ 'actor_id': record.actor_id,
95
+ 'roles': record.roles,
96
+ 'action': record.action,
97
+ 'model': record.model,
98
+ 'entity_id': record.entity_id,
99
+ 'storage_type': record.storage_type,
100
+ 'provider': record.provider,
101
+ 'success': record.success,
102
+ 'before': record.before,
103
+ 'after': record.after,
104
+ 'changed_fields': record.changed_fields,
105
+ 'trace_id': record.trace_id,
106
+ 'request_id': record.request_id,
107
+ 'ip_address': record.ip_address,
108
+ 'user_agent': record.user_agent,
109
+ 'error': record.error,
110
+ 'hash': record.hash,
111
+ 'previous_hash': record.previous_hash,
112
+ })
113
+
114
+ def verify_chain(self, tenant_id: Optional[str] = None) -> bool:
115
+ """Verify hash chain integrity"""
116
+ from ..query import QueryBuilder, Operator
117
+
118
+ builder = QueryBuilder()
119
+
120
+ if tenant_id is not None:
121
+ builder.where('tenant_id', Operator.EQ, tenant_id)
122
+
123
+ builder.order_by('timestamp', descending=False)
124
+
125
+ records = self.sql.query_linq('polydb_audit_log', builder)
126
+
127
+ if not records:
128
+ return True
129
+
130
+ prev_hash = None
131
+ for record in records:
132
+ if record.get('previous_hash') != prev_hash:
133
+ return False
134
+ prev_hash = record.get('hash')
135
+
136
+ return True
@@ -0,0 +1,7 @@
1
+ # src/polydb/audit/__init__.py
2
+ from .models import AuditRecord
3
+ from .context import AuditContext
4
+ from .manager import AuditManager
5
+ from .AuditStorage import AuditStorage
6
+
7
+ __all__ = ['AuditRecord', 'AuditContext', 'AuditManager', 'AuditStorage']
@@ -0,0 +1,53 @@
1
+ # src/polydb/audit/AuditContext.py
2
+ from contextvars import ContextVar
3
+ from typing import Optional, List
4
+
5
+ class AuditContext:
6
+ """Context variables for audit trail"""
7
+
8
+ actor_id: ContextVar[Optional[str]] = ContextVar("actor_id", default=None)
9
+ roles: ContextVar[List[str]] = ContextVar("roles", default=[])
10
+ tenant_id: ContextVar[Optional[str]] = ContextVar("tenant_id", default=None)
11
+ trace_id: ContextVar[Optional[str]] = ContextVar("trace_id", default=None)
12
+ request_id: ContextVar[Optional[str]] = ContextVar("request_id", default=None)
13
+ ip_address: ContextVar[Optional[str]] = ContextVar("ip_address", default=None)
14
+ user_agent: ContextVar[Optional[str]] = ContextVar("user_agent", default=None)
15
+
16
+ @classmethod
17
+ def set(
18
+ cls,
19
+ *,
20
+ actor_id: Optional[str] = None,
21
+ roles: Optional[List[str]] = None,
22
+ tenant_id: Optional[str] = None,
23
+ trace_id: Optional[str] = None,
24
+ request_id: Optional[str] = None,
25
+ ip_address: Optional[str] = None,
26
+ user_agent: Optional[str] = None,
27
+ ):
28
+ """Set audit context for current request"""
29
+ if actor_id is not None:
30
+ cls.actor_id.set(actor_id)
31
+ if roles is not None:
32
+ cls.roles.set(roles)
33
+ if tenant_id is not None:
34
+ cls.tenant_id.set(tenant_id)
35
+ if trace_id is not None:
36
+ cls.trace_id.set(trace_id)
37
+ if request_id is not None:
38
+ cls.request_id.set(request_id)
39
+ if ip_address is not None:
40
+ cls.ip_address.set(ip_address)
41
+ if user_agent is not None:
42
+ cls.user_agent.set(user_agent)
43
+
44
+ @classmethod
45
+ def clear(cls):
46
+ """Clear all context variables"""
47
+ cls.actor_id.set(None)
48
+ cls.roles.set([])
49
+ cls.tenant_id.set(None)
50
+ cls.trace_id.set(None)
51
+ cls.request_id.set(None)
52
+ cls.ip_address.set(None)
53
+ cls.user_agent.set(None)
@@ -0,0 +1,47 @@
1
+ # src/polydb/audit/manager.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, Dict, Any, List
5
+
6
+ from .models import AuditRecord
7
+ from .AuditStorage import AuditStorage
8
+ from .context import AuditContext
9
+
10
+
11
+ class AuditManager:
12
+ def __init__(self):
13
+ self.storage = AuditStorage()
14
+
15
+ def record(
16
+ self,
17
+ *,
18
+ action: str,
19
+ model: str,
20
+ entity_id: Optional[str],
21
+ storage_type: str,
22
+ provider: str,
23
+ success: bool,
24
+ before: Optional[Dict[str, Any]],
25
+ after: Optional[Dict[str, Any]],
26
+ error: Optional[str],
27
+ changed_fields: List[str] | None
28
+ ) -> None:
29
+ tenant_id = AuditContext.tenant_id.get()
30
+ previous_hash = self.storage.get_last_hash(tenant_id)
31
+
32
+ record = AuditRecord.create(
33
+ action=action,
34
+ model=model,
35
+ entity_id=entity_id,
36
+ storage_type=storage_type,
37
+ provider=provider,
38
+ success=success,
39
+ before=before,
40
+ after=after,
41
+ changed_fields=changed_fields,
42
+ error=error,
43
+ context=AuditContext,
44
+ previous_hash=previous_hash,
45
+ )
46
+
47
+ self.storage.persist(record)
polydb/audit/models.py ADDED
@@ -0,0 +1,86 @@
1
+ # src/polydb/audit/models.py
2
+
3
+ from dataclasses import dataclass, asdict
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional
6
+ import uuid
7
+ import hashlib
8
+ import json
9
+
10
+
11
+ @dataclass
12
+ class AuditRecord:
13
+ audit_id: str
14
+ timestamp: str
15
+ tenant_id: Optional[str]
16
+ actor_id: Optional[str]
17
+ roles: List[str]
18
+ action: str
19
+ model: str
20
+ entity_id: Optional[str]
21
+ storage_type: str
22
+ provider: str
23
+ success: bool
24
+
25
+ before: Optional[Dict[str, Any]]
26
+ after: Optional[Dict[str, Any]]
27
+ changed_fields: Optional[List[str]]
28
+
29
+ trace_id: Optional[str]
30
+ request_id: Optional[str]
31
+ ip_address: Optional[str]
32
+ user_agent: Optional[str]
33
+
34
+ error: Optional[str]
35
+
36
+ hash: Optional[str] = None
37
+ previous_hash: Optional[str] = None
38
+
39
+ @classmethod
40
+ def create(
41
+ cls,
42
+ *,
43
+ action: str,
44
+ model: str,
45
+ entity_id: Optional[str],
46
+ storage_type: str,
47
+ provider: str,
48
+ success: bool,
49
+ before: Optional[Dict[str, Any]],
50
+ after: Optional[Dict[str, Any]],
51
+ changed_fields: Optional[List[str]],
52
+ error: Optional[str],
53
+ context,
54
+ previous_hash: Optional[str] = None,
55
+ ):
56
+ now = datetime.utcnow().isoformat()
57
+ audit_id = str(uuid.uuid4())
58
+
59
+ record = cls(
60
+ audit_id=audit_id,
61
+ timestamp=now,
62
+ tenant_id=context.tenant_id.get(),
63
+ actor_id=context.actor_id.get(),
64
+ roles=context.roles.get(),
65
+ action=action,
66
+ model=model,
67
+ entity_id=entity_id,
68
+ storage_type=storage_type,
69
+ provider=provider,
70
+ success=success,
71
+ before=before,
72
+ after=after,
73
+ changed_fields=changed_fields,
74
+ trace_id=context.trace_id.get(),
75
+ request_id=context.request_id.get(),
76
+ ip_address=context.ip_address.get(),
77
+ user_agent=context.user_agent.get(),
78
+ error=error,
79
+ previous_hash=previous_hash,
80
+ )
81
+
82
+ record.hash = hashlib.sha256(
83
+ json.dumps(asdict(record), sort_keys=True).encode()
84
+ ).hexdigest()
85
+
86
+ return record
@@ -0,0 +1,301 @@
1
+ # src/polydb/adapters/NoSQLKVAdapter.py
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import threading
7
+ from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
8
+
9
+ from ..errors import NoSQLError, StorageError
10
+ from ..retry import retry
11
+ from ..query import QueryBuilder, Operator
12
+ from ..types import JsonDict, Lookup
13
+
14
+ if TYPE_CHECKING:
15
+ from ..models import PartitionConfig
16
+
17
+
18
+ class NoSQLKVAdapter:
19
+ """Base with auto-overflow and LINQ support"""
20
+
21
+ def __init__(self, partition_config: Optional[PartitionConfig] = None):
22
+ from ..utils import setup_logger
23
+ self.logger = setup_logger(self.__class__.__name__)
24
+ self.partition_config = partition_config
25
+ self.object_storage = None
26
+ self._lock = threading.Lock()
27
+ self.max_size = 1024 * 1024 # 1MB
28
+
29
+ def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
30
+ """Extract PK/RK from model metadata"""
31
+ meta = getattr(model, '__polydb__', {})
32
+ pk_field = meta.get('pk_field', 'id')
33
+ rk_field = meta.get('rk_field')
34
+
35
+ if self.partition_config:
36
+ try:
37
+ pk = self.partition_config.partition_key_template.format(**data)
38
+ except KeyError:
39
+ pk = f"default_{data.get(pk_field, hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()[:8])}"
40
+ else:
41
+ pk = str(data.get(pk_field, 'default'))
42
+
43
+ if rk_field and rk_field in data:
44
+ rk = str(data[rk_field])
45
+ elif self.partition_config and self.partition_config.row_key_template:
46
+ try:
47
+ rk = self.partition_config.row_key_template.format(**data)
48
+ except KeyError:
49
+ rk = hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()
50
+ else:
51
+ rk = data.get('id', hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest())
52
+
53
+ return str(pk), str(rk)
54
+
55
+ @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
56
+ def _check_overflow(self, data: JsonDict) -> Tuple[JsonDict, Optional[str]]:
57
+ """Check size and store in blob if needed"""
58
+ data_bytes = json.dumps(data).encode()
59
+ data_size = len(data_bytes)
60
+
61
+ if data_size > self.max_size:
62
+ with self._lock:
63
+ if not self.object_storage:
64
+ from ..factory import CloudDatabaseFactory
65
+ factory = CloudDatabaseFactory()
66
+ self.object_storage = factory.get_object_storage()
67
+
68
+ blob_id = hashlib.md5(data_bytes).hexdigest()
69
+ blob_key = f"overflow/{blob_id}.json"
70
+
71
+ try:
72
+ self.object_storage.put(blob_key, data_bytes)
73
+ except Exception as e:
74
+ raise StorageError(f"Overflow storage failed: {str(e)}")
75
+
76
+ return {
77
+ "_overflow": True,
78
+ "_blob_key": blob_key,
79
+ "_size": data_size,
80
+ "_checksum": blob_id
81
+ }, blob_key
82
+
83
+ return data, None
84
+
85
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
86
+ def _retrieve_overflow(self, data: JsonDict) -> JsonDict:
87
+ """Retrieve from blob if overflow"""
88
+ if not data.get("_overflow"):
89
+ return data
90
+
91
+ with self._lock:
92
+ if not self.object_storage:
93
+ from ..factory import CloudDatabaseFactory
94
+ factory = CloudDatabaseFactory()
95
+ self.object_storage = factory.get_object_storage()
96
+
97
+ try:
98
+ blob_data = self.object_storage.get(data["_blob_key"])
99
+ retrieved = json.loads(blob_data.decode())
100
+
101
+ # Verify checksum
102
+ checksum = hashlib.md5(blob_data).hexdigest()
103
+ if checksum != data.get("_checksum"):
104
+ raise StorageError("Checksum mismatch on overflow retrieval")
105
+
106
+ return retrieved
107
+ except Exception as e:
108
+ raise StorageError(f"Overflow retrieval failed: {str(e)}")
109
+
110
+ def _apply_filters(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
111
+ """Apply filters in-memory for NoSQL"""
112
+ if not builder.filters:
113
+ return results
114
+
115
+ filtered = []
116
+ for item in results:
117
+ match = True
118
+ for f in builder.filters:
119
+ value = item.get(f.field)
120
+
121
+ if f.operator == Operator.EQ and value != f.value:
122
+ match = False
123
+ elif f.operator == Operator.NE and value == f.value:
124
+ match = False
125
+ elif f.operator == Operator.GT and not (value and value > f.value):
126
+ match = False
127
+ elif f.operator == Operator.GTE and not (value and value >= f.value):
128
+ match = False
129
+ elif f.operator == Operator.LT and not (value and value < f.value):
130
+ match = False
131
+ elif f.operator == Operator.LTE and not (value and value <= f.value):
132
+ match = False
133
+ elif f.operator == Operator.IN and value not in f.value:
134
+ match = False
135
+ elif f.operator == Operator.NOT_IN and value in f.value:
136
+ match = False
137
+ elif f.operator == Operator.CONTAINS and (not value or f.value not in str(value)):
138
+ match = False
139
+ elif f.operator == Operator.STARTS_WITH and (not value or not str(value).startswith(f.value)):
140
+ match = False
141
+ elif f.operator == Operator.ENDS_WITH and (not value or not str(value).endswith(f.value)):
142
+ match = False
143
+
144
+ if not match:
145
+ break
146
+
147
+ if match:
148
+ filtered.append(item)
149
+
150
+ return filtered
151
+
152
+ def _apply_ordering(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
153
+ """Apply ordering"""
154
+ if not builder.order_by_fields:
155
+ return results
156
+
157
+ for field, desc in reversed(builder.order_by_fields):
158
+ results = sorted(
159
+ results,
160
+ key=lambda x: x.get(field, ''),
161
+ reverse=desc
162
+ )
163
+
164
+ return results
165
+
166
+ def _apply_pagination(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
167
+ """Apply skip/take"""
168
+ if builder.skip_count:
169
+ results = results[builder.skip_count:]
170
+
171
+ if builder.take_count:
172
+ results = results[:builder.take_count]
173
+
174
+ return results
175
+
176
+ def _apply_projection(self, results: List[JsonDict], builder: QueryBuilder) -> List[JsonDict]:
177
+ """Apply field selection"""
178
+ if not builder.select_fields:
179
+ return results
180
+
181
+ return [
182
+ {k: v for k, v in item.items() if k in builder.select_fields}
183
+ for item in results
184
+ ]
185
+
186
+ # Abstract methods to implement
187
+ def _put_raw(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
188
+ raise NotImplementedError
189
+
190
+ def _get_raw(self, model: type, pk: str, rk: str) -> Optional[JsonDict]:
191
+ raise NotImplementedError
192
+
193
+ def _query_raw(self, model: type, filters: Dict[str, Any], limit: Optional[int]) -> List[JsonDict]:
194
+ raise NotImplementedError
195
+
196
+ def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
197
+ raise NotImplementedError
198
+
199
+ # Protocol implementation
200
+ def put(self, model: type, data: JsonDict) -> JsonDict:
201
+ pk, rk = self._get_pk_rk(model, data)
202
+ store_data, _ = self._check_overflow(data)
203
+ return self._put_raw(model, pk, rk, store_data)
204
+
205
+ def query(
206
+ self,
207
+ model: type,
208
+ query: Optional[Lookup] = None,
209
+ limit: Optional[int] = None,
210
+ no_cache: bool = False,
211
+ cache_ttl: Optional[int] = None
212
+ ) -> List[JsonDict]:
213
+ results = self._query_raw(model, query or {}, limit)
214
+ return [self._retrieve_overflow(r) for r in results]
215
+
216
+ def query_page(
217
+ self,
218
+ model: type,
219
+ query: Lookup,
220
+ page_size: int,
221
+ continuation_token: Optional[str] = None
222
+ ) -> Tuple[List[JsonDict], Optional[str]]:
223
+ # Basic implementation - override per provider
224
+ offset = int(continuation_token) if continuation_token else 0
225
+ results = self.query(model, query, limit=page_size + 1)
226
+
227
+ has_more = len(results) > page_size
228
+ if has_more:
229
+ results = results[:page_size]
230
+
231
+ next_token = str(offset + page_size) if has_more else None
232
+ return results, next_token
233
+
234
+ def patch(
235
+ self,
236
+ model: type,
237
+ entity_id: Union[Any, Lookup],
238
+ data: JsonDict,
239
+ *,
240
+ etag: Optional[str] = None,
241
+ replace: bool = False
242
+ ) -> JsonDict:
243
+ if isinstance(entity_id, dict):
244
+ pk = entity_id.get('partition_key') or entity_id.get('pk')
245
+ rk = entity_id.get('row_key') or entity_id.get('rk') or entity_id.get('id')
246
+ else:
247
+ pk, rk = self._get_pk_rk(model, {'id': entity_id})
248
+
249
+ if not replace:
250
+ existing = self._get_raw(model, pk, rk) # type: ignore
251
+ if existing:
252
+ existing = self._retrieve_overflow(existing)
253
+ existing.update(data)
254
+ data = existing
255
+
256
+ store_data, _ = self._check_overflow(data)
257
+ return self._put_raw(model, pk, rk, store_data) # type: ignore
258
+
259
+ def upsert(self, model: type, data: JsonDict, *, replace: bool = False) -> JsonDict:
260
+ return self.put(model, data)
261
+
262
+ def delete(
263
+ self,
264
+ model: type,
265
+ entity_id: Union[Any, Lookup],
266
+ *,
267
+ etag: Optional[str] = None
268
+ ) -> JsonDict:
269
+ if isinstance(entity_id, dict):
270
+ pk = entity_id.get('partition_key') or entity_id.get('pk')
271
+ rk = entity_id.get('row_key') or entity_id.get('rk') or entity_id.get('id')
272
+ else:
273
+ pk, rk = self._get_pk_rk(model, {'id': entity_id})
274
+
275
+ return self._delete_raw(model, pk, rk, etag) # type: ignore
276
+
277
+ def query_linq(self, model: type, builder: QueryBuilder) -> Union[List[JsonDict], int]:
278
+ """LINQ-style query"""
279
+ results = self._query_raw(model, {}, None)
280
+ results = [self._retrieve_overflow(r) for r in results]
281
+
282
+ results = self._apply_filters(results, builder)
283
+
284
+ if builder.count_only:
285
+ return len(results)
286
+
287
+ results = self._apply_ordering(results, builder)
288
+ results = self._apply_pagination(results, builder)
289
+ results = self._apply_projection(results, builder)
290
+
291
+ if builder.distinct:
292
+ seen = set()
293
+ unique = []
294
+ for r in results:
295
+ key = json.dumps(r, sort_keys=True)
296
+ if key not in seen:
297
+ seen.add(key)
298
+ unique.append(r)
299
+ results = unique
300
+
301
+ return results
@@ -0,0 +1,42 @@
1
+ from polydb.utils import setup_logger
2
+ from abc import ABC, abstractmethod
3
+ from typing import List, Optional
4
+
5
+
6
+ class ObjectStorageAdapter(ABC):
7
+ """Base class for Object Storage with automatic optimization"""
8
+
9
+ def __init__(self):
10
+ self.logger = setup_logger(self.__class__.__name__)
11
+
12
+ def put(
13
+ self, key: str, data: bytes, optimize: bool = True, media_type: Optional[str] = None
14
+ ) -> str:
15
+ """Store object with optional optimization"""
16
+ if optimize and media_type:
17
+ data = self._optimize_media(data, media_type)
18
+ return self._put_raw(key, data)
19
+
20
+ def _optimize_media(self, data: bytes, media_type: str) -> bytes:
21
+ """Optimize images and videos - placeholder for implementation"""
22
+ return data
23
+
24
+ @abstractmethod
25
+ def _put_raw(self, key: str, data: bytes) -> str:
26
+ """Provider-specific put"""
27
+ pass
28
+
29
+ @abstractmethod
30
+ def get(self, key: str) -> bytes:
31
+ """Get object"""
32
+ pass
33
+
34
+ @abstractmethod
35
+ def delete(self, key: str) -> bool:
36
+ """Delete object"""
37
+ pass
38
+
39
+ @abstractmethod
40
+ def list(self, prefix: str = "") -> List[str]:
41
+ """List objects with prefix"""
42
+ pass