hexdag 0.5.0.dev1__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.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,450 @@
1
+ """Azure Blob Storage adapter for hexDAG framework.
2
+
3
+ Provides file storage and retrieval for pipelines and agents.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ from typing import Any
9
+
10
+ from hexdag.core.ports.healthcheck import HealthStatus
11
+
12
+
13
+ class AzureBlobAdapter:
14
+ """Azure Blob Storage adapter for file operations.
15
+
16
+ Provides scalable file storage for documents, artifacts, and pipeline
17
+ outputs using Azure Blob Storage.
18
+
19
+ Parameters
20
+ ----------
21
+ connection_string : str
22
+ Azure Storage connection string (auto-resolved from AZURE_STORAGE_CONNECTION_STRING)
23
+ container_name : str
24
+ Blob container name (default: "hexdag")
25
+ account_url : str, optional
26
+ Storage account URL (for managed identity auth)
27
+ use_managed_identity : bool
28
+ Use Managed Identity instead of connection string (default: False)
29
+
30
+ Examples
31
+ --------
32
+ YAML configuration::
33
+
34
+ spec:
35
+ ports:
36
+ storage:
37
+ adapter: hexdag_plugins.azure.AzureBlobAdapter
38
+ config:
39
+ container_name: "pipeline-artifacts"
40
+
41
+ With managed identity::
42
+
43
+ spec:
44
+ ports:
45
+ storage:
46
+ adapter: hexdag_plugins.azure.AzureBlobAdapter
47
+ config:
48
+ account_url: "https://mystorageaccount.blob.core.windows.net"
49
+ container_name: "pipeline-artifacts"
50
+ use_managed_identity: true
51
+
52
+ Python usage::
53
+
54
+ from hexdag_plugins.azure import AzureBlobAdapter
55
+
56
+ adapter = AzureBlobAdapter(
57
+ connection_string="...", # or auto-resolved
58
+ container_name="pipeline-artifacts"
59
+ )
60
+
61
+ # Upload file
62
+ await adapter.aupload("reports/output.json", json_bytes)
63
+
64
+ # Download file
65
+ content = await adapter.adownload("reports/output.json")
66
+
67
+ # List files
68
+ files = await adapter.alist("reports/")
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ connection_string: str | None = None,
74
+ container_name: str = "hexdag",
75
+ account_url: str | None = None,
76
+ use_managed_identity: bool = False,
77
+ ):
78
+ """Initialize Azure Blob Storage adapter.
79
+
80
+ Args
81
+ ----
82
+ connection_string: Azure Storage connection string
83
+ (auto-resolved from AZURE_STORAGE_CONNECTION_STRING)
84
+ container_name: Blob container name (default: "hexdag")
85
+ account_url: Storage account URL (for managed identity)
86
+ use_managed_identity: Use Managed Identity (default: False)
87
+ """
88
+ self.connection_string = connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING")
89
+ self.container_name = container_name
90
+ self.account_url = account_url
91
+ self.use_managed_identity = use_managed_identity
92
+
93
+ self._container_client = None
94
+
95
+ async def _get_container(self):
96
+ """Get or create blob container client."""
97
+ if self._container_client is None:
98
+ try:
99
+ from azure.storage.blob.aio import BlobServiceClient
100
+ except ImportError as e:
101
+ raise ImportError(
102
+ "Azure Storage SDK not installed. Install with: pip install azure-storage-blob"
103
+ ) from e
104
+
105
+ if self.use_managed_identity:
106
+ if not self.account_url:
107
+ raise ValueError("account_url is required when use_managed_identity=True")
108
+ try:
109
+ from azure.identity.aio import DefaultAzureCredential
110
+
111
+ credential = DefaultAzureCredential()
112
+ service_client = BlobServiceClient(
113
+ account_url=self.account_url, credential=credential
114
+ )
115
+ except ImportError as e:
116
+ raise ImportError(
117
+ "Azure Identity SDK not installed. Install with: pip install azure-identity"
118
+ ) from e
119
+ else:
120
+ if not self.connection_string:
121
+ raise ValueError(
122
+ "connection_string is required when use_managed_identity=False"
123
+ )
124
+ service_client = BlobServiceClient.from_connection_string(self.connection_string)
125
+
126
+ # Create container if not exists
127
+ self._container_client = service_client.get_container_client(self.container_name)
128
+ try:
129
+ await self._container_client.create_container()
130
+ except Exception: # noqa: SIM105 - contextlib.suppress doesn't work with async
131
+ # Container might already exist
132
+ pass
133
+
134
+ return self._container_client
135
+
136
+ async def aupload(
137
+ self,
138
+ blob_name: str,
139
+ data: bytes | str,
140
+ content_type: str | None = None,
141
+ metadata: dict[str, str] | None = None,
142
+ overwrite: bool = True,
143
+ ) -> str:
144
+ """Upload data to blob storage.
145
+
146
+ Args
147
+ ----
148
+ blob_name: Name/path of the blob
149
+ data: Data to upload (bytes or string)
150
+ content_type: MIME type of the content
151
+ metadata: Optional blob metadata
152
+ overwrite: Overwrite existing blob (default: True)
153
+
154
+ Returns
155
+ -------
156
+ URL of the uploaded blob
157
+ """
158
+ container = await self._get_container()
159
+
160
+ if isinstance(data, str):
161
+ data = data.encode("utf-8")
162
+ if content_type is None:
163
+ content_type = "text/plain"
164
+
165
+ blob_client = container.get_blob_client(blob_name)
166
+
167
+ await blob_client.upload_blob(
168
+ data,
169
+ content_type=content_type,
170
+ metadata=metadata,
171
+ overwrite=overwrite,
172
+ )
173
+
174
+ return blob_client.url
175
+
176
+ async def adownload(self, blob_name: str) -> bytes:
177
+ """Download blob content.
178
+
179
+ Args
180
+ ----
181
+ blob_name: Name/path of the blob
182
+
183
+ Returns
184
+ -------
185
+ Blob content as bytes
186
+
187
+ Raises
188
+ ------
189
+ FileNotFoundError: If blob doesn't exist
190
+ """
191
+ container = await self._get_container()
192
+ blob_client = container.get_blob_client(blob_name)
193
+
194
+ try:
195
+ stream = await blob_client.download_blob()
196
+ return await stream.readall()
197
+ except Exception as e:
198
+ if "BlobNotFound" in str(e):
199
+ raise FileNotFoundError(f"Blob '{blob_name}' not found") from e
200
+ raise
201
+
202
+ async def adownload_text(self, blob_name: str, encoding: str = "utf-8") -> str:
203
+ """Download blob content as text.
204
+
205
+ Args
206
+ ----
207
+ blob_name: Name/path of the blob
208
+ encoding: Text encoding (default: "utf-8")
209
+
210
+ Returns
211
+ -------
212
+ Blob content as string
213
+ """
214
+ content = await self.adownload(blob_name)
215
+ return content.decode(encoding)
216
+
217
+ async def adelete(self, blob_name: str) -> bool:
218
+ """Delete a blob.
219
+
220
+ Args
221
+ ----
222
+ blob_name: Name/path of the blob
223
+
224
+ Returns
225
+ -------
226
+ True if deleted, False if not found
227
+ """
228
+ container = await self._get_container()
229
+ blob_client = container.get_blob_client(blob_name)
230
+
231
+ try:
232
+ await blob_client.delete_blob()
233
+ return True
234
+ except Exception:
235
+ return False
236
+
237
+ async def aexists(self, blob_name: str) -> bool:
238
+ """Check if blob exists.
239
+
240
+ Args
241
+ ----
242
+ blob_name: Name/path of the blob
243
+
244
+ Returns
245
+ -------
246
+ True if exists, False otherwise
247
+ """
248
+ container = await self._get_container()
249
+ blob_client = container.get_blob_client(blob_name)
250
+ return await blob_client.exists()
251
+
252
+ async def alist(
253
+ self,
254
+ prefix: str | None = None,
255
+ max_results: int | None = None,
256
+ ) -> list[dict[str, Any]]:
257
+ """List blobs in container.
258
+
259
+ Args
260
+ ----
261
+ prefix: Optional prefix to filter blobs
262
+ max_results: Maximum number of results
263
+
264
+ Returns
265
+ -------
266
+ List of blob info dicts with name, size, last_modified, etc.
267
+ """
268
+ container = await self._get_container()
269
+
270
+ results = []
271
+ count = 0
272
+
273
+ async for blob in container.list_blobs(name_starts_with=prefix):
274
+ results.append(
275
+ {
276
+ "name": blob.name,
277
+ "size": blob.size,
278
+ "last_modified": blob.last_modified,
279
+ "content_type": blob.content_settings.content_type
280
+ if blob.content_settings
281
+ else None,
282
+ "metadata": blob.metadata,
283
+ }
284
+ )
285
+ count += 1
286
+ if max_results and count >= max_results:
287
+ break
288
+
289
+ return results
290
+
291
+ async def acopy(self, source_blob: str, dest_blob: str) -> str:
292
+ """Copy blob within container.
293
+
294
+ Args
295
+ ----
296
+ source_blob: Source blob name
297
+ dest_blob: Destination blob name
298
+
299
+ Returns
300
+ -------
301
+ URL of the copied blob
302
+ """
303
+ container = await self._get_container()
304
+ source_client = container.get_blob_client(source_blob)
305
+ dest_client = container.get_blob_client(dest_blob)
306
+
307
+ await dest_client.start_copy_from_url(source_client.url)
308
+ return dest_client.url
309
+
310
+ async def aget_url(self, blob_name: str, expiry_hours: int = 1) -> str:
311
+ """Get a SAS URL for blob access.
312
+
313
+ Args
314
+ ----
315
+ blob_name: Name/path of the blob
316
+ expiry_hours: URL expiry time in hours
317
+
318
+ Returns
319
+ -------
320
+ SAS URL for blob access
321
+ """
322
+ from datetime import datetime, timedelta
323
+
324
+ try:
325
+ from azure.storage.blob import BlobSasPermissions, generate_blob_sas
326
+ except ImportError as e:
327
+ raise ImportError(
328
+ "Azure Storage SDK not installed. Install with: pip install azure-storage-blob"
329
+ ) from e
330
+
331
+ container = await self._get_container()
332
+ blob_client = container.get_blob_client(blob_name)
333
+
334
+ # Generate SAS token
335
+ sas_token = generate_blob_sas(
336
+ account_name=blob_client.account_name,
337
+ container_name=self.container_name,
338
+ blob_name=blob_name,
339
+ account_key=self._get_account_key(),
340
+ permission=BlobSasPermissions(read=True),
341
+ expiry=datetime.utcnow() + timedelta(hours=expiry_hours),
342
+ )
343
+
344
+ return f"{blob_client.url}?{sas_token}"
345
+
346
+ def _get_account_key(self) -> str | None:
347
+ """Extract account key from connection string."""
348
+ if not self.connection_string:
349
+ return None
350
+
351
+ for part in self.connection_string.split(";"):
352
+ if part.startswith("AccountKey="):
353
+ return part[11:]
354
+ return None
355
+
356
+ async def aupload_json(
357
+ self,
358
+ blob_name: str,
359
+ data: dict | list,
360
+ metadata: dict[str, str] | None = None,
361
+ ) -> str:
362
+ """Upload JSON data to blob storage.
363
+
364
+ Args
365
+ ----
366
+ blob_name: Name/path of the blob
367
+ data: JSON-serializable data
368
+ metadata: Optional blob metadata
369
+
370
+ Returns
371
+ -------
372
+ URL of the uploaded blob
373
+ """
374
+ import json as json_module
375
+
376
+ json_str = json_module.dumps(data, indent=2, default=str)
377
+ return await self.aupload(
378
+ blob_name,
379
+ json_str.encode("utf-8"),
380
+ content_type="application/json",
381
+ metadata=metadata,
382
+ )
383
+
384
+ async def adownload_json(self, blob_name: str) -> dict | list:
385
+ """Download and parse JSON blob.
386
+
387
+ Args
388
+ ----
389
+ blob_name: Name/path of the blob
390
+
391
+ Returns
392
+ -------
393
+ Parsed JSON data
394
+ """
395
+ import json as json_module
396
+
397
+ content = await self.adownload_text(blob_name)
398
+ return json_module.loads(content)
399
+
400
+ async def ahealth_check(self) -> HealthStatus:
401
+ """Check Azure Blob Storage connectivity.
402
+
403
+ Returns
404
+ -------
405
+ HealthStatus with connectivity details
406
+ """
407
+ try:
408
+ start_time = time.time()
409
+ container = await self._get_container()
410
+
411
+ # Count blobs to verify access
412
+ count = 0
413
+ async for _ in container.list_blobs():
414
+ count += 1
415
+ if count >= 10: # Sample only
416
+ break
417
+
418
+ latency_ms = (time.time() - start_time) * 1000
419
+
420
+ return HealthStatus(
421
+ status="healthy",
422
+ adapter_name="AzureBlob",
423
+ latency_ms=latency_ms,
424
+ details={
425
+ "container": self.container_name,
426
+ "sample_blob_count": count,
427
+ },
428
+ )
429
+
430
+ except Exception as e:
431
+ return HealthStatus(
432
+ status="unhealthy",
433
+ adapter_name="AzureBlob",
434
+ latency_ms=0.0,
435
+ details={"error": str(e)},
436
+ )
437
+
438
+ async def aclose(self) -> None:
439
+ """Close the blob storage client."""
440
+ if self._container_client:
441
+ await self._container_client.close()
442
+ self._container_client = None
443
+
444
+ def to_dict(self) -> dict[str, Any]:
445
+ """Serialize adapter configuration (excluding secrets)."""
446
+ return {
447
+ "container_name": self.container_name,
448
+ "account_url": self.account_url,
449
+ "use_managed_identity": self.use_managed_identity,
450
+ }