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
@@ -1,61 +1,190 @@
1
1
  # src/polydb/adapters/S3Adapter.py
2
+
2
3
  """
3
- S3 adapter
4
+ S3 adapter (AWS + LocalStack compatible)
4
5
  """
5
6
 
7
+ from __future__ import annotations
8
+
9
+ import mimetypes
6
10
  import os
7
11
  import threading
8
- from typing import List, Optional
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import boto3
15
+ from botocore.exceptions import ClientError
16
+
9
17
  from ..base.ObjectStorageAdapter import ObjectStorageAdapter
10
18
  from ..errors import StorageError, ConnectionError
11
19
  from ..retry import retry
20
+
21
+
12
22
  class S3Adapter(ObjectStorageAdapter):
13
- """AWS S3 with client reuse"""
23
+ """AWS S3 adapter with client reuse and automatic bucket creation"""
14
24
 
15
- def __init__(self):
25
+ def __init__(self, bucket_name: str = "", region: str = "", endpoint_url: str = ""):
16
26
  super().__init__()
17
- self.bucket_name = os.getenv("S3_BUCKET_NAME", "default")
18
- self._client = None
27
+
28
+ self.bucket_name = bucket_name or os.getenv("S3_BUCKET_NAME", "polydb-test")
29
+
30
+ self.region = (
31
+ region or os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") or "us-east-1"
32
+ )
33
+
34
+ self.endpoint_url = endpoint_url or os.getenv("AWS_ENDPOINT_URL")
35
+
36
+ self._client: Any = None
19
37
  self._lock = threading.Lock()
38
+
20
39
  self._initialize_client()
21
40
 
41
+ # ---------------------------------------------------------
42
+ # Client initialization
43
+ # ---------------------------------------------------------
44
+
22
45
  def _initialize_client(self):
23
46
  """Initialize S3 client once"""
24
47
  try:
25
- import boto3
26
-
27
48
  with self._lock:
28
- if not self._client:
29
- self._client = boto3.client("s3")
30
- self.logger.info("Initialized S3 client")
49
+ if self._client:
50
+ return
51
+
52
+ self._client = boto3.client(
53
+ "s3",
54
+ region_name=self.region,
55
+ endpoint_url=self.endpoint_url,
56
+ aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", "test"),
57
+ aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", "test"),
58
+ )
59
+
60
+ self._ensure_bucket_exists()
61
+
62
+ self.logger.info(
63
+ f"Initialized S3 client (region={self.region}, endpoint={self.endpoint_url or 'aws'})"
64
+ )
65
+
31
66
  except Exception as e:
32
- raise ConnectionError(f"Failed to initialize S3 client: {str(e)}")
67
+ raise ConnectionError(f"Failed to initialize S3 client: {e}")
68
+
69
+ # ---------------------------------------------------------
70
+ # Bucket management
71
+ # ---------------------------------------------------------
72
+
73
+ def _ensure_bucket_exists(self):
74
+ """Create bucket if it doesn't exist (safe for AWS + LocalStack)"""
75
+ if not self._client:
76
+ return
77
+
78
+ try:
79
+ self._client.head_bucket(Bucket=self.bucket_name)
80
+ return
81
+ except ClientError:
82
+ pass
83
+
84
+ try:
85
+ if self.region == "us-east-1":
86
+ self._client.create_bucket(Bucket=self.bucket_name)
87
+ else:
88
+ self._client.create_bucket(
89
+ Bucket=self.bucket_name,
90
+ CreateBucketConfiguration={"LocationConstraint": self.region},
91
+ )
92
+
93
+ self.logger.info(f"Created S3 bucket: {self.bucket_name}")
94
+
95
+ except ClientError as e:
96
+ code = e.response.get("Error", {}).get("Code")
97
+ if code not in ("BucketAlreadyOwnedByYou", "BucketAlreadyExists"):
98
+ raise StorageError(f"S3 bucket creation failed: {e}")
99
+
100
+ # ---------------------------------------------------------
101
+ # Core operations
102
+ # ---------------------------------------------------------
33
103
 
