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.
- srx_lib_azure-0.3.0/PKG-INFO +134 -0
- srx_lib_azure-0.3.0/README.md +114 -0
- {srx_lib_azure-0.1.7 → srx_lib_azure-0.3.0}/pyproject.toml +18 -2
- srx_lib_azure-0.3.0/src/srx_lib_azure/__init__.py +23 -0
- {srx_lib_azure-0.1.7 → srx_lib_azure-0.3.0}/src/srx_lib_azure/blob.py +116 -16
- srx_lib_azure-0.3.0/src/srx_lib_azure/document.py +262 -0
- {srx_lib_azure-0.1.7 → srx_lib_azure-0.3.0}/src/srx_lib_azure/email.py +55 -4
- srx_lib_azure-0.3.0/src/srx_lib_azure/speech.py +296 -0
- {srx_lib_azure-0.1.7 → srx_lib_azure-0.3.0}/src/srx_lib_azure/table.py +119 -23
- srx_lib_azure-0.1.7/PKG-INFO +0 -70
- srx_lib_azure-0.1.7/README.md +0 -58
- srx_lib_azure-0.1.7/src/srx_lib_azure/__init__.py +0 -6
- {srx_lib_azure-0.1.7 → srx_lib_azure-0.3.0}/.github/workflows/publish.yml +0 -0
- {srx_lib_azure-0.1.7 → srx_lib_azure-0.3.0}/.gitignore +0 -0
|
@@ -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.
|
|
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
|
|
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(
|
|
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"
|
|
137
|
+
logger.error(f"Unexpected error uploading {file.filename}: {e}")
|
|
112
138
|
return None
|
|
113
139
|
|
|
114
|
-
async def upload_stream(
|
|
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"
|
|
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
|
|
143
|
-
|
|
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(
|
|
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"
|
|
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"
|
|
301
|
+
logger.error(f"Unexpected error checking existence of {blob_path}: {e}")
|
|
202
302
|
return False
|