altcodepro-polydb-python 2.5.9__py3-none-any.whl → 2.5.11__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 (64) hide show
  1. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/METADATA +1 -1
  2. altcodepro_polydb_python-2.5.11.dist-info/RECORD +74 -0
  3. polydb/PolyDB.py +1 -1
  4. polydb/__init__.py +25 -14
  5. polydb/adapters/AzureBlobStorageAdapter.py +2 -1
  6. polydb/adapters/AzureQueueAdapter.py +3 -5
  7. polydb/adapters/AzureTableStorageAdapter.py +27 -9
  8. polydb/adapters/BlockchainBlobAdapter.py +0 -1
  9. polydb/adapters/BlockchainFileAdapter.py +1 -1
  10. polydb/adapters/BlockchainKVAdapter.py +20 -5
  11. polydb/adapters/BlockchainQueueAdapter.py +0 -1
  12. polydb/adapters/DynamoDBAdapter.py +2 -6
  13. polydb/adapters/EFSAdapter.py +1 -1
  14. polydb/adapters/GCPFilestoreAdapter.py +1 -1
  15. polydb/adapters/GCPPubSubAdapter.py +2 -2
  16. polydb/adapters/GCPStorageAdapter.py +1 -1
  17. polydb/adapters/KafkaQueueAdapter.py +10 -16
  18. polydb/adapters/MongoDBAdapter.py +3 -5
  19. polydb/adapters/PostgreSQLAdapter.py +38 -13
  20. polydb/adapters/RabbitMQAdapter.py +2 -2
  21. polydb/adapters/S3Adapter.py +1 -3
  22. polydb/adapters/S3CompatibleAdapter.py +1 -1
  23. polydb/adapters/SQSAdapter.py +1 -2
  24. polydb/adapters/VercelBlobAdapter.py +1 -1
  25. polydb/adapters/VercelKVAdapter.py +6 -5
  26. polydb/adapters/VercelQueueAdapter.py +18 -4
  27. polydb/advanced_query.py +5 -4
  28. polydb/aio.py +198 -0
  29. polydb/audit/AuditStorage.py +14 -19
  30. polydb/audit/__init__.py +2 -2
  31. polydb/audit/context.py +6 -5
  32. polydb/audit/manager.py +3 -3
  33. polydb/audit/models.py +3 -2
  34. polydb/base/NoSQLKVAdapter.py +22 -14
  35. polydb/base/ObjectStorageAdapter.py +3 -2
  36. polydb/base/QueueAdapter.py +2 -3
  37. polydb/base/SecretsAdapter.py +2 -1
  38. polydb/base/SharedFilesAdapter.py +2 -3
  39. polydb/batch.py +38 -49
  40. polydb/cache.py +5 -2
  41. polydb/cloudDatabaseFactory.py +59 -22
  42. polydb/databaseFactory.py +7 -13
  43. polydb/errors.py +2 -0
  44. polydb/json_safe.py +1 -2
  45. polydb/models.py +1 -1
  46. polydb/monitoring.py +27 -15
  47. polydb/multitenancy.py +50 -59
  48. polydb/observability/logging.py +0 -1
  49. polydb/overflow_gc.py +179 -0
  50. polydb/query.py +3 -1
  51. polydb/retry.py +1 -1
  52. polydb/schema.py +45 -60
  53. polydb/security.py +7 -9
  54. polydb/services/__init__.py +2 -2
  55. polydb/services/compliance_service.py +10 -4
  56. polydb/services/security_service.py +9 -12
  57. polydb/types.py +28 -62
  58. polydb/utils.py +7 -8
  59. polydb/validation.py +50 -52
  60. altcodepro_polydb_python-2.5.9.dist-info/RECORD +0 -73
  61. polydb/decorators.py +0 -1040
  62. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/WHEEL +0 -0
  63. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/licenses/LICENSE +0 -0
  64. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/top_level.txt +0 -0
@@ -11,9 +11,8 @@ import os
11
11
  import threading
12
12
  from typing import Any, Dict, List, Optional