34
104
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
35
- def _put_raw(self, key: str, data: bytes) -> str:
36
- """Store object"""
105
+ def _put_raw(
106
+ self,
107
+ key: str,
108
+ data: bytes,
109
+ fileName: str = "",
110
+ media_type: Optional[str] = None,
111
+ metadata: Dict[str, Any] | None = None,
112
+ ) -> str:
113
+ """Upload object to S3-compatible storage with metadata and media type"""
37
114
  try:
38
115
  if not self._client:
39
116
  self._initialize_client()
40
- if self._client:
41
- self._client.put_object(Bucket=self.bucket_name, Key=key, Body=data)
42
- self.logger.debug(f"Uploaded to S3: {key}")
43
- return key
117
+
118
+ # --------------------------------------------------
119
+ # Resolve filename
120
+ # --------------------------------------------------
121
+ filename = fileName or os.path.basename(key)
122
+
123
+ # --------------------------------------------------
124
+ # Ensure extension from media_type
125
+ # --------------------------------------------------
126
+ if media_type:
127
+ ext = mimetypes.guess_extension(media_type) or ""
128
+ if ext and not filename.lower().endswith(ext):
129
+ filename += ext
130
+
131
+ # --------------------------------------------------
132
+ # Final key
133
+ # --------------------------------------------------
134
+ blob_key = f"{key.rstrip('/')}/{filename}" if fileName else key
135
+
136
+ # --------------------------------------------------
137
+ # Metadata (string only)
138
+ # --------------------------------------------------
139
+ safe_metadata = {k: str(v) for k, v in (metadata or {}).items()}
140
+ safe_metadata["filename"] = filename
141
+
142
+ # --------------------------------------------------
143
+ # Upload
144
+ # --------------------------------------------------
145
+ self._client.put_object( # type: ignore
146
+ Bucket=self.bucket_name,
147
+ Key=blob_key,
148
+ Body=data,
149
+ ContentType=media_type or "application/octet-stream",
150
+ Metadata=safe_metadata,
151
+ )
152
+
153
+ self.logger.debug(f"S3 uploaded: {blob_key}, type={media_type}")
154
+
155
+ # --------------------------------------------------
156
+ # Return URL
157
+ # --------------------------------------------------
158
+ if self.endpoint_url:
159
+ # MinIO / Spaces / custom endpoint
160
+ url = f"{self.endpoint_url.rstrip('/')}/{self.bucket_name}/{blob_key}"
161
+ else:
162
+ # AWS S3 default
163
+ url = f"https://{self.bucket_name}.s3.amazonaws.com/{blob_key}"
164
+
165
+ return url
166
+
44
167
  except Exception as e:
45
- raise StorageError(f"S3 put failed: {str(e)}")
168
+ raise StorageError(f"S3-compatible put failed: {str(e)}")
46
169
 
47
170
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
48
171
  def get(self, key: str) -> bytes | None:
49
- """Get object"""
172
+ """Download object"""
50
173
  try:
51
174
  if not self._client:
52
175
  self._initialize_client()
53
- if self._client:
54
- response = self._client.get_object(Bucket=self.bucket_name, Key=key)
55
- return response["Body"].read()
176
+
177
+ response = self._client.get_object(
178
+ Bucket=self.bucket_name,
179
+ Key=key,
180
+ )
181
+
182
+ return response["Body"].read()
183
+
184
+ except self._client.exceptions.NoSuchKey: # type: ignore
56
185
  return None
57
186
  except Exception as e:
58
- raise StorageError(f"S3 get failed: {str(e)}")
187
+ raise StorageError(f"S3 get failed: {e}")
59
188
 
60
189
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
61
190
  def delete(self, key: str) -> bool:
@@ -63,12 +192,16 @@ class S3Adapter(ObjectStorageAdapter):
63
192
  try:
64
193
  if not self._client:
65
194
  self._initialize_client()
