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
polydb/adapters/S3Adapter.py
CHANGED
|
@@ -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
|
-
|
|
18
|
-
self.
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
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: {
|
|
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(
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
"""
|
|
172
|
+
"""Download object"""
|
|
50
173
|
try:
|
|
51
174
|
if not self._client:
|
|
52
175
|
self._initialize_client()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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: {
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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: {
|
|
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(
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
polydb/adapters/SQSAdapter.py
CHANGED
|
@@ -1,86 +1,163 @@
|
|
|
1
|
-
from
|
|
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
|
|
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
|
-
|
|
24
|
-
self.
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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"
|
|
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:
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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: {
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
142
|
+
|
|
143
|
+
return out
|
|
144
|
+
|
|
72
145
|
except Exception as e:
|
|
73
|
-
raise QueueError(f"SQS receive failed: {
|
|
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,
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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: {
|
|
163
|
+
raise QueueError(f"SQS delete failed: {e}")
|