altcodepro-polydb-python 2.3.10__tar.gz → 2.3.13__tar.gz
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.
- {altcodepro_polydb_python-2.3.10/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.13}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/pyproject.toml +1 -1
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/PolyDB.py +2 -1
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/AzureTableStorageAdapter.py +8 -2
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/PostgreSQLAdapter.py +25 -15
- altcodepro_polydb_python-2.3.13/src/polydb/audit/AuditStorage.py +164 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/models.py +41 -6
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/query.py +9 -6
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/retry.py +23 -12
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/security.py +7 -4
- altcodepro_polydb_python-2.3.10/src/polydb/audit/AuditStorage.py +0 -136
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/LICENSE +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/README.md +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/setup.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/FirestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/MongoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/S3Adapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/SQSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/VercelFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/NoSQLKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/ObjectStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/SharedFilesAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/cloudDatabaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/databaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_vercel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.13
|
|
4
4
|
Summary: Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety
|
|
5
5
|
Author: AltCodePro
|
|
6
6
|
Project-URL: Homepage, https://github.com/altcodepro/polydb-python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "altcodepro-polydb-python"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.13"
|
|
8
8
|
description = "Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.13
|
|
4
4
|
Summary: Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety
|
|
5
5
|
Author: AltCodePro
|
|
6
6
|
Project-URL: Homepage, https://github.com/altcodepro/polydb-python
|
|
@@ -23,7 +23,6 @@ from .types import JsonDict, Lookup
|
|
|
23
23
|
from .utils import setup_logger
|
|
24
24
|
from .validation import ModelValidator, SchemaValidator
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
ModelRef = Union[Type, str]
|
|
28
27
|
|
|
29
28
|
|
|
@@ -336,6 +335,7 @@ class PolyDB:
|
|
|
336
335
|
metadata: Optional[Dict[str, Any]] = None,
|
|
337
336
|
storage_name: str = "default",
|
|
338
337
|
optimize: bool = True,
|
|
338
|
+
container_name: Optional[str] = None,
|
|
339
339
|
) -> str:
|
|
340
340
|
storage = self.get_object_storage(storage_name)
|
|
341
341
|
return storage.put(
|
|
@@ -345,6 +345,7 @@ class PolyDB:
|
|
|
345
345
|
optimize=optimize,
|
|
346
346
|
media_type=media_type,
|
|
347
347
|
metadata=metadata or {},
|
|
348
|
+
container_name=container_name,
|
|
348
349
|
)
|
|
349
350
|
|
|
350
351
|
def get_blob(
|
|
@@ -116,11 +116,17 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
116
116
|
if v is None:
|
|
117
117
|
return None
|
|
118
118
|
|
|
119
|
+
# Treat empty containers as absent — let the row omit the property.
|
|
120
|
+
if isinstance(v, (list, tuple, dict)) and len(v) == 0:
|
|
121
|
+
return None
|
|
122
|
+
|
|
119
123
|
if isinstance(v, bytes):
|
|
120
124
|
return _BYTES_PREFIX + base64.b64encode(v).decode("ascii")
|
|
121
125
|
|
|
122
|
-
if isinstance(v, (dict, list)):
|
|
123
|
-
return _JSON_PREFIX + json.dumps(
|
|
126
|
+
if isinstance(v, (dict, list, tuple)):
|
|
127
|
+
return _JSON_PREFIX + json.dumps(
|
|
128
|
+
list(v) if isinstance(v, tuple) else v, default=json_safe
|
|
129
|
+
)
|
|
124
130
|
|
|
125
131
|
if isinstance(v, UUID):
|
|
126
132
|
return str(v)
|
|
@@ -8,8 +8,6 @@ import hashlib
|
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
import json
|
|
10
10
|
from datetime import datetime, date
|
|
11
|
-
|
|
12
|
-
|
|
13
11
|
from ..errors import DatabaseError, ConnectionError
|
|
14
12
|
from ..retry import retry
|
|
15
13
|
from ..utils import validate_table_name, validate_column_name
|
|
@@ -102,34 +100,46 @@ class PostgreSQLAdapter:
|
|
|
102
100
|
|
|
103
101
|
def _serialize_value(self, v: Any) -> Any:
|
|
104
102
|
"""
|
|
105
|
-
Make
|
|
103
|
+
Make outgoing values safe for psycopg2 across mixed column types.
|
|
106
104
|
|
|
107
105
|
Rules:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
None / empty list / empty dict -> None (becomes NULL on any column)
|
|
107
|
+
list of primitives (str/int/...) -> native list (psycopg2 maps to TEXT[]/INT[])
|
|
108
|
+
list containing dicts -> Json(list) (for JSONB columns)
|
|
109
|
+
dict -> Json(dict)
|
|
110
|
+
datetime/date -> native
|
|
111
|
+
Decimal -> float
|
|
112
|
+
everything else -> as-is
|
|
113
113
|
"""
|
|
114
|
+
# NULL-ify empties so they're valid for TEXT[], JSONB, and plain columns alike.
|
|
114
115
|
from psycopg2.extras import Json
|
|
115
116
|
|
|
116
117
|
if v is None:
|
|
117
118
|
return None
|
|
119
|
+
if isinstance(v, (list, tuple)) and len(v) == 0:
|
|
120
|
+
return None
|
|
121
|
+
if isinstance(v, dict) and len(v) == 0:
|
|
122
|
+
return None
|
|
118
123
|
|
|
119
|
-
# Dict ->
|
|
124
|
+
# Dict -> JSONB
|
|
120
125
|
if isinstance(v, dict):
|
|
121
126
|
return Json(self._json_safe(v))
|
|
122
127
|
|
|
123
|
-
# List:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
# List: route by element type.
|
|
129
|
+
if isinstance(v, (list, tuple)):
|
|
130
|
+
v = list(v)
|
|
131
|
+
# If ANY element is a dict, treat as JSON payload (for JSONB columns).
|
|
132
|
+
if any(isinstance(x, dict) for x in v):
|
|
133
|
+
return Json(v)
|
|
134
|
+
# If ALL elements are primitives, send as native list for TEXT[]/INT[].
|
|
135
|
+
if all(isinstance(x, (str, int, float, bool, type(None))) for x in v):
|
|
136
|
+
return v
|
|
137
|
+
# Mixed / nested -> safest is JSONB
|
|
128
138
|
return Json(v)
|
|
129
139
|
|
|
130
140
|
# Datetime / date
|
|
131
141
|
if isinstance(v, (datetime, date)):
|
|
132
|
-
return v
|
|
142
|
+
return v
|
|
133
143
|
|
|
134
144
|
# Decimal
|
|
135
145
|
if isinstance(v, Decimal):
|
|
@@ -0,0 +1,164 @@
|
|
|
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 ..cloudDatabaseFactory 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
|
+
@staticmethod
|
|
22
|
+
def _is_unique_violation(exc: Exception) -> bool:
|
|
23
|
+
s = str(exc).lower()
|
|
24
|
+
return "23505" in s or "duplicate key" in s or "unique constraint" in s
|
|
25
|
+
|
|
26
|
+
def _ensure_table(self):
|
|
27
|
+
"""Create audit table if not exists"""
|
|
28
|
+
try:
|
|
29
|
+
schema = """
|
|
30
|
+
CREATE TABLE IF NOT EXISTS polydb_audit_log (
|
|
31
|
+
audit_id UUID PRIMARY KEY,
|
|
32
|
+
timestamp TIMESTAMP NOT NULL,
|
|
33
|
+
tenant_id VARCHAR(255),
|
|
34
|
+
actor_id VARCHAR(255),
|
|
35
|
+
roles TEXT[],
|
|
36
|
+
action VARCHAR(50) NOT NULL,
|
|
37
|
+
model VARCHAR(255) NOT NULL,
|
|
38
|
+
entity_id VARCHAR(255),
|
|
39
|
+
storage_type VARCHAR(20) NOT NULL,
|
|
40
|
+
provider VARCHAR(50) NOT NULL,
|
|
41
|
+
success BOOLEAN NOT NULL,
|
|
42
|
+
before JSONB,
|
|
43
|
+
after JSONB,
|
|
44
|
+
changed_fields TEXT[],
|
|
45
|
+
trace_id VARCHAR(255),
|
|
46
|
+
request_id VARCHAR(255),
|
|
47
|
+
ip_address VARCHAR(45),
|
|
48
|
+
user_agent TEXT,
|
|
49
|
+
error TEXT,
|
|
50
|
+
hash VARCHAR(64) NOT NULL,
|
|
51
|
+
previous_hash VARCHAR(64) NOT NULL DEFAULT '',
|
|
52
|
+
CONSTRAINT uq_audit_chain UNIQUE (tenant_id, previous_hash),
|
|
53
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant_timestamp
|
|
57
|
+
ON polydb_audit_log(tenant_id, timestamp DESC);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_audit_model_entity
|
|
59
|
+
ON polydb_audit_log(model, entity_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_audit_actor
|
|
61
|
+
ON polydb_audit_log(actor_id, timestamp DESC);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_audit_hash_chain
|
|
63
|
+
ON polydb_audit_log(tenant_id, timestamp DESC, previous_hash);
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
self.sql.execute(schema)
|
|
67
|
+
except Exception:
|
|
68
|
+
# Table may already exist
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def get_last_hash(self, tenant_id: Optional[str]) -> Optional[str]:
|
|
72
|
+
"""Get most recent hash with strict ordering (distributed-safe)"""
|
|
73
|
+
with self._lock:
|
|
74
|
+
try:
|
|
75
|
+
from ..query import QueryBuilder, Operator
|
|
76
|
+
|
|
77
|
+
builder = QueryBuilder()
|
|
78
|
+
|
|
79
|
+
if tenant_id is not None:
|
|
80
|
+
builder.where("tenant_id", Operator.EQ, tenant_id)
|
|
81
|
+
|
|
82
|
+
builder.order_by("timestamp", descending=True).take(1)
|
|
83
|
+
|
|
84
|
+
results = self.sql.query_linq("polydb_audit_log", builder)
|
|
85
|
+
|
|
86
|
+
if results and len(results) > 0:
|
|
87
|
+
return results[0].get("hash")
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
except Exception:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def persist(self, record: AuditRecord) -> None:
|
|
94
|
+
"""Append to the hash chain. Concurrency-safe ACROSS PROCESSES via the
|
|
95
|
+
UNIQUE(tenant_id, previous_hash) constraint + bounded retry: if two
|
|
96
|
+
writers race on the same predecessor, the loser re-reads the new tail
|
|
97
|
+
and re-chains instead of forking. (threading.Lock alone was only
|
|
98
|
+
process-local — the old "distributed-safe" claim was false.)"""
|
|
99
|
+
from dataclasses import asdict
|
|
100
|
+
from .models import compute_audit_hash
|
|
101
|
+
|
|
102
|
+
last_err: Optional[Exception] = None
|
|
103
|
+
for _ in range(8):
|
|
104
|
+
with self._lock:
|
|
105
|
+
prev = self.get_last_hash(record.tenant_id) or ""
|
|
106
|
+
record.previous_hash = prev
|
|
107
|
+
record.hash = compute_audit_hash(asdict(record))
|
|
108
|
+
row = {
|
|
109
|
+
"audit_id": record.audit_id,
|
|
110
|
+
"timestamp": record.timestamp,
|
|
111
|
+
"tenant_id": record.tenant_id,
|
|
112
|
+
"actor_id": record.actor_id,
|
|
113
|
+
"roles": record.roles or None,
|
|
114
|
+
"action": record.action,
|
|
115
|
+
"model": record.model,
|
|
116
|
+
"entity_id": record.entity_id,
|
|
117
|
+
"storage_type": record.storage_type,
|
|
118
|
+
"provider": record.provider,
|
|
119
|
+
"success": record.success,
|
|
120
|
+
"before": record.before,
|
|
121
|
+
"after": record.after,
|
|
122
|
+
"changed_fields": record.changed_fields or None,
|
|
123
|
+
"trace_id": record.trace_id,
|
|
124
|
+
"request_id": record.request_id,
|
|
125
|
+
"ip_address": record.ip_address,
|
|
126
|
+
"user_agent": record.user_agent,
|
|
127
|
+
"error": record.error,
|
|
128
|
+
"hash": record.hash,
|
|
129
|
+
"previous_hash": record.previous_hash,
|
|
130
|
+
}
|
|
131
|
+
try:
|
|
132
|
+
self.sql.insert("polydb_audit_log", row)
|
|
133
|
+
return
|
|
134
|
+
except Exception as e:
|
|
135
|
+
if self._is_unique_violation(e):
|
|
136
|
+
last_err = e
|
|
137
|
+
continue
|
|
138
|
+
raise
|
|
139
|
+
raise last_err or RuntimeError("audit persist failed after retries")
|
|
140
|
+
|
|
141
|
+
def verify_chain(self, tenant_id: Optional[str] = None) -> bool:
|
|
142
|
+
"""Verify BOTH chain linkage AND per-record content integrity.
|
|
143
|
+
The old version only checked previous_hash linkage, so editing
|
|
144
|
+
before/after/action while leaving `hash` intact passed silently."""
|
|
145
|
+
from ..query import QueryBuilder, Operator
|
|
146
|
+
from .models import compute_audit_hash
|
|
147
|
+
|
|
148
|
+
builder = QueryBuilder()
|
|
149
|
+
if tenant_id is not None:
|
|
150
|
+
builder.where("tenant_id", Operator.EQ, tenant_id)
|
|
151
|
+
builder.order_by("timestamp", descending=False)
|
|
152
|
+
|
|
153
|
+
records = self.sql.query_linq("polydb_audit_log", builder)
|
|
154
|
+
if not records:
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
prev = ""
|
|
158
|
+
for r in records:
|
|
159
|
+
if (r.get("previous_hash") or "") != prev:
|
|
160
|
+
return False
|
|
161
|
+
if r.get("hash") != compute_audit_hash(r): # content tamper check
|
|
162
|
+
return False
|
|
163
|
+
prev = r.get("hash")
|
|
164
|
+
return True
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/models.py
RENAMED
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
# src/polydb/audit/models.py
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, asdict
|
|
4
|
-
from datetime import datetime
|
|
5
4
|
from typing import Any, Dict, List, Optional
|
|
6
5
|
import uuid
|
|
7
6
|
import hashlib
|
|
8
7
|
import json
|
|
9
|
-
|
|
8
|
+
from datetime import datetime, timezone
|
|
10
9
|
from ..json_safe import json_safe
|
|
11
10
|
|
|
12
11
|
|
|
12
|
+
def _iso(ts: Any) -> str:
|
|
13
|
+
return ts.isoformat() if hasattr(ts, "isoformat") else str(ts)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def canonical_audit_payload(src: Dict[str, Any]) -> str:
|
|
17
|
+
"""Deterministic JSON for the hash chain. Identical whether `src` is a
|
|
18
|
+
freshly-built record (asdict) or a row read back from Postgres, so the
|
|
19
|
+
create-time hash and the verify-time recomputed hash match.
|
|
20
|
+
Normalizes [] vs NULL and timestamp formatting; excludes `hash`."""
|
|
21
|
+
payload = {
|
|
22
|
+
"audit_id": src.get("audit_id"),
|
|
23
|
+
"timestamp": _iso(src.get("timestamp")),
|
|
24
|
+
"tenant_id": src.get("tenant_id"),
|
|
25
|
+
"actor_id": src.get("actor_id"),
|
|
26
|
+
"roles": list(src.get("roles") or []),
|
|
27
|
+
"action": src.get("action"),
|
|
28
|
+
"model": src.get("model"),
|
|
29
|
+
"entity_id": src.get("entity_id"),
|
|
30
|
+
"storage_type": src.get("storage_type"),
|
|
31
|
+
"provider": src.get("provider"),
|
|
32
|
+
"success": bool(src.get("success")),
|
|
33
|
+
"before": src.get("before"),
|
|
34
|
+
"after": src.get("after"),
|
|
35
|
+
"changed_fields": list(src.get("changed_fields") or []),
|
|
36
|
+
"trace_id": src.get("trace_id"),
|
|
37
|
+
"request_id": src.get("request_id"),
|
|
38
|
+
"ip_address": src.get("ip_address"),
|
|
39
|
+
"user_agent": src.get("user_agent"),
|
|
40
|
+
"error": src.get("error"),
|
|
41
|
+
"previous_hash": src.get("previous_hash") or "",
|
|
42
|
+
}
|
|
43
|
+
return json.dumps(payload, sort_keys=True, default=json_safe)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_audit_hash(src: Dict[str, Any]) -> str:
|
|
47
|
+
return hashlib.sha256(canonical_audit_payload(src).encode()).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
13
50
|
@dataclass
|
|
14
51
|
class AuditRecord:
|
|
15
52
|
audit_id: str
|
|
@@ -55,7 +92,7 @@ class AuditRecord:
|
|
|
55
92
|
context,
|
|
56
93
|
previous_hash: Optional[str] = None,
|
|
57
94
|
):
|
|
58
|
-
now = datetime.
|
|
95
|
+
now = datetime.now(timezone.utc).replace(tzinfo=None).isoformat()
|
|
59
96
|
audit_id = str(uuid.uuid4())
|
|
60
97
|
|
|
61
98
|
record = cls(
|
|
@@ -81,8 +118,6 @@ class AuditRecord:
|
|
|
81
118
|
previous_hash=previous_hash,
|
|
82
119
|
)
|
|
83
120
|
|
|
84
|
-
record.hash =
|
|
85
|
-
json.dumps(asdict(record), sort_keys=True,default=json_safe).encode()
|
|
86
|
-
).hexdigest()
|
|
121
|
+
record.hash = compute_audit_hash(asdict(record))
|
|
87
122
|
|
|
88
123
|
return record
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Any, Dict, List, Optional, Union
|
|
6
6
|
from enum import Enum
|
|
7
|
+
from .utils import validate_column_name
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class Operator(Enum):
|
|
@@ -130,6 +131,7 @@ class QueryBuilder:
|
|
|
130
131
|
params = []
|
|
131
132
|
|
|
132
133
|
for f in self.filters:
|
|
134
|
+
validate_column_name(f.field)
|
|
133
135
|
|
|
134
136
|
if f.operator == Operator.EQ:
|
|
135
137
|
clauses.append(f"{f.field} = %s")
|
|
@@ -156,14 +158,15 @@ class QueryBuilder:
|
|
|
156
158
|
params.append(f.value)
|
|
157
159
|
|
|
158
160
|
elif f.operator == Operator.IN:
|
|
159
|
-
|
|
160
161
|
if isinstance(f.value, (list, tuple)):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
if not f.value:
|
|
163
|
+
clauses.append("1=0") # empty IN → match nothing
|
|
164
|
+
else:
|
|
165
|
+
placeholders = ",".join(["%s"] * len(f.value))
|
|
166
|
+
clauses.append(f"{f.field} IN ({placeholders})")
|
|
167
|
+
params.extend(f.value)
|
|
165
168
|
else:
|
|
166
|
-
clauses.append(f"{f.field}
|
|
169
|
+
clauses.append(f"{f.field} = %s") # scalar IN == equality
|
|
167
170
|
params.append(f.value)
|
|
168
171
|
|
|
169
172
|
elif f.operator == Operator.NOT_IN:
|
|
@@ -12,42 +12,49 @@ from typing import Callable, Optional, Tuple, Type
|
|
|
12
12
|
# Metrics hooks for enterprise monitoring
|
|
13
13
|
class MetricsHooks:
|
|
14
14
|
"""Metrics hooks that users can override for monitoring"""
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
@staticmethod
|
|
17
17
|
def on_query_start(operation: str, **kwargs):
|
|
18
18
|
"""Called when query starts"""
|
|
19
19
|
pass
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
@staticmethod
|
|
22
22
|
def on_query_end(operation: str, duration: float, success: bool, **kwargs):
|
|
23
23
|
"""Called when query ends"""
|
|
24
24
|
pass
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
@staticmethod
|
|
27
27
|
def on_error(operation: str, error: Exception, **kwargs):
|
|
28
28
|
"""Called when error occurs"""
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def retry(
|
|
33
|
-
|
|
32
|
+
def retry(
|
|
33
|
+
max_attempts: int = 3,
|
|
34
|
+
delay: float = 1.0,
|
|
35
|
+
backoff: float = 2.0,
|
|
36
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
37
|
+
):
|
|
34
38
|
"""
|
|
35
39
|
Retry decorator with exponential backoff
|
|
36
|
-
|
|
40
|
+
|
|
37
41
|
Args:
|
|
38
42
|
max_attempts: Maximum number of retry attempts
|
|
39
43
|
delay: Initial delay between retries (seconds)
|
|
40
44
|
backoff: Backoff multiplier
|
|
41
45
|
exceptions: Tuple of exceptions to catch
|
|
42
46
|
"""
|
|
47
|
+
if max_attempts < 1:
|
|
48
|
+
raise ValueError("max_attempts must be >= 1")
|
|
49
|
+
|
|
43
50
|
def decorator(func: Callable) -> Callable:
|
|
44
51
|
@wraps(func)
|
|
45
52
|
def wrapper(*args, **kwargs):
|
|
46
53
|
attempt = 0
|
|
47
54
|
current_delay = delay
|
|
48
|
-
|
|
55
|
+
|
|
49
56
|
logger = logging.getLogger(__name__)
|
|
50
|
-
|
|
57
|
+
|
|
51
58
|
while attempt < max_attempts:
|
|
52
59
|
start_time = time.time()
|
|
53
60
|
try:
|
|
@@ -61,16 +68,20 @@ def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0,
|
|
|
61
68
|
duration = time.time() - start_time
|
|
62
69
|
MetricsHooks.on_query_end(func.__name__, duration, False)
|
|
63
70
|
MetricsHooks.on_error(func.__name__, e)
|
|
64
|
-
|
|
71
|
+
|
|
65
72
|
if attempt >= max_attempts:
|
|
66
73
|
raise
|
|
67
|
-
|
|
74
|
+
|
|
68
75
|
logger.warning(
|
|
69
76
|
f"Attempt {attempt}/{max_attempts} failed for {func.__name__}: {str(e)}. "
|
|
70
77
|
f"Retrying in {current_delay}s..."
|
|
71
78
|
)
|
|
72
79
|
time.sleep(current_delay)
|
|
73
80
|
current_delay *= backoff
|
|
74
|
-
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
f"{func.__name__} exhausted {max_attempts} attempts without returning"
|
|
83
|
+
)
|
|
84
|
+
|
|
75
85
|
return wrapper
|
|
76
|
-
|
|
86
|
+
|
|
87
|
+
return decorator
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Security features: encryption, masking, row-level security
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from typing import Dict, Any, List, Optional, Callable, Union
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
import hashlib
|
|
@@ -50,7 +51,7 @@ class FieldEncryption:
|
|
|
50
51
|
"""Encrypt arbitrary value (serialize if non-str)"""
|
|
51
52
|
if value is None:
|
|
52
53
|
return ""
|
|
53
|
-
data = json.dumps(value,default=json_safe) if not isinstance(value, str) else value
|
|
54
|
+
data = json.dumps(value, default=json_safe) if not isinstance(value, str) else value
|
|
54
55
|
try:
|
|
55
56
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
56
57
|
|
|
@@ -91,9 +92,11 @@ class FieldEncryption:
|
|
|
91
92
|
except ImportError:
|
|
92
93
|
raise ImportError("cryptography not installed")
|
|
93
94
|
except Exception as e:
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
# Fail loud. Returning ciphertext as if it were plaintext masks
|
|
96
|
+
# key-rotation errors / corruption and leaks the 'encrypted:' blob
|
|
97
|
+
# into application data.
|
|
98
|
+
logger.error("Field decryption failed: %s", e)
|
|
99
|
+
raise
|
|
97
100
|
|
|
98
101
|
def encrypt_fields(self, data: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
|
|
99
102
|
"""Encrypt specified fields in data dict"""
|
|
@@ -1,136 +0,0 @@
|
|
|
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 ..cloudDatabaseFactory 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/requirements-generic.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/S3Adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/adapters/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/advanced_query.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/context.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/audit/manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/QueueAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/databaseFactory.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/monitoring.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/multitenancy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/src/polydb/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_blockchain.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_cloud_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_multi_engine.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.10 → altcodepro_polydb_python-2.3.13}/tests/test_postgresql.py
RENAMED
|
File without changes
|
|
File without changes
|