13
13
 
14
-
15
14
  from ..base.ObjectStorageAdapter import ObjectStorageAdapter
16
- from ..errors import StorageError, ConnectionError
15
+ from ..errors import ConnectionError, StorageError
17
16
  from ..retry import retry
18
17
 
19
18
 
@@ -72,7 +71,6 @@ class S3Adapter(ObjectStorageAdapter):
72
71
 
73
72
  def _ensure_bucket_exists(self):
74
73
  """Create bucket if it doesn't exist (safe for AWS + LocalStack)"""
75
- import boto3
76
74
  from botocore.exceptions import ClientError
77
75
 
78
76
  if not self._client:
@@ -5,7 +5,7 @@ import threading
5
5
  from typing import Any, Dict, List, Optional
6
6
 
7
7
  from ..base.ObjectStorageAdapter import ObjectStorageAdapter
8
- from ..errors import StorageError, ConnectionError
8
+ from ..errors import ConnectionError, StorageError
9
9
  from ..retry import retry
10
10
 
11
11
 
@@ -5,11 +5,10 @@ import os
5
5
  import threading
6
6
  from typing import Any, Dict, List
7
7
 
8
-
9
8
  from ..base.QueueAdapter import QueueAdapter
10
9
  from ..errors import ConnectionError, QueueError
11
- from ..retry import retry
12
10
  from ..json_safe import json_safe
11
+ from ..retry import retry
13
12
 
14
13
 
15
14
  class SQSAdapter(QueueAdapter):
@@ -1,5 +1,5 @@
1
- import os
2
1
  import mimetypes
2
+ import os
3
3
  from pathlib import Path
4
4
  from typing import Any, Dict, List, Optional
5
5
 
@@ -1,16 +1,17 @@
1
1
  # src/polydb/adapters/VercelKVAdapter.py
2
2
 
3
- import os
4
3
  import json
5
- import redis
4
+ import os
6
5
  from typing import Any, Dict, List, Optional, Tuple
7
6
 
7
+ import redis
8
+
9
+ from ..base.NoSQLKVAdapter import NoSQLKVAdapter
10
+ from ..errors import DatabaseError, NoSQLError
8
11
  from ..json_safe import json_safe
9
- from ..errors import NoSQLError, DatabaseError
12
+ from ..models import PartitionConfig
10
13
  from ..retry import retry
11
14
  from ..types import JsonDict
12
- from ..models import PartitionConfig
13
- from ..base.NoSQLKVAdapter import NoSQLKVAdapter
14
15
 
15
16
 
16
17
  class VercelKVAdapter(NoSQLKVAdapter):
@@ -1,17 +1,31 @@
1
- import os
2
1
  import json
3
- import requests
2
+ import os
3
+ from typing import Any, Dict, List, Optional
4
+
4
5
  import redis
5
- from typing import Dict, Any, List, Optional
6
+ import requests
6
7
 
7
8
  from ..base.QueueAdapter import QueueAdapter
8
9
  from ..errors import QueueError
9
10
  from ..retry import retry
10
11
 
11
12
 
12
- class VercelQueueAdapter:
13
+ class VercelQueueAdapter(QueueAdapter):
14
+ """Every sibling queue adapter (SQS/Azure/RabbitMQ) extends the real
15
+ QueueAdapter(ABC) contract -- this one didn't (a real, found-while-
16
+ adding-CI-lint drift, not a deliberate design choice: `send`/
17
+ `receive`/`delete` below already satisfy every abstract method the
18
+ base requires). Without it, `nack`/`purge`/`declare`/`status`/
19
+ `extend`/`delay`/`cancel` fell through to a plain AttributeError
20
+ instead of the base's own intentional, well-named
21
+ NotImplementedError('VercelQueueAdapter does not implement ...') --
22
+ and this class silently opted out of `isinstance(adapter,
23
+ QueueAdapter)` checks. `super().__init__()` is a no-op beyond setting
24
+ `self.logger` (unused here today, same as every other adapter's own
25
+ inherited logger until something actually calls it)."""
13
26
 
