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,529 @@
1
+ """Plugin development commands for hexDAG CLI."""
2
+
3
+ import subprocess # nosec B404 - subprocess is used safely with controlled inputs
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+
13
+ app = typer.Typer()
14
+ console = Console()
15
+
16
+
17
+ def get_plugin_dir() -> Path:
18
+ """Get the hexdag_plugins directory path."""
19
+ # Try to find the project root
20
+ current = Path.cwd()
21
+ while current != current.parent:
22
+ if (current / "hexdag_plugins").exists():
23
+ return current / "hexdag_plugins"
24
+ if (current / "pyproject.toml").exists():
25
+ with Path.open(current / "pyproject.toml") as f:
26
+ if "hexdag" in f.read():
27
+ plugin_dir = current / "hexdag_plugins"
28
+ if not plugin_dir.exists():
29
+ plugin_dir.mkdir(parents=True, exist_ok=True)
30
+ return plugin_dir
31
+ current = current.parent
32
+
33
+ # Default to current directory
34
+ plugin_dir = Path.cwd() / "hexdag_plugins"
35
+ if not plugin_dir.exists():
36
+ console.print("[yellow]Creating hexdag_plugins directory in current location[/yellow]")
37
+ plugin_dir.mkdir(parents=True, exist_ok=True)
38
+ return plugin_dir
39
+
40
+
41
+ @app.command("new")
42
+ def create_plugin(
43
+ name: Annotated[str, typer.Argument(help="Name of the new plugin (e.g., redis_adapter)")],
44
+ port: Annotated[str, typer.Option("--port", "-p", help="Port type to implement")] = "database",
45
+ author: Annotated[
46
+ str, typer.Option("--author", "-a", help="Plugin author name")
47
+ ] = "HexDAG Team",
48
+ ) -> None:
49
+ """Create a new plugin from template."""
50
+ plugin_dir = get_plugin_dir()
51
+ plugin_path = plugin_dir / name
52
+
53
+ if plugin_path.exists():
54
+ console.print(f"[red]Error: Plugin '{name}' already exists at {plugin_path}[/red]")
55
+ raise typer.Exit(1)
56
+
57
+ console.print(f"[green]Creating new plugin: {name}[/green]")
58
+
59
+ plugin_path.mkdir(parents=True)
60
+ (plugin_path / "tests").mkdir()
61
+
62
+ class_name = name.replace("_", " ").title().replace(" ", "")
63
+ init_content = f'''"""${name} plugin for hexDAG."""
64
+
65
+ from .{name} import {class_name}
66
+
67
+ __all__ = ["{class_name}"]
68
+ '''
69
+ (plugin_path / "__init__.py").write_text(init_content)
70
+
71
+ class_name = name.replace("_", " ").title().replace(" ", "")
72
+ adapter_content = f'''"""{class_name} implementation."""
73
+
74
+ import os
75
+ from typing import Any
76
+
77
+
78
+ class {class_name}:
79
+ """{class_name} adapter for hexDAG.
80
+
81
+ This adapter implements the {port} port interface.
82
+
83
+ YAML Usage:
84
+ ```yaml
85
+ ports:
86
+ {port}:
87
+ adapter: hexdag_plugins.{name}.{class_name}
88
+ config:
89
+ # your config here
90
+ ```
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ # Add your configuration parameters here
96
+ # Example with secret:
97
+ # api_key: str | None = None,
98
+ **kwargs: Any,
99
+ ) -> None:
100
+ """Initialize {name}.
101
+
102
+ Parameters
103
+ ----------
104
+ **kwargs : Any
105
+ Additional configuration options
106
+ """
107
+ # Example secret resolution:
108
+ # self.api_key = api_key or os.getenv("MY_API_KEY")
109
+ # if not self.api_key:
110
+ # raise ValueError("api_key required (pass directly or set MY_API_KEY)")
111
+ pass
112
+
113
+ # TODO: Implement the {port} port interface methods
114
+ # Check hexdag/core/ports/{port}.py for the interface definition
115
+
116
+ def __repr__(self) -> str:
117
+ """String representation."""
118
+ return f"{class_name}()"
119
+ '''
120
+ (plugin_path / f"{name}.py").write_text(adapter_content)
121
+
122
+ pyproject_content = f"""[project]
123
+ name = "hexdag-{name.replace("_", "-")}"
124
+ version = "0.1.0"
125
+ description = "{class_name} plugin for hexDAG"
126
+ authors = [{{ name = "{author}" }}]
127
+ requires-python = "~=3.12.0"
128
+ readme = "README.md"
129
+ license = "MIT"
130
+ keywords = ["hexdag", "plugin", "{port}", "adapter"]
131
+ classifiers = [
132
+ "Development Status :: 3 - Alpha",
133
+ "Intended Audience :: Developers",
134
+ "License :: OSI Approved :: MIT License",
135
+ "Programming Language :: Python :: 3",
136
+ "Programming Language :: Python :: 3.12",
137
+ ]
138
+ dependencies = [
139
+ # Do not add hexdag as a dependency (it's the parent project)
140
+ ]
141
+
142
+ [build-system]
143
+ requires = ["hatchling"]
144
+ build-backend = "hatchling.build"
145
+
146
+ [tool.ruff]
147
+ line-length = 100
148
+ target-version = "py312"
149
+
150
+ [tool.mypy]
151
+ python_version = "3.12"
152
+ warn_return_any = true
153
+ disallow_untyped_defs = false
154
+ check_untyped_defs = true
155
+ """
156
+ (plugin_path / "pyproject.toml").write_text(pyproject_content)
157
+
158
+ readme_content = f"""# {class_name}
159
+
160
+ A hexDAG plugin that provides {name.replace("_", " ")} functionality.
161
+
162
+ ## Installation
163
+
164
+ ```bash
165
+ # From the hexDAG root directory
166
+ pip install -e hexdag_plugins/{name}/
167
+ ```
168
+
169
+ ## Usage in YAML Pipelines
170
+
171
+ The {class_name} implements the `{port}` port interface.
172
+
173
+ ```yaml
174
+ apiVersion: hexdag/v1
175
+ kind: Pipeline
176
+ metadata:
177
+ name: my-pipeline
178
+ spec:
179
+ ports:
180
+ {port}:
181
+ adapter: hexdag_plugins.{name}.{class_name}
182
+ config:
183
+ # your config here
184
+
185
+ nodes:
186
+ # your nodes here
187
+ ```
188
+
189
+ ## Usage in Python
190
+
191
+ ```python
192
+ from hexdag_plugins.{name} import {class_name}
193
+
194
+ adapter = {class_name}()
195
+ ```
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ # Run tests
201
+ hexdag plugin test {name}
202
+
203
+ # Format code
204
+ hexdag plugin format {name}
205
+
206
+ # Lint code
207
+ hexdag plugin lint {name}
208
+ ```
209
+ """
210
+ (plugin_path / "README.md").write_text(readme_content)
211
+
212
+ test_content = f'''"""Tests for {name}."""
213
+
214
+ import pytest
215
+
216
+
217
+ class Test{class_name}:
218
+ """Test suite for {class_name}."""
219
+
220
+ def test_adapter_initialization(self):
221
+ """{class_name} should initialize without errors."""
222
+ from hexdag_plugins.{name}.{name} import {class_name}
223
+
224
+ adapter = {class_name}()
225
+ assert adapter is not None
226
+ assert repr(adapter) == "{class_name}()"
227
+
228
+ # TODO: Add more tests for your adapter functionality
229
+ '''
230
+ (plugin_path / "tests" / f"test_{name}.py").write_text(test_content)
231
+
232
+ license_content = """MIT License
233
+
234
+ Copyright (c) 2024 HexDAG Team
235
+
236
+ Permission is hereby granted, free of charge, to any person obtaining a copy
237
+ of this software and associated documentation files (the "Software"), to deal
238
+ in the Software without restriction, including without limitation the rights
239
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
240
+ copies of the Software, and to permit persons to whom the Software is
241
+ furnished to do so, subject to the following conditions:
242
+
243
+ The above copyright notice and this permission notice shall be included in all
244
+ copies or substantial portions of the Software.
245
+
246
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
247
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
248
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
249
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
250
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
251
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
252
+ SOFTWARE.
253
+ """
254
+ (plugin_path / "LICENSE").write_text(license_content)
255
+
256
+ # Display success message with next steps
257
+ console.print(Panel(f"[green]✓ Plugin '{name}' created successfully![/green]"))
258
+
259
+ table = Table(title="Next Steps", show_header=False, box=None)
260
+ table.add_row("1.", f"Edit [cyan]{plugin_path / f'{name}.py'}[/cyan] to implement your adapter")
261
+ table.add_row(
262
+ "2.", f"Update the port interface methods based on [cyan]hexdag/core/ports/{port}.py[/cyan]"
263
+ )
264
+ table.add_row("3.", f"Add dependencies to [cyan]{plugin_path / 'pyproject.toml'}[/cyan]")
265
+ table.add_row("4.", f"Run [yellow]hexdag plugin lint {name}[/yellow] to check your code")
266
+ table.add_row(
267
+ "5.", f"Run [yellow]hexdag plugin test {name}[/yellow] to test your implementation"
268
+ )
269
+ console.print(table)
270
+
271
+
272
+ @app.command("list")
273
+ def list_plugins() -> None:
274
+ """List all available plugins."""
275
+ plugin_dir = get_plugin_dir()
276
+
277
+ if not plugin_dir.exists():
278
+ console.print("[yellow]No plugins directory found[/yellow]")
279
+ return
280
+
281
+ plugins = [
282
+ d
283
+ for d in plugin_dir.iterdir()
284
+ if d.is_dir() and not d.name.startswith(".") and d.name != "__pycache__"
285
+ ]
286
+
287
+ if not plugins:
288
+ console.print("[yellow]No plugins found[/yellow]")
289
+ return
290
+
291
+ table = Table(title="Available Plugins", show_header=True)
292
+ table.add_column("Plugin", style="cyan")
293
+ table.add_column("Version", style="green")
294
+ table.add_column("Port", style="yellow")
295
+ table.add_column("Description")
296
+
297
+ for plugin_path in plugins:
298
+ name = plugin_path.name
299
+ pyproject_path = plugin_path / "pyproject.toml"
300
+
301
+ if pyproject_path.exists():
302
+ import tomllib
303
+
304
+ with Path.open(pyproject_path, "rb") as f:
305
+ data = tomllib.load(f)
306
+ version = data.get("project", {}).get("version", "unknown")
307
+ plugin_info = data.get("tool", {}).get("hexdag", {}).get("plugin", {})
308
+ port = plugin_info.get("port", "unknown")
309
+ description = plugin_info.get("description", "No description")
310
+ else:
311
+ version = "unknown"
312
+ port = "unknown"
313
+ description = "No pyproject.toml found"
314
+
315
+ table.add_row(name, version, port, description)
316
+
317
+ console.print(table)
318
+
319
+
320
+ @app.command("lint")
321
+ def lint_plugin(
322
+ name: Annotated[str, typer.Argument(help="Plugin name to lint")],
323
+ fix: Annotated[bool, typer.Option("--fix", "-f", help="Auto-fix issues")] = True,
324
+ ) -> None:
325
+ """Lint a plugin's code."""
326
+ plugin_dir = get_plugin_dir()
327
+ plugin_path = plugin_dir / name
328
+
329
+ if not plugin_path.exists():
330
+ console.print(f"[red]Plugin '{name}' not found[/red]")
331
+ raise typer.Exit(1)
332
+
333
+ console.print(f"[yellow]Linting {name}...[/yellow]")
334
+
335
+ # Run ruff check
336
+ check_cmd = ["ruff", "check", str(plugin_path)]
337
+ if fix:
338
+ check_cmd.append("--fix")
339
+
340
+ result = subprocess.run(check_cmd, capture_output=True, text=True) # nosec B603
341
+ if result.stdout:
342
+ console.print(result.stdout)
343
+ if result.stderr:
344
+ console.print(f"[red]{result.stderr}[/red]")
345
+
346
+ # Run ruff format
347
+ format_cmd = ["ruff", "format", str(plugin_path)]
348
+ result = subprocess.run(format_cmd, capture_output=True, text=True) # nosec B603
349
+ if result.stdout:
350
+ console.print(result.stdout)
351
+
352
+ if result.returncode == 0:
353
+ console.print(f"[green]✓ Plugin '{name}' linted successfully[/green]")
354
+ else:
355
+ console.print(f"[red]✗ Linting failed for '{name}'[/red]")
356
+ raise typer.Exit(1)
357
+
358
+
359
+ @app.command("format")
360
+ def format_plugin(
361
+ name: Annotated[str, typer.Argument(help="Plugin name to format")],
362
+ ) -> None:
363
+ """Format a plugin's code."""
364
+ plugin_dir = get_plugin_dir()
365
+ plugin_path = plugin_dir / name
366
+
367
+ if not plugin_path.exists():
368
+ console.print(f"[red]Plugin '{name}' not found[/red]")
369
+ raise typer.Exit(1)
370
+
371
+ console.print(f"[yellow]Formatting {name}...[/yellow]")
372
+
373
+ # Run ruff format
374
+ cmd = ["ruff", "format", str(plugin_path), "--line-length=100"]
375
+ result = subprocess.run(cmd, capture_output=True, text=True) # nosec B603
376
+
377
+ if result.stdout:
378
+ console.print(result.stdout)
379
+
380
+ if result.returncode == 0:
381
+ console.print(f"[green]✓ Plugin '{name}' formatted successfully[/green]")
382
+ else:
383
+ console.print(f"[red]✗ Formatting failed for '{name}'[/red]")
384
+ if result.stderr:
385
+ console.print(f"[red]{result.stderr}[/red]")
386
+ raise typer.Exit(1)
387
+
388
+
389
+ @app.command("test")
390
+ def test_plugin(
391
+ name: Annotated[str, typer.Argument(help="Plugin name to test")],
392
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Verbose output")] = False,
393
+ ) -> None:
394
+ """Run tests for a plugin."""
395
+ plugin_dir = get_plugin_dir()
396
+ plugin_path = plugin_dir / name
397
+
398
+ if not plugin_path.exists():
399
+ console.print(f"[red]Plugin '{name}' not found[/red]")
400
+ raise typer.Exit(1)
401
+
402
+ test_dir = plugin_path / "tests"
403
+ if not test_dir.exists():
404
+ console.print(f"[yellow]No tests found for '{name}'[/yellow]")
405
+ return
406
+
407
+ console.print(f"[yellow]Testing {name}...[/yellow]")
408
+
409
+ # Run pytest
410
+ cmd = [sys.executable, "-m", "pytest", str(test_dir)]
411
+ if verbose:
412
+ cmd.append("-v")
413
+
414
+ result = subprocess.run(cmd, capture_output=True, text=True) # nosec B603
415
+
416
+ if result.stdout:
417
+ console.print(result.stdout)
418
+
419
+ if result.returncode == 0:
420
+ console.print(f"[green]✓ All tests passed for '{name}'[/green]")
421
+ else:
422
+ console.print(f"[red]✗ Tests failed for '{name}'[/red]")
423
+ if result.stderr:
424
+ console.print(f"[red]{result.stderr}[/red]")
425
+ raise typer.Exit(1)
426
+
427
+
428
+ @app.command("install")
429
+ def install_plugin(
430
+ name: Annotated[str, typer.Argument(help="Plugin name to install")],
431
+ editable: Annotated[
432
+ bool, typer.Option("--editable", "-e", help="Install in editable mode")
433
+ ] = True,
434
+ ) -> None:
435
+ """Install a plugin in development mode."""
436
+ plugin_dir = get_plugin_dir()
437
+ plugin_path = plugin_dir / name
438
+
439
+ if not plugin_path.exists():
440
+ console.print(f"[red]Plugin '{name}' not found[/red]")
441
+ raise typer.Exit(1)
442
+
443
+ console.print(f"[yellow]Installing {name}...[/yellow]")
444
+
445
+ # Install with pip
446
+ cmd = [sys.executable, "-m", "pip", "install"]
447
+ if editable:
448
+ cmd.append("-e")
449
+ cmd.append(str(plugin_path))
450
+
451
+ result = subprocess.run(cmd, capture_output=True, text=True) # nosec B603
452
+
453
+ if result.stdout:
454
+ console.print(result.stdout)
455
+
456
+ if result.returncode == 0:
457
+ console.print(f"[green]✓ Plugin '{name}' installed successfully[/green]")
458
+ else:
459
+ console.print(f"[red]✗ Installation failed for '{name}'[/red]")
460
+ if result.stderr:
461
+ console.print(f"[red]{result.stderr}[/red]")
462
+ raise typer.Exit(1)
463
+
464
+
465
+ @app.command("check-all")
466
+ def check_all_plugins() -> None:
467
+ """Run lint and test for all plugins."""
468
+ plugin_dir = get_plugin_dir()
469
+
470
+ if not plugin_dir.exists():
471
+ console.print("[yellow]No plugins directory found[/yellow]")
472
+ return
473
+
474
+ plugins = [
475
+ d.name
476
+ for d in plugin_dir.iterdir()
477
+ if d.is_dir() and not d.name.startswith(".") and d.name != "__pycache__"
478
+ ]
479
+
480
+ if not plugins:
481
+ console.print("[yellow]No plugins found[/yellow]")
482
+ return
483
+
484
+ console.print(f"[bold]Checking {len(plugins)} plugins...[/bold]\n")
485
+
486
+ results = []
487
+ for plugin_name in plugins:
488
+ console.print(f"[cyan]Checking {plugin_name}...[/cyan]")
489
+
490
+ # Lint
491
+ lint_success = True
492
+ try:
493
+ lint_plugin(plugin_name, fix=True)
494
+ except typer.Exit:
495
+ lint_success = False
496
+
497
+ # Test
498
+ test_success = True
499
+ try:
500
+ test_plugin(plugin_name)
501
+ except typer.Exit:
502
+ test_success = False
503
+
504
+ results.append((plugin_name, lint_success, test_success))
505
+ console.print() # Add spacing
506
+
507
+ # Summary
508
+ console.print("[bold]Summary:[/bold]")
509
+ table = Table(show_header=True)
510
+ table.add_column("Plugin", style="cyan")
511
+ table.add_column("Lint", style="green")
512
+ table.add_column("Test", style="green")
513
+
514
+ for plugin_name, lint_ok, test_ok in results:
515
+ lint_status = "[green]✓[/green]" if lint_ok else "[red]✗[/red]"
516
+ test_status = "[green]✓[/green]" if test_ok else "[red]✗[/red]"
517
+ table.add_row(plugin_name, lint_status, test_status)
518
+
519
+ console.print(table)
520
+
521
+ if all(lint_ok and test_ok for _, lint_ok, test_ok in results):
522
+ console.print("\n[green]✓ All plugins passed checks![/green]")
523
+ else:
524
+ console.print("\n[red]✗ Some plugins failed checks[/red]")
525
+ raise typer.Exit(1)
526
+
527
+
528
+ if __name__ == "__main__":
529
+ app()