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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- 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
|
+
}
|