srx-lib-azure 0.1.7__tar.gz → 0.3.0__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.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: srx-lib-azure
3
+ Version: 0.3.0
4
+ Summary: Azure helpers for SRX services: Blob, Email, Table, Document Intelligence, Speech Services
5
+ Author-email: SRX <dev@srx.id>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: azure-ai-documentintelligence>=1.0.0
8
+ Requires-Dist: azure-communication-email>=1.0.0
9
+ Requires-Dist: azure-data-tables>=12.7.0
10
+ Requires-Dist: azure-storage-blob>=12.22.0
11
+ Requires-Dist: loguru>=0.7.2
12
+ Provides-Extra: all
13
+ Requires-Dist: azure-ai-documentintelligence>=1.0.0; extra == 'all'
14
+ Requires-Dist: azure-cognitiveservices-speech>=1.41.1; extra == 'all'
15
+ Provides-Extra: document
16
+ Requires-Dist: azure-ai-documentintelligence>=1.0.0; extra == 'document'
17
+ Provides-Extra: speech
18
+ Requires-Dist: azure-cognitiveservices-speech>=1.41.1; extra == 'speech'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # srx-lib-azure
22
+
23
+ Lightweight wrappers over Azure SDKs used across SRX services.
24
+
25
+ What it includes:
26
+ - **Blob**: upload/download helpers, SAS URL generation
27
+ - **Email** (Azure Communication Services): simple async sender
28
+ - **Table**: simple CRUD helpers
29
+ - **Document Intelligence** (OCR): document analysis from URLs or bytes
30
+
31
+ ## Install
32
+
33
+ PyPI (public):
34
+
35
+ - `pip install srx-lib-azure`
36
+
37
+ uv (pyproject):
38
+ ```
39
+ [project]
40
+ dependencies = ["srx-lib-azure>=0.1.0"]
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Blob:
46
+ ```
47
+ from srx_lib_azure.blob import AzureBlobService
48
+ blob = AzureBlobService()
49
+ url = await blob.upload_file(upload_file, "documents/report.pdf")
50
+ ```
51
+
52
+ Email:
53
+ ```
54
+ from srx_lib_azure.email import EmailService
55
+ svc = EmailService()
56
+ await svc.send_notification("user@example.com", "Subject", "Hello", html=False)
57
+ ```
58
+
59
+ Table:
60
+ ```
61
+ from srx_lib_azure.table import AzureTableService
62
+ store = AzureTableService()
63
+ store.ensure_table("events")
64
+ store.upsert_entity("events", {"PartitionKey":"p","RowKey":"r","EventType":"x"})
65
+ ```
66
+
67
+ Document Intelligence (OCR):
68
+ ```python
69
+ from srx_lib_azure import AzureDocumentIntelligenceService
70
+
71
+ # Initialize with endpoint and key
72
+ doc_service = AzureDocumentIntelligenceService(
73
+ endpoint="https://your-resource.cognitiveservices.azure.com/",
74
+ key="your-api-key"
75
+ )
76
+
77
+ # Analyze document from URL
78
+ result = await doc_service.analyze_document_from_url(
79
+ url="https://example.com/document.pdf",
80
+ model_id="prebuilt-read" # or "prebuilt-layout", "prebuilt-invoice", etc.
81
+ )
82
+
83
+ # Analyze document from bytes
84
+ with open("document.pdf", "rb") as f:
85
+ content = f.read()
86
+ result = await doc_service.analyze_document_from_bytes(
87
+ file_content=content,
88
+ model_id="prebuilt-read"
89
+ )
90
+
91
+ # Result structure:
92
+ # {
93
+ # "success": True/False,
94
+ # "content": "extracted text...",
95
+ # "pages": [{"page_number": 1, "width": 8.5, ...}, ...],
96
+ # "page_count": 10,
97
+ # "confidence": 0.98,
98
+ # "model_id": "prebuilt-read",
99
+ # "metadata": {...},
100
+ # "error": None # or error message if failed
101
+ # }
102
+ ```
103
+
104
+ ## Environment Variables
105
+
106
+ - **Blob & Table**: `AZURE_STORAGE_CONNECTION_STRING` (required)
107
+ - **Email (ACS)**: `ACS_CONNECTION_STRING`, `EMAIL_SENDER`
108
+ - **Document Intelligence**: `AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT`, `AZURE_DOCUMENT_INTELLIGENCE_KEY`
109
+ - **Optional**: `AZURE_STORAGE_ACCOUNT_KEY`, `AZURE_BLOB_URL`, `AZURE_SAS_TOKEN`
110
+
111
+ ## Optional Dependencies
112
+
113
+ All services are optional and won't break if their dependencies aren't installed:
114
+
115
+ ```bash
116
+ # Base installation (includes all services by default)
117
+ pip install srx-lib-azure
118
+
119
+ # Or install only what you need - document intelligence is optional
120
+ pip install srx-lib-azure[document] # Adds Document Intelligence support
121
+
122
+ # Install with all optional dependencies
123
+ pip install srx-lib-azure[all]
124
+ ```
125
+
126
+ If you import a service without its required Azure SDK, it will log a warning but won't crash.
127
+
128
+ ## Release
129
+
130
+ Tag `vX.Y.Z` to publish to GitHub Packages via Actions.
131
+
132
+ ## License
133
+
134
+ Proprietary © SRX
@@ -0,0 +1,114 @@
1
+ # srx-lib-azure
2
+
3
+ Lightweight wrappers over Azure SDKs used across SRX services.
4
+
5
+ What it includes:
6
+ - **Blob**: upload/download helpers, SAS URL generation
7
+ - **Email** (Azure Communication Services): simple async sender
8
+ - **Table**: simple CRUD helpers
9
+ - **Document Intelligence** (OCR): document analysis from URLs or bytes
10
+
11
+ ## Install
12
+
13
+ PyPI (public):
14
+
15
+ - `pip install srx-lib-azure`
16
+
17
+ uv (pyproject):
18
+ ```
19
+ [project]
20
+ dependencies = ["srx-lib-azure>=0.1.0"]
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Blob:
26
+ ```
27
+ from srx_lib_azure.blob import AzureBlobService
28
+ blob = AzureBlobService()
29
+ url = await blob.upload_file(upload_file, "documents/report.pdf")
30
+ ```
31
+
32
+ Email:
33
+ ```
34
+ from srx_lib_azure.email import EmailService
35
+ svc = EmailService()
36
+ await svc.send_notification("user@example.com", "Subject", "Hello", html=False)
37
+ ```
38
+
39
+ Table:
40
+ ```
41
+ from srx_lib_azure.table import AzureTableService
42
+ store = AzureTableService()
43
+ store.ensure_table("events")
44
+ store.upsert_entity("events", {"PartitionKey":"p","RowKey":"r","EventType":"x"})
45
+ ```
46
+
47
+ Document Intelligence (OCR):
48
+ ```python
49
+ from srx_lib_azure import AzureDocumentIntelligenceService
50
+
51
+ # Initialize with endpoint and key
52
+ doc_service = AzureDocumentIntelligenceService(
53
+ endpoint="https://your-resource.cognitiveservices.azure.com/",
54
+ key="your-api-key"
55
+ )
56
+
57
+ # Analyze document from URL
58
+ result = await doc_service.analyze_document_from_url(
59
+ url="https://example.com/document.pdf",
60
+ model_id="prebuilt-read" # or "prebuilt-layout", "prebuilt-invoice", etc.
61
+ )
62
+
63
+ # Analyze document from bytes
64
+ with open("document.pdf", "rb") as f:
65
+ content = f.read()
66
+ result = await doc_service.analyze_document_from_bytes(
67
+ file_content=content,
68
+ model_id="prebuilt-read"
69
+ )
70
+
71
+ # Result structure:
72
+ # {
73
+ # "success": True/False,
74
+ # "content": "extracted text...",
75
+ # "pages": [{"page_number": 1, "width": 8.5, ...}, ...],
76
+ # "page_count": 10,
77
+ # "confidence": 0.98,
78
+ # "model_id": "prebuilt-read",
79
+ # "metadata": {...},
80
+ # "error": None # or error message if failed
81
+ # }
82
+ ```
83
+
84
+ ## Environment Variables
85
+
86
+ - **Blob & Table**: `AZURE_STORAGE_CONNECTION_STRING` (required)
87
+ - **Email (ACS)**: `ACS_CONNECTION_STRING`, `EMAIL_SENDER`
88
+ - **Document Intelligence**: `AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT`, `AZURE_DOCUMENT_INTELLIGENCE_KEY`
89
+ - **Optional**: `AZURE_STORAGE_ACCOUNT_KEY`, `AZURE_BLOB_URL`, `AZURE_SAS_TOKEN`
90
+
91
+ ## Optional Dependencies
92
+
93
+ All services are optional and won't break if their dependencies aren't installed:
94
+
95
+ ```bash
96
+ # Base installation (includes all services by default)
97
+ pip install srx-lib-azure
98
+
99
+ # Or install only what you need - document intelligence is optional
100
+ pip install srx-lib-azure[document] # Adds Document Intelligence support
101
+
102
+ # Install with all optional dependencies
103
+ pip install srx-lib-azure[all]
104
+ ```
105
+
106
+ If you import a service without its required Azure SDK, it will log a warning but won't crash.
107
+
108
+ ## Release
109
+
110
+ Tag `vX.Y.Z` to publish to GitHub Packages via Actions.
111
+
112
+ ## License
113
+
114
+ Proprietary © SRX
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "srx-lib-azure"
7
- version = "0.1.7"
8
- description = "Azure helpers for SRX services: Blob, Email, Table"
7
+ version = "0.3.0"
8
+ description = "Azure helpers for SRX services: Blob, Email, Table, Document Intelligence, Speech Services"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
  authors = [{ name = "SRX", email = "dev@srx.id" }]
