srx-lib-azure 0.1.3__tar.gz → 0.1.5__tar.gz
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.
Potentially problematic release.
This version of srx-lib-azure might be problematic. Click here for more details.
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/PKG-INFO +1 -1
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/pyproject.toml +1 -1
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/src/srx_lib_azure/blob.py +75 -0
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/src/srx_lib_azure/email.py +26 -8
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/.github/workflows/publish.yml +0 -0
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/.gitignore +0 -0
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/README.md +0 -0
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/src/srx_lib_azure/__init__.py +0 -0
- {srx_lib_azure-0.1.3 → srx_lib_azure-0.1.5}/src/srx_lib_azure/table.py +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import tempfile
|
|
2
3
|
from datetime import datetime, timedelta, timezone
|
|
3
4
|
from typing import Optional, BinaryIO, Tuple
|
|
4
5
|
|
|
@@ -111,3 +112,77 @@ class AzureBlobService:
|
|
|
111
112
|
logger.error(f"Failed to upload stream to {blob_path}: {e}")
|
|
112
113
|
return None
|
|
113
114
|
|
|
115
|
+
async def download_file(self, blob_path: str) -> Optional[bytes]:
|
|
116
|
+
"""Download a blob's content as bytes."""
|
|
117
|
+
if not self.connection_string:
|
|
118
|
+
logger.error("Azure Storage connection string not configured")
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
client = self._get_blob_service()
|
|
122
|
+
container = client.get_container_client(self.container_name)
|
|
123
|
+
blob_client = container.get_blob_client(blob_path)
|
|
124
|
+
download_stream = blob_client.download_blob()
|
|
125
|
+
content = download_stream.readall()
|
|
126
|
+
logger.info(f"Successfully downloaded {blob_path}")
|
|
127
|
+
return content
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to download {blob_path}: {e}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
async def download_to_temp_file(self, blob_path: str) -> Optional[str]:
|
|
133
|
+
"""Download a blob to a temporary file and return its path."""
|
|
134
|
+
content = await self.download_file(blob_path)
|
|
135
|
+
if content is None:
|
|
136
|
+
return None
|
|
137
|
+
try:
|
|
138
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(blob_path)[1]) as tf:
|
|
139
|
+
tf.write(content)
|
|
140
|
+
path = tf.name
|
|
141
|
+
logger.info(f"Downloaded {blob_path} to temporary file: {path}")
|
|
142
|
+
return path
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"Failed to create temporary file for {blob_path}: {e}")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def get_blob_url(self, blob_path: str, generate_sas: bool = True) -> Optional[str]:
|
|
148
|
+
"""Get a direct URL for a blob; optionally generate a SAS URL."""
|
|
149
|
+
if generate_sas:
|
|
150
|
+
try:
|
|
151
|
+
return self._generate_sas_url(blob_path)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Failed to generate SAS URL for {blob_path}: {e}")
|
|
154
|
+
return None
|
|
155
|
+
if self.base_blob_url:
|
|
156
|
+
return f"{self.base_blob_url.rstrip('/')}/{blob_path}"
|
|
157
|
+
logger.error("Cannot generate blob URL without base URL")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
async def delete_file(self, blob_path: str) -> bool:
|
|
161
|
+
"""Delete a blob and return True on success."""
|
|
162
|
+
if not self.connection_string:
|
|
163
|
+
logger.error("Azure Storage connection string not configured")
|
|
164
|
+
return False
|
|
165
|
+
try:
|
|
166
|
+
client = self._get_blob_service()
|
|
167
|
+
container = client.get_container_client(self.container_name)
|
|
168
|
+
blob_client = container.get_blob_client(blob_path)
|
|
169
|
+
blob_client.delete_blob()
|
|
170
|
+
logger.info(f"Successfully deleted {blob_path}")
|
|
171
|
+
return True
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Failed to delete {blob_path}: {e}")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
async def file_exists(self, blob_path: str) -> bool:
|
|
177
|
+
"""Check if a blob exists in the container."""
|
|
178
|
+
if not self.connection_string:
|
|
179
|
+
logger.error("Azure Storage connection string not configured")
|
|
180
|
+
return False
|
|
181
|
+
try:
|
|
182
|
+
client = self._get_blob_service()
|
|
183
|
+
container = client.get_container_client(self.container_name)
|
|
184
|
+
blob_client = container.get_blob_client(blob_path)
|
|
185
|
+
return blob_client.exists()
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"Failed to check existence of {blob_path}: {e}")
|
|
188
|
+
return False
|
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from azure.communication.email.aio import EmailClient
|
|
3
|
-
|
|
4
2
|
import logging
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from azure.communication.email.aio import EmailClient
|
|
7
|
+
except Exception: # pragma: no cover - optional dependency at import time
|
|
8
|
+
EmailClient = None # type: ignore
|
|
5
9
|
|
|
6
10
|
logger = logging.getLogger(__name__)
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
class EmailService:
|
|
10
|
-
"""Thin wrapper over Azure Communication Services EmailClient.
|
|
14
|
+
"""Thin wrapper over Azure Communication Services EmailClient.
|
|
15
|
+
|
|
16
|
+
Does not raise on missing configuration to keep the library optional.
|
|
17
|
+
If not configured, send calls are skipped with a warning and a 'skipped' status.
|
|
18
|
+
"""
|
|
11
19
|
|
|
12
20
|
def __init__(self):
|
|
13
21
|
self.connection_string = os.getenv("ACS_CONNECTION_STRING")
|
|
14
22
|
self.sender_address = os.getenv("EMAIL_SENDER")
|
|
15
|
-
if not self.connection_string or not self.sender_address:
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
if not self.connection_string or not self.sender_address or EmailClient is None:
|
|
24
|
+
self.email_client = None
|
|
25
|
+
logger.warning(
|
|
26
|
+
"EmailService not configured (missing ACS_CONNECTION_STRING/EMAIL_SENDER or azure SDK). Calls will be skipped."
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
try:
|
|
30
|
+
self.email_client = EmailClient.from_connection_string(self.connection_string)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
self.email_client = None
|
|
33
|
+
logger.warning("EmailService initialization failed: %s", e)
|
|
18
34
|
|
|
19
|
-
async def send_notification(self, recipient: str, subject: str, body: str, html: bool = False):
|
|
35
|
+
async def send_notification(self, recipient: str, subject: str, body: str, html: bool = False) -> Dict[str, Any]:
|
|
36
|
+
if not self.email_client or not self.sender_address:
|
|
37
|
+
logger.warning("Email skipped: service not configured")
|
|
38
|
+
return {"status": "skipped", "message": "Email service not configured"}
|
|
20
39
|
message = {
|
|
21
40
|
"content": {"subject": subject},
|
|
22
41
|
"recipients": {"to": [{"address": recipient}]},
|
|
@@ -38,4 +57,3 @@ class EmailService:
|
|
|
38
57
|
except Exception as e:
|
|
39
58
|
logger.error("Email send exception: %s", e)
|
|
40
59
|
return {"status": "error", "message": str(e)}
|
|
41
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|