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,408 @@
1
+ """MySQL database adapter implementation."""
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from typing import Any
8
+
9
+ import pymysql
10
+ import pymysql.cursors
11
+ from hexdag.core.registry.decorators import adapter
12
+ from pymysql.connections import Connection
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @adapter(
18
+ name="mysql",
19
+ implements_port="database",
20
+ namespace="plugin",
21
+ description="MySQL database adapter for production-ready scalable storage",
22
+ )
23
+ class MySQLAdapter:
24
+ """MySQL adapter for database port.
25
+
26
+ Provides a robust, scalable database solution for production deployments
27
+ with support for transactions, connection pooling, and high concurrency.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ host: str = "localhost",
33
+ port: int = 3306,
34
+ user: str = "root",
35
+ password: str = "",
36
+ database: str = "hexdag",
37
+ charset: str = "utf8mb4",
38
+ **kwargs: Any,
39
+ ) -> None:
40
+ """Initialize MySQL adapter.
41
+
42
+ Args:
43
+ host: MySQL server host
44
+ port: MySQL server port
45
+ user: Database user
46
+ password: Database password
47
+ database: Database name to use
48
+ charset: Character set for connection
49
+ **kwargs: Additional connection options
50
+ """
51
+ self.connection_params: dict[str, Any] = {
52
+ "host": host,
53
+ "port": port,
54
+ "user": user,
55
+ "password": password,
56
+ "database": database,
57
+ "charset": charset,
58
+ "cursorclass": pymysql.cursors.DictCursor,
59
+ **kwargs,
60
+ }
61
+ self._ensure_database()
62
+ self._ensure_tables()
63
+
64
+ @contextmanager
65
+ def _get_connection(self) -> Generator[Connection, None, None]:
66
+ """Context manager for database connections."""
67
+ connection = pymysql.connect(**self.connection_params)
68
+ try:
69
+ yield connection
70
+ connection.commit()
71
+ except Exception:
72
+ connection.rollback()
73
+ raise
74
+ finally:
75
+ connection.close()
76
+
77
+ def _ensure_database(self) -> None:
78
+ """Ensure the database exists."""
79
+ # Connect without specifying database
80
+ params = self.connection_params.copy()
81
+ database = params.pop("database")
82
+
83
+ try:
84
+ connection = pymysql.connect(**params)
85
+ with connection.cursor() as cursor:
86
+ cursor.execute(f"CREATE DATABASE IF NOT EXISTS {database}")
87
+ connection.commit()
88
+ connection.close()
89
+ except pymysql.err.OperationalError as e:
90
+ logger.warning(f"Could not create database: {e}")
91
+ # Database might already exist or we don't have permissions
92
+
93
+ def _ensure_tables(self) -> None:
94
+ """Ensure required tables exist."""
95
+ with self._get_connection() as connection, connection.cursor() as cursor:
96
+ # Create main document store table
97
+ cursor.execute("""
98
+ CREATE TABLE IF NOT EXISTS hexdag_documents (
99
+ id VARCHAR(255) PRIMARY KEY,
100
+ collection VARCHAR(255) NOT NULL,
101
+ document JSON NOT NULL,
102
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
103
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
104
+ INDEX idx_collection (collection),
105
+ INDEX idx_collection_id (collection, id)
106
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
107
+ """)
108
+
109
+ # Create metadata table
110
+ cursor.execute("""
111
+ CREATE TABLE IF NOT EXISTS hexdag_metadata (
112
+ collection VARCHAR(255) PRIMARY KEY,
113
+ count INT DEFAULT 0,
114
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
115
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
116
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
117
+ """)
118
+
119
+ async def ainsert(self, collection: str, data: dict[str, Any]) -> str:
120
+ """Insert data into collection.
121
+
122
+ Args:
123
+ collection: Collection/table name
124
+ data: Data to insert
125
+
126
+ Returns:
127
+ ID of inserted record
128
+ """
129
+ import asyncio
130
+
131
+ loop = asyncio.get_event_loop()
132
+
133
+ def insert() -> str:
134
+ with self._get_connection() as connection, connection.cursor() as cursor:
135
+ # Generate ID if not provided
136
+ doc_id = data.get("_id") or data.get("id") or self._generate_id()
137
+ data["_id"] = doc_id
138
+
139
+ # Insert document
140
+ cursor.execute(
141
+ """
142
+ INSERT INTO hexdag_documents (id, collection, document)
143
+ VALUES (%s, %s, %s)
144
+ ON DUPLICATE KEY UPDATE
145
+ document = VALUES(document),
146
+ updated_at = CURRENT_TIMESTAMP
147
+ """,
148
+ (doc_id, collection, json.dumps(data)),
149
+ )
150
+
151
+ # Update collection metadata
152
+ cursor.execute(
153
+ """
154
+ INSERT INTO hexdag_metadata (collection, count)
155
+ VALUES (%s, 1)
156
+ ON DUPLICATE KEY UPDATE
157
+ count = count + 1,
158
+ updated_at = CURRENT_TIMESTAMP
159
+ """,
160
+ (collection,),
161
+ )
162
+
163
+ return str(doc_id)
164
+
165
+ return await loop.run_in_executor(None, insert)
166
+
167
+ async def aget(self, collection: str, id: str) -> dict[str, Any] | None:
168
+ """Get document by ID.
169
+
170
+ Args:
171
+ collection: Collection/table name
172
+ id: Document ID
173
+
174
+ Returns:
175
+ Document data or None if not found
176
+ """
177
+ import asyncio
178
+
179
+ loop = asyncio.get_event_loop()
180
+
181
+ def get() -> dict[str, Any] | None:
182
+ with self._get_connection() as connection, connection.cursor() as cursor:
183
+ cursor.execute(
184
+ """
185
+ SELECT document FROM hexdag_documents
186
+ WHERE collection = %s AND id = %s
187
+ """,
188
+ (collection, str(id)),
189
+ )
190
+ result = cursor.fetchone()
191
+ if result:
192
+ doc: dict[str, Any] = json.loads(result["document"])
193
+ return doc
194
+ return None
195
+
196
+ return await loop.run_in_executor(None, get)
197
+
198
+ async def aquery(
199
+ self, collection: str, filter: dict[str, Any] | None = None, limit: int | None = None
200
+ ) -> list[dict[str, Any]]:
201
+ """Query documents from collection.
202
+
203
+ Args:
204
+ collection: Collection/table name
205
+ filter: Query filter (JSON path based)
206
+ limit: Maximum number of results
207
+
208
+ Returns:
209
+ List of matching documents
210
+ """
211
+ import asyncio
212
+
213
+ loop = asyncio.get_event_loop()
214
+
215
+ def query() -> list[dict[str, Any]]:
216
+ with self._get_connection() as connection, connection.cursor() as cursor:
217
+ sql = "SELECT document FROM hexdag_documents WHERE collection = %s"
218
+ params: list[Any] = [collection]
219
+
220
+ # Add JSON-based filtering
221
+ if filter:
222
+ for key, value in filter.items():
223
+ sql += " AND JSON_EXTRACT(document, %s) = %s"
224
+ params.extend([f"$.{key}", json.dumps(value)])
225
+
226
+ # Add limit
227
+ if limit:
228
+ sql += f" LIMIT {limit}"
229
+
230
+ cursor.execute(sql, params)
231
+ results: list[dict[str, Any]] = []
232
+ for row in cursor.fetchall():
233
+ doc: dict[str, Any] = json.loads(row["document"])
234
+ results.append(doc)
235
+ return results
236
+
237
+ return await loop.run_in_executor(None, query)
238
+
239
+ async def aupdate(self, collection: str, id: str, data: dict[str, Any]) -> bool:
240
+ """Update document in collection.
241
+
242
+ Args:
243
+ collection: Collection/table name
244
+ id: Document ID
245
+ data: Updated data (will be merged with existing)
246
+
247
+ Returns:
248
+ True if updated, False if not found
249
+ """
250
+ import asyncio
251
+
252
+ loop = asyncio.get_event_loop()
253
+
254
+ def update() -> bool:
255
+ with self._get_connection() as connection, connection.cursor() as cursor:
256
+ # Get existing document
257
+ cursor.execute(
258
+ """
259
+ SELECT document FROM hexdag_documents
260
+ WHERE collection = %s AND id = %s
261
+ """,
262
+ (collection, str(id)),
263
+ )
264
+ result = cursor.fetchone()
265
+
266
+ if not result:
267
+ return False
268
+
269
+ # Merge with existing data
270
+ existing = json.loads(result["document"])
271
+ existing.update(data)
272
+ existing["_id"] = str(id)
273
+
274
+ # Update document
275
+ cursor.execute(
276
+ """
277
+ UPDATE hexdag_documents
278
+ SET document = %s, updated_at = CURRENT_TIMESTAMP
279
+ WHERE collection = %s AND id = %s
280
+ """,
281
+ (json.dumps(existing), collection, str(id)),
282
+ )
283
+
284
+ return bool(cursor.rowcount > 0)
285
+
286
+ return await loop.run_in_executor(None, update)
287
+
288
+ async def adelete(self, collection: str, id: str) -> bool:
289
+ """Delete document from collection.
290
+
291
+ Args:
292
+ collection: Collection/table name
293
+ id: Document ID
294
+
295
+ Returns:
296
+ True if deleted, False if not found
297
+ """
298
+ import asyncio
299
+
300
+ loop = asyncio.get_event_loop()
301
+
302
+ def delete() -> bool:
303
+ with self._get_connection() as connection, connection.cursor() as cursor:
304
+ # Delete document
305
+ cursor.execute(
306
+ """
307
+ DELETE FROM hexdag_documents
308
+ WHERE collection = %s AND id = %s
309
+ """,
310
+ (collection, str(id)),
311
+ )
312
+
313
+ if cursor.rowcount > 0:
314
+ # Update collection metadata
315
+ cursor.execute(
316
+ """
317
+ UPDATE hexdag_metadata
318
+ SET count = GREATEST(0, count - 1),
319
+ updated_at = CURRENT_TIMESTAMP
320
+ WHERE collection = %s
321
+ """,
322
+ (collection,),
323
+ )
324
+ return True
325
+ return False
326
+
327
+ return await loop.run_in_executor(None, delete)
328
+
329
+ async def alist_collections(self) -> list[str]:
330
+ """List all collections in database.
331
+
332
+ Returns:
333
+ List of collection names
334
+ """
335
+ import asyncio
336
+
337
+ loop = asyncio.get_event_loop()
338
+
339
+ def list_collections() -> list[str]:
340
+ with self._get_connection() as connection, connection.cursor() as cursor:
341
+ cursor.execute("SELECT DISTINCT collection FROM hexdag_documents")
342
+ return [row["collection"] for row in cursor.fetchall()]
343
+
344
+ return await loop.run_in_executor(None, list_collections)
345
+
346
+ async def acount(self, collection: str) -> int:
347
+ """Count documents in collection.
348
+
349
+ Args:
350
+ collection: Collection name
351
+
352
+ Returns:
353
+ Number of documents
354
+ """
355
+ import asyncio
356
+
357
+ loop = asyncio.get_event_loop()
358
+
359
+ def count() -> int:
360
+ with self._get_connection() as connection, connection.cursor() as cursor:
361
+ cursor.execute(
362
+ "SELECT COUNT(*) as count FROM hexdag_documents WHERE collection = %s",
363
+ (collection,),
364
+ )
365
+ result = cursor.fetchone()
366
+ return int(result["count"] if result else 0)
367
+
368
+ return await loop.run_in_executor(None, count)
369
+
370
+ async def adrop_collection(self, collection: str) -> bool:
371
+ """Drop entire collection.
372
+
373
+ Args:
374
+ collection: Collection name to drop
375
+
376
+ Returns:
377
+ True if dropped, False if not found
378
+ """
379
+ import asyncio
380
+
381
+ loop = asyncio.get_event_loop()
382
+
383
+ def drop() -> bool:
384
+ with self._get_connection() as connection, connection.cursor() as cursor:
385
+ # Delete all documents
386
+ cursor.execute("DELETE FROM hexdag_documents WHERE collection = %s", (collection,))
387
+
388
+ if cursor.rowcount > 0:
389
+ # Remove metadata
390
+ cursor.execute(
391
+ "DELETE FROM hexdag_metadata WHERE collection = %s",
392
+ (collection,),
393
+ )
394
+ return bool(cursor.rowcount > 0)
395
+
396
+ return await loop.run_in_executor(None, drop)
397
+
398
+ def _generate_id(self) -> str:
399
+ """Generate a unique document ID."""
400
+ import uuid
401
+
402
+ return str(uuid.uuid4())
403
+
404
+ def __repr__(self) -> str:
405
+ """String representation."""
406
+ host = self.connection_params["host"]
407
+ database = self.connection_params["database"]
408
+ return f"MySQLAdapter(host='{host}', database='{database}')"
@@ -0,0 +1,93 @@
1
+ [project]
2
+ name = "hexdag-mysql-adapter"
3
+ version = "1.0.0"
4
+ description = "MySQL database adapter plugin for hexDAG framework"
5
+ authors = [
6
+ { name = "HexDAG Team", email = "team@hexdag.ai" }
7
+ ]
8
+ readme = "README.md"
9
+ license = { text = "MIT" }
10
+ requires-python = ">=3.12"
11
+ keywords = ["hexdag", "mysql", "database", "adapter", "plugin"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ "Topic :: Database",
20
+ ]
21
+
22
+ dependencies = [
23
+ "pymysql>=1.1.0",
24
+ # hexdag is the parent project - not a PyPI dependency
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=7.0.0",
30
+ "pytest-asyncio>=0.21.0",
31
+ "pytest-mock>=3.10.0",
32
+ "pytest-cov>=4.0.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ "Homepage" = "https://github.com/hexdag/hexdag-mysql-adapter"
37
+ "Bug Reports" = "https://github.com/hexdag/hexdag-mysql-adapter/issues"
38
+ "Source" = "https://github.com/hexdag/hexdag-mysql-adapter"
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build]
45
+ include = [
46
+ "mysql_adapter/**/*.py",
47
+ "README.md",
48
+ "LICENSE",
49
+ ]
50
+ exclude = [
51
+ "tests",
52
+ "*.pyc",
53
+ "__pycache__",
54
+ ]
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["mysql_adapter"]
58
+
59
+ # Plugin registration for hexDAG
60
+ [tool.hexdag.plugin]
61
+ name = "mysql"
62
+ module = "hexdag_plugins.mysql_adapter"
63
+ port = "database"
64
+ description = "Production-ready MySQL database adapter with JSON support"
65
+ requires_env = ["MYSQL_HOST", "MYSQL_USER", "MYSQL_PASSWORD"]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+ python_files = ["test_*.py"]
70
+ python_classes = ["Test*"]
71
+ python_functions = ["test_*"]
72
+ asyncio_mode = "auto"
73
+
74
+ [tool.ruff]
75
+ line-length = 100
76
+ target-version = "py312"
77
+
78
+ [tool.ruff.lint]
79
+ select = [
80
+ "E", # pycodestyle errors
81
+ "W", # pycodestyle warnings
82
+ "F", # pyflakes
83
+ "UP", # pyupgrade
84
+ "B", # flake8-bugbear
85
+ "SIM", # flake8-simplify
86
+ "I", # isort
87
+ ]
88
+
89
+ [tool.mypy]
90
+ python_version = "3.12"
91
+ warn_return_any = true
92
+ warn_unused_configs = true
93
+ disallow_untyped_defs = true