@@ -14,6 +14,22 @@ dependencies = [
14
14
  "azure-storage-blob>=12.22.0",
15
15
  "azure-communication-email>=1.0.0",
16
16
  "azure-data-tables>=12.7.0",
17
+ "azure-ai-documentintelligence>=1.0.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ # Optional extra for Document Intelligence (OCR)
22
+ document = [
23
+ "azure-ai-documentintelligence>=1.0.0",
24
+ ]
25
+ # Optional extra for Speech Services (audio transcription)
26
+ speech = [
27
+ "azure-cognitiveservices-speech>=1.41.1",
28
+ ]
29
+ # Install all optional dependencies
30
+ all = [
31
+ "azure-ai-documentintelligence>=1.0.0",
32
+ "azure-cognitiveservices-speech>=1.41.1",
17
33
  ]
18
34
 
19
35
  [tool.hatch.build.targets.wheel]
@@ -0,0 +1,23 @@
1
+ from .blob import AzureBlobService
2
+ from .document import AzureDocumentIntelligenceService
3
+ from .email import EmailService
4
+ from .table import AzureTableService
5
+
6
+ # Optional import - only available if speech extra is installed
7
+ try:
8
+ from .speech import AzureSpeechService
9
+ __all__ = [
10
+ "AzureBlobService",
11
+ "AzureDocumentIntelligenceService",
12
+ "AzureTableService",
13
+ "EmailService",
14
+ "AzureSpeechService",
15
+ ]
16
+ except ImportError:
17
+ # Speech SDK not installed - service not available
18
+ __all__ = [
19
+ "AzureBlobService",
20
+ "AzureDocumentIntelligenceService",
21
+ "AzureTableService",
22
+ "EmailService",
23
+ ]
@@ -4,6 +4,11 @@ from datetime import datetime, timedelta, timezone
4
4
  from typing import Optional, BinaryIO, Tuple
5
5
 
6
6
  from azure.storage.blob import BlobServiceClient, BlobSasPermissions, generate_blob_sas
7
+ from azure.core.exceptions import (
8
+ ResourceNotFoundError,
9
+ ClientAuthenticationError,
10
+ HttpResponseError,
11
+ )
7
12
  from fastapi import UploadFile
8
13
 
9
14
  from loguru import logger
@@ -49,9 +54,7 @@ class AzureBlobService:
49
54
  return None, None
50
55
  try:
51
56
  clean = self.connection_string.strip().strip('"').strip("'")
52
- parts = dict(
53
- seg.split("=", 1) for seg in clean.split(";") if "=" in seg
54
- )
57
+ parts = dict(seg.split("=", 1) for seg in clean.split(";") if "=" in seg)
55
58
  account_name = parts.get("AccountName")
56
59
  account_key = parts.get("AccountKey") or self.account_key
57
60
  return account_name, account_key
@@ -93,9 +96,20 @@ class AzureBlobService:
93
96
  if self.base_blob_url:
94
97
  base_url = self.base_blob_url.strip().strip('"').strip("'").rstrip("/")
95
98
  return f"{base_url}/{blob_name}?{sas}"
96
- return f"https://{account_name}.blob.core.windows.net/{self.container_name}/{blob_name}?{sas}"
99
+ return (
100
+ f"https://{account_name}.blob.core.windows.net/{self.container_name}/{blob_name}?{sas}"
101
+ )
97
102
 
98
103
  async def upload_file(self, file: UploadFile, blob_path: str) -> Optional[str]:
104
+ """Upload a file to Azure Blob Storage and return a SAS URL.
105
+
106
+ Args:
107
+ file: File to upload
108
+ blob_path: Destination path in the container
109
+
110
+ Returns:
111
+ SAS URL if successful, None on error
112
+ """
99
113
  if not self.connection_string:
100
114
  logger.error("Azure Storage connection string not configured")
101
115
  return None
@@ -105,13 +119,37 @@ class AzureBlobService:
105
119
  container = client.get_container_client(self.container_name)
106
120
  content = await file.read()
107
121
  blob_client = container.get_blob_client(blob_path)
108
- blob_client.upload_blob(content, overwrite=True, content_type=file.content_type or "application/octet-stream")
122
+ blob_client.upload_blob(
123
+ content,
124
+ overwrite=True,
125
+ content_type=file.content_type or "application/octet-stream",
126
+ )
109
127
  return self._generate_sas_url(blob_path)
128
+ except ClientAuthenticationError as e:
129
+ logger.error(f"Authentication failed uploading {file.filename}: {e}")
130
+ return None
131
+ except HttpResponseError as e:
132
+ logger.error(
133
+ f"Azure service error uploading {file.filename}: {e.status_code} - {e.message}"
134
+ )
135
+ return None
110
136
  except Exception as e:
111
- logger.error(f"Failed to upload file {file.filename}: {e}")
137
+ logger.error(f"Unexpected error uploading {file.filename}: {e}")
112
138
  return None
113
139
 
114
- async def upload_stream(self, stream: BinaryIO, blob_path: str, content_type: str = "application/octet-stream") -> Optional[str]:
140
+ async def upload_stream(
141
+ self, stream: BinaryIO, blob_path: str, content_type: str = "application/octet-stream"
142
+ ) -> Optional[str]:
143
+ """Upload a binary stream to Azure Blob Storage and return a SAS URL.
144
+
145
+ Args:
146
+ stream: Binary stream to upload
147
+ blob_path: Destination path in the container
148
+ content_type: MIME type of the content
149
+
150
+ Returns:
151
+ SAS URL if successful, None on error
152
+ """
115
153
  if not self.connection_string:
116
154
  logger.error("Azure Storage connection string not configured")
117
155
  return None
@@ -122,12 +160,27 @@ class AzureBlobService:
122
160
  blob_client = container.get_blob_client(blob_path)
123
161
  blob_client.upload_blob(stream, overwrite=True, content_type=content_type)
124
162
  return self._generate_sas_url(blob_path)
163
+ except ClientAuthenticationError as e:
164
+ logger.error(f"Authentication failed uploading stream to {blob_path}: {e}")
165
+ return None
166
+ except HttpResponseError as e:
167
+ logger.error(
168
+ f"Azure service error uploading stream to {blob_path}: {e.status_code} - {e.message}"
169
+ )
170
+ return None
125
171
  except Exception as e:
126
- logger.error(f"Failed to upload stream to {blob_path}: {e}")
172
+ logger.error(f"Unexpected error uploading stream to {blob_path}: {e}")
127
173
  return None
128
174
 
129
175
  async def download_file(self, blob_path: str) -> Optional[bytes]:
130
- """Download a blob's content as bytes."""
176
+ """Download a blob's content as bytes.
177
+
178
+ Returns:
179
+ bytes if successful, None if blob doesn't exist
180
+
181
+ Raises:
182
+ RuntimeError: For connection/auth errors (caller should handle)
183
+ """
131
184
  if not self.connection_string:
132
185
  logger.error("Azure Storage connection string not configured")
133
186
  return None
@@ -139,9 +192,24 @@ class AzureBlobService:
139
192
  content = download_stream.readall()
140
193
  logger.info(f"Successfully downloaded {blob_path}")
141
194
  return content
142
- except Exception as e:
143
- logger.error(f"Failed to download {blob_path}: {e}")
195
+ except ResourceNotFoundError:
196
+ # Blob doesn't exist - this is expected in many scenarios
197
+ logger.info(f"Blob not found: {blob_path}")
144
198
  return None
199
+ except ClientAuthenticationError as e:
200
+ # Auth errors should not be retried - they need credential fixes
201
+ logger.error(f"Authentication failed for {blob_path}: {e}")
202
+ raise RuntimeError(f"Azure authentication failed: {e}") from e
203
+ except HttpResponseError as e:
204
+ # Other Azure service errors (rate limits, service issues, etc.)
205
+ logger.error(
206
+ f"Azure service error downloading {blob_path}: {e.status_code} - {e.message}"
207
+ )
208
+ raise RuntimeError(f"Azure Blob download failed for {blob_path}: {e.message}") from e
209
+ except Exception as e:
210
+ # Catch-all for unexpected errors (network, etc.)
211
+ logger.error(f"Unexpected error downloading {blob_path}: {e}")
212
+ raise RuntimeError(f"Unexpected error downloading {blob_path}: {e}") from e
145
213
 
146
214
  async def download_to_temp_file(self, blob_path: str) -> Optional[str]:
147
215
  """Download a blob to a temporary file and return its path."""
@@ -149,7 +217,9 @@ class AzureBlobService:
149
217
  if content is None:
150
218
  return None
151
219
  try:
152
- with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(blob_path)[1]) as tf:
220
+ with tempfile.NamedTemporaryFile(
221
+ delete=False, suffix=os.path.splitext(blob_path)[1]
222
+ ) as tf:
153
223
  tf.write(content)
