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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: srx-lib-azure
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Azure helpers for SRX services: Blob, Email, Table
5
5
  Author-email: SRX <dev@srx.id>
6
6
  Requires-Python: >=3.12
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "srx-lib-azure"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Azure helpers for SRX services: Blob, Email, Table"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
- raise ValueError("Missing ACS_CONNECTION_STRING or EMAIL_SENDER in environment variables")
17
- self.email_client = EmailClient.from_connection_string(self.connection_string)
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