quantum-framework 0.18.0__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 (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,548 @@
1
+ """
2
+ Quantum CLI - Database Migration Commands
3
+ Applies migrations to a datasource declared in quantum.config.yaml (DB-6)
4
+ """
5
+ import os
6
+ import sys
7
+ import hashlib
8
+ import logging
9
+ from pathlib import Path
10
+ from datetime import datetime
11
+ from typing import Optional, List, Dict, Any
12
+
13
+ import click
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MigrationError(Exception):
19
+ """Migration operation failed"""
20
+ pass
21
+
22
+
23
+ class Migration:
24
+ """Represents a single migration file"""
25
+
26
+ def __init__(self, version: str, name: str, path: Path):
27
+ self.version = version
28
+ self.name = name
29
+ self.path = path
30
+ self._content = None
31
+
32
+ @property
33
+ def content(self) -> str:
34
+ if self._content is None:
35
+ self._content = self.path.read_text(encoding="utf-8") # DB-8: nunca a codificacao do sistema
36
+ return self._content
37
+
38
+ @property
39
+ def checksum(self) -> str:
40
+ return hashlib.md5(self.content.encode()).hexdigest()
41
+
42
+ def __repr__(self):
43
+ return f"<Migration {self.version}: {self.name}>"
44
+
45
+
46
+ class DatabaseConnection:
47
+ """Connection to the datasource the migrations are for (DB-6).
48
+
49
+ It used to read a separate `database:` section that nothing else uses and,
50
+ without one, try a PostgreSQL at localhost/quantum with user postgres before
51
+ falling back to ./data/quantum.db. A project whose pages query
52
+ datasources.db (./data/app.db) had its migrations applied to a different
53
+ file — or to whatever local PostgreSQL answered.
54
+ """
55
+
56
+ def __init__(self, project_path: Path = None, datasource: str = None):
57
+ self.project_path = project_path or Path.cwd()
58
+ self.datasource = datasource
59
+ self._conn = None
60
+ self._db_type = None
61
+
62
+ def connect(self) -> Any:
63
+ nome, config = self._datasource_config()
64
+ driver = str(config.get('driver', '')).lower()
65
+ if driver == 'sqlite':
66
+ import sqlite3
67
+ caminho = Path(config.get('database') or '')
68
+ if not config.get('database'):
69
+ raise MigrationError(f"datasource '{nome}' (sqlite) has no database: path")
70
+ if not caminho.is_absolute():
71
+ caminho = self.project_path / caminho
72
+ caminho.parent.mkdir(parents=True, exist_ok=True)
73
+ # isolation_level=None: execute_script opens the transaction
74
+ # itself, so a CREATE TABLE is inside it too (DB-8).
75
+ self._conn = sqlite3.connect(str(caminho), isolation_level=None)
76
+ self._db_type = 'sqlite'
77
+ logger.info("Migrating datasource %s (SQLite at %s)", nome, caminho)
78
+ return self._conn
79
+ if driver in ('postgres', 'postgresql'):
80
+ try:
81
+ import psycopg2
82
+ except ImportError as exc:
83
+ raise MigrationError("datasource '%s' is PostgreSQL: pip install psycopg2-binary" % nome) from exc
84
+ self._conn = psycopg2.connect(
85
+ host=config.get('host', 'localhost'), port=int(config.get('port', 5432)),
86
+ dbname=config.get('database'), user=config.get('username') or config.get('user'),
87
+ password=config.get('password', ''), connect_timeout=5)
88
+ self._db_type = 'postgres'
89
+ logger.info("Migrating datasource %s (PostgreSQL %s)", nome, config.get('database'))
90
+ return self._conn
91
+ raise MigrationError(f"datasource '{nome}': driver {driver!r} is not supported by quantum migrate "
92
+ f"(use sqlite or postgres)")
93
+
94
+ def _datasource_config(self):
95
+ config_path = self.project_path / "quantum.config.yaml"
96
+ datasources = {}
97
+ if config_path.exists():
98
+ import yaml
99
+ from quantum.core.config_env import expand_env
100
+ with open(config_path, encoding='utf-8') as f:
101
+ datasources = (expand_env(yaml.safe_load(f) or {}).get('datasources') or {})
102
+ if not datasources:
103
+ raise MigrationError(
104
+ "quantum.config.yaml declares no datasources. Add one, for example:\n"
105
+ " datasources:\n db:\n driver: sqlite\n database: ./data/app.db")
106
+ if self.datasource:
107
+ if self.datasource not in datasources:
108
+ raise MigrationError(f"no datasource '{self.datasource}'; declared: {', '.join(datasources)}")
109
+ return self.datasource, datasources[self.datasource]
110
+ if len(datasources) > 1:
111
+ raise MigrationError(f"several datasources are declared ({', '.join(datasources)}); "
112
+ f"choose one with --datasource")
113
+ nome = next(iter(datasources))
114
+ return nome, datasources[nome]
115
+
116
+ def _ensure_connected(self):
117
+ if self._conn is None:
118
+ self.connect()
119
+
120
+ @property
121
+ def db_type(self) -> str:
122
+ return self._db_type
123
+
124
+ def execute(self, sql: str, params: tuple = None):
125
+ """Execute SQL statement"""
126
+ cursor = self._conn.cursor()
127
+ if params:
128
+ cursor.execute(sql, params)
129
+ else:
130
+ cursor.execute(sql)
131
+ return cursor
132
+
133
+ def execute_script(self, sql: str):
134
+ """Run a whole migration file, in one transaction (DB-8).
135
+
136
+ sqlite3's execute() takes ONE statement, so every migration with more
137
+ than one — a table and its index — failed with "You can only execute
138
+ one statement at a time". The statements are split here and run
139
+ inside an explicit BEGIN, which commit()/rollback() close: a failure
140
+ halfway leaves no table behind.
141
+ """
142
+ if self._db_type != 'sqlite':
143
+ return self.execute(sql)
144
+ import sqlite3
145
+ cursor = self._conn.cursor()
146
+ if not self._conn.in_transaction:
147
+ cursor.execute('BEGIN')
148
+ pedaco = ''
149
+ for linha in sql.splitlines(keepends=True):
150
+ pedaco += linha
151
+ if sqlite3.complete_statement(pedaco):
152
+ if pedaco.strip():
153
+ cursor.execute(pedaco)
154
+ pedaco = ''
155
+ if pedaco.strip() and not all(l.strip().startswith('--') or not l.strip() for l in pedaco.splitlines()):
156
+ raise MigrationError(f"incomplete SQL statement at the end: {pedaco.strip()[:80]!r}")
157
+ return cursor
158
+
159
+ def commit(self):
160
+ self._conn.commit()
161
+
162
+ def rollback(self):
163
+ self._conn.rollback()
164
+
165
+ def close(self):
166
+ if self._conn:
167
+ self._conn.close()
168
+
169
+
170
+ def _paths_migrations(project_path: Path) -> str:
171
+ """CFG-3: paths.migrations in quantum.config.yaml (default ./migrations)."""
172
+ try:
173
+ import yaml
174
+ from quantum.core.config_env import expand_env
175
+ with open(project_path / "quantum.config.yaml", encoding="utf-8") as f:
176
+ config = expand_env(yaml.safe_load(f) or {})
177
+ return (config.get("paths") or {}).get("migrations") or "migrations"
178
+ except (OSError, ValueError, AttributeError):
179
+ return "migrations"
180
+
181
+
182
+ class MigrationRunner:
183
+ """Runs database migrations"""
184
+
185
+ MIGRATIONS_TABLE = "_migrations"
186
+
187
+ def __init__(self, project_path: Path = None, datasource: str = None):
188
+ self.project_path = project_path or Path.cwd()
189
+ self.migrations_dir = self.project_path / _paths_migrations(self.project_path)
190
+ self.db = DatabaseConnection(self.project_path, datasource)
191
+
192
+ def _ensure_migrations_table(self):
193
+ """Create migrations tracking table if it doesn't exist"""
194
+ if self.db.db_type == 'postgres':
195
+ sql = f"""
196
+ CREATE TABLE IF NOT EXISTS {self.MIGRATIONS_TABLE} (
197
+ version VARCHAR(50) PRIMARY KEY,
198
+ name VARCHAR(255) NOT NULL,
199
+ checksum VARCHAR(32) NOT NULL,
200
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
201
+ )
202
+ """
203
+ else: # SQLite
204
+ sql = f"""
205
+ CREATE TABLE IF NOT EXISTS {self.MIGRATIONS_TABLE} (
206
+ version TEXT PRIMARY KEY,
207
+ name TEXT NOT NULL,
208
+ checksum TEXT NOT NULL,
209
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP
210
+ )
211
+ """
212
+ self.db.execute(sql)
213
+ self.db.commit()
214
+
215
+ def _get_applied_migrations(self) -> Dict[str, Dict]:
216
+ """Get list of already applied migrations"""
217
+ cursor = self.db.execute(
218
+ f"SELECT version, name, checksum, applied_at FROM {self.MIGRATIONS_TABLE} ORDER BY version"
219
+ )
220
+ rows = cursor.fetchall()
221
+ return {
222
+ row[0]: {
223
+ 'version': row[0],
224
+ 'name': row[1],
225
+ 'checksum': row[2],
226
+ 'applied_at': row[3]
227
+ }
228
+ for row in rows
229
+ }
230
+
231
+ def _get_pending_migrations(self) -> List[Migration]:
232
+ """Get list of migrations that need to be applied"""
233
+ if not self.migrations_dir.exists():
234
+ return []
235
+
236
+ applied = self._get_applied_migrations()
237
+ pending = []
238
+
239
+ for f in sorted(self.migrations_dir.glob("*.sql")):
240
+ # `create` writes a PAIR: V001_name.sql and V001_name.down.sql.
241
+ # This glob used to pick up both, and the rollback file sorted
242
+ # FIRST ('.down.sql' < '.sql'), so `migrate up` executed the
243
+ # rollback — DROP TABLE and all — before the migration it belongs
244
+ # to, and then recorded the version as applied. On an already
245
+ # migrated database that is data destruction from a command whose
246
+ # entire job is to move forward. Reproduced before fixing.
247
+ if f.name.endswith('.down.sql'):
248
+ continue
249
+
250
+ # Expected format: V001_create_users.sql
251
+ parts = f.stem.split('_', 1)
252
+ if len(parts) < 2:
253
+ continue
254
+
255
+ version = parts[0]
256
+ name = parts[1]
257
+
258
+ if version not in applied:
259
+ pending.append(Migration(version, name, f))
260
+
261
+ return pending
262
+
263
+ def _record_migration(self, migration: Migration):
264
+ """Record that a migration was applied"""
265
+ if self.db.db_type == 'postgres':
266
+ sql = f"""
267
+ INSERT INTO {self.MIGRATIONS_TABLE} (version, name, checksum)
268
+ VALUES (%s, %s, %s)
269
+ """
270
+ else: # SQLite
271
+ sql = f"""
272
+ INSERT INTO {self.MIGRATIONS_TABLE} (version, name, checksum)
273
+ VALUES (?, ?, ?)
274
+ """
275
+ self.db.execute(sql, (migration.version, migration.name, migration.checksum))
276
+
277
+ def _remove_migration_record(self, version: str):
278
+ """Remove migration record for rollback"""
279
+ if self.db.db_type == 'postgres':
280
+ self.db.execute(f"DELETE FROM {self.MIGRATIONS_TABLE} WHERE version = %s", (version,))
281
+ else:
282
+ self.db.execute(f"DELETE FROM {self.MIGRATIONS_TABLE} WHERE version = ?", (version,))
283
+
284
+ def status(self) -> Dict:
285
+ """Get migration status"""
286
+ self.db.connect()
287
+ self._ensure_migrations_table()
288
+
289
+ applied = self._get_applied_migrations()
290
+ pending = self._get_pending_migrations()
291
+
292
+ self.db.close()
293
+
294
+ return {
295
+ 'database_type': self.db.db_type,
296
+ 'applied': list(applied.values()),
297
+ 'pending': [{'version': m.version, 'name': m.name} for m in pending],
298
+ 'total_applied': len(applied),
299
+ 'total_pending': len(pending)
300
+ }
301
+
302
+ def up(self, count: int = None) -> List[Dict]:
303
+ """Apply pending migrations"""
304
+ self.db.connect()
305
+ self._ensure_migrations_table()
306
+
307
+ pending = self._get_pending_migrations()
308
+ if count:
309
+ pending = pending[:count]
310
+
311
+ results = []
312
+
313
+ for migration in pending:
314
+ try:
315
+ click.echo(f"Applying {migration.version}: {migration.name}...")
316
+
317
+ # Execute migration SQL
318
+ self.db.execute_script(migration.content)
319
+
320
+ # Record migration
321
+ self._record_migration(migration)
322
+ self.db.commit()
323
+
324
+ results.append({
325
+ 'version': migration.version,
326
+ 'name': migration.name,
327
+ 'status': 'applied',
328
+ 'checksum': migration.checksum
329
+ })
330
+
331
+ click.echo(f" [OK] Applied {migration.version}")
332
+
333
+ except Exception as e:
334
+ self.db.rollback()
335
+ results.append({
336
+ 'version': migration.version,
337
+ 'name': migration.name,
338
+ 'status': 'failed',
339
+ 'error': str(e)
340
+ })
341
+ click.echo(f" [FAILED] {e}")
342
+ break # Stop on first failure
343
+
344
+ self.db.close()
345
+ return results
346
+
347
+ def down(self, count: int = 1) -> List[Dict]:
348
+ """Rollback migrations"""
349
+ self.db.connect()
350
+ self._ensure_migrations_table()
351
+
352
+ applied = self._get_applied_migrations()
353
+ if not applied:
354
+ click.echo("No migrations to rollback")
355
+ self.db.close()
356
+ return []
357
+
358
+ # Get migrations in reverse order
359
+ versions = sorted(applied.keys(), reverse=True)[:count]
360
+ results = []
361
+
362
+ for version in versions:
363
+ migration_info = applied[version]
364
+
365
+ # Look for down migration file
366
+ down_file = self.migrations_dir / f"{version}_{migration_info['name']}.down.sql"
367
+
368
+ if not down_file.exists():
369
+ click.echo(f"Warning: No down migration for {version}")
370
+ results.append({
371
+ 'version': version,
372
+ 'status': 'skipped',
373
+ 'reason': 'No down migration file'
374
+ })
375
+ continue
376
+
377
+ try:
378
+ click.echo(f"Rolling back {version}: {migration_info['name']}...")
379
+
380
+ # Execute rollback SQL
381
+ self.db.execute_script(down_file.read_text(encoding="utf-8"))
382
+
383
+ # Remove migration record
384
+ self._remove_migration_record(version)
385
+ self.db.commit()
386
+
387
+ results.append({
388
+ 'version': version,
389
+ 'name': migration_info['name'],
390
+ 'status': 'rolled_back'
391
+ })
392
+
393
+ click.echo(f" [OK] Rolled back {version}")
394
+
395
+ except Exception as e:
396
+ self.db.rollback()
397
+ results.append({
398
+ 'version': version,
399
+ 'status': 'failed',
400
+ 'error': str(e)
401
+ })
402
+ click.echo(f" [FAILED] {e}")
403
+ break
404
+
405
+ self.db.close()
406
+ return results
407
+
408
+ def create(self, name: str) -> Path:
409
+ """Create a new migration file"""
410
+ self.migrations_dir.mkdir(parents=True, exist_ok=True)
411
+
412
+ # Get next version number
413
+ existing = sorted(self.migrations_dir.glob("V*.sql"))
414
+ if existing:
415
+ last_version = existing[-1].stem.split('_')[0]
416
+ next_num = int(last_version[1:]) + 1
417
+ else:
418
+ next_num = 1
419
+
420
+ version = f"V{next_num:03d}"
421
+ filename = f"{version}_{name}.sql"
422
+ filepath = self.migrations_dir / filename
423
+
424
+ # Create template
425
+ template = f"""-- Migration: {name}
426
+ -- Version: {version}
427
+ -- Created: {datetime.now().isoformat()}
428
+
429
+ -- Write your SQL here
430
+
431
+ """
432
+ filepath.write_text(template)
433
+
434
+ # Also create down file
435
+ down_filepath = self.migrations_dir / f"{version}_{name}.down.sql"
436
+ down_template = f"""-- Rollback Migration: {name}
437
+ -- Version: {version}
438
+
439
+ -- Write your rollback SQL here
440
+
441
+ """
442
+ down_filepath.write_text(down_template)
443
+
444
+ return filepath
445
+
446
+
447
+ # CLI Commands
448
+
449
+ @click.group()
450
+ def migrate():
451
+ """Database migration commands"""
452
+ pass
453
+
454
+
455
+ @migrate.command()
456
+ @click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
457
+ def status(as_json: bool):
458
+ """Show migration status"""
459
+ runner = MigrationRunner()
460
+
461
+ try:
462
+ result = runner.status()
463
+
464
+ if as_json:
465
+ import json
466
+ click.echo(json.dumps(result, indent=2, default=str))
467
+ else:
468
+ click.echo(f"\nDatabase: {result['database_type']}")
469
+ click.echo(f"Applied migrations: {result['total_applied']}")
470
+ click.echo(f"Pending migrations: {result['total_pending']}")
471
+
472
+ if result['applied']:
473
+ click.echo("\nApplied:")
474
+ for m in result['applied']:
475
+ click.echo(f" {m['version']}: {m['name']} (applied: {m['applied_at']})")
476
+
477
+ if result['pending']:
478
+ click.echo("\nPending:")
479
+ for m in result['pending']:
480
+ click.echo(f" {m['version']}: {m['name']}")
481
+
482
+ except Exception as e:
483
+ click.echo(f"Error: {e}", err=True)
484
+ sys.exit(1)
485
+
486
+
487
+ @migrate.command()
488
+ @click.option('--count', '-n', type=int, help='Number of migrations to apply')
489
+ def up(count: Optional[int]):
490
+ """Apply pending migrations"""
491
+ runner = MigrationRunner()
492
+
493
+ try:
494
+ results = runner.up(count=count)
495
+
496
+ applied = sum(1 for r in results if r.get('status') == 'applied')
497
+ failed = sum(1 for r in results if r.get('status') == 'failed')
498
+
499
+ click.echo(f"\nApplied: {applied}, Failed: {failed}")
500
+
501
+ if failed:
502
+ sys.exit(1)
503
+
504
+ except Exception as e:
505
+ click.echo(f"Error: {e}", err=True)
506
+ sys.exit(1)
507
+
508
+
509
+ @migrate.command()
510
+ @click.option('--count', '-n', type=int, default=1, help='Number of migrations to rollback')
511
+ def down(count: int):
512
+ """Rollback migrations"""
513
+ runner = MigrationRunner()
514
+
515
+ try:
516
+ results = runner.down(count=count)
517
+
518
+ rolled_back = sum(1 for r in results if r.get('status') == 'rolled_back')
519
+ failed = sum(1 for r in results if r.get('status') == 'failed')
520
+
521
+ click.echo(f"\nRolled back: {rolled_back}, Failed: {failed}")
522
+
523
+ if failed:
524
+ sys.exit(1)
525
+
526
+ except Exception as e:
527
+ click.echo(f"Error: {e}", err=True)
528
+ sys.exit(1)
529
+
530
+
531
+ @migrate.command()
532
+ @click.argument('name')
533
+ def create(name: str):
534
+ """Create a new migration"""
535
+ runner = MigrationRunner()
536
+
537
+ try:
538
+ filepath = runner.create(name)
539
+ click.echo(f"Created migration: {filepath}")
540
+ click.echo(f"Also created rollback: {filepath.with_suffix('.down.sql')}")
541
+
542
+ except Exception as e:
543
+ click.echo(f"Error: {e}", err=True)
544
+ sys.exit(1)
545
+
546
+
547
+ if __name__ == '__main__':
548
+ migrate()