hexdag 0.5.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- hexdag_plugins/storage/vector/pgvector.py +502 -0
|
@@ -0,0 +1,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
|