66
- if self._client:
67
- self._client.delete_object(Bucket=self.bucket_name, Key=key)
68
- return True
69
- return False
195
+
196
+ self._client.delete_object(
197
+ Bucket=self.bucket_name,
198
+ Key=key,
199
+ )
200
+
201
+ return True
202
+
70
203
  except Exception as e:
71
- raise StorageError(f"S3 delete failed: {str(e)}")
204
+ raise StorageError(f"S3 delete failed: {e}")
72
205
 
73
206
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
74
207
  def list(self, prefix: str = "") -> List[str]:
@@ -76,11 +209,15 @@ class S3Adapter(ObjectStorageAdapter):
76
209
  try:
77
210
  if not self._client:
78
211
  self._initialize_client()
79
- if self._client:
80
- response = self._client.list_objects_v2(Bucket=self.bucket_name, Prefix=prefix)
81
- return [obj["Key"] for obj in response.get("Contents", [])]
82
- return []
83
- except Exception as e:
84
- raise StorageError(f"S3 list failed: {str(e)}")
85
212
 
213
+ response = self._client.list_objects_v2(
214
+ Bucket=self.bucket_name,
215
+ Prefix=prefix,
216
+ )
217
+
218
+ contents = response.get("Contents", [])
86
219
 
220
+ return [obj["Key"] for obj in contents]
221
+
222
+ except Exception as e:
223
+ raise StorageError(f"S3 list failed: {e}")
@@ -1,11 +1,13 @@
1
1
  # src/polydb/adapters/S3CompatibleAdapter.py
2
+ import mimetypes
2
3
  import os
3
4
  import threading
4
- from typing import List
5
+ from typing import Any, Dict, List, Optional
5
6
  from ..base.ObjectStorageAdapter import ObjectStorageAdapter
6
7
  from ..errors import StorageError, ConnectionError
7
8
  from ..retry import retry
8
9
 
10
+
9
11
  class S3CompatibleAdapter(ObjectStorageAdapter):
10
12
  """S3-compatible storage (MinIO, DigitalOcean Spaces) with client reuse"""
11
13
 
@@ -37,15 +39,68 @@ class S3CompatibleAdapter(ObjectStorageAdapter):
37
39
  raise ConnectionError(f"Failed to initialize S3-compatible client: {str(e)}")
38
40
 
39
41
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
40
- def _put_raw(self, key: str, data: bytes) -> str:
41
- """Store object"""
42
+ def _put_raw(
43
+ self,
44
+ key: str,
45
+ data: bytes,
46
+ fileName: str = "",
47
+ media_type: Optional[str] = None,
48
+ metadata: Dict[str, Any] | None = None,
49
+ ) -> str:
50
+ """Upload object to S3-compatible storage with metadata and media type"""
42
51
  try:
43
52
  if not self._client:
44
53
  self._initialize_client()
45
- if self._client:
46
- self._client.put_object(Bucket=self.bucket_name, Key=key, Body=data)
47
- self.logger.debug(f"Uploaded to S3-compatible: {key}")
48
- return key
54
+
55
+ # --------------------------------------------------
56
+ # Resolve filename
57
+ # --------------------------------------------------
58
+ filename = fileName or os.path.basename(key)
59
+
60
+ # --------------------------------------------------
61
+ # Ensure extension from media_type
62
+ # --------------------------------------------------
63
+ if media_type:
64
+ ext = mimetypes.guess_extension(media_type) or ""
65
+ if ext and not filename.lower().endswith(ext):
66
+ filename += ext
67
+
68
+ # --------------------------------------------------
69
+ # Final key
70
+ # --------------------------------------------------
71
+ blob_key = f"{key.rstrip('/')}/{filename}" if fileName else key
72
+
73
+ # --------------------------------------------------
74
+ # Metadata (string only)
75
+ # --------------------------------------------------
76
+ safe_metadata = {k: str(v) for k, v in (metadata or {}).items()}
77
+ safe_metadata["filename"] = filename
78
+
79
+ # --------------------------------------------------
80
+ # Upload
81
+ # --------------------------------------------------
82
+ self._client.put_object( # type: ignore
83
+ Bucket=self.bucket_name,
84
+ Key=blob_key,
85
+ Body=data,
86
+ ContentType=media_type or "application/octet-stream",
87
+ Metadata=safe_metadata,
88
+ )
89
+
90
+ self.logger.debug(f"S3 uploaded: {blob_key}, type={media_type}")
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
103
+
49
104
  except Exception as e:
