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,259 @@
|
|
|
1
|
+
"""Tests for MySQL adapter plugin."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from hexdag_plugins.mysql_adapter.mysql_adapter import MySQLAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestMySQLAdapter:
|
|
12
|
+
"""Test external MySQL plugin functionality."""
|
|
13
|
+
|
|
14
|
+
def test_mysql_plugin_registration_decorator(self):
|
|
15
|
+
"""Test that MySQL adapter is properly decorated for registry."""
|
|
16
|
+
# Check that the adapter has registry attributes
|
|
17
|
+
assert hasattr(MySQLAdapter, "_hexdag_type")
|
|
18
|
+
assert hasattr(MySQLAdapter, "_hexdag_name")
|
|
19
|
+
assert hasattr(MySQLAdapter, "_hexdag_namespace")
|
|
20
|
+
assert hasattr(MySQLAdapter, "_hexdag_description")
|
|
21
|
+
|
|
22
|
+
assert MySQLAdapter._hexdag_type == "adapter"
|
|
23
|
+
assert MySQLAdapter._hexdag_name == "mysql"
|
|
24
|
+
assert MySQLAdapter._hexdag_namespace == "plugin"
|
|
25
|
+
assert "MySQL database adapter" in MySQLAdapter._hexdag_description
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_mysql_adapter_with_mock_connection(self):
|
|
29
|
+
"""Test MySQL adapter operations with mocked connection."""
|
|
30
|
+
# Mock pymysql to avoid needing actual MySQL server
|
|
31
|
+
with patch("hexdag_plugins.mysql_adapter.mysql_adapter.pymysql") as mock_pymysql:
|
|
32
|
+
# Setup mock connection
|
|
33
|
+
mock_connection = MagicMock()
|
|
34
|
+
mock_cursor = MagicMock()
|
|
35
|
+
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
|
|
36
|
+
mock_connection.__enter__.return_value = mock_connection
|
|
37
|
+
mock_connection.__exit__.return_value = None
|
|
38
|
+
mock_pymysql.connect.return_value = mock_connection
|
|
39
|
+
|
|
40
|
+
# Mock cursor responses
|
|
41
|
+
mock_cursor.fetchone.return_value = None # For initial checks
|
|
42
|
+
mock_cursor.fetchall.return_value = []
|
|
43
|
+
mock_cursor.rowcount = 1
|
|
44
|
+
|
|
45
|
+
# Create adapter - this will try to create database and tables
|
|
46
|
+
adapter = MySQLAdapter(
|
|
47
|
+
host="localhost", user="test", password="test", database="test_hexdag"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Test insert
|
|
51
|
+
doc_id = await adapter.ainsert(
|
|
52
|
+
"test_collection", {"name": "Test Document", "value": 42}
|
|
53
|
+
)
|
|
54
|
+
assert doc_id is not None
|
|
55
|
+
|
|
56
|
+
# Test get (mock returns a document)
|
|
57
|
+
mock_cursor.fetchone.return_value = {
|
|
58
|
+
"document": '{"name": "Test Document", "value": 42}'
|
|
59
|
+
}
|
|
60
|
+
doc = await adapter.aget("test_collection", doc_id)
|
|
61
|
+
assert doc is not None
|
|
62
|
+
assert doc["name"] == "Test Document"
|
|
63
|
+
|
|
64
|
+
# Test query
|
|
65
|
+
mock_cursor.fetchall.return_value = [
|
|
66
|
+
{"id": "1", "document": '{"name": "Doc1"}'},
|
|
67
|
+
{"id": "2", "document": '{"name": "Doc2"}'},
|
|
68
|
+
]
|
|
69
|
+
docs = await adapter.aquery("test_collection")
|
|
70
|
+
assert len(docs) == 2
|
|
71
|
+
|
|
72
|
+
# Test update
|
|
73
|
+
result = await adapter.aupdate("test_collection", doc_id, {"status": "updated"})
|
|
74
|
+
assert result is True
|
|
75
|
+
|
|
76
|
+
# Test delete
|
|
77
|
+
result = await adapter.adelete("test_collection", doc_id)
|
|
78
|
+
assert result is True
|
|
79
|
+
|
|
80
|
+
# Test count
|
|
81
|
+
mock_cursor.fetchone.return_value = {"count": 5}
|
|
82
|
+
count = await adapter.acount("test_collection")
|
|
83
|
+
assert count == 5
|
|
84
|
+
|
|
85
|
+
# Test list collections
|
|
86
|
+
mock_cursor.fetchall.return_value = [{"collection": "col1"}, {"collection": "col2"}]
|
|
87
|
+
collections = await adapter.alist_collections()
|
|
88
|
+
assert len(collections) == 2
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
@pytest.mark.skipif(
|
|
92
|
+
not os.environ.get("MYSQL_TEST_HOST"), reason="MySQL test server not configured"
|
|
93
|
+
)
|
|
94
|
+
async def test_mysql_adapter_with_real_server(self):
|
|
95
|
+
"""Test MySQL adapter with real MySQL server (if available).
|
|
96
|
+
|
|
97
|
+
Set environment variables to run this test:
|
|
98
|
+
- MYSQL_TEST_HOST: MySQL server host
|
|
99
|
+
- MYSQL_TEST_USER: MySQL user
|
|
100
|
+
- MYSQL_TEST_PASSWORD: MySQL password
|
|
101
|
+
- MYSQL_TEST_DATABASE: Test database name
|
|
102
|
+
"""
|
|
103
|
+
adapter = MySQLAdapter(
|
|
104
|
+
host=os.environ.get("MYSQL_TEST_HOST", "localhost"),
|
|
105
|
+
user=os.environ.get("MYSQL_TEST_USER", "root"),
|
|
106
|
+
password=os.environ.get("MYSQL_TEST_PASSWORD", ""),
|
|
107
|
+
database=os.environ.get("MYSQL_TEST_DATABASE", "test_hexdag"),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Clean up test collection
|
|
111
|
+
await adapter.adrop_collection("test_mysql")
|
|
112
|
+
|
|
113
|
+
# Test full CRUD cycle
|
|
114
|
+
# Insert
|
|
115
|
+
doc_id = await adapter.ainsert(
|
|
116
|
+
"test_mysql",
|
|
117
|
+
{"title": "MySQL Test", "type": "integration", "tags": ["mysql", "test", "database"]},
|
|
118
|
+
)
|
|
119
|
+
assert doc_id is not None
|
|
120
|
+
|
|
121
|
+
# Get
|
|
122
|
+
doc = await adapter.aget("test_mysql", doc_id)
|
|
123
|
+
assert doc is not None
|
|
124
|
+
assert doc["title"] == "MySQL Test"
|
|
125
|
+
assert doc["_id"] == doc_id
|
|
126
|
+
|
|
127
|
+
# Update
|
|
128
|
+
updated = await adapter.aupdate("test_mysql", doc_id, {"status": "verified", "score": 100})
|
|
129
|
+
assert updated is True
|
|
130
|
+
|
|
131
|
+
# Verify update
|
|
132
|
+
doc = await adapter.aget("test_mysql", doc_id)
|
|
133
|
+
assert doc["status"] == "verified"
|
|
134
|
+
assert doc["score"] == 100
|
|
135
|
+
assert doc["title"] == "MySQL Test" # Original field preserved
|
|
136
|
+
|
|
137
|
+
# Query
|
|
138
|
+
docs = await adapter.aquery("test_mysql", filter={"type": "integration"})
|
|
139
|
+
assert len(docs) >= 1
|
|
140
|
+
assert any(d["_id"] == doc_id for d in docs)
|
|
141
|
+
|
|
142
|
+
# Count
|
|
143
|
+
count = await adapter.acount("test_mysql")
|
|
144
|
+
assert count >= 1
|
|
145
|
+
|
|
146
|
+
# List collections
|
|
147
|
+
collections = await adapter.alist_collections()
|
|
148
|
+
assert "test_mysql" in collections
|
|
149
|
+
|
|
150
|
+
# Delete
|
|
151
|
+
deleted = await adapter.adelete("test_mysql", doc_id)
|
|
152
|
+
assert deleted is True
|
|
153
|
+
|
|
154
|
+
# Verify deletion
|
|
155
|
+
doc = await adapter.aget("test_mysql", doc_id)
|
|
156
|
+
assert doc is None
|
|
157
|
+
|
|
158
|
+
# Clean up
|
|
159
|
+
await adapter.adrop_collection("test_mysql")
|
|
160
|
+
|
|
161
|
+
@pytest.mark.asyncio
|
|
162
|
+
async def test_mysql_concurrent_operations(self):
|
|
163
|
+
"""Test concurrent operations with MySQL adapter."""
|
|
164
|
+
with patch("hexdag_plugins.mysql_adapter.mysql_adapter.pymysql") as mock_pymysql:
|
|
165
|
+
# Setup mock
|
|
166
|
+
mock_connection = MagicMock()
|
|
167
|
+
mock_cursor = MagicMock()
|
|
168
|
+
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
|
|
169
|
+
mock_connection.__enter__.return_value = mock_connection
|
|
170
|
+
mock_connection.__exit__.return_value = None
|
|
171
|
+
mock_pymysql.connect.return_value = mock_connection
|
|
172
|
+
|
|
173
|
+
mock_cursor.fetchone.return_value = None
|
|
174
|
+
mock_cursor.fetchall.return_value = []
|
|
175
|
+
mock_cursor.rowcount = 1
|
|
176
|
+
|
|
177
|
+
adapter = MySQLAdapter()
|
|
178
|
+
|
|
179
|
+
# Concurrent inserts
|
|
180
|
+
async def insert_doc(i):
|
|
181
|
+
return await adapter.ainsert("concurrent", {"id": f"doc_{i}", "index": i})
|
|
182
|
+
|
|
183
|
+
# Insert 10 documents concurrently
|
|
184
|
+
doc_ids = await asyncio.gather(*[insert_doc(i) for i in range(10)])
|
|
185
|
+
|
|
186
|
+
assert len(doc_ids) == 10
|
|
187
|
+
assert all(doc_id is not None for doc_id in doc_ids)
|
|
188
|
+
|
|
189
|
+
def test_mysql_connection_parameters(self):
|
|
190
|
+
"""Test MySQL adapter initialization with various parameters."""
|
|
191
|
+
with patch("hexdag_plugins.mysql_adapter.mysql_adapter.pymysql") as mock_pymysql:
|
|
192
|
+
mock_connection = MagicMock()
|
|
193
|
+
mock_cursor = MagicMock()
|
|
194
|
+
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
|
|
195
|
+
mock_connection.__enter__.return_value = mock_connection
|
|
196
|
+
mock_connection.__exit__.return_value = None
|
|
197
|
+
mock_pymysql.connect.return_value = mock_connection
|
|
198
|
+
|
|
199
|
+
# Test with custom parameters
|
|
200
|
+
adapter = MySQLAdapter(
|
|
201
|
+
host="db.example.com",
|
|
202
|
+
port=3307,
|
|
203
|
+
user="custom_user",
|
|
204
|
+
password="secure_pass",
|
|
205
|
+
database="custom_db",
|
|
206
|
+
charset="utf8",
|
|
207
|
+
connect_timeout=30,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Verify connection parameters
|
|
211
|
+
assert adapter.connection_params["host"] == "db.example.com"
|
|
212
|
+
assert adapter.connection_params["port"] == 3307
|
|
213
|
+
assert adapter.connection_params["user"] == "custom_user"
|
|
214
|
+
assert adapter.connection_params["database"] == "custom_db"
|
|
215
|
+
assert adapter.connection_params["connect_timeout"] == 30
|
|
216
|
+
|
|
217
|
+
@pytest.mark.asyncio
|
|
218
|
+
async def test_mysql_json_handling(self):
|
|
219
|
+
"""Test MySQL adapter with complex JSON data."""
|
|
220
|
+
with patch("hexdag_plugins.mysql_adapter.mysql_adapter.pymysql") as mock_pymysql:
|
|
221
|
+
mock_connection = MagicMock()
|
|
222
|
+
mock_cursor = MagicMock()
|
|
223
|
+
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
|
|
224
|
+
mock_connection.__enter__.return_value = mock_connection
|
|
225
|
+
mock_connection.__exit__.return_value = None
|
|
226
|
+
mock_pymysql.connect.return_value = mock_connection
|
|
227
|
+
|
|
228
|
+
mock_cursor.fetchone.return_value = None
|
|
229
|
+
mock_cursor.rowcount = 1
|
|
230
|
+
|
|
231
|
+
adapter = MySQLAdapter()
|
|
232
|
+
|
|
233
|
+
# Complex nested data
|
|
234
|
+
complex_data = {
|
|
235
|
+
"id": "complex1",
|
|
236
|
+
"metadata": {
|
|
237
|
+
"author": "Test Author",
|
|
238
|
+
"tags": ["tag1", "tag2", "tag3"],
|
|
239
|
+
"settings": {"enabled": True, "threshold": 0.85, "options": ["opt1", "opt2"]},
|
|
240
|
+
},
|
|
241
|
+
"data": {
|
|
242
|
+
"values": [1, 2, 3, 4, 5],
|
|
243
|
+
"nested": {"level1": {"level2": {"level3": "deep value"}}},
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Test insert with complex data
|
|
248
|
+
doc_id = await adapter.ainsert("complex", complex_data)
|
|
249
|
+
assert doc_id is not None
|
|
250
|
+
|
|
251
|
+
# Mock retrieval
|
|
252
|
+
import json
|
|
253
|
+
|
|
254
|
+
mock_cursor.fetchone.return_value = {"document": json.dumps(complex_data)}
|
|
255
|
+
|
|
256
|
+
doc = await adapter.aget("complex", doc_id)
|
|
257
|
+
assert doc is not None
|
|
258
|
+
assert doc["metadata"]["settings"]["threshold"] == 0.85
|
|
259
|
+
assert doc["data"]["nested"]["level1"]["level2"]["level3"] == "deep value"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# hexdag-storage
|
|
2
|
+
|
|
3
|
+
Low-level storage infrastructure for hexDAG - SQL databases, vector stores, and file storage.
|
|
4
|
+
|
|
5
|
+
## What This Package Provides
|
|
6
|
+
|
|
7
|
+
- **SQL Adapters**: PostgreSQL, MySQL, SQLite (with connection pooling)
|
|
8
|
+
- **Vector Stores**: pgvector, ChromaDB, in-memory
|
|
9
|
+
- **File Storage**: Local filesystem (S3, Azure, GCS planned)
|
|
10
|
+
- **Production-Ready**: Connection pooling, health checks, async-first
|
|
11
|
+
|
|
12
|
+
## What This Is NOT
|
|
13
|
+
|
|
14
|
+
- ❌ RAG business logic (no document processing, chunking, embeddings)
|
|
15
|
+
- ❌ AI/LLM integration
|
|
16
|
+
- ❌ High-level workflows or pipelines
|
|
17
|
+
- ❌ Domain-specific operations
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
**Note:** The storage plugin is bundled with the main hexdag package. Install hexdag with storage extras:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Install hexdag with all storage backends
|
|
25
|
+
pip install hexdag[storage-all]
|
|
26
|
+
|
|
27
|
+
# Or install specific storage backends:
|
|
28
|
+
pip install hexdag[storage-postgresql] # PostgreSQL + pgvector
|
|
29
|
+
pip install hexdag[storage-mysql] # MySQL
|
|
30
|
+
pip install hexdag[storage-sqlite] # SQLite
|
|
31
|
+
pip install hexdag[storage-chromadb] # ChromaDB
|
|
32
|
+
|
|
33
|
+
# Basic hexdag installation (no storage backends)
|
|
34
|
+
pip install hexdag
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Development Installation
|
|
38
|
+
|
|
39
|
+
For local development from this repository:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Install hexdag with all storage dependencies
|
|
43
|
+
uv sync --all-extras
|
|
44
|
+
|
|
45
|
+
# Or install specific storage extras
|
|
46
|
+
uv pip install -e '.[storage-postgresql]'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Vector Stores
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from hexdag.core.registry import registry
|
|
55
|
+
from hexdag.core.bootstrap import bootstrap_registry
|
|
56
|
+
|
|
57
|
+
bootstrap_registry()
|
|
58
|
+
|
|
59
|
+
# In-memory (for testing)
|
|
60
|
+
vector_store = registry.get("in_memory_vector", namespace="storage")()
|
|
61
|
+
await vector_store.aadd_documents(
|
|
62
|
+
documents=[{"text": "Python"}],
|
|
63
|
+
embeddings=[[0.1, 0.2, 0.3]]
|
|
64
|
+
)
|
|
65
|
+
results = await vector_store.asearch("", query_embedding=[0.1, 0.2, 0.3])
|
|
66
|
+
|
|
67
|
+
# PostgreSQL + pgvector (production)
|
|
68
|
+
pgvector = registry.get("pgvector", namespace="storage")(
|
|
69
|
+
connection_string="postgresql+asyncpg://localhost/mydb",
|
|
70
|
+
pool_size=10,
|
|
71
|
+
table_name="embeddings",
|
|
72
|
+
embedding_dim=384
|
|
73
|
+
)
|
|
74
|
+
await pgvector.asetup()
|
|
75
|
+
await pgvector.aadd_documents(documents, embeddings)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### File Storage
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Local file storage
|
|
82
|
+
storage = registry.get("local", namespace="storage")(
|
|
83
|
+
base_path="./data"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Upload file
|
|
87
|
+
await storage.aupload("document.pdf", "docs/document.pdf")
|
|
88
|
+
|
|
89
|
+
# List files
|
|
90
|
+
files = await storage.alist(prefix="docs/")
|
|
91
|
+
|
|
92
|
+
# Download file
|
|
93
|
+
await storage.adownload("docs/document.pdf", "/tmp/document.pdf")
|
|
94
|
+
|
|
95
|
+
# Check exists
|
|
96
|
+
exists = await storage.aexists("docs/document.pdf")
|
|
97
|
+
|
|
98
|
+
# Get metadata
|
|
99
|
+
metadata = await storage.aget_metadata("docs/document.pdf")
|
|
100
|
+
|
|
101
|
+
# Delete file
|
|
102
|
+
await storage.adelete("docs/document.pdf")
|
|
103
|
+
|
|
104
|
+
# Health check
|
|
105
|
+
health = await storage.ahealth_check()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Architecture
|
|
109
|
+
|
|
110
|
+
### Ports
|
|
111
|
+
|
|
112
|
+
**In hexDAG Core:**
|
|
113
|
+
- `DatabasePort` - SQL database operations
|
|
114
|
+
- `FileStoragePort` - File storage operations
|
|
115
|
+
|
|
116
|
+
**In hexdag-storage:**
|
|
117
|
+
- `VectorStorePort` - Specialized for vector similarity search
|
|
118
|
+
|
|
119
|
+
### Implementations
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
hexdag_plugins/storage/
|
|
123
|
+
├── sql/ # DatabasePort implementations
|
|
124
|
+
│ ├── postgresql.py
|
|
125
|
+
│ ├── mysql.py
|
|
126
|
+
│ └── sqlite.py
|
|
127
|
+
│
|
|
128
|
+
├── vector/ # VectorStorePort implementations
|
|
129
|
+
│ ├── pgvector.py # PostgreSQL + pgvector
|
|
130
|
+
│ ├── chromadb.py # ChromaDB
|
|
131
|
+
│ └── in_memory.py # In-memory (testing)
|
|
132
|
+
│
|
|
133
|
+
└── file/ # FileStoragePort implementations
|
|
134
|
+
└── local.py # Local filesystem
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Benefits
|
|
138
|
+
|
|
139
|
+
### SQLAlchemy Connection Pooling
|
|
140
|
+
|
|
141
|
+
All SQL-based adapters use SQLAlchemy's async engine with:
|
|
142
|
+
- Connection pooling (5-20 connections)
|
|
143
|
+
- Automatic health checks (`pool_pre_ping`)
|
|
144
|
+
- Connection recycling
|
|
145
|
+
- Production-ready performance
|
|
146
|
+
|
|
147
|
+
### Consistent Configuration
|
|
148
|
+
|
|
149
|
+
All adapters use:
|
|
150
|
+
- `AdapterConfig` + `SecretField` for configuration
|
|
151
|
+
- Environment variable support
|
|
152
|
+
- Type validation with Pydantic
|
|
153
|
+
|
|
154
|
+
### Health Monitoring
|
|
155
|
+
|
|
156
|
+
All adapters implement `ahealth_check()`:
|
|
157
|
+
- Test connectivity
|
|
158
|
+
- Check pool status
|
|
159
|
+
- Measure latency
|
|
160
|
+
- Return structured `HealthStatus`
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Run tests
|
|
166
|
+
uv run pytest tests/
|
|
167
|
+
|
|
168
|
+
# Linting
|
|
169
|
+
uv run ruff check .
|
|
170
|
+
|
|
171
|
+
# Type checking
|
|
172
|
+
uv run pyright .
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Future Plans
|
|
176
|
+
|
|
177
|
+
- S3 file storage adapter
|
|
178
|
+
- Azure Blob storage adapter
|
|
179
|
+
- Google Cloud Storage adapter
|
|
180
|
+
- Redis vector store adapter
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""hexdag-storage: Low-level storage infrastructure for hexDAG.
|
|
2
|
+
|
|
3
|
+
Provides database, vector store, and file storage adapters with
|
|
4
|
+
connection pooling, health checks, and async-first operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .file import LocalFileStorage
|
|
8
|
+
from .vector import ChromaDBAdapter, InMemoryVectorStore, PgVectorAdapter
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
# Vector Stores
|
|
14
|
+
"InMemoryVectorStore",
|
|
15
|
+
"PgVectorAdapter",
|
|
16
|
+
"ChromaDBAdapter",
|
|
17
|
+
# File Storage
|
|
18
|
+
"LocalFileStorage",
|
|
19
|
+
]
|