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,187 @@
1
+ """Base SQL adapter using SQLAlchemy for connection pooling and async operations."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import SecretStr
6
+ from sqlalchemy import text
7
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
8
+
9
+ from hexdag.core import AdapterConfig, ConfigurableAdapter, SecretField
10
+ from hexdag.core.ports.healthcheck import HealthStatus
11
+
12
+
13
+ class DatabaseConfig(AdapterConfig):
14
+ """Base configuration for SQL database adapters.
15
+
16
+ Attributes
17
+ ----------
18
+ connection_string : SecretStr
19
+ Database connection string (e.g., "postgresql+asyncpg://user:pass@host/db")
20
+ pool_size : int
21
+ Number of permanent connections in the pool (default: 5)
22
+ max_overflow : int
23
+ Maximum number of connections beyond pool_size (default: 10)
24
+ pool_timeout : float
25
+ Timeout in seconds when getting connection from pool (default: 30.0)
26
+ pool_recycle : int
27
+ Recycle connections after this many seconds (default: 3600)
28
+ pool_pre_ping : bool
29
+ Test connections before using them (default: True)
30
+ """
31
+
32
+ connection_string: SecretStr = SecretField(
33
+ env_var="DATABASE_URL",
34
+ description="Database connection string",
35
+ )
36
+ pool_size: int = 5
37
+ max_overflow: int = 10
38
+ pool_timeout: float = 30.0
39
+ pool_recycle: int = 3600
40
+ pool_pre_ping: bool = True
41
+
42
+
43
+ class SQLAdapter(ConfigurableAdapter):
44
+ """Base SQL adapter with SQLAlchemy connection pooling.
45
+
46
+ This base class provides common functionality for all SQL databases:
47
+ - Async connection pooling
48
+ - Health checks
49
+ - Basic query execution
50
+ - Proper resource cleanup
51
+ """
52
+
53
+ Config = DatabaseConfig
54
+
55
+ def __init__(self, **kwargs):
56
+ """Initialize SQL adapter with configuration."""
57
+ super().__init__(**kwargs)
58
+ self._engine: AsyncEngine | None = None
59
+
60
+ async def asetup(self):
61
+ """Initialize SQLAlchemy async engine with connection pool."""
62
+ self._engine = create_async_engine(
63
+ self.config.connection_string.get_secret_value(),
64
+ pool_size=self.config.pool_size,
65
+ max_overflow=self.config.max_overflow,
66
+ pool_timeout=self.config.pool_timeout,
67
+ pool_recycle=self.config.pool_recycle,
68
+ pool_pre_ping=self.config.pool_pre_ping,
69
+ echo=False,
70
+ )
71
+
72
+ async def aclose(self):
73
+ """Close database connection pool."""
74
+ if self._engine:
75
+ await self._engine.dispose()
76
+ self._engine = None
77
+
78
+ async def aexecute(self, query: str, params: dict[str, Any] | None = None) -> Any:
79
+ """Execute a SQL query (INSERT, UPDATE, DELETE, DDL).
80
+
81
+ Args:
82
+ query: SQL query string
83
+ params: Query parameters for parameterized queries
84
+
85
+ Returns:
86
+ Result of the execution
87
+
88
+ Raises:
89
+ RuntimeError: If adapter not set up
90
+ """
91
+ if not self._engine:
92
+ msg = "Adapter not set up. Call asetup() first."
93
+ raise RuntimeError(msg)
94
+
95
+ async with AsyncSession(self._engine) as session:
96
+ result = await session.execute(text(query), params or {})
97
+ await session.commit()
98
+ return result
99
+
100
+ async def afetch_one(
101
+ self, query: str, params: dict[str, Any] | None = None
102
+ ) -> dict[str, Any] | None:
103
+ """Fetch a single row from the database.
104
+
105
+ Args:
106
+ query: SQL query string
107
+ params: Query parameters for parameterized queries
108
+
109
+ Returns:
110
+ Single row as dictionary or None if no results
111
+
112
+ Raises:
113
+ RuntimeError: If adapter not set up
114
+ """
115
+ if not self._engine:
116
+ msg = "Adapter not set up. Call asetup() first."
117
+ raise RuntimeError(msg)
118
+
119
+ async with AsyncSession(self._engine) as session:
120
+ result = await session.execute(text(query), params or {})
121
+ row = result.fetchone()
122
+ return dict(row._mapping) if row else None
123
+
124
+ async def afetch_all(
125
+ self, query: str, params: dict[str, Any] | None = None
126
+ ) -> list[dict[str, Any]]:
127
+ """Fetch all rows from the database.
128
+
129
+ Args:
130
+ query: SQL query string
131
+ params: Query parameters for parameterized queries
132
+
133
+ Returns:
134
+ List of rows as dictionaries
135
+
136
+ Raises:
137
+ RuntimeError: If adapter not set up
138
+ """
139
+ if not self._engine:
140
+ msg = "Adapter not set up. Call asetup() first."
141
+ raise RuntimeError(msg)
142
+
143
+ async with AsyncSession(self._engine) as session:
144
+ result = await session.execute(text(query), params or {})
145
+ rows = result.fetchall()
146
+ return [dict(row._mapping) for row in rows]
147
+
148
+ async def ahealth_check(self) -> HealthStatus:
149
+ """Check database connection health.
150
+
151
+ Returns:
152
+ HealthStatus with database availability and connection pool info
153
+ """
154
+ if not self._engine:
155
+ return HealthStatus(
156
+ status="unhealthy",
157
+ adapter_name=self.__class__.__name__,
158
+ port_name="database",
159
+ details={"message": "Database not initialized"},
160
+ )
161
+
162
+ try:
163
+ # Test query
164
+ async with AsyncSession(self._engine) as session:
165
+ await session.execute(text("SELECT 1"))
166
+
167
+ # Get pool statistics
168
+ pool = self._engine.pool
169
+ return HealthStatus(
170
+ status="healthy",
171
+ adapter_name=self.__class__.__name__,
172
+ port_name="database",
173
+ details={
174
+ "pool_size": pool.size(),
175
+ "checked_in": pool.checkedin(),
176
+ "checked_out": pool.checkedout(),
177
+ "overflow": pool.overflow(),
178
+ },
179
+ )
180
+ except Exception as e:
181
+ return HealthStatus(
182
+ status="unhealthy",
183
+ adapter_name=self.__class__.__name__,
184
+ port_name="database",
185
+ error=e,
186
+ details={"error": str(e)},
187
+ )
@@ -0,0 +1,27 @@
1
+ """MySQL adapter using SQLAlchemy with aiomysql driver."""
2
+
3
+ from hexdag.core.registry.decorators import adapter
4
+
5
+ from .base import DatabaseConfig, SQLAdapter
6
+
7
+
8
+ @adapter("database", name="mysql", namespace="storage")
9
+ class MySQLAdapter(SQLAdapter):
10
+ """MySQL database adapter with async connection pooling.
11
+
12
+ Uses SQLAlchemy with aiomysql driver for async MySQL operations.
13
+ Provides connection pooling, health checks, and standard SQL operations.
14
+
15
+ Example connection string:
16
+ mysql+aiomysql://user:password@host:port/database
17
+
18
+ Configuration:
19
+ - connection_string: MySQL connection URL (env: DATABASE_URL)
20
+ - pool_size: Number of permanent connections (default: 5)
21
+ - max_overflow: Extra connections beyond pool_size (default: 10)
22
+ - pool_timeout: Timeout for getting connection (default: 30.0)
23
+ - pool_recycle: Recycle connections after seconds (default: 3600)
24
+ - pool_pre_ping: Test connections before use (default: True)
25
+ """
26
+
27
+ Config = DatabaseConfig
@@ -0,0 +1,27 @@
1
+ """PostgreSQL adapter using SQLAlchemy with asyncpg driver."""
2
+
3
+ from hexdag.core.registry.decorators import adapter
4
+
5
+ from .base import DatabaseConfig, SQLAdapter
6
+
7
+
8
+ @adapter("database", name="postgresql", namespace="storage")
9
+ class PostgreSQLAdapter(SQLAdapter):
10
+ """PostgreSQL database adapter with async connection pooling.
11
+
12
+ Uses SQLAlchemy with asyncpg driver for async PostgreSQL operations.
13
+ Provides connection pooling, health checks, and standard SQL operations.
14
+
15
+ Example connection string:
16
+ postgresql+asyncpg://user:password@host:port/database
17
+
18
+ Configuration:
19
+ - connection_string: PostgreSQL connection URL (env: DATABASE_URL)
20
+ - pool_size: Number of permanent connections (default: 5)
21
+ - max_overflow: Extra connections beyond pool_size (default: 10)
22
+ - pool_timeout: Timeout for getting connection (default: 30.0)
23
+ - pool_recycle: Recycle connections after seconds (default: 3600)
24
+ - pool_pre_ping: Test connections before use (default: True)
25
+ """
26
+
27
+ Config = DatabaseConfig
@@ -0,0 +1 @@
1
+ """Tests for hexdag-storage."""
@@ -0,0 +1,161 @@
1
+ """Tests for local file storage adapter."""
2
+
3
+ import pytest
4
+
5
+ from hexdag_plugins.storage.file import LocalFileStorage
6
+
7
+
8
+ class TestLocalFileStorage:
9
+ """Test suite for LocalFileStorage adapter."""
10
+
11
+ @pytest.fixture
12
+ async def storage(self, tmp_path):
13
+ """Create a test storage instance."""
14
+ storage = LocalFileStorage(base_path=str(tmp_path))
15
+ yield storage
16
+
17
+ @pytest.fixture
18
+ def test_file(self, tmp_path):
19
+ """Create a test file."""
20
+ test_file = tmp_path / "test.txt"
21
+ test_file.write_text("Hello, World!")
22
+ return test_file
23
+
24
+ @pytest.mark.asyncio
25
+ async def test_initialization(self, tmp_path):
26
+ """Test storage initialization."""
27
+ storage = LocalFileStorage(base_path=str(tmp_path))
28
+ assert storage._base_path == tmp_path
29
+ assert tmp_path.exists()
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_upload(self, storage, test_file, tmp_path):
33
+ """Test file upload."""
34
+ result = await storage.aupload(str(test_file), "docs/test.txt")
35
+
36
+ assert result["uploaded"] is True
37
+ assert result["remote_path"] == "docs/test.txt"
38
+ assert (tmp_path / "docs/test.txt").exists()
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_download(self, storage, test_file, tmp_path):
42
+ """Test file download."""
43
+ # First upload a file
44
+ await storage.aupload(str(test_file), "docs/test.txt")
45
+
46
+ # Download to a different location
47
+ download_path = tmp_path / "downloaded.txt"
48
+ result = await storage.adownload("docs/test.txt", str(download_path))
49
+
50
+ assert result["downloaded"] is True
51
+ assert download_path.exists()
52
+ assert download_path.read_text() == "Hello, World!"
53
+
54
+ @pytest.mark.asyncio
55
+ async def test_exists(self, storage, test_file):
56
+ """Test file existence check."""
57
+ # File doesn't exist yet
58
+ assert await storage.aexists("docs/test.txt") is False
59
+
60
+ # Upload file
61
+ await storage.aupload(str(test_file), "docs/test.txt")
62
+
63
+ # Now it exists
64
+ assert await storage.aexists("docs/test.txt") is True
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_delete(self, storage, test_file):
68
+ """Test file deletion."""
69
+ # Upload file
70
+ await storage.aupload(str(test_file), "docs/test.txt")
71
+ assert await storage.aexists("docs/test.txt") is True
72
+
73
+ # Delete file
74
+ result = await storage.adelete("docs/test.txt")
75
+
76
+ assert result["deleted"] is True
77
+ assert await storage.aexists("docs/test.txt") is False
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_list(self, storage, test_file):
81
+ """Test file listing."""
82
+ # Upload multiple files
83
+ await storage.aupload(str(test_file), "docs/file1.txt")
84
+ await storage.aupload(str(test_file), "docs/file2.txt")
85
+ await storage.aupload(str(test_file), "images/pic.jpg")
86
+
87
+ # List all files
88
+ all_files = await storage.alist()
89
+ assert len(all_files) == 3
90
+ assert "docs/file1.txt" in all_files
91
+ assert "docs/file2.txt" in all_files
92
+ assert "images/pic.jpg" in all_files
93
+
94
+ # List with prefix
95
+ docs_files = await storage.alist(prefix="docs/")
96
+ assert len(docs_files) == 2
97
+ assert all(f.startswith("docs/") for f in docs_files)
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_get_metadata(self, storage, test_file):
101
+ """Test getting file metadata."""
102
+ await storage.aupload(str(test_file), "test.txt")
103
+
104
+ metadata = await storage.aget_metadata("test.txt")
105
+
106
+ assert metadata["path"] == "test.txt"
107
+ assert metadata["size_bytes"] > 0
108
+ assert metadata["is_file"] is True
109
+ assert "modified_time" in metadata
110
+ assert "created_time" in metadata
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_health_check(self, storage):
114
+ """Test health check."""
115
+ health = await storage.ahealth_check()
116
+
117
+ assert health.status == "healthy"
118
+ assert health.adapter_name == "local_file_storage"
119
+ assert health.port_name == "file_storage"
120
+ assert health.details["writable"] is True
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_upload_creates_directories(self, storage, test_file):
124
+ """Test that upload creates nested directories."""
125
+ result = await storage.aupload(str(test_file), "a/b/c/test.txt")
126
+
127
+ assert result["uploaded"] is True
128
+ assert await storage.aexists("a/b/c/test.txt") is True
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_upload_nonexistent_file(self, storage):
132
+ """Test uploading a nonexistent file."""
133
+ with pytest.raises(FileNotFoundError):
134
+ await storage.aupload("/nonexistent/file.txt", "test.txt")
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_download_nonexistent_file(self, storage, tmp_path):
138
+ """Test downloading a nonexistent file."""
139
+ with pytest.raises(FileNotFoundError):
140
+ await storage.adownload("nonexistent.txt", str(tmp_path / "out.txt"))
141
+
142
+ @pytest.mark.asyncio
143
+ async def test_delete_nonexistent_file(self, storage):
144
+ """Test deleting a nonexistent file."""
145
+ with pytest.raises(FileNotFoundError):
146
+ await storage.adelete("nonexistent.txt")
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_get_metadata_nonexistent_file(self, storage):
150
+ """Test getting metadata for nonexistent file."""
151
+ with pytest.raises(FileNotFoundError):
152
+ await storage.aget_metadata("nonexistent.txt")
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_repr(self, tmp_path):
156
+ """Test string representation."""
157
+ storage = LocalFileStorage(base_path=str(tmp_path))
158
+ repr_str = repr(storage)
159
+
160
+ assert "LocalFileStorage" in repr_str
161
+ assert str(tmp_path) in repr_str
@@ -0,0 +1,212 @@
1
+ """Tests for SQL adapters (MySQL, PostgreSQL)."""
2
+
3
+ import pytest
4
+
5
+ from hexdag_plugins.storage.sql import MySQLAdapter, PostgreSQLAdapter
6
+
7
+
8
+ class TestSQLAdapterInterface:
9
+ """Test SQL adapter interface and basic functionality."""
10
+
11
+ @pytest.mark.asyncio
12
+ async def test_mysql_adapter_creation(self):
13
+ """Test MySQL adapter can be created with config."""
14
+ adapter = MySQLAdapter(connection_string="mysql+aiomysql://test:test@localhost/test")
15
+ assert adapter is not None
16
+ assert (
17
+ adapter.config.connection_string.get_secret_value()
18
+ == "mysql+aiomysql://test:test@localhost/test"
19
+ )
20
+ assert adapter.config.pool_size == 5
21
+ assert adapter.config.max_overflow == 10
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_postgresql_adapter_creation(self):
25
+ """Test PostgreSQL adapter can be created with config."""
26
+ adapter = PostgreSQLAdapter(
27
+ connection_string="postgresql+asyncpg://test:test@localhost/test"
28
+ )
29
+ assert adapter is not None
30
+ assert (
31
+ adapter.config.connection_string.get_secret_value()
32
+ == "postgresql+asyncpg://test:test@localhost/test"
33
+ )
34
+ assert adapter.config.pool_size == 5
35
+ assert adapter.config.max_overflow == 10
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_mysql_adapter_custom_pool_config(self):
39
+ """Test MySQL adapter with custom pool configuration."""
40
+ adapter = MySQLAdapter(
41
+ connection_string="mysql+aiomysql://test:test@localhost/test",
42
+ pool_size=10,
43
+ max_overflow=20,
44
+ pool_timeout=60.0,
45
+ pool_recycle=7200,
46
+ pool_pre_ping=False,
47
+ )
48
+ assert adapter.config.pool_size == 10
49
+ assert adapter.config.max_overflow == 20
50
+ assert adapter.config.pool_timeout == 60.0
51
+ assert adapter.config.pool_recycle == 7200
52
+ assert adapter.config.pool_pre_ping is False
53
+
54
+ @pytest.mark.asyncio
55
+ async def test_postgresql_adapter_custom_pool_config(self):
56
+ """Test PostgreSQL adapter with custom pool configuration."""
57
+ adapter = PostgreSQLAdapter(
58
+ connection_string="postgresql+asyncpg://test:test@localhost/test",
59
+ pool_size=15,
60
+ max_overflow=25,
61
+ pool_timeout=45.0,
62
+ pool_recycle=1800,
63
+ pool_pre_ping=False,
64
+ )
65
+ assert adapter.config.pool_size == 15
66
+ assert adapter.config.max_overflow == 25
67
+ assert adapter.config.pool_timeout == 45.0
68
+ assert adapter.config.pool_recycle == 1800
69
+ assert adapter.config.pool_pre_ping is False
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_adapter_not_setup_error(self):
73
+ """Test that operations fail before setup."""
74
+ adapter = MySQLAdapter(connection_string="mysql+aiomysql://test:test@localhost/test")
75
+
76
+ with pytest.raises(RuntimeError, match="Adapter not set up"):
77
+ await adapter.aexecute("SELECT 1")
78
+
79
+ with pytest.raises(RuntimeError, match="Adapter not set up"):
80
+ await adapter.afetch_one("SELECT 1")
81
+
82
+ with pytest.raises(RuntimeError, match="Adapter not set up"):
83
+ await adapter.afetch_all("SELECT 1")
84
+
85
+ @pytest.mark.asyncio
86
+ async def test_health_check_before_setup(self):
87
+ """Test health check before setup returns unhealthy status."""
88
+ adapter = MySQLAdapter(connection_string="mysql+aiomysql://test:test@localhost/test")
89
+
90
+ health = await adapter.ahealth_check()
91
+ assert health.status == "unhealthy"
92
+ assert "not initialized" in health.details.get("message", "").lower()
93
+
94
+
95
+ # Integration tests require real database connections
96
+ # These are marked with @pytest.mark.integration and skipped by default
97
+
98
+
99
+ @pytest.mark.integration
100
+ @pytest.mark.asyncio
101
+ async def test_mysql_adapter_integration():
102
+ """Integration test for MySQL adapter with real database.
103
+
104
+ Requires MySQL running at localhost with test database.
105
+ Set MYSQL_TEST_URL environment variable or skip this test.
106
+ """
107
+ import os
108
+
109
+ connection_string = os.getenv("MYSQL_TEST_URL", "mysql+aiomysql://test:test@localhost/test")
110
+
111
+ adapter = MySQLAdapter(connection_string=connection_string)
112
+
113
+ try:
114
+ await adapter.asetup()
115
+
116
+ # Test health check
117
+ health = await adapter.ahealth_check()
118
+ assert health.is_healthy() is True
119
+
120
+ # Test table creation
121
+ await adapter.aexecute(
122
+ """
123
+ CREATE TABLE IF NOT EXISTS test_table (
124
+ id INT PRIMARY KEY AUTO_INCREMENT,
125
+ name VARCHAR(255),
126
+ value INT
127
+ )
128
+ """
129
+ )
130
+
131
+ # Test insert
132
+ await adapter.aexecute(
133
+ "INSERT INTO test_table (name, value) VALUES (:name, :value)",
134
+ {"name": "test", "value": 42},
135
+ )
136
+
137
+ # Test fetch_one
138
+ result = await adapter.afetch_one(
139
+ "SELECT * FROM test_table WHERE name = :name", {"name": "test"}
140
+ )
141
+ assert result is not None
142
+ assert result["name"] == "test"
143
+ assert result["value"] == 42
144
+
145
+ # Test fetch_all
146
+ results = await adapter.afetch_all("SELECT * FROM test_table")
147
+ assert len(results) >= 1
148
+
149
+ # Cleanup
150
+ await adapter.aexecute("DROP TABLE test_table")
151
+
152
+ finally:
153
+ await adapter.aclose()
154
+
155
+
156
+ @pytest.mark.integration
157
+ @pytest.mark.asyncio
158
+ async def test_postgresql_adapter_integration():
159
+ """Integration test for PostgreSQL adapter with real database.
160
+
161
+ Requires PostgreSQL running at localhost with test database.
162
+ Set POSTGRES_TEST_URL environment variable or skip this test.
163
+ """
164
+ import os
165
+
166
+ connection_string = os.getenv(
167
+ "POSTGRES_TEST_URL", "postgresql+asyncpg://test:test@localhost/test"
168
+ )
169
+
170
+ adapter = PostgreSQLAdapter(connection_string=connection_string)
171
+
172
+ try:
173
+ await adapter.asetup()
174
+
175
+ # Test health check
176
+ health = await adapter.ahealth_check()
177
+ assert health.is_healthy() is True
178
+
179
+ # Test table creation
180
+ await adapter.aexecute(
181
+ """
182
+ CREATE TABLE IF NOT EXISTS test_table (
183
+ id SERIAL PRIMARY KEY,
184
+ name VARCHAR(255),
185
+ value INT
186
+ )
187
+ """
188
+ )
189
+
190
+ # Test insert
191
+ await adapter.aexecute(
192
+ "INSERT INTO test_table (name, value) VALUES (:name, :value)",
193
+ {"name": "test", "value": 42},
194
+ )
195
+
196
+ # Test fetch_one
197
+ result = await adapter.afetch_one(
198
+ "SELECT * FROM test_table WHERE name = :name", {"name": "test"}
199
+ )
200
+ assert result is not None
201
+ assert result["name"] == "test"
202
+ assert result["value"] == 42
203
+
204
+ # Test fetch_all
205
+ results = await adapter.afetch_all("SELECT * FROM test_table")
206
+ assert len(results) >= 1
207
+
208
+ # Cleanup
209
+ await adapter.aexecute("DROP TABLE test_table")
210
+
211
+ finally:
212
+ await adapter.aclose()
@@ -0,0 +1,7 @@
1
+ """Vector store adapters."""
2
+
3
+ from .chromadb import ChromaDBAdapter
4
+ from .in_memory import InMemoryVectorStore
5
+ from .pgvector import PgVectorAdapter
6
+
7
+ __all__ = ["ChromaDBAdapter", "InMemoryVectorStore", "PgVectorAdapter"]