14
27
  def __init__(self, url: str = "", token: str = ""):
28
+ super().__init__()
15
29
  self.url = url or os.getenv("KV_REST_API_URL")
16
30
  self.token = token or os.getenv("KV_REST_API_TOKEN")
17
31
 
polydb/advanced_query.py CHANGED
@@ -2,13 +2,14 @@
2
2
  """
3
3
  Advanced query capabilities: JOIN, subqueries, aggregates
4
4
  """
5
- from typing import List, Optional, Any, Dict
5
+
6
+ import re
6
7
  from dataclasses import dataclass, field
7
8
  from enum import Enum
8
- import re
9
+ from typing import Any, Dict, List, Optional
9
10
 
10
11
  from .errors import ValidationError
11
- from .utils import validate_table_name, validate_column_name
12
+ from .utils import validate_column_name, validate_table_name
12
13
 
13
14
  # table.column / alias.column - each half validated with the same allowlists
14
15
  # used elsewhere for identifiers, joined by a single literal dot.
@@ -155,7 +156,7 @@ class AdvancedQueryBuilder:
155
156
  select_parts = list(self.group_by_fields) + select_parts
156
157
  sql = f"SELECT {', '.join(select_parts)}"
157
158
  else:
158
- sql = f"SELECT *"
159
+ sql = "SELECT *"
159
160
 
160
161
  # FROM clause
161
162
  sql += f" FROM {self.table}"