50
105
  raise StorageError(f"S3-compatible put failed: {str(e)}")
51
106
 
@@ -87,4 +142,3 @@ class S3CompatibleAdapter(ObjectStorageAdapter):
87
142
  return []
88
143
  except Exception as e:
89
144
  raise StorageError(f"S3-compatible list failed: {str(e)}")
90
-
@@ -1,86 +1,163 @@
1
- from polydb.base.QueueAdapter import QueueAdapter
2
- from polydb.errors import ConnectionError, QueueError
3
- from polydb.retry import retry
4
-
5
-
6
- import boto3
7
- from botocore.client import BaseClient
8
-
1
+ from __future__ import annotations
9
2
 
10
3
  import json
11
4
  import os
12
5
  import threading
13
6
  from typing import Any, Dict, List
14
7
 
8
+ import boto3
9
+ from botocore.exceptions import ClientError
10
+
11
+ from ..base.QueueAdapter import QueueAdapter
12
+ from ..errors import ConnectionError, QueueError
13
+ from ..retry import retry
15
14
  from ..json_safe import json_safe
16
15
 
17
16
 
18
17
  class SQSAdapter(QueueAdapter):
19
- """AWS SQS with client reuse"""
18
+ """AWS SQS adapter with automatic queue creation (AWS + LocalStack compatible)"""
20
19
 
21
- def __init__(self):
20
+ def __init__(self, queue_name: str = "", region: str = "", endpoint_url: str = ""):
22
21
  super().__init__()
23
- self.queue_url = os.getenv("SQS_QUEUE_URL")
24
- self._client: BaseClient | None = None
22
+
23
+ self.queue_name = queue_name or os.getenv("SQS_QUEUE_NAME", "polydb-queue")
24
+
25
+ self.region = (
26
+ region or os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") or "us-east-1"
27
+ )
28
+
29
+ self.endpoint_url = endpoint_url or os.getenv("AWS_ENDPOINT_URL")
30
+
31
+ self._client: Any = None
32
+ self._queue_url = None
25
33
  self._lock = threading.Lock()
34
+
26
35
  self._initialize_client()
27
36
 
37
+ # ---------------------------------------------------------
38
+ # Client initialization
39
+ # ---------------------------------------------------------
40
+
28
41
  def _initialize_client(self):
29
- """Initialize SQS client once"""
30
42
  try:
31
- import boto3
32
-
33
43
  with self._lock:
34
- if not self._client:
35
- self._client = boto3.client("sqs")
36
- self.logger.info("Initialized SQS client")
44
+ if self._client:
45
+ return
46
+
47
+ self._client = boto3.client(
48
+ "sqs",
49
+ region_name=self.region,
50
+ endpoint_url=self.endpoint_url,
51
+ aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", "test"),
52
+ aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", "test"),
53
+ )
54
+
55
+ self._queue_url = self._ensure_queue_exists(self.queue_name)
56
+
57
+ self.logger.info(
58
+ f"Initialized SQS client (queue={self.queue_name}, endpoint={self.endpoint_url or 'aws'})"
59
+ )
60
+
37
61
  except Exception as e:
