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,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
+ ]
@@ -0,0 +1,5 @@
1
+ """File storage adapters."""
2
+
3
+ from .local import LocalFileStorage
4
+
5
+ __all__ = ["LocalFileStorage"]