154
224
  path = tf.name
155
225
  logger.info(f"Downloaded {blob_path} to temporary file: {path}")
@@ -172,7 +242,14 @@ class AzureBlobService:
172
242
  return None
173
243
 
174
244
  async def delete_file(self, blob_path: str) -> bool:
175
- """Delete a blob and return True on success."""
245
+ """Delete a blob and return True on success.
246
+
247
+ Args:
248
+ blob_path: Path to the blob to delete
249
+
250
+ Returns:
251
+ True if deleted successfully or blob doesn't exist, False on error
252
+ """
176
253
  if not self.connection_string:
177
254
  logger.error("Azure Storage connection string not configured")
178
255
  return False
@@ -183,12 +260,29 @@ class AzureBlobService:
183
260
  blob_client.delete_blob()
184
261
  logger.info(f"Successfully deleted {blob_path}")
185
262
  return True
263
+ except ResourceNotFoundError:
264
+ # Blob already doesn't exist - this is still success
265
+ logger.info(f"Blob {blob_path} already deleted or doesn't exist")
266
+ return True
267
+ except ClientAuthenticationError as e:
268
+ logger.error(f"Authentication failed when deleting {blob_path}: {e}")
269
+ return False
270
+ except HttpResponseError as e:
271
+ logger.error(f"Azure service error deleting {blob_path}: {e.status_code} - {e.message}")
272
+ return False
186
273
  except Exception as e:
