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.
Files changed (38) hide show
  1. altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
  2. altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
  3. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
  4. polydb/__init__.py +2 -2
  5. polydb/adapters/AzureBlobStorageAdapter.py +146 -41
  6. polydb/adapters/AzureFileStorageAdapter.py +148 -43
  7. polydb/adapters/AzureQueueAdapter.py +96 -34
  8. polydb/adapters/AzureTableStorageAdapter.py +462 -119
  9. polydb/adapters/BlockchainBlobAdapter.py +111 -0
  10. polydb/adapters/BlockchainKVAdapter.py +152 -0
  11. polydb/adapters/BlockchainQueueAdapter.py +116 -0
  12. polydb/adapters/DynamoDBAdapter.py +463 -176
  13. polydb/adapters/FirestoreAdapter.py +320 -148
  14. polydb/adapters/GCPPubSubAdapter.py +217 -0
  15. polydb/adapters/GCPStorageAdapter.py +184 -39
  16. polydb/adapters/MongoDBAdapter.py +159 -39
  17. polydb/adapters/PostgreSQLAdapter.py +285 -83
  18. polydb/adapters/S3Adapter.py +172 -35
  19. polydb/adapters/S3CompatibleAdapter.py +62 -8
  20. polydb/adapters/SQSAdapter.py +121 -44
  21. polydb/adapters/VercelBlobAdapter.py +196 -0
  22. polydb/adapters/VercelKVAdapter.py +275 -283
  23. polydb/adapters/VercelQueueAdapter.py +61 -0
  24. polydb/audit/AuditStorage.py +1 -1
  25. polydb/base/NoSQLKVAdapter.py +113 -101
  26. polydb/base/ObjectStorageAdapter.py +42 -6
  27. polydb/base/QueueAdapter.py +2 -2
  28. polydb/base/SharedFilesAdapter.py +2 -2
  29. polydb/cloudDatabaseFactory.py +200 -0
  30. polydb/databaseFactory.py +434 -101
  31. polydb/models.py +63 -1
  32. polydb/query.py +111 -42
  33. altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
  34. altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
  35. polydb/adapters/PubSubAdapter.py +0 -85
  36. polydb/factory.py +0 -107
  37. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
  38. {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, cast
5
- from google.cloud.firestore import DocumentSnapshot
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
- """GCP Cloud Storage with client reuse"""
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
- self.bucket_name = os.getenv("GCS_BUCKET_NAME", "default")
17
- self._client = None
18
- self._bucket = None
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
- def _initialize_client(self):
23
- """Initialize GCS client once"""
24
- try:
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 not self._client:
29
- self._client = storage.Client()
30
- self._bucket = self._client.bucket(self.bucket_name)
31
- self.logger.info("Initialized GCS client")
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(self, key: str, data: bytes) -> str:
37
- """Store object"""
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
- blob = self._bucket.blob(key)
41
- blob.upload_from_string(data)
42
- self.logger.debug(f"Uploaded blob: {key}")
43
- return key
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 | None:
49
- """Get object"""
155
+ def get(self, key: str) -> Optional[bytes]:
156
+ """Download object from GCS"""
50
157
  try:
51
- if self._bucket:
52
- blob = self._bucket.blob(key)
53
- return blob.download_as_bytes()
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
- blob = self._bucket.blob(key)
64
- blob.delete()
65
- return True
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
- blobs = self._bucket.list_blobs(prefix=prefix)
76
- return [blob.name for blob in blobs]
77
- return []
78
- except Exception as e:
79
- raise StorageError(f"GCS list failed: {str(e)}")
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)}")