polydb/aio.py ADDED
@@ -0,0 +1,198 @@
1
+ # Async API for PolyDB -- closes CLAUDE.md's own known-gap #5 ("No async
2
+ # API. Everything is synchronous... A documented stance would at least
3
+ # set expectations").
4
+ #
5
+ # THE STANCE, stated plainly: PolyDB's entire adapter layer is built on
6
+ # genuinely synchronous SDKs -- psycopg2 (not asyncpg), boto3 (not
7
+ # aioboto3), the sync azure-* / google-cloud-* clients, pymongo (not
8
+ # motor), pika, kafka-python. A "real" native-async rewrite would mean
9
+ # swapping every one of those for an async-native equivalent across every
10
+ # adapter in `adapters/*.py` -- a from-scratch, parallel adapter layer
11
+ # roughly doubling this package's own maintenance surface (every fix,
12
+ # every new provider feature, landing twice), for a benefit (not blocking
13
+ # the event loop) that a MUCH smaller, purely additive wrapper already
14
+ # delivers for the overwhelming majority of real callers: an
15
+ # asyncio/FastAPI app that doesn't want a slow Postgres/S3/DynamoDB call
16
+ # to stall its event loop.
17
+ #
18
+ # So this module is that smaller thing, not a rewrite: `AsyncPolyDB`/
19
+ # `AsyncDatabaseFactory` wrap the real, unchanged, synchronous
20
+ # `PolyDB`/`DatabaseFactory` instance and run every one of its methods in
21
+ # a worker thread via `asyncio.to_thread` (or a dedicated
22
+ # `ThreadPoolExecutor` when `max_workers` is given), returning an
23
+ # awaitable. This is the same "wrap a blocking DB-API driver in a thread
24
+ # pool" pattern `encode/databases` and Starlette's own docs recommend for
25
+ # exactly this situation -- not a novel trick, the standard one.
26
+ #
27
+ # Genuinely additive: `PolyDB`/`DatabaseFactory` themselves are completely
28
+ # untouched by this module -- every existing synchronous caller keeps
29
+ # working byte-for-byte unchanged. A caller who needs real native async
30
+ # (avoiding thread-pool overhead entirely, or wanting async cancellation
31
+ # semantics psycopg2 can't give) still needs the from-scratch rewrite
32
+ # described above -- that is real, separate, much larger future work,
33
+ # not something this module pretends to already be.
34
+ from __future__ import annotations
35
+
36
+ import asyncio
37
+ import functools
38
+ from concurrent.futures import ThreadPoolExecutor
39
+ from typing import Any, Callable, Generic, Optional, TypeVar
40
+
41
+ _T = TypeVar("_T")
42
+
43
+
44
+ class _AsyncProxy(Generic[_T]):
45
+ """Wraps any synchronous object `sync` and exposes every one of its
46
+ public callables as an `async def`-equivalent (an async function
47
+ returned from `__getattr__`, run via `asyncio.to_thread`/a dedicated
48
+ executor); every non-callable attribute passes through unchanged,
49
+ read synchronously (cheap, in-memory access -- never worth a thread
50
+ hop). Not specific to PolyDB/DatabaseFactory -- either concrete
51
+ subclass below just fixes the wrapped type for clearer type hints
52
+ and a more specific `__repr__`.
53
+
54
+ `max_workers=None` (the default) runs every call via
55
+ `asyncio.to_thread`, i.e. the running loop's own default executor --
56
+ shared with anything else in the same process already using
57
+ `asyncio.to_thread`, the same default a plain asyncio app already
58
+ has. Pass `max_workers=N` for a DEDICATED thread pool sized for this
59
+ instance's own expected DB/cloud-call concurrency, decoupled from
60
+ whatever else the host application runs on the default executor --
61
+ worth doing once real concurrent load is a concern, since the
62
+ default executor is sized `min(32, os.cpu_count() + 4)` by Python
63
+ itself, which can bottleneck a database-heavy workload sharing it
64
+ with other blocking work.
65
+ """
66
+
67
+ def __init__(self, sync: _T, *, max_workers: Optional[int] = None) -> None:
68
+ self._sync = sync
69
+ self._executor: Optional[ThreadPoolExecutor] = (
70
+ ThreadPoolExecutor(
71
+ max_workers=max_workers, thread_name_prefix=f"{type(self).__name__.lower()}"
72
+ )
73
+ if max_workers is not None
74
+ else None
75
+ )
76
+ self._wrapper_cache: dict[str, Callable[..., Any]] = {}
77
+
78
+ @property
79
+ def sync(self) -> _T:
80
+ """Escape hatch to the real, wrapped, synchronous instance -- for
81
+ anything this wrapper doesn't cover, or to call something
82
+ deliberately on the current thread (already running inside a
83
+ worker thread, a one-off script with no running event loop,
84
+ etc.)."""
85
+ return self._sync
86
+
87
+ async def _run(self, fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
88
+ bound = functools.partial(fn, *args, **kwargs)
89
+ if self._executor is not None:
90
+ loop = asyncio.get_running_loop()
91
+ return await loop.run_in_executor(self._executor, bound)
92
+ return await asyncio.to_thread(bound)
93
+
94
+ def __getattr__(self, name: str) -> Any:
95
+ if name.startswith("_"):
96
+ raise AttributeError(name)
97
+ cached = self._wrapper_cache.get(name)
98
+ if cached is not None:
99
+ return cached
100
+ attr = getattr(self._sync, name)
101
+ if not callable(attr):
102
+ return attr
103
+
104
+ async def _async_call(*args: Any, __attr: Callable[..., Any] = attr, **kwargs: Any) -> Any:
105
+ return await self._run(__attr, *args, **kwargs)
106
+
107
+ _async_call.__name__ = name
108
+ _async_call.__doc__ = getattr(attr, "__doc__", None)
109
+ self._wrapper_cache[name] = _async_call
110
+ return _async_call
111
+
112
+ def __dir__(self) -> list[str]:
113
+ return sorted(set(super().__dir__()) | set(dir(self._sync)))
114
+
115
+ def __repr__(self) -> str:
116
+ return f"{type(self).__name__}({self._sync!r})"
117
+
118
+ def close(self) -> None:
119
+ """Shuts down the dedicated executor, if one was created
120
+ (`max_workers` was given). A no-op when running off the shared
121
+ default executor (`max_workers=None`) -- there is nothing this
122
+ instance owns to shut down in that case."""
123
+ if self._executor is not None:
124
+ self._executor.shutdown(wait=True)
125
+
126
+ async def __aenter__(self) -> "_AsyncProxy[_T]":
127
+ return self
128
+
129
+ async def __aexit__(self, *exc_info: Any) -> None:
130
+ self.close()
131
+
132
+
133
+ class AsyncDatabaseFactory(
134
+ _AsyncProxy["DatabaseFactory"]
135
+ ): # noqa: F821 -- DatabaseFactory imported lazily below for typing only
136
+ """Async wrapper over `DatabaseFactory` -- for callers using the
137
+ lower-level factory directly rather than the `PolyDB` facade. See
138
+ this module's own top comment for the full design rationale."""
139
+
140
+ def __init__(
141
+ self,
142
+ *args: Any,
143
+ max_workers: Optional[int] = None,
144
+ sync: Optional[Any] = None,
145
+ **kwargs: Any,
146
+ ) -> None:
147
+ if sync is None:
148
+ from .databaseFactory import DatabaseFactory
149
+
150
+ sync = DatabaseFactory(*args, **kwargs)
151
+ elif args or kwargs:
152
+ raise TypeError(
153
+ "AsyncDatabaseFactory: pass either `sync=<existing DatabaseFactory>` or constructor args, not both"
154
+ )
155
+ super().__init__(sync, max_workers=max_workers)
156
+
157
+
158
+ class AsyncPolyDB(
159
+ _AsyncProxy["PolyDB"]
160
+ ): # noqa: F821 -- PolyDB imported lazily below for typing only
161
+ """Async wrapper over the `PolyDB` facade -- the primary
162
+ developer-facing async entrypoint. See this module's own top comment
163
+ for the full design rationale (why this is a thread-pool wrapper, not
164
+ a native-async rewrite, and what a real native-async story would
165
+ require).
166
+
167
+ Usage::
168
+
169
+ db = AsyncPolyDB(provider=CloudProvider.POSTGRESQL)
170
+ row = await db.create(MyModel, {"id": "1", "name": "hi"})
171
+ rows = await db.read(MyModel, {"name": "hi"})
172
+
173
+ # or, for a dedicated thread pool sized for this instance:
174
+ db = AsyncPolyDB(provider=CloudProvider.POSTGRESQL, max_workers=20)
175
+
176
+ # or, wrapping an already-constructed sync PolyDB:
177
+ db = AsyncPolyDB(sync=existing_polydb_instance)
178
+
179
+ async with db:
180
+ ... # db.close() runs automatically on exit
181
+ """
182
+
183
+ def __init__(
184
+ self,
185
+ *args: Any,
186
+ max_workers: Optional[int] = None,
187
+ sync: Optional[Any] = None,
188
+ **kwargs: Any,
189
+ ) -> None:
190
+ if sync is None:
191
+ from .PolyDB import PolyDB
192
+
193
+ sync = PolyDB(*args, **kwargs)
194
+ elif args or kwargs:
195
+ raise TypeError(
196
+ "AsyncPolyDB: pass either `sync=<existing PolyDB>` or constructor args, not both"
197
+ )
198
+ super().__init__(sync, max_workers=max_workers)
@@ -4,10 +4,10 @@ from __future__ import annotations
4
4
  import hmac
5
5
  import threading
6
6
  from dataclasses import dataclass
7
- from typing import Optional, Dict, Any
7
+ from typing import Optional
8
8
 
9
- from .models import AuditRecord
10
9
  from ..cloudDatabaseFactory import CloudDatabaseFactory
10
+ from .models import AuditRecord
11
11
 
12
12
 
13
13
  class AuditStorage:
@@ -54,14 +54,14 @@ class AuditStorage:
54
54
  CONSTRAINT uq_audit_chain UNIQUE (tenant_id, previous_hash),
55
55
  created_at TIMESTAMP DEFAULT NOW()
56
56
  );