38
- raise ConnectionError(f"Failed to initialize SQS client: {str(e)}")
62
+ raise ConnectionError(f"SQS init failed: {e}")
63
+
64
+ # ---------------------------------------------------------
65
+ # Queue management
66
+ # ---------------------------------------------------------
67
+
68
+ def _ensure_queue_exists(self, queue_name: str) -> str:
69
+ """Create queue if it does not exist"""
70
+ if not self._client:
71
+ raise ConnectionError("SQS client not initialized")
72
+
73
+ try:
74
+ response = self._client.get_queue_url(QueueName=queue_name)
75
+ return response["QueueUrl"]
76
+
77
+ except self._client.exceptions.QueueDoesNotExist: # type: ignore
78
+
79
+ response = self._client.create_queue(QueueName=queue_name)
80
+ return response["QueueUrl"]
81
+
82
+ except ClientError as e:
83
+ raise QueueError(f"SQS queue creation failed: {e}")
84
+
85
+ # ---------------------------------------------------------
86
+ # Queue operations
87
+ # ---------------------------------------------------------
39
88
 
40
89
  @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
41
- def send(self, message: Dict[str, Any], queue_name: str = "default") -> str:
90
+ def send(self, message: Any, queue_name: str = "default") -> str:
42
91
  """Send message to queue"""
43
92
  try:
44
- import json
45
-
46
93
  if not self._client:
47
94
  self._initialize_client()
48
- if self._client:
49
- response = self._client.send_message(
50
- QueueUrl=self.queue_url, MessageBody=json.dumps(message,default=json_safe)
51
- )
52
- return response["MessageId"]
53
- return ""
95
+
96
+ body = (
97
+ json.dumps(message, default=json_safe) if not isinstance(message, str) else message
98
+ )
99
+
100
+ resp = self._client.send_message(
101
+ QueueUrl=self._queue_url,
102
+ MessageBody=body,
103
+ )
104
+
105
+ return resp["MessageId"]
106
+
54
107
  except Exception as e:
55
- raise QueueError(f"SQS send failed: {str(e)}")
108
+ raise QueueError(f"SQS send failed: {e}")
56
109
 
57
110
  @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
58
111
  def receive(self, queue_name: str = "default", max_messages: int = 1) -> List[Dict[str, Any]]:
59
112
  """Receive messages from queue"""
60
113
  try:
61
- import json
62
-
63
114
  if not self._client:
64
115
  self._initialize_client()
65
- if self._client:
66
- response = self._client.receive_message(
67
- QueueUrl=self.queue_url, MaxNumberOfMessages=max_messages, WaitTimeSeconds=5
116
+
117
+ resp = self._client.receive_message(
118
+ QueueUrl=self._queue_url,
119
+ MaxNumberOfMessages=max_messages,
120
+ WaitTimeSeconds=1,
121
+ )
122
+
123
+ messages = resp.get("Messages", [])
124
+
125
+ out: List[Dict[str, Any]] = []
126
+
127
+ for m in messages:
128
+ body = m.get("Body")
129
+
130
+ try:
131
+ body = json.loads(body)
132
+ except Exception:
133
+ pass
134
+
135
+ out.append(
136
+ {
137
+ "body": body,
138
+ "receipt_handle": m["ReceiptHandle"],
139
+ "message_id": m["MessageId"],
140
+ }
68
141
  )
69
- messages = response.get("Messages", [])
70
- return [json.loads(msg["Body"]) for msg in messages]
71
- return []
142
+
143
+ return out
144
+
72
145
  except Exception as e:
73
- raise QueueError(f"SQS receive failed: {str(e)}")
146
+ raise QueueError(f"SQS receive failed: {e}")
74
147
 
75
148
  @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
76
- def delete(self, message_id: str, queue_name: str = "default") -> bool:
149
+ def delete(self, receipt_handle: str, queue_name: str = "default") -> bool:
77
150
  """Delete message from queue"""
78
151
  try:
79
152
  if not self._client:
80
153
  self._initialize_client()
81
- if self._client:
82
- self._client.delete_message(QueueUrl=self.queue_url, ReceiptHandle=message_id)
83
- return True
84
- return False
154
+
155
+ self._client.delete_message(
156
+ QueueUrl=self._queue_url,
157
+ ReceiptHandle=receipt_handle,
158
+ )
159
+
160
+ return True
161
+
85
162
  except Exception as e:
86
- raise QueueError(f"SQS delete failed: {str(e)}")
163
+ raise QueueError(f"SQS delete failed: {e}")