altcodepro-polydb-python 2.3.9__py3-none-any.whl → 2.3.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.
- {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/METADATA +11 -1
- altcodepro_polydb_python-2.3.11.dist-info/RECORD +61 -0
- polydb/PolyDB.py +2 -1
- polydb/adapters/AzureBlobStorageAdapter.py +92 -90
- polydb/adapters/AzureFileStorageAdapter.py +74 -74
- polydb/adapters/AzureQueueAdapter.py +9 -5
- polydb/adapters/AzureTableStorageAdapter.py +5 -5
- polydb/adapters/BlockchainBlobAdapter.py +1 -1
- polydb/adapters/BlockchainFileAdapter.py +217 -0
- polydb/adapters/BlockchainKVAdapter.py +4 -3
- polydb/adapters/BlockchainQueueAdapter.py +3 -2
- polydb/adapters/DynamoDBAdapter.py +12 -3
- polydb/adapters/EFSAdapter.py +45 -19
- polydb/adapters/FirestoreAdapter.py +15 -13
- polydb/adapters/GCPFilestoreAdapter.py +77 -0
- polydb/adapters/GCPPubSubAdapter.py +4 -4
- polydb/adapters/GCPStorageAdapter.py +78 -117
- polydb/adapters/PostgreSQLAdapter.py +2 -1
- polydb/adapters/S3Adapter.py +5 -2
- polydb/adapters/S3CompatibleAdapter.py +87 -57
- polydb/adapters/SQSAdapter.py +5 -2
- polydb/adapters/VercelFileAdapter.py +29 -0
- polydb/audit/__init__.py +1 -1
- polydb/base/SharedFilesAdapter.py +5 -5
- polydb/cloudDatabaseFactory.py +37 -66
- polydb/databaseFactory.py +23 -7
- altcodepro_polydb_python-2.3.9.dist-info/RECORD +0 -58
- {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/WHEEL +0 -0
- {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/top_level.txt +0 -0
|
@@ -3,7 +3,6 @@ import json
|
|
|
3
3
|
import threading
|
|
4
4
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
5
5
|
|
|
6
|
-
from google.cloud import pubsub_v1
|
|
7
6
|
from google.api_core.exceptions import AlreadyExists, NotFound
|
|
8
7
|
|
|
9
8
|
from ..base.QueueAdapter import QueueAdapter
|
|
@@ -11,7 +10,6 @@ from ..errors import ConnectionError, QueueError
|
|
|
11
10
|
from ..retry import retry
|
|
12
11
|
from ..json_safe import json_safe
|
|
13
12
|
|
|
14
|
-
|
|
15
13
|
JsonLike = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
|
|
16
14
|
|
|
17
15
|
|
|
@@ -40,13 +38,15 @@ class GCPPubSubAdapter(QueueAdapter):
|
|
|
40
38
|
"PUBSUB_SUBSCRIPTION", "polydb-sub"
|
|
41
39
|
)
|
|
42
40
|
|
|
43
|
-
self._publisher
|
|
44
|
-
self._subscriber
|
|
41
|
+
self._publisher = None
|
|
42
|
+
self._subscriber = None
|
|
45
43
|
self._lock = threading.Lock()
|
|
46
44
|
|
|
47
45
|
self._initialize_clients()
|
|
48
46
|
|
|
49
47
|
def _initialize_clients(self) -> None:
|
|
48
|
+
from google.cloud import pubsub_v1
|
|
49
|
+
|
|
50
50
|
try:
|
|
51
51
|
with self._lock:
|
|
52
52
|
if self._publisher and self._subscriber:
|
|
@@ -5,9 +5,6 @@ import os
|
|
|
5
5
|
import threading
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
7
7
|
|
|
8
|
-
from google.cloud import storage
|
|
9
|
-
from google.api_core.exceptions import NotFound
|
|
10
|
-
|
|
11
8
|
from ..base.ObjectStorageAdapter import ObjectStorageAdapter
|
|
12
9
|
from ..errors import StorageError, ConnectionError
|
|
13
10
|
from ..retry import retry
|
|
@@ -17,13 +14,11 @@ class GCPStorageAdapter(ObjectStorageAdapter):
|
|
|
17
14
|
"""
|
|
18
15
|
Production-grade Google Cloud Storage adapter.
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
- Retry support
|
|
26
|
-
- Structured logging
|
|
17
|
+
- One client, many buckets (resolved + cached per call)
|
|
18
|
+
- Automatic bucket creation, emulator support (fake-gcs-server)
|
|
19
|
+
- put/get/delete are symmetric: a blob is stored at `key` and fetched at `key`
|
|
20
|
+
- per-call `container_name` overrides the bucket (generic name kept for
|
|
21
|
+
cross-provider parity)
|
|
27
22
|
"""
|
|
28
23
|
|
|
29
24
|
def __init__(self, project_id: str, endpoint: Optional[str], bucket_name: Optional[str] = None):
|
|
@@ -31,22 +26,21 @@ class GCPStorageAdapter(ObjectStorageAdapter):
|
|
|
31
26
|
|
|
32
27
|
self.bucket_name: str = bucket_name or os.getenv("GCS_BUCKET_NAME", "default")
|
|
33
28
|
self.project_id: str = project_id or os.getenv("GOOGLE_CLOUD_PROJECT", "polydb-test")
|
|
34
|
-
|
|
35
29
|
self._endpoint: Optional[str] = endpoint or os.getenv("GCS_ENDPOINT")
|
|
36
30
|
|
|
37
|
-
self._client
|
|
38
|
-
self.
|
|
39
|
-
|
|
31
|
+
self._client = None
|
|
32
|
+
self._buckets: Dict[str, Any] = {}
|
|
40
33
|
self._lock = threading.Lock()
|
|
41
34
|
|
|
42
35
|
self._initialize_client()
|
|
43
36
|
|
|
44
37
|
# ------------------------------------------------------------------
|
|
45
|
-
# Client
|
|
38
|
+
# Client / bucket resolution
|
|
46
39
|
# ------------------------------------------------------------------
|
|
47
|
-
|
|
48
40
|
def _initialize_client(self) -> None:
|
|
49
|
-
"""Initialize GCS client once (thread-safe)"""
|
|
41
|
+
"""Initialize the shared GCS client once (thread-safe)."""
|
|
42
|
+
from google.cloud import storage # lazy: only required for this provider
|
|
43
|
+
|
|
50
44
|
try:
|
|
51
45
|
with self._lock:
|
|
52
46
|
if self._client:
|
|
@@ -61,27 +55,61 @@ class GCPStorageAdapter(ObjectStorageAdapter):
|
|
|
61
55
|
else:
|
|
62
56
|
self._client = storage.Client(project=self.project_id)
|
|
63
57
|
|
|
64
|
-
self.
|
|
58
|
+
self.logger.info(f"GCS client initialized (project={self.project_id})")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise ConnectionError(f"Failed to initialize GCS: {str(e)}")
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.logger.info(f"Created GCS bucket: {self.bucket_name}")
|
|
71
|
-
except Exception:
|
|
72
|
-
# fake-gcs-server does not support bucket.exists()
|
|
73
|
-
pass
|
|
62
|
+
def _get_bucket(self, container_name: Optional[str] = None):
|
|
63
|
+
"""Resolve (and cache) a bucket, auto-creating it."""
|
|
64
|
+
if self._client is None:
|
|
65
|
+
raise ConnectionError("GCS client not initialized")
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
f"GCS initialized (bucket={self.bucket_name}, project={self.project_id})"
|
|
77
|
-
)
|
|
67
|
+
name = container_name or self.bucket_name
|
|
78
68
|
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
cached = self._buckets.get(name)
|
|
70
|
+
if cached is not None:
|
|
71
|
+
return cached
|
|
72
|
+
|
|
73
|
+
with self._lock:
|
|
74
|
+
cached = self._buckets.get(name) # re-check under lock
|
|
75
|
+
if cached is not None:
|
|
76
|
+
return cached
|
|
77
|
+
|
|
78
|
+
bucket = self._client.bucket(name)
|
|
79
|
+
try:
|
|
80
|
+
if not bucket.exists():
|
|
81
|
+
bucket = self._client.create_bucket(name)
|
|
82
|
+
self.logger.info(f"Created GCS bucket: {name}")
|
|
83
|
+
except Exception:
|
|
84
|
+
# fake-gcs-server does not support bucket.exists()
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
self._buckets[name] = bucket
|
|
88
|
+
return bucket
|
|
81
89
|
|
|
82
90
|
# ------------------------------------------------------------------
|
|
83
|
-
# Put
|
|
91
|
+
# Put
|
|
84
92
|
# ------------------------------------------------------------------
|
|
93
|
+
def put(
|
|
94
|
+
self,
|
|
95
|
+
key: str,
|
|
96
|
+
data: bytes,
|
|
97
|
+
fileName: str = "",
|
|
98
|
+
optimize: bool = True,
|
|
99
|
+
media_type: Optional[str] = None,
|
|
100
|
+
metadata: Dict[str, Any] | None = None,
|
|
101
|
+
container_name: Optional[str] = None,
|
|
102
|
+
) -> str:
|
|
103
|
+
if optimize and media_type:
|
|
104
|
+
data = self._optimize_media(data, media_type)
|
|
105
|
+
return self._put_raw(
|
|
106
|
+
key=key,
|
|
107
|
+
data=data,
|
|
108
|
+
fileName=fileName,
|
|
109
|
+
media_type=media_type,
|
|
110
|
+
metadata=metadata,
|
|
111
|
+
container_name=container_name,
|
|
112
|
+
)
|
|
85
113
|
|
|
86
114
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
87
115
|
def _put_raw(
|
|
@@ -91,135 +119,68 @@ class GCPStorageAdapter(ObjectStorageAdapter):
|
|
|
91
119
|
fileName: str = "",
|
|
92
120
|
media_type: Optional[str] = None,
|
|
93
121
|
metadata: Dict[str, Any] | None = None,
|
|
122
|
+
container_name: Optional[str] = None,
|
|
94
123
|
) -> str:
|
|
95
|
-
"""Upload object
|
|
124
|
+
"""Upload object. Stored at `key` so get/delete find it by the same key."""
|
|
96
125
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
bucket = self._get_bucket(container_name)
|
|
127
|
+
name = container_name or self.bucket_name
|
|
99
128
|
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
# --------------------------------------------------
|
|
103
|
-
filename = fileName or os.path.basename(key)
|
|
104
|
-
|
|
105
|
-
# --------------------------------------------------
|
|
106
|
-
# Ensure extension from media_type
|
|
107
|
-
# --------------------------------------------------
|
|
129
|
+
# filename is metadata only — it must NOT alter the blob key
|
|
130
|
+
filename = fileName or os.path.basename(key) or key
|
|
108
131
|
if media_type:
|
|
109
132
|
ext = mimetypes.guess_extension(media_type) or ""
|
|
110
133
|
if ext and not filename.lower().endswith(ext):
|
|
111
134
|
filename += ext
|
|
112
135
|
|
|
113
|
-
|
|
114
|
-
# Final blob key
|
|
115
|
-
# --------------------------------------------------
|
|
116
|
-
blob_key = f"{key.rstrip('/')}/{filename}" if fileName else key
|
|
117
|
-
|
|
118
|
-
blob = self._bucket.blob(blob_key)
|
|
136
|
+
blob = bucket.blob(key)
|
|
119
137
|
|
|
120
|
-
# --------------------------------------------------
|
|
121
|
-
# Metadata (must be string)
|
|
122
|
-
# --------------------------------------------------
|
|
123
138
|
safe_metadata = {k: str(v) for k, v in (metadata or {}).items()}
|
|
124
139
|
safe_metadata["filename"] = filename
|
|
125
|
-
|
|
126
140
|
blob.metadata = safe_metadata
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
#
|
|
130
|
-
# --------------------------------------------------
|
|
131
|
-
blob.upload_from_string(
|
|
132
|
-
data,
|
|
133
|
-
content_type=media_type or "application/octet-stream",
|
|
134
|
-
)
|
|
142
|
+
blob.upload_from_string(data, content_type=media_type or "application/octet-stream")
|
|
143
|
+
blob.patch() # persist metadata
|
|
135
144
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
self.logger.debug(f"GCS uploaded blob: {blob_key}, type={media_type}")
|
|
140
|
-
|
|
141
|
-
# --------------------------------------------------
|
|
142
|
-
# Return public URL
|
|
143
|
-
# --------------------------------------------------
|
|
144
|
-
return f"https://storage.googleapis.com/{self.bucket_name}/{blob_key}"
|
|
145
|
+
self.logger.debug(f"GCS uploaded blob: {name}/{key}, type={media_type}")
|
|
146
|
+
return f"https://storage.googleapis.com/{name}/{key}"
|
|
145
147
|
|
|
146
148
|
except Exception as e:
|
|
147
149
|
raise StorageError(f"GCS put failed: {str(e)}")
|
|
148
150
|
|
|
149
151
|
# ------------------------------------------------------------------
|
|
150
|
-
# Get
|
|
152
|
+
# Get / Delete / List
|
|
151
153
|
# ------------------------------------------------------------------
|
|
152
|
-
|
|
153
154
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
154
|
-
def get(self, key: str) -> Optional[bytes]:
|
|
155
|
-
"""Download object from GCS"""
|
|
155
|
+
def get(self, key: str, container_name: Optional[str] = None) -> Optional[bytes]:
|
|
156
156
|
try:
|
|
157
|
-
|
|
158
|
-
raise ConnectionError("GCS bucket not initialized")
|
|
159
|
-
|
|
160
|
-
blob = self._bucket.blob(key)
|
|
161
|
-
|
|
157
|
+
blob = self._get_bucket(container_name).blob(key)
|
|
162
158
|
if not blob.exists():
|
|
163
159
|
return None
|
|
164
|
-
|
|
165
160
|
data = blob.download_as_bytes()
|
|
166
|
-
|
|
167
161
|
self.logger.debug(f"GCS downloaded blob: {key}")
|
|
168
|
-
|
|
169
162
|
return data
|
|
170
|
-
|
|
171
|
-
except NotFound:
|
|
172
|
-
return None
|
|
173
|
-
|
|
174
163
|
except Exception as e:
|
|
175
164
|
raise StorageError(f"GCS get failed: {str(e)}")
|
|
176
165
|
|
|
177
|
-
# ------------------------------------------------------------------
|
|
178
|
-
# Delete object
|
|
179
|
-
# ------------------------------------------------------------------
|
|
180
|
-
|
|
181
166
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
182
|
-
def delete(self, key: str) -> bool:
|
|
183
|
-
"""Delete object from GCS"""
|
|
167
|
+
def delete(self, key: str, container_name: Optional[str] = None) -> bool:
|
|
184
168
|
try:
|
|
185
|
-
|
|
186
|
-
raise ConnectionError("GCS bucket not initialized")
|
|
187
|
-
|
|
188
|
-
blob = self._bucket.blob(key)
|
|
189
|
-
|
|
169
|
+
blob = self._get_bucket(container_name).blob(key)
|
|
190
170
|
if not blob.exists():
|
|
191
171
|
return False
|
|
192
|
-
|
|
193
172
|
blob.delete()
|
|
194
|
-
|
|
195
173
|
self.logger.debug(f"GCS deleted blob: {key}")
|
|
196
|
-
|
|
197
174
|
return True
|
|
198
|
-
|
|
199
|
-
except NotFound:
|
|
200
|
-
return False
|
|
201
|
-
|
|
202
175
|
except Exception as e:
|
|
203
176
|
raise StorageError(f"GCS delete failed: {str(e)}")
|
|
204
177
|
|
|
205
|
-
# ------------------------------------------------------------------
|
|
206
|
-
# List objects
|
|
207
|
-
# ------------------------------------------------------------------
|
|
208
|
-
|
|
209
178
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
210
|
-
def list(self, prefix: str = "") -> List[str]:
|
|
211
|
-
"""List objects with optional prefix"""
|
|
179
|
+
def list(self, prefix: str = "", container_name: Optional[str] = None) -> List[str]:
|
|
212
180
|
try:
|
|
213
|
-
|
|
214
|
-
raise ConnectionError("GCS bucket not initialized")
|
|
215
|
-
|
|
216
|
-
blobs = self._bucket.list_blobs(prefix=prefix)
|
|
217
|
-
|
|
181
|
+
blobs = self._get_bucket(container_name).list_blobs(prefix=prefix)
|
|
218
182
|
results = [blob.name for blob in blobs]
|
|
219
|
-
|
|
220
183
|
self.logger.debug(f"GCS listed {len(results)} blobs (prefix={prefix})")
|
|
221
|
-
|
|
222
184
|
return results
|
|
223
|
-
|
|
224
185
|
except Exception as e:
|
|
225
186
|
raise StorageError(f"GCS list failed: {str(e)}")
|
|
@@ -8,7 +8,7 @@ import hashlib
|
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
import json
|
|
10
10
|
from datetime import datetime, date
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
from ..errors import DatabaseError, ConnectionError
|
|
14
14
|
from ..retry import retry
|
|
@@ -111,6 +111,7 @@ class PostgreSQLAdapter:
|
|
|
111
111
|
- Decimal -> convert to float
|
|
112
112
|
- everything else -> pass as-is
|
|
113
113
|
"""
|
|
114
|
+
from psycopg2.extras import Json
|
|
114
115
|
|
|
115
116
|
if v is None:
|
|
116
117
|
return None
|
polydb/adapters/S3Adapter.py
CHANGED
|
@@ -11,8 +11,6 @@ import os
|
|
|
11
11
|
import threading
|
|
12
12
|
from typing import Any, Dict, List, Optional
|
|
13
13
|
|
|
14
|
-
import boto3
|
|
15
|
-
from botocore.exceptions import ClientError
|
|
16
14
|
|
|
17
15
|
from ..base.ObjectStorageAdapter import ObjectStorageAdapter
|
|
18
16
|
from ..errors import StorageError, ConnectionError
|
|
@@ -44,6 +42,8 @@ class S3Adapter(ObjectStorageAdapter):
|
|
|
44
42
|
|
|
45
43
|
def _initialize_client(self):
|
|
46
44
|
"""Initialize S3 client once"""
|
|
45
|
+
import boto3
|
|
46
|
+
|
|
47
47
|
try:
|
|
48
48
|
with self._lock:
|
|
49
49
|
if self._client:
|
|
@@ -72,6 +72,9 @@ class S3Adapter(ObjectStorageAdapter):
|
|
|
72
72
|
|
|
73
73
|
def _ensure_bucket_exists(self):
|
|
74
74
|
"""Create bucket if it doesn't exist (safe for AWS + LocalStack)"""
|
|
75
|
+
import boto3
|
|
76
|
+
from botocore.exceptions import ClientError
|
|
77
|
+
|
|
75
78
|
if not self._client:
|
|
76
79
|
return
|
|
77
80
|
|
|
@@ -3,28 +3,35 @@ import mimetypes
|
|
|
3
3
|
import os
|
|
4
4
|
import threading
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
6
7
|
from ..base.ObjectStorageAdapter import ObjectStorageAdapter
|
|
7
8
|
from ..errors import StorageError, ConnectionError
|
|
8
9
|
from ..retry import retry
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class S3CompatibleAdapter(ObjectStorageAdapter):
|
|
12
|
-
"""S3-compatible storage (MinIO, DigitalOcean Spaces) with client reuse
|
|
13
|
+
"""S3-compatible storage (AWS S3, MinIO, DigitalOcean Spaces) with client reuse.
|
|
14
|
+
|
|
15
|
+
- put/get/delete are symmetric: a blob is stored at `key` and fetched at `key`
|
|
16
|
+
- per-call `container_name` overrides the bucket (generic name kept for
|
|
17
|
+
cross-provider parity with the Azure adapter)
|
|
18
|
+
- get() returns None when the object does not exist
|
|
19
|
+
"""
|
|
13
20
|
|
|
14
|
-
def __init__(self):
|
|
21
|
+
def __init__(self, bucket_name: str = ""):
|
|
15
22
|
super().__init__()
|
|
16
23
|
self.endpoint = os.getenv("S3_ENDPOINT_URL")
|
|
17
24
|
self.access_key = os.getenv("S3_ACCESS_KEY")
|
|
18
25
|
self.secret_key = os.getenv("S3_SECRET_KEY")
|
|
19
|
-
self.bucket_name = os.getenv("S3_BUCKET_NAME", "default")
|
|
26
|
+
self.bucket_name = bucket_name or os.getenv("S3_BUCKET_NAME", "default")
|
|
20
27
|
self._client = None
|
|
21
28
|
self._lock = threading.Lock()
|
|
22
29
|
self._initialize_client()
|
|
23
30
|
|
|
24
31
|
def _initialize_client(self):
|
|
25
|
-
"""Initialize S3-compatible client once"""
|
|
32
|
+
"""Initialize S3-compatible client once."""
|
|
26
33
|
try:
|
|
27
|
-
import boto3
|
|
34
|
+
import boto3 # lazy: boto3 is only required for this provider
|
|
28
35
|
|
|
29
36
|
with self._lock:
|
|
30
37
|
if not self._client:
|
|
@@ -38,6 +45,50 @@ class S3CompatibleAdapter(ObjectStorageAdapter):
|
|
|
38
45
|
except Exception as e:
|
|
39
46
|
raise ConnectionError(f"Failed to initialize S3-compatible client: {str(e)}")
|
|
40
47
|
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# helpers
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
def _bucket(self, container_name: Optional[str] = None) -> str:
|
|
52
|
+
return container_name or self.bucket_name
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _is_not_found(exc: Exception) -> bool:
|
|
56
|
+
# inspect botocore ClientError without importing botocore at module load
|
|
57
|
+
resp = getattr(exc, "response", None)
|
|
58
|
+
if isinstance(resp, dict):
|
|
59
|
+
code = str(resp.get("Error", {}).get("Code", ""))
|
|
60
|
+
return code in {"NoSuchKey", "404"}
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def _url(self, bucket: str, key: str) -> str:
|
|
64
|
+
if self.endpoint: # MinIO / Spaces / custom endpoint (path-style)
|
|
65
|
+
return f"{self.endpoint.rstrip('/')}/{bucket}/{key}"
|
|
66
|
+
return f"https://{bucket}.s3.amazonaws.com/{key}" # AWS default (virtual-host)
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
# put
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
def put(
|
|
72
|
+
self,
|
|
73
|
+
key: str,
|
|
74
|
+
data: bytes,
|
|
75
|
+
fileName: str = "",
|
|
76
|
+
optimize: bool = True,
|
|
77
|
+
media_type: Optional[str] = None,
|
|
78
|
+
metadata: Dict[str, Any] | None = None,
|
|
79
|
+
container_name: Optional[str] = None,
|
|
80
|
+
) -> str:
|
|
81
|
+
if optimize and media_type:
|
|
82
|
+
data = self._optimize_media(data, media_type)
|
|
83
|
+
return self._put_raw(
|
|
84
|
+
key=key,
|
|
85
|
+
data=data,
|
|
86
|
+
fileName=fileName,
|
|
87
|
+
media_type=media_type,
|
|
88
|
+
metadata=metadata,
|
|
89
|
+
container_name=container_name,
|
|
90
|
+
)
|
|
91
|
+
|
|
41
92
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
42
93
|
def _put_raw(
|
|
43
94
|
self,
|
|
@@ -46,99 +97,78 @@ class S3CompatibleAdapter(ObjectStorageAdapter):
|
|
|
46
97
|
fileName: str = "",
|
|
47
98
|
media_type: Optional[str] = None,
|
|
48
99
|
metadata: Dict[str, Any] | None = None,
|
|
100
|
+
container_name: Optional[str] = None,
|
|
49
101
|
) -> str:
|
|
50
|
-
"""Upload object
|
|
102
|
+
"""Upload object. Stored at `key` so get/delete find it by the same key."""
|
|
51
103
|
try:
|
|
52
104
|
if not self._client:
|
|
53
105
|
self._initialize_client()
|
|
54
106
|
|
|
55
|
-
|
|
56
|
-
# Resolve filename
|
|
57
|
-
# --------------------------------------------------
|
|
58
|
-
filename = fileName or os.path.basename(key)
|
|
107
|
+
bucket = self._bucket(container_name)
|
|
59
108
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
# --------------------------------------------------
|
|
109
|
+
# filename is metadata only — it must NOT alter the object key
|
|
110
|
+
filename = fileName or os.path.basename(key) or key
|
|
63
111
|
if media_type:
|
|
64
112
|
ext = mimetypes.guess_extension(media_type) or ""
|
|
65
113
|
if ext and not filename.lower().endswith(ext):
|
|
66
114
|
filename += ext
|
|
67
115
|
|
|
68
|
-
# --------------------------------------------------
|
|
69
|
-
# Final key
|
|
70
|
-
# --------------------------------------------------
|
|
71
|
-
blob_key = f"{key.rstrip('/')}/{filename}" if fileName else key
|
|
72
|
-
|
|
73
|
-
# --------------------------------------------------
|
|
74
|
-
# Metadata (string only)
|
|
75
|
-
# --------------------------------------------------
|
|
76
116
|
safe_metadata = {k: str(v) for k, v in (metadata or {}).items()}
|
|
77
117
|
safe_metadata["filename"] = filename
|
|
78
118
|
|
|
79
|
-
# --------------------------------------------------
|
|
80
|
-
# Upload
|
|
81
|
-
# --------------------------------------------------
|
|
82
119
|
self._client.put_object( # type: ignore
|
|
83
|
-
Bucket=
|
|
84
|
-
Key=
|
|
120
|
+
Bucket=bucket,
|
|
121
|
+
Key=key,
|
|
85
122
|
Body=data,
|
|
86
123
|
ContentType=media_type or "application/octet-stream",
|
|
87
124
|
Metadata=safe_metadata,
|
|
88
125
|
)
|
|
89
126
|
|
|
90
|
-
self.logger.debug(f"S3 uploaded: {
|
|
91
|
-
|
|
92
|
-
# --------------------------------------------------
|
|
93
|
-
# Return URL
|
|
94
|
-
# --------------------------------------------------
|
|
95
|
-
if self.endpoint:
|
|
96
|
-
# MinIO / Spaces / custom endpoint
|
|
97
|
-
url = f"{self.endpoint.rstrip('/')}/{self.bucket_name}/{blob_key}"
|
|
98
|
-
else:
|
|
99
|
-
# AWS S3 default
|
|
100
|
-
url = f"https://{self.bucket_name}.s3.amazonaws.com/{blob_key}"
|
|
101
|
-
|
|
102
|
-
return url
|
|
127
|
+
self.logger.debug(f"S3 uploaded: {bucket}/{key}, type={media_type}")
|
|
128
|
+
return self._url(bucket, key)
|
|
103
129
|
|
|
104
130
|
except Exception as e:
|
|
105
131
|
raise StorageError(f"S3-compatible put failed: {str(e)}")
|
|
106
132
|
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# get / delete / list
|
|
135
|
+
# ------------------------------------------------------------------
|
|
107
136
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
108
|
-
def get(self, key: str) -> bytes | None:
|
|
109
|
-
"""Get object"""
|
|
137
|
+
def get(self, key: str, container_name: Optional[str] = None) -> bytes | None:
|
|
110
138
|
try:
|
|
111
139
|
if not self._client:
|
|
112
140
|
self._initialize_client()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
141
|
+
response = self._client.get_object( # type: ignore
|
|
142
|
+
Bucket=self._bucket(container_name), Key=key
|
|
143
|
+
)
|
|
144
|
+
return response["Body"].read()
|
|
117
145
|
except Exception as e:
|
|
146
|
+
if self._is_not_found(e):
|
|
147
|
+
return None
|
|
118
148
|
raise StorageError(f"S3-compatible get failed: {str(e)}")
|
|
119
149
|
|
|
120
150
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
121
|
-
def delete(self, key: str) -> bool:
|
|
122
|
-
"""Delete object"""
|
|
151
|
+
def delete(self, key: str, container_name: Optional[str] = None) -> bool:
|
|
123
152
|
try:
|
|
124
153
|
if not self._client:
|
|
125
154
|
self._initialize_client()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return True
|
|
129
|
-
return False
|
|
155
|
+
self._client.delete_object(Bucket=self._bucket(container_name), Key=key) # type: ignore
|
|
156
|
+
return True
|
|
130
157
|
except Exception as e:
|
|
158
|
+
if self._is_not_found(e):
|
|
159
|
+
return False
|
|
131
160
|
raise StorageError(f"S3-compatible delete failed: {str(e)}")
|
|
132
161
|
|
|
133
162
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
134
|
-
def list(self, prefix: str = "") -> List[str]:
|
|
135
|
-
"""List objects with prefix"""
|
|
163
|
+
def list(self, prefix: str = "", container_name: Optional[str] = None) -> List[str]:
|
|
136
164
|
try:
|
|
137
165
|
if not self._client:
|
|
138
166
|
self._initialize_client()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
paginator = self._client.get_paginator("list_objects_v2") # type: ignore
|
|
168
|
+
results: List[str] = []
|
|
169
|
+
for page in paginator.paginate(Bucket=self._bucket(container_name), Prefix=prefix):
|
|
170
|
+
results.extend(obj["Key"] for obj in page.get("Contents", []))
|
|
171
|
+
self.logger.debug(f"S3 listed {len(results)} objects prefix={prefix}")
|
|
172
|
+
return results
|
|
143
173
|
except Exception as e:
|
|
144
174
|
raise StorageError(f"S3-compatible list failed: {str(e)}")
|
polydb/adapters/SQSAdapter.py
CHANGED
|
@@ -5,8 +5,6 @@ import os
|
|
|
5
5
|
import threading
|
|
6
6
|
from typing import Any, Dict, List
|
|
7
7
|
|
|
8
|
-
import boto3
|
|
9
|
-
from botocore.exceptions import ClientError
|
|
10
8
|
|
|
11
9
|
from ..base.QueueAdapter import QueueAdapter
|
|
12
10
|
from ..errors import ConnectionError, QueueError
|
|
@@ -39,6 +37,8 @@ class SQSAdapter(QueueAdapter):
|
|
|
39
37
|
# ---------------------------------------------------------
|
|
40
38
|
|
|
41
39
|
def _initialize_client(self):
|
|
40
|
+
import boto3
|
|
41
|
+
|
|
42
42
|
try:
|
|
43
43
|
with self._lock:
|
|
44
44
|
if self._client:
|
|
@@ -67,6 +67,9 @@ class SQSAdapter(QueueAdapter):
|
|
|
67
67
|
|
|
68
68
|
def _ensure_queue_exists(self, queue_name: str) -> str:
|
|
69
69
|
"""Create queue if it does not exist"""
|
|
70
|
+
|
|
71
|
+
from botocore.exceptions import ClientError
|
|
72
|
+
|
|
70
73
|
if not self._client:
|
|
71
74
|
raise ConnectionError("SQS client not initialized")
|
|
72
75
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# src/polydb/adapters/VercelFileAdapter.py
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from ..base.SharedFilesAdapter import SharedFilesAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VercelFileAdapter(SharedFilesAdapter):
|
|
9
|
+
"""
|
|
10
|
+
Vercel has no shared/persistent filesystem (only ephemeral per-invocation /tmp).
|
|
11
|
+
Shared file storage is not supported — use object storage (Vercel Blob) instead.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_MSG = (
|
|
15
|
+
"Vercel has no shared file storage (only ephemeral /tmp). "
|
|
16
|
+
"Use object storage (get_object_storage) instead."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def write(self, path: str, data: bytes, share_name: Optional[str] = None) -> bool:
|
|
20
|
+
raise NotImplementedError(self._MSG)
|
|
21
|
+
|
|
22
|
+
def read(self, path: str, share_name: Optional[str] = None) -> bytes | None:
|
|
23
|
+
raise NotImplementedError(self._MSG)
|
|
24
|
+
|
|
25
|
+
def delete(self, path: str, share_name: Optional[str] = None) -> bool:
|
|
26
|
+
raise NotImplementedError(self._MSG)
|
|
27
|
+
|
|
28
|
+
def list(self, directory: str = "", share_name: Optional[str] = None) -> List[str]:
|
|
29
|
+
raise NotImplementedError(self._MSG)
|
polydb/audit/__init__.py
CHANGED