57
-
58
- CREATE INDEX IF NOT EXISTS idx_audit_tenant_timestamp
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_audit_tenant_timestamp
59
59
  ON polydb_audit_log(tenant_id, timestamp DESC);
60
- CREATE INDEX IF NOT EXISTS idx_audit_model_entity
60
+ CREATE INDEX IF NOT EXISTS idx_audit_model_entity
61
61
  ON polydb_audit_log(model, entity_id);
62
- CREATE INDEX IF NOT EXISTS idx_audit_actor
62
+ CREATE INDEX IF NOT EXISTS idx_audit_actor
63
63
  ON polydb_audit_log(actor_id, timestamp DESC);
64
- CREATE INDEX IF NOT EXISTS idx_audit_hash_chain
64
+ CREATE INDEX IF NOT EXISTS idx_audit_hash_chain
65
65
  ON polydb_audit_log(tenant_id, timestamp DESC, previous_hash);
66
66
  """
67
67
 
@@ -74,7 +74,7 @@ class AuditStorage:
74
74
  """Get most recent hash with strict ordering (distributed-safe)"""
75
75
  with self._lock:
76
76
  try:
77
- from ..query import QueryBuilder, Operator
77
+ from ..query import Operator, QueryBuilder
78
78
 
79
79
  builder = QueryBuilder()
80
80
 
@@ -99,9 +99,10 @@ class AuditStorage:
99
99
  and re-chains instead of forking. (threading.Lock alone was only
100
100
  process-local — the old "distributed-safe" claim was false.)"""