187
- logger.error(f"Failed to delete {blob_path}: {e}")
274
+ logger.error(f"Unexpected error deleting {blob_path}: {e}")
188
275
  return False
189
276
 
190
277
  async def file_exists(self, blob_path: str) -> bool:
191
- """Check if a blob exists in the container."""
278
+ """Check if a blob exists in the container.
279
+
280
+ Args:
281
+ blob_path: Path to the blob to check
282
+
283
+ Returns:
284
+ True if blob exists, False otherwise (including on errors)
285
+ """
192
286
  if not self.connection_string:
193
287
  logger.error("Azure Storage connection string not configured")
194
288
  return False
@@ -197,6 +291,12 @@ class AzureBlobService:
197
291
  container = client.get_container_client(self.container_name)
198
292
  blob_client = container.get_blob_client(blob_path)
199
293
  return blob_client.exists()
294
+ except ClientAuthenticationError as e:
295
+ logger.error(f"Authentication failed checking {blob_path}: {e}")
296
+ return False
297
+ except HttpResponseError as e:
298
+ logger.error(f"Azure service error checking {blob_path}: {e.status_code} - {e.message}")
299
+ return False
200
300
  except Exception as e:
201
- logger.error(f"Failed to check existence of {blob_path}: {e}")
301
+ logger.error(f"Unexpected error checking existence of {blob_path}: {e}")
202
302
  return False