altcodepro-polydb-python 2.2.2__py3-none-any.whl → 2.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
- altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
- polydb/__init__.py +2 -2
- polydb/adapters/AzureBlobStorageAdapter.py +146 -41
- polydb/adapters/AzureFileStorageAdapter.py +148 -43
- polydb/adapters/AzureQueueAdapter.py +96 -34
- polydb/adapters/AzureTableStorageAdapter.py +462 -119
- polydb/adapters/BlockchainBlobAdapter.py +111 -0
- polydb/adapters/BlockchainKVAdapter.py +152 -0
- polydb/adapters/BlockchainQueueAdapter.py +116 -0
- polydb/adapters/DynamoDBAdapter.py +463 -176
- polydb/adapters/FirestoreAdapter.py +320 -148
- polydb/adapters/GCPPubSubAdapter.py +217 -0
- polydb/adapters/GCPStorageAdapter.py +184 -39
- polydb/adapters/MongoDBAdapter.py +159 -39
- polydb/adapters/PostgreSQLAdapter.py +285 -83
- polydb/adapters/S3Adapter.py +172 -35
- polydb/adapters/S3CompatibleAdapter.py +62 -8
- polydb/adapters/SQSAdapter.py +121 -44
- polydb/adapters/VercelBlobAdapter.py +196 -0
- polydb/adapters/VercelKVAdapter.py +275 -283
- polydb/adapters/VercelQueueAdapter.py +61 -0
- polydb/audit/AuditStorage.py +1 -1
- polydb/base/NoSQLKVAdapter.py +113 -101
- polydb/base/ObjectStorageAdapter.py +42 -6
- polydb/base/QueueAdapter.py +2 -2
- polydb/base/SharedFilesAdapter.py +2 -2
- polydb/cloudDatabaseFactory.py +200 -0
- polydb/databaseFactory.py +434 -101
- polydb/models.py +63 -1
- polydb/query.py +111 -42
- altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
- altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
- polydb/adapters/PubSubAdapter.py +0 -85
- polydb/factory.py +0 -107
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
from google.cloud import pubsub_v1
|
|
7
|
+
from google.api_core.exceptions import AlreadyExists, NotFound
|
|
8
|
+
|
|
9
|
+
from ..base.QueueAdapter import QueueAdapter
|
|
10
|
+
from ..errors import ConnectionError, QueueError
|
|
11
|
+
from ..retry import retry
|
|
12
|
+
from ..json_safe import json_safe
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
JsonLike = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GCPPubSubAdapter(QueueAdapter):
|
|
19
|
+
"""
|
|
20
|
+
Production-grade GCP Pub/Sub adapter.
|
|
21
|
+
|
|
22
|
+
- Emulator support via PUBSUB_EMULATOR_HOST (google client honors it)
|
|
23
|
+
- Auto topic + subscription creation
|
|
24
|
+
- send accepts Any (tests send str)
|
|
25
|
+
- receive returns [{"id","ack_id","body"}...]
|
|
26
|
+
- ack() method for tests
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
project_id: Optional[str] = None,
|
|
32
|
+
topic: Optional[str] = None,
|
|
33
|
+
subscription: Optional[str] = None,
|
|
34
|
+
):
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
self.project_id: str = project_id or os.getenv("GOOGLE_CLOUD_PROJECT", "polydb-test")
|
|
38
|
+
self.default_topic: str = topic or os.getenv("PUBSUB_TOPIC", "polydb-topic")
|
|
39
|
+
self.default_subscription: str = subscription or os.getenv(
|
|
40
|
+
"PUBSUB_SUBSCRIPTION", "polydb-sub"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self._publisher: Optional[pubsub_v1.PublisherClient] = None
|
|
44
|
+
self._subscriber: Optional[pubsub_v1.SubscriberClient] = None
|
|
45
|
+
self._lock = threading.Lock()
|
|
46
|
+
|
|
47
|
+
self._initialize_clients()
|
|
48
|
+
|
|
49
|
+
def _initialize_clients(self) -> None:
|
|
50
|
+
try:
|
|
51
|
+
with self._lock:
|
|
52
|
+
if self._publisher and self._subscriber:
|
|
53
|
+
return
|
|
54
|
+
# google client auto-detects PUBSUB_EMULATOR_HOST if set
|
|
55
|
+
self._publisher = pubsub_v1.PublisherClient()
|
|
56
|
+
self._subscriber = pubsub_v1.SubscriberClient()
|
|
57
|
+
self.logger.info("Initialized Pub/Sub clients")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise ConnectionError(f"Failed to initialize Pub/Sub: {e}")
|
|
60
|
+
|
|
61
|
+
def _resolve_names(self, queue_name: str) -> Tuple[str, str]:
|
|
62
|
+
"""
|
|
63
|
+
Your tests call send() / receive() without queue_name,
|
|
64
|
+
so they pass default="default". That should map to adapter defaults.
|
|
65
|
+
"""
|
|
66
|
+
if not queue_name or queue_name == "default":
|
|
67
|
+
return self.default_topic, self.default_subscription
|
|
68
|
+
# If user passes a custom name, use it for both (simple convention)
|
|
69
|
+
return queue_name, queue_name
|
|
70
|
+
|
|
71
|
+
def _topic_path(self, topic: str) -> str:
|
|
72
|
+
if not self._publisher:
|
|
73
|
+
raise ConnectionError("Pub/Sub publisher not initialized")
|
|
74
|
+
return self._publisher.topic_path(self.project_id, topic)
|
|
75
|
+
|
|
76
|
+
def _subscription_path(self, subscription: str) -> str:
|
|
77
|
+
if not self._subscriber:
|
|
78
|
+
raise ConnectionError("Pub/Sub subscriber not initialized")
|
|
79
|
+
return self._subscriber.subscription_path(self.project_id, subscription)
|
|
80
|
+
|
|
81
|
+
def _ensure_topic(self, topic: str) -> str:
|
|
82
|
+
if not self._publisher:
|
|
83
|
+
raise ConnectionError("Pub/Sub publisher not initialized")
|
|
84
|
+
path = self._topic_path(topic)
|
|
85
|
+
try:
|
|
86
|
+
self._publisher.get_topic(request={"topic": path})
|
|
87
|
+
except NotFound:
|
|
88
|
+
try:
|
|
89
|
+
self._publisher.create_topic(request={"name": path})
|
|
90
|
+
self.logger.info(f"Created Pub/Sub topic: {topic}")
|
|
91
|
+
except AlreadyExists:
|
|
92
|
+
pass
|
|
93
|
+
return path
|
|
94
|
+
|
|
95
|
+
def _ensure_subscription(self, topic: str, subscription: str) -> str:
|
|
96
|
+
if not self._subscriber:
|
|
97
|
+
raise ConnectionError("Pub/Sub subscriber not initialized")
|
|
98
|
+
if not self._publisher:
|
|
99
|
+
raise ConnectionError("Pub/Sub publisher not initialized")
|
|
100
|
+
|
|
101
|
+
topic_path = self._ensure_topic(topic)
|
|
102
|
+
sub_path = self._subscription_path(subscription)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
self._subscriber.get_subscription(request={"subscription": sub_path})
|
|
106
|
+
except NotFound:
|
|
107
|
+
try:
|
|
108
|
+
self._subscriber.create_subscription(
|
|
109
|
+
request={"name": sub_path, "topic": topic_path}
|
|
110
|
+
)
|
|
111
|
+
self.logger.info(f"Created Pub/Sub subscription: {subscription}")
|
|
112
|
+
except AlreadyExists:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
return sub_path
|
|
116
|
+
|
|
117
|
+
# -------------------------
|
|
118
|
+
# API
|
|
119
|
+
# -------------------------
|
|
120
|
+
|
|
121
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
|
|
122
|
+
def send(self, message: JsonLike, queue_name: str = "default") -> str:
|
|
123
|
+
"""
|
|
124
|
+
Publish message to Pub/Sub.
|
|
125
|
+
Tests send a string; production may send dict.
|
|
126
|
+
We envelope it as {"body": <message>}.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
if not self._publisher:
|
|
130
|
+
raise ConnectionError("Pub/Sub publisher not initialized")
|
|
131
|
+
|
|
132
|
+
topic, subscription = self._resolve_names(queue_name)
|
|
133
|
+
topic_path = self._ensure_topic(topic)
|
|
134
|
+
# Ensure subscription exists so receive works immediately in emulator
|
|
135
|
+
self._ensure_subscription(topic, subscription)
|
|
136
|
+
|
|
137
|
+
payload = {"body": message}
|
|
138
|
+
data = json.dumps(payload, default=json_safe).encode("utf-8")
|
|
139
|
+
|
|
140
|
+
future = self._publisher.publish(topic_path, data=data)
|
|
141
|
+
msg_id = future.result(timeout=10)
|
|
142
|
+
|
|
143
|
+
self.logger.debug(f"Published Pub/Sub message {msg_id} (topic={topic})")
|
|
144
|
+
return msg_id
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise QueueError(f"Pub/Sub send failed: {e}")
|
|
148
|
+
|
|
149
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
|
|
150
|
+
def receive(self, queue_name: str = "default", max_messages: int = 1) -> List[Dict[str, Any]]:
|
|
151
|
+
"""
|
|
152
|
+
Pull messages.
|
|
153
|
+
IMPORTANT: Do NOT auto-ack here (so test_ack_message can ack explicitly).
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
if not self._subscriber:
|
|
157
|
+
raise ConnectionError("Pub/Sub subscriber not initialized")
|
|
158
|
+
|
|
159
|
+
topic, subscription = self._resolve_names(queue_name)
|
|
160
|
+
sub_path = self._ensure_subscription(topic, subscription)
|
|
161
|
+
|
|
162
|
+
resp = self._subscriber.pull(
|
|
163
|
+
request={"subscription": sub_path, "max_messages": max_messages},
|
|
164
|
+
timeout=5,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
out: List[Dict[str, Any]] = []
|
|
168
|
+
for r in resp.received_messages:
|
|
169
|
+
body: Any = None
|
|
170
|
+
try:
|
|
171
|
+
decoded = json.loads(r.message.data.decode("utf-8"))
|
|
172
|
+
if isinstance(decoded, dict) and "body" in decoded:
|
|
173
|
+
body = decoded["body"]
|
|
174
|
+
else:
|
|
175
|
+
body = decoded
|
|
176
|
+
except Exception:
|
|
177
|
+
body = r.message.data.decode("utf-8", errors="replace")
|
|
178
|
+
|
|
179
|
+
out.append(
|
|
180
|
+
{
|
|
181
|
+
"id": r.message.message_id,
|
|
182
|
+
"ack_id": r.ack_id,
|
|
183
|
+
"body": body,
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
raise QueueError(f"Pub/Sub receive failed: {e}")
|
|
191
|
+
|
|
192
|
+
def delete(self, message_id: str, queue_name: str = "default", pop_receipt: str = "") -> bool:
|
|
193
|
+
"""
|
|
194
|
+
Pub/Sub delete == ack.
|
|
195
|
+
- pop_receipt maps to ack_id (preferred).
|
|
196
|
+
- message_id fallback kept for backward compatibility.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
if not self._subscriber:
|
|
200
|
+
raise ConnectionError("Pub/Sub subscriber not initialized")
|
|
201
|
+
|
|
202
|
+
_, subscription = self._resolve_names(queue_name)
|
|
203
|
+
sub_path = self._subscription_path(subscription)
|
|
204
|
+
|
|
205
|
+
ack_id = pop_receipt or message_id
|
|
206
|
+
if not ack_id:
|
|
207
|
+
raise QueueError("Pub/Sub delete requires ack_id (use pop_receipt from receive())")
|
|
208
|
+
|
|
209
|
+
self._subscriber.acknowledge(request={"subscription": sub_path, "ack_ids": [ack_id]})
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise QueueError(f"Pub/Sub delete failed: {e}")
|
|
214
|
+
|
|
215
|
+
# Alias that your tests want
|
|
216
|
+
def ack(self, ack_id: str, queue_name: str = "default") -> bool:
|
|
217
|
+
return self.delete(message_id="", queue_name=queue_name, pop_receipt=ack_id)
|
|
@@ -1,81 +1,226 @@
|
|
|
1
1
|
# src/polydb/adapters/GCPStorageAdapter.py
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
2
4
|
import os
|
|
3
5
|
import threading
|
|
4
|
-
from typing import List, Optional
|
|
5
|
-
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from google.cloud import storage
|
|
9
|
+
from google.api_core.exceptions import NotFound
|
|
10
|
+
|
|
6
11
|
from ..base.ObjectStorageAdapter import ObjectStorageAdapter
|
|
7
12
|
from ..errors import StorageError, ConnectionError
|
|
8
13
|
from ..retry import retry
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class GCPStorageAdapter(ObjectStorageAdapter):
|
|
12
|
-
"""
|
|
17
|
+
"""
|
|
18
|
+
Production-grade Google Cloud Storage adapter.
|
|
19
|
+
|
|
20
|
+
Features
|
|
21
|
+
--------
|
|
22
|
+
- Thread-safe client initialization
|
|
23
|
+
- Automatic bucket creation
|
|
24
|
+
- Emulator support (fake-gcs-server)
|
|
25
|
+
- Retry support
|
|
26
|
+
- Structured logging
|
|
27
|
+
"""
|
|
13
28
|
|
|
14
|
-
def __init__(self):
|
|
29
|
+
def __init__(self, bucket_name: Optional[str] = None):
|
|
15
30
|
super().__init__()
|
|
16
|
-
|
|
17
|
-
self.
|
|
18
|
-
self.
|
|
31
|
+
|
|
32
|
+
self.bucket_name: str = bucket_name or os.getenv("GCS_BUCKET_NAME", "default")
|
|
33
|
+
self.project_id: str = os.getenv("GOOGLE_CLOUD_PROJECT", "polydb-test")
|
|
34
|
+
|
|
35
|
+
# Emulator support
|
|
36
|
+
self._endpoint: Optional[str] = os.getenv("GCS_ENDPOINT")
|
|
37
|
+
|
|
38
|
+
self._client: Optional[storage.Client] = None
|
|
39
|
+
self._bucket: Optional[storage.Bucket] = None
|
|
40
|
+
|
|
19
41
|
self._lock = threading.Lock()
|
|
42
|
+
|
|
20
43
|
self._initialize_client()
|
|
21
44
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
from google.cloud import storage
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Client initialization
|
|
47
|
+
# ------------------------------------------------------------------
|
|
26
48
|
|
|
49
|
+
def _initialize_client(self) -> None:
|
|
50
|
+
"""Initialize GCS client once (thread-safe)"""
|
|
51
|
+
try:
|
|
27
52
|
with self._lock:
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
if self._client:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if self._endpoint:
|
|
57
|
+
self.logger.info(f"Using GCS emulator: {self._endpoint}")
|
|
58
|
+
self._client = storage.Client(
|
|
59
|
+
project=self.project_id,
|
|
60
|
+
client_options={"api_endpoint": self._endpoint},
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
self._client = storage.Client(project=self.project_id)
|
|
64
|
+
|
|
65
|
+
self._bucket = self._client.bucket(self.bucket_name)
|
|
66
|
+
|
|
67
|
+
# Ensure bucket exists
|
|
68
|
+
try:
|
|
69
|
+
if not self._bucket.exists():
|
|
70
|
+
self._bucket = self._client.create_bucket(self.bucket_name)
|
|
71
|
+
self.logger.info(f"Created GCS bucket: {self.bucket_name}")
|
|
72
|
+
except Exception:
|
|
73
|
+
# fake-gcs-server does not support bucket.exists()
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
self.logger.info(
|
|
77
|
+
f"GCS initialized (bucket={self.bucket_name}, project={self.project_id})"
|
|
78
|
+
)
|
|
79
|
+
|
|
32
80
|
except Exception as e:
|
|
33
81
|
raise ConnectionError(f"Failed to initialize GCS: {str(e)}")
|
|
34
82
|
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Put object
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
35
87
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
36
|
-
def _put_raw(
|
|
37
|
-
|
|
88
|
+
def _put_raw(
|
|
89
|
+
self,
|
|
90
|
+
key: str,
|
|
91
|
+
data: bytes,
|
|
92
|
+
fileName: str = "",
|
|
93
|
+
media_type: Optional[str] = None,
|
|
94
|
+
metadata: Dict[str, Any] | None = None,
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Upload object to GCS with media type, metadata, filename handling"""
|
|
38
97
|
try:
|
|
39
|
-
if self._bucket:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
if not self._bucket:
|
|
99
|
+
raise ConnectionError("GCS bucket not initialized")
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------
|
|
102
|
+
# Resolve filename
|
|
103
|
+
# --------------------------------------------------
|
|
104
|
+
filename = fileName or os.path.basename(key)
|
|
105
|
+
|
|
106
|
+
# --------------------------------------------------
|
|
107
|
+
# Ensure extension from media_type
|
|
108
|
+
# --------------------------------------------------
|
|
109
|
+
if media_type:
|
|
110
|
+
ext = mimetypes.guess_extension(media_type) or ""
|
|
111
|
+
if ext and not filename.lower().endswith(ext):
|
|
112
|
+
filename += ext
|
|
113
|
+
|
|
114
|
+
# --------------------------------------------------
|
|
115
|
+
# Final blob key
|
|
116
|
+
# --------------------------------------------------
|
|
117
|
+
blob_key = f"{key.rstrip('/')}/{filename}" if fileName else key
|
|
118
|
+
|
|
119
|
+
blob = self._bucket.blob(blob_key)
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------
|
|
122
|
+
# Metadata (must be string)
|
|
123
|
+
# --------------------------------------------------
|
|
124
|
+
safe_metadata = {k: str(v) for k, v in (metadata or {}).items()}
|
|
125
|
+
safe_metadata["filename"] = filename
|
|
126
|
+
|
|
127
|
+
blob.metadata = safe_metadata
|
|
128
|
+
|
|
129
|
+
# --------------------------------------------------
|
|
130
|
+
# Upload with content type
|
|
131
|
+
# --------------------------------------------------
|
|
132
|
+
blob.upload_from_string(
|
|
133
|
+
data,
|
|
134
|
+
content_type=media_type or "application/octet-stream",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Persist metadata (required in GCS)
|
|
138
|
+
blob.patch()
|
|
139
|
+
|
|
140
|
+
self.logger.debug(f"GCS uploaded blob: {blob_key}, type={media_type}")
|
|
141
|
+
|
|
142
|
+
# --------------------------------------------------
|
|
143
|
+
# Return public URL
|
|
144
|
+
# --------------------------------------------------
|
|
145
|
+
return f"https://storage.googleapis.com/{self.bucket_name}/{blob_key}"
|
|
146
|
+
|
|
44
147
|
except Exception as e:
|
|
45
148
|
raise StorageError(f"GCS put failed: {str(e)}")
|
|
46
149
|
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Get object
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
47
154
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
48
|
-
def get(self, key: str) -> bytes
|
|
49
|
-
"""
|
|
155
|
+
def get(self, key: str) -> Optional[bytes]:
|
|
156
|
+
"""Download object from GCS"""
|
|
50
157
|
try:
|
|
51
|
-
if self._bucket:
|
|
52
|
-
|
|
53
|
-
|
|
158
|
+
if not self._bucket:
|
|
159
|
+
raise ConnectionError("GCS bucket not initialized")
|
|
160
|
+
|
|
161
|
+
blob = self._bucket.blob(key)
|
|
162
|
+
|
|
163
|
+
if not blob.exists():
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
data = blob.download_as_bytes()
|
|
167
|
+
|
|
168
|
+
self.logger.debug(f"GCS downloaded blob: {key}")
|
|
169
|
+
|
|
170
|
+
return data
|
|
171
|
+
|
|
172
|
+
except NotFound:
|
|
54
173
|
return None
|
|
174
|
+
|
|
55
175
|
except Exception as e:
|
|
56
176
|
raise StorageError(f"GCS get failed: {str(e)}")
|
|
57
177
|
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# Delete object
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
|
|
58
182
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
59
183
|
def delete(self, key: str) -> bool:
|
|
60
|
-
"""Delete object"""
|
|
184
|
+
"""Delete object from GCS"""
|
|
61
185
|
try:
|
|
62
|
-
if self._bucket:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
186
|
+
if not self._bucket:
|
|
187
|
+
raise ConnectionError("GCS bucket not initialized")
|
|
188
|
+
|
|
189
|
+
blob = self._bucket.blob(key)
|
|
190
|
+
|
|
191
|
+
if not blob.exists():
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
blob.delete()
|
|
195
|
+
|
|
196
|
+
self.logger.debug(f"GCS deleted blob: {key}")
|
|
197
|
+
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
except NotFound:
|
|
66
201
|
return False
|
|
202
|
+
|
|
67
203
|
except Exception as e:
|
|
68
204
|
raise StorageError(f"GCS delete failed: {str(e)}")
|
|
69
205
|
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
# List objects
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
|
|
70
210
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
71
211
|
def list(self, prefix: str = "") -> List[str]:
|
|
72
|
-
"""List objects with prefix"""
|
|
212
|
+
"""List objects with optional prefix"""
|
|
73
213
|
try:
|
|
74
|
-
if self._bucket:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
214
|
+
if not self._bucket:
|
|
215
|
+
raise ConnectionError("GCS bucket not initialized")
|
|
216
|
+
|
|
217
|
+
blobs = self._bucket.list_blobs(prefix=prefix)
|
|
218
|
+
|
|
219
|
+
results = [blob.name for blob in blobs]
|
|
80
220
|
|
|
221
|
+
self.logger.debug(f"GCS listed {len(results)} blobs (prefix={prefix})")
|
|
81
222
|
|
|
223
|
+
return results
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
raise StorageError(f"GCS list failed: {str(e)}")
|