101
101
  from dataclasses import asdict
102
+
102
103
  from .models import (
103
- AuditKeyMissingError,
104
104
  AUDIT_HMAC_KEY_ENV,
105
+ AuditKeyMissingError,
105
106
  audit_hmac_key,
106
107
  compute_audit_hash,
107
108
  require_hmac,
@@ -157,9 +158,7 @@ class AuditStorage:
157
158
  """Verify BOTH chain linkage AND per-record content integrity."""
158
159
  return self.verify_chain_detailed(tenant_id).ok
159
160
 
160
- def verify_chain_detailed(
161
- self, tenant_id: Optional[str] = None
162
- ) -> "ChainVerification":
161
+ def verify_chain_detailed(self, tenant_id: Optional[str] = None) -> "ChainVerification":
163
162
  """Verify the chain, reporting *why* rather than only whether.
164
163
 
165
164
  Two things are checked per record: that it links to its predecessor,
@@ -184,7 +183,7 @@ class AuditStorage:
184
183
  as everything after it. ``legacy_records`` reports how much of the
185
184
  chain is still resting on that seal.
186
185
  """
187
- from ..query import QueryBuilder, Operator
186
+ from ..query import Operator, QueryBuilder
188
187
  from .models import audit_hmac_key, compute_audit_hash
189
188
 
190
189
  builder = QueryBuilder()
@@ -215,13 +214,9 @@ class AuditStorage:
215
214
 
216
215
  stored = r.get("hash") or ""
217
216
 
218
- if key is not None and hmac.compare_digest(
219
- stored, compute_audit_hash(r, key=key)
220
- ):
217
+ if key is not None and hmac.compare_digest(stored, compute_audit_hash(r, key=key)):
221
218
  seen_keyed = True
222
- elif not seen_keyed and hmac.compare_digest(
223
- stored, compute_audit_hash(r, key=None)
224
- ):
219
+ elif not seen_keyed and hmac.compare_digest(stored, compute_audit_hash(r, key=None)):
225
220
  # Predates the key. Sealed by the first keyed record that
226
221
  # follows it, so it is only trusted while none has appeared.
227
222
  legacy += 1
polydb/audit/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # src/polydb/audit/__init__.py
2
- from .models import AuditRecord
2
+ from .AuditStorage import AuditStorage
3
3
  from .context import AuditContext
4
4
  from .manager import AuditManager
5
- from .AuditStorage import AuditStorage
5
+ from .models import AuditRecord
6
6
 
7
7
  __all__ = ["AuditRecord", "AuditContext", "AuditManager", "AuditStorage"]
polydb/audit/context.py CHANGED
@@ -1,10 +1,11 @@
1
1
  # src/polydb/audit/AuditContext.py
2
2
  from contextvars import ContextVar
3
- from typing import Optional, List
3
+ from typing import List, Optional
4
+
4
5
 
5
6
  class AuditContext:
6
7
  """Context variables for audit trail"""
7
-
8
+
8
9
  actor_id: ContextVar[Optional[str]] = ContextVar("actor_id", default=None)
9
10
  roles: ContextVar[List[str]] = ContextVar("roles", default=[])
10
11
  tenant_id: ContextVar[Optional[str]] = ContextVar("tenant_id", default=None)
@@ -12,7 +13,7 @@ class AuditContext:
12
13
  request_id: ContextVar[Optional[str]] = ContextVar("request_id", default=None)
13
14
  ip_address: ContextVar[Optional[str]] = ContextVar("ip_address", default=None)
14
15
  user_agent: ContextVar[Optional[str]] = ContextVar("user_agent", default=None)
15
-
16
+
16
17
  @classmethod
17
18
  def set(
18
19
  cls,
@@ -40,7 +41,7 @@ class AuditContext:
40
41
  cls.ip_address.set(ip_address)
41
42
  if user_agent is not None:
42
43
  cls.user_agent.set(user_agent)
43
-
44
+
44
45
  @classmethod
45
46
  def clear(cls):
46
47
  """Clear all context variables"""
@@ -50,4 +51,4 @@ class AuditContext:
50
51
  cls.trace_id.set(None)
51
52
  cls.request_id.set(None)
52
53
  cls.ip_address.set(None)
53
- cls.user_agent.set(None)
54
+ cls.user_agent.set(None)
polydb/audit/manager.py CHANGED
@@ -1,11 +1,11 @@
1
1
  # src/polydb/audit/manager.py
2
2
  from __future__ import annotations
3
3
 
4
- from typing import Optional, Dict, Any, List
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
- from .models import AuditRecord
7
6
  from .AuditStorage import AuditStorage
8
7
  from .context import AuditContext
8
+ from .models import AuditRecord
9
9
 
10
10
 
11
11
  class AuditManager:
@@ -24,7 +24,7 @@ class AuditManager:
24
24
  before: Optional[Dict[str, Any]],
25
25
  after: Optional[Dict[str, Any]],
26
26
  error: Optional[str],
27
- changed_fields: List[str] | None
27
+ changed_fields: List[str] | None,
28
28
  ) -> None:
29
29
  tenant_id = AuditContext.tenant_id.get()
30
30
  previous_hash = self.storage.get_last_hash(tenant_id)
polydb/audit/models.py CHANGED
@@ -18,15 +18,16 @@ Records written before a key was configured stay verifiable. See
18
18
  than a loophole.
19
19
  """
20
20
 
21
- from dataclasses import dataclass, asdict
22
- from typing import Any, Dict, List, Optional
23
21
  import hashlib
24
22
  import hmac
25
23
  import json
26
24
  import logging
27
25
  import os
28
26
  import uuid
27
+ from dataclasses import asdict, dataclass
29
28
  from datetime import datetime, timezone
29
+ from typing import Any, Dict, List, Optional
30
+
30
31
  from ..json_safe import json_safe
31
32
 
32
33
  logger = logging.getLogger("polydb.audit")
@@ -5,14 +5,12 @@ import base64
5
5
  import hashlib
6
6
  import json
7
7
  import threading
8
- from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING, cast
9
-
10
-
11
- from ..json_safe import json_safe
8
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
12
9
 
13
10
  from ..errors import NoSQLError, StorageError
11
+ from ..json_safe import json_safe
12
+ from ..query import Operator, QueryBuilder
14
13
  from ..retry import retry
15
- from ..query import QueryBuilder, Operator
16
14
  from ..types import JsonDict, Lookup
17
15
 
18
16
  if TYPE_CHECKING:
@@ -234,7 +232,8 @@ class NoSQLKVAdapter:
234
232
  # Protocol implementation
235
233
  def put(self, model: type, data: JsonDict) -> JsonDict:
236
234
  pk, rk = self._get_pk_rk(model, data)
237
- return self._put_raw(model, pk, rk, data)
235
+ store_data, _ = self._check_overflow(data)
236
+ return self._put_raw(model, pk, rk, store_data)
238
237
 
239
238
  def query(
240
239
  self,
@@ -340,8 +339,11 @@ class NoSQLKVAdapter:
340
339
  # ---------------------------------------------------------------
341
340
 
342
341
  @property
343
- def capabilities(self) -> "BackendCapabilities":
342
+ def capabilities(
343
+ self,
344
+ ) -> "BackendCapabilities": # noqa: F821 -- lazily imported below, quoted on purpose
344
345
  from ..models import BackendCapabilities
346
+
345
347
  return BackendCapabilities()
346
348
 
347
349
  def _encode_cursor(self, data: dict) -> str:
@@ -353,7 +355,11 @@ class NoSQLKVAdapter:
353
355
  except Exception:
354
356
  return {}
355
357
 
356
- def query_paged(self, model: type, request: "PageRequest") -> "PageResult":
358
+ def query_paged(
359
+ self,
360
+ model: type,
361
+ request: "PageRequest", # noqa: F821 -- lazily imported below, quoted on purpose
362
+ ) -> "PageResult": # noqa: F821 -- lazily imported below, quoted on purpose
357
363
  """In-memory pagination with opaque offset cursor. Subclasses may override."""
358
364
  from ..models import PageResult
359
365
 
@@ -378,14 +384,16 @@ class NoSQLKVAdapter:
378
384
 
379
385
  has_more = len(results) > request.limit
380
386
  if has_more:
381
- results = results[:request.limit]
387
+ results = results[: request.limit]
382
388
 
383
389
  next_cursor = None
384
390
  if has_more:
385
- next_cursor = self._encode_cursor({
386
- "offset": offset + request.limit,
387
- "order_by": request.order_by,
388
- "desc": request.order_desc,
389
- })
391
+ next_cursor = self._encode_cursor(
392
+ {
393
+ "offset": offset + request.limit,
394
+ "order_by": request.order_by,
395
+ "desc": request.order_desc,
396
+ }
397
+ )
390
398
 
391
399
  return PageResult(items=results, next_cursor=next_cursor, has_more=has_more)
@@ -1,8 +1,9 @@
1
- from ..errors import StorageError
2
- from ..utils import setup_logger
3
1
  from abc import ABC, abstractmethod
4
2
  from typing import Any, Dict, List, Optional
5
3
 
4
+ from ..errors import StorageError
5
+ from ..utils import setup_logger
6
+
6
7
 
7
8
  class ObjectStorageAdapter(ABC):
8
9
  """Base class for Object Storage with automatic optimization"""
@@ -1,9 +1,8 @@
1
- from polydb.utils import setup_logger
2
-
3
-
4
1
  from abc import ABC, abstractmethod
5
2
  from typing import Any, Dict, List, Optional
6
3
 
4
+ from polydb.utils import setup_logger
5
+
7
6
 
8
7
  class QueueAdapter(ABC):
9
8
  """Base class for Queue/Message services"""
@@ -1,7 +1,8 @@
1
- from ..utils import setup_logger
2
1
  from abc import ABC, abstractmethod
3
2
  from typing import Optional
4
3
 
4
+ from ..utils import setup_logger
5
+
5
6
 
6
7
  class SecretsAdapter(ABC):
7
8
  """Base class for cloud secret managers (Key Vault / Secrets Manager /
@@ -1,9 +1,8 @@
1
- from polydb.utils import setup_logger
2
-
3
-
4
1
  from abc import ABC, abstractmethod
5
2
  from typing import List, Optional
6
3
 
4
+ from polydb.utils import setup_logger
5
+
7
6
 
8
7
  class SharedFilesAdapter(ABC):
9
8
  """Base class for Shared File Storage"""