quantum-framework 0.18.0__tar.gz → 0.19.0__tar.gz

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 (372) hide show
  1. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/PKG-INFO +1 -1
  2. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/pyproject.toml +1 -1
  3. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/runner.py +15 -1
  4. quantum_framework-0.19.0/quantum/cli/schema_plan.py +273 -0
  5. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/ast_nodes.py +19 -1
  6. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/parser.py +17 -1
  7. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/action_handler.py +3 -0
  8. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/database_service.py +12 -0
  9. quantum_framework-0.19.0/quantum/runtime/history.py +203 -0
  10. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_page_renderer.py +33 -2
  11. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_view.py +11 -1
  12. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/web_server.py +11 -5
  13. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/PKG-INFO +1 -1
  14. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/SOURCES.txt +2 -0
  15. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/LICENSE +0 -0
  16. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/README.md +0 -0
  17. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/__init__.py +0 -0
  18. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/__init__.py +0 -0
  19. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/admin.py +0 -0
  20. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/check.py +0 -0
  21. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/__init__.py +0 -0
  22. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/build.py +0 -0
  23. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/dev.py +0 -0
  24. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/docs.py +0 -0
  25. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/lint.py +0 -0
  26. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/migrate.py +0 -0
  27. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/new.py +0 -0
  28. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/serve.py +0 -0
  29. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/test.py +0 -0
  30. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/deploy.py +0 -0
  31. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/hot_reload.py +0 -0
  32. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/jobs.py +0 -0
  33. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/mq.py +0 -0
  34. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/pkg.py +0 -0
  35. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/server_process.py +0 -0
  36. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/utils.py +0 -0
  37. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/__init__.py +0 -0
  38. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/base_generator.py +0 -0
  39. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/cli.py +0 -0
  40. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/expression_transformer.py +0 -0
  41. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/javascript/__init__.py +0 -0
  42. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/javascript/generator.py +0 -0
  43. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/optimizer.py +0 -0
  44. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/python/__init__.py +0 -0
  45. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/python/generator.py +0 -0
  46. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/python/runtime.py +0 -0
  47. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/transpiler.py +0 -0
  48. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/__init__.py +0 -0
  49. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/ast_nodes.py +0 -0
  50. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/config_env.py +0 -0
  51. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/docs_links.py +0 -0
  52. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/expression_diagnostics.py +0 -0
  53. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/expression_stdlib.py +0 -0
  54. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/expressions.py +0 -0
  55. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/agents/src/__init__.py +0 -0
  56. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/agents/src/ast_node.py +0 -0
  57. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/conditionals/src/__init__.py +0 -0
  58. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/conditionals/src/ast_node.py +0 -0
  59. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/data_import/src/__init__.py +0 -0
  60. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/data_import/src/ast_node.py +0 -0
  61. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/data_import/src/runtime.py +0 -0
  62. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/__init__.py +0 -0
  63. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/ast_node.py +0 -0
  64. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/parser.py +0 -0
  65. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/runtime.py +0 -0
  66. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/functions/src/__init__.py +0 -0
  67. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/functions/src/ast_node.py +0 -0
  68. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/game_engine_2d/src/__init__.py +0 -0
  69. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/game_engine_2d/src/ast_nodes.py +0 -0
  70. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/game_engine_2d/src/parser.py +0 -0
  71. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/invocation/src/__init__.py +0 -0
  72. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/invocation/src/ast_node.py +0 -0
  73. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/invocation/src/runtime.py +0 -0
  74. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/knowledge_base/src/__init__.py +0 -0
  75. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/knowledge_base/src/ast_node.py +0 -0
  76. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/knowledge_base/src/parser.py +0 -0
  77. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/__init__.py +0 -0
  78. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/ast_node.py +0 -0
  79. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/parser.py +0 -0
  80. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/runtime.py +0 -0
  81. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/loops/src/__init__.py +0 -0
  82. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/loops/src/ast_node.py +0 -0
  83. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/query/src/__init__.py +0 -0
  84. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/query/src/query_validators.py +0 -0
  85. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/state_management/src/__init__.py +0 -0
  86. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/state_management/src/ast_node.py +0 -0
  87. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/terminal_engine/src/__init__.py +0 -0
  88. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/terminal_engine/src/ast_nodes.py +0 -0
  89. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/terminal_engine/src/parser.py +0 -0
  90. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/testing_engine/src/__init__.py +0 -0
  91. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/testing_engine/src/ast_nodes.py +0 -0
  92. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/testing_engine/src/parser.py +0 -0
  93. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/theming/src/__init__.py +0 -0
  94. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/theming/src/ast_node.py +0 -0
  95. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/theming/src/presets.py +0 -0
  96. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/__init__.py +0 -0
  97. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/websocket/src/__init__.py +0 -0
  98. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/websocket/src/ast_node.py +0 -0
  99. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/guards.py +0 -0
  100. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/html_compat.py +0 -0
  101. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parser.py +0 -0
  102. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parser_registry.py +0 -0
  103. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/__init__.py +0 -0
  104. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/__init__.py +0 -0
  105. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/agent_parser.py +0 -0
  106. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/knowledge_parser.py +0 -0
  107. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/llm_parser.py +0 -0
  108. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/team_parser.py +0 -0
  109. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/base.py +0 -0
  110. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/composition/__init__.py +0 -0
  111. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/composition/import_parser.py +0 -0
  112. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/composition/slot_parser.py +0 -0
  113. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/__init__.py +0 -0
  114. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/if_parser.py +0 -0
  115. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/loop_parser.py +0 -0
  116. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/set_parser.py +0 -0
  117. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/__init__.py +0 -0
  118. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/data_parser.py +0 -0
  119. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/invoke_parser.py +0 -0
  120. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/query_parser.py +0 -0
  121. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/transaction_parser.py +0 -0
  122. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/events/__init__.py +0 -0
  123. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/events/dispatch_event_parser.py +0 -0
  124. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/__init__.py +0 -0
  125. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/action_parser.py +0 -0
  126. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/flash_parser.py +0 -0
  127. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/redirect_parser.py +0 -0
  128. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/__init__.py +0 -0
  129. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/function_parser.py +0 -0
  130. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/param_parser.py +0 -0
  131. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/return_parser.py +0 -0
  132. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/html/__init__.py +0 -0
  133. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/html/component_call_parser.py +0 -0
  134. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/html/html_parser.py +0 -0
  135. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/__init__.py +0 -0
  136. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/job_parser.py +0 -0
  137. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/schedule_parser.py +0 -0
  138. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/thread_parser.py +0 -0
  139. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/__init__.py +0 -0
  140. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/message_ack_parser.py +0 -0
  141. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/message_nack_parser.py +0 -0
  142. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/message_parser.py +0 -0
  143. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/queue_parser.py +0 -0
  144. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/websocket_parser.py +0 -0
  145. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/routing/__init__.py +0 -0
  146. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/routing/route_parser.py +0 -0
  147. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/__init__.py +0 -0
  148. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/pyclass_parser.py +0 -0
  149. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/pydecorator_parser.py +0 -0
  150. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/pyimport_parser.py +0 -0
  151. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/python_parser.py +0 -0
  152. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/__init__.py +0 -0
  153. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/dump_parser.py +0 -0
  154. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/file_parser.py +0 -0
  155. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/log_parser.py +0 -0
  156. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/mail_parser.py +0 -0
  157. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/tiers.py +0 -0
  158. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/xml_lines.py +0 -0
  159. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/__init__.py +0 -0
  160. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/manager.py +0 -0
  161. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/manifest.py +0 -0
  162. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/registry.py +0 -0
  163. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/resolver.py +0 -0
  164. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/__init__.py +0 -0
  165. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/hooks.py +0 -0
  166. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/loader.py +0 -0
  167. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/manifest.py +0 -0
  168. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/registry.py +0 -0
  169. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/__init__.py +0 -0
  170. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/__init__.py +0 -0
  171. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/memory_adapter.py +0 -0
  172. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/rabbitmq_adapter.py +0 -0
  173. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/redis_adapter.py +0 -0
  174. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/sqlite_adapter.py +0 -0
  175. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/agent_service.py +0 -0
  176. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ast_cache.py +0 -0
  177. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/auth_service.py +0 -0
  178. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/component.py +0 -0
  179. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/component_composer.py +0 -0
  180. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/component_resolver.py +0 -0
  181. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/conditions.py +0 -0
  182. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/db_schema.py +0 -0
  183. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/dev_panel.py +0 -0
  184. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/email_service.py +0 -0
  185. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/error_handler.py +0 -0
  186. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/execution_context.py +0 -0
  187. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executor_registry.py +0 -0
  188. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/__init__.py +0 -0
  189. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/__init__.py +0 -0
  190. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/agent_executor.py +0 -0
  191. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/knowledge_executor.py +0 -0
  192. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/llm_executor.py +0 -0
  193. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/team_executor.py +0 -0
  194. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/base.py +0 -0
  195. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/__init__.py +0 -0
  196. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/if_executor.py +0 -0
  197. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/loop_executor.py +0 -0
  198. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/redirect_executor.py +0 -0
  199. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/set_executor.py +0 -0
  200. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/__init__.py +0 -0
  201. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/data_executor.py +0 -0
  202. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/invoke_executor.py +0 -0
  203. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/query_executor.py +0 -0
  204. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/transaction_executor.py +0 -0
  205. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/__init__.py +0 -0
  206. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/job_executor.py +0 -0
  207. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/schedule_executor.py +0 -0
  208. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/thread_executor.py +0 -0
  209. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/__init__.py +0 -0
  210. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/message_ack_executor.py +0 -0
  211. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/message_executor.py +0 -0
  212. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/queue_executor.py +0 -0
  213. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/websocket_executor.py +0 -0
  214. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/__init__.py +0 -0
  215. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/pyclass_executor.py +0 -0
  216. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/pyimport_executor.py +0 -0
  217. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/python_executor.py +0 -0
  218. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/__init__.py +0 -0
  219. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/dump_executor.py +0 -0
  220. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/file_executor.py +0 -0
  221. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/log_executor.py +0 -0
  222. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/mail_executor.py +0 -0
  223. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/expression_cache.py +0 -0
  224. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/file_upload_service.py +0 -0
  225. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/function_registry.py +0 -0
  226. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/game_builder.py +0 -0
  227. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/game_code_generator.py +0 -0
  228. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/game_templates.py +0 -0
  229. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/godot_code_generator.py +0 -0
  230. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/godot_templates.py +0 -0
  231. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/job_executor.py +0 -0
  232. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/knowledge_service.py +0 -0
  233. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/llm_cache.py +0 -0
  234. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/llm_providers.py +0 -0
  235. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/llm_service.py +0 -0
  236. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/local_server.py +0 -0
  237. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/logging_setup.py +0 -0
  238. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/message_broker.py +0 -0
  239. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/message_queue_service.py +0 -0
  240. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/param_validation.py +0 -0
  241. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/pypy_compat.py +0 -0
  242. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/python_bridge.py +0 -0
  243. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/query_validators.py +0 -0
  244. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/renderer.py +0 -0
  245. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/service_container.py +0 -0
  246. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/table_params.py +0 -0
  247. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/terminal_builder.py +0 -0
  248. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/terminal_code_generator.py +0 -0
  249. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/terminal_templates.py +0 -0
  250. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/testing_builder.py +0 -0
  251. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/testing_code_generator.py +0 -0
  252. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/testing_templates.py +0 -0
  253. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_builder.py +0 -0
  254. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_console.py +0 -0
  255. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_desktop.py +0 -0
  256. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_form_rules.py +0 -0
  257. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_html_adapter.py +0 -0
  258. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_html_templates.py +0 -0
  259. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_mobile_adapter.py +0 -0
  260. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_mobile_templates.py +0 -0
  261. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_pager.py +0 -0
  262. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_table_edit.py +0 -0
  263. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_textual_adapter.py +0 -0
  264. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_textual_templates.py +0 -0
  265. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_tokens.py +0 -0
  266. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_validator.py +0 -0
  267. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/validators.py +0 -0
  268. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/websocket_adapter.py +0 -0
  269. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/websocket_service.py +0 -0
  270. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/websocket_transport.py +0 -0
  271. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/wsgi.py +0 -0
  272. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/services.py +0 -0
  273. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/utils/__init__.py +0 -0
  274. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/__init__.py +0 -0
  275. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/assets/quantum-admin-connectors.js +0 -0
  276. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/assets/quantum-admin.css +0 -0
  277. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/assets/quantum-source-highlight.js +0 -0
  278. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/__init__.py +0 -0
  279. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/audit_service.py +0 -0
  280. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/auth_service.py +0 -0
  281. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/component_discovery.py +0 -0
  282. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/config_generator.py +0 -0
  283. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/connector_service.py +0 -0
  284. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/crud.py +0 -0
  285. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/database.py +0 -0
  286. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/db_setup_service.py +0 -0
  287. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/deploy_service.py +0 -0
  288. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/docker_service.py +0 -0
  289. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/environment_service.py +0 -0
  290. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/git_service.py +0 -0
  291. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/health_service.py +0 -0
  292. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/job_service.py +0 -0
  293. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/log_service.py +0 -0
  294. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/main.py +0 -0
  295. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/models.py +0 -0
  296. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/pipeline_service.py +0 -0
  297. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/resource_manager.py +0 -0
  298. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/schemas.py +0 -0
  299. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/secret_manager.py +0 -0
  300. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/settings_service.py +0 -0
  301. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/template_service.py +0 -0
  302. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/test_execution_service.py +0 -0
  303. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/test_generator.py +0 -0
  304. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/webhook_service.py +0 -0
  305. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/websocket_manager.py +0 -0
  306. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/_layout/AdminShell.q +0 -0
  307. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/agents.q +0 -0
  308. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/app/[name].q +0 -0
  309. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/applications.q +0 -0
  310. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/component/[...path].q +0 -0
  311. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/components.q +0 -0
  312. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/connectors.q +0 -0
  313. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/dashboard.q +0 -0
  314. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/database.q +0 -0
  315. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/features.q +0 -0
  316. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/index.q +0 -0
  317. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/jobs.q +0 -0
  318. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/login.q +0 -0
  319. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/logout.q +0 -0
  320. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/settings.q +0 -0
  321. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/source.q +0 -0
  322. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/tests.q +0 -0
  323. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/run.py +0 -0
  324. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/__init__.py +0 -0
  325. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/_base.py +0 -0
  326. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/_component_test_generator.py +0 -0
  327. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/_yaml_editor.py +0 -0
  328. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/apps.py +0 -0
  329. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/auth.py +0 -0
  330. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/components.py +0 -0
  331. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/connectors.py +0 -0
  332. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/projects.py +0 -0
  333. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/repository.py +0 -0
  334. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/settings.py +0 -0
  335. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/yaml_import.py +0 -0
  336. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/dependency_links.txt +0 -0
  337. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/entry_points.txt +0 -0
  338. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/requires.txt +0 -0
  339. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/top_level.txt +0 -0
  340. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/setup.cfg +0 -0
  341. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_agent.py +0 -0
  342. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_animations.py +0 -0
  343. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ast_cache.py +0 -0
  344. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_cli_commands.py +0 -0
  345. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_component_library.py +0 -0
  346. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_expression_cache.py +0 -0
  347. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_form_validation.py +0 -0
  348. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_codegen.py +0 -0
  349. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_e2e.py +0 -0
  350. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_engine.py +0 -0
  351. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_parser.py +0 -0
  352. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_godot_codegen.py +0 -0
  353. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_job_executor.py +0 -0
  354. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_llm_providers.py +0 -0
  355. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_message_broker.py +0 -0
  356. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_message_queue.py +0 -0
  357. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_mobile_target.py +0 -0
  358. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_multi_agent.py +0 -0
  359. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_packages.py +0 -0
  360. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_plugins.py +0 -0
  361. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_pypy_compat.py +0 -0
  362. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_python_scripting.py +0 -0
  363. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_terminal_codegen.py +0 -0
  364. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_terminal_parser.py +0 -0
  365. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_testing_codegen.py +0 -0
  366. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_testing_parser.py +0 -0
  367. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_theming.py +0 -0
  368. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_html_adapter.py +0 -0
  369. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_parity.py +0 -0
  370. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_parser.py +0 -0
  371. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_textual_adapter.py +0 -0
  372. {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantum-framework
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: Full-stack declarative framework for building web, desktop, and mobile applications
5
5
  Author-email: Daniel Gregorio <danielgregorio2611@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quantum-framework"
7
- version = "0.18.0"
7
+ version = "0.19.0"
8
8
  description = "Full-stack declarative framework for building web, desktop, and mobile applications"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -399,6 +399,15 @@ Examples:
399
399
  migrate_create_parser = migrate_subparsers.add_parser('create', help='Create a new migration')
400
400
  migrate_create_parser.add_argument('name', help='Migration name')
401
401
 
402
+ # M6 (DB-10): schema.sql as the source of truth
403
+ migrate_plan_parser = migrate_subparsers.add_parser(
404
+ 'plan', help='Compare schema.sql with the migrations and plan the migration')
405
+ migrate_plan_parser.add_argument('--schema', default='schema.sql', help='Schema file (default: schema.sql)')
406
+ migrate_plan_parser.add_argument('--write', metavar='NAME', help='Save the plan as migration V00N_NAME')
407
+ migrate_plan_parser.add_argument('--yes', action='store_true', help='Do not ask before writing')
408
+ migrate_plan_parser.add_argument('--allow-data-loss', action='store_true',
409
+ help='Write a plan that drops tables or columns')
410
+
402
411
  # Parse arguments
403
412
  args = parser.parse_args()
404
413
 
@@ -541,13 +550,18 @@ Examples:
541
550
  if failed:
542
551
  sys.exit(1)
543
552
 
553
+ elif args.migrate_command == 'plan':
554
+ from quantum.cli.schema_plan import run_plan
555
+ sys.exit(run_plan(Path.cwd(), args.schema, args.write, args.yes, args.allow_data_loss,
556
+ interactive=sys.stdin.isatty()))
557
+
544
558
  elif args.migrate_command == 'create':
545
559
  filepath = runner.create(args.name)
546
560
  print(f"Created migration: {filepath}")
547
561
  print(f"Also created rollback: {filepath.with_suffix('.down.sql')}")
548
562
 
549
563
  else:
550
- print("Usage: quantum migrate <status|up|down|create> [options]")
564
+ print("Usage: quantum migrate <status|up|down|create|plan> [options]")
551
565
  sys.exit(1)
552
566
 
553
567
  sys.exit(0)
@@ -0,0 +1,273 @@
1
+ """`quantum migrate plan`: a declarative schema (M6, DB-10).
2
+
3
+ You edit `schema.sql` — the tables as they should be. `quantum migrate plan`
4
+ compares it with the schema the migrations produce, shows what changes, marks
5
+ what loses data, and `--write` saves the migration (and its rollback) after
6
+ confirmation. `quantum migrate up` applies it as any other.
7
+
8
+ Both sides are built in in-memory SQLite databases — the migrations applied in
9
+ order, and schema.sql — and compared with the schema reader of `quantum check`
10
+ (quantum/runtime/db_schema.py). The development database is not read: the
11
+ plan is the same on any machine and in CI.
12
+
13
+ SQLite can add a column but not change one, so any other change to a table
14
+ rebuilds it: create the new table, copy the columns both have, drop the old,
15
+ rename. Before a plan is written it is checked: applied to the migrations'
16
+ schema it must produce exactly schema.sql's.
17
+ """
18
+
19
+ import re
20
+ import sqlite3
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional, Tuple
24
+
25
+ from quantum.runtime.db_schema import Column, read_schema
26
+
27
+ # Tables that belong to Quantum, not to the app's schema.
28
+ INTERNAL_TABLES = {'_migrations', 'quantum_history'}
29
+
30
+
31
+ class SchemaPlanError(Exception):
32
+ """The plan cannot be made (no schema.sql, invalid SQL, a change it cannot express)."""
33
+
34
+
35
+ @dataclass
36
+ class Step:
37
+ kind: str # add-table, drop-table, add-column, rebuild, add-index, drop-index
38
+ target: str # "posts" or "posts.summary"
39
+ detail: str = ''
40
+ sql: List[str] = field(default_factory=list)
41
+ loses_data: bool = False
42
+ warning: str = ''
43
+
44
+ def line(self) -> str:
45
+ mark = {'add-table': '+', 'add-column': '+', 'add-index': '+', 'drop-table': '-',
46
+ 'drop-index': '-', 'rebuild': '~'}.get(self.kind, '?')
47
+ text = f'{mark} {self.kind.replace("-", " ")} {self.target}'
48
+ if self.detail:
49
+ text += f' ({self.detail})'
50
+ if self.loses_data:
51
+ text += ' !! loses data'
52
+ if self.warning:
53
+ text += f' !! {self.warning}'
54
+ return text
55
+
56
+
57
+ def _database(scripts: List[Tuple[str, str]]) -> sqlite3.Connection:
58
+ conn = sqlite3.connect(':memory:')
59
+ for origin, sql in scripts:
60
+ try:
61
+ conn.executescript(sql)
62
+ except sqlite3.Error as exc:
63
+ raise SchemaPlanError(f'{origin}: {exc}') from exc
64
+ return conn
65
+
66
+
67
+ def _migration_scripts(migrations_dir: Path) -> List[Tuple[str, str]]:
68
+ if not migrations_dir.exists():
69
+ return []
70
+ files = [f for f in sorted(migrations_dir.glob('*.sql')) if not f.name.endswith('.down.sql')]
71
+ return [(f.name, f.read_text(encoding='utf-8')) for f in files if '_' in f.stem]
72
+
73
+
74
+ def _tables(conn) -> Dict[str, object]:
75
+ return {name: t for name, t in read_schema(conn).items() if name not in INTERNAL_TABLES}
76
+
77
+
78
+ def _create_sql(conn, table: str) -> str:
79
+ return conn.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
80
+ (table,)).fetchone()[0]
81
+
82
+
83
+ def _indexes(conn) -> Dict[str, Tuple[str, str]]:
84
+ """Explicit indexes: name -> (table, sql)."""
85
+ return {name: (table, sql) for name, table, sql in conn.execute(
86
+ "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL")
87
+ if table not in INTERNAL_TABLES}
88
+
89
+
90
+ def _column_sql(column: Column) -> str:
91
+ parts = [f'"{column.name}"']
92
+ if column.type:
93
+ parts.append(column.type)
94
+ if column.not_null:
95
+ parts.append('NOT NULL')
96
+ if column.default is not None:
97
+ parts.append(f'DEFAULT {column.default}')
98
+ if column.options:
99
+ options = ', '.join("'" + o.replace("'", "''") + "'" for o in column.options)
100
+ parts.append(f'CHECK ("{column.name}" IN ({options}))')
101
+ if column.references:
102
+ parts.append(f'REFERENCES "{column.references[0]}"("{column.references[1]}")')
103
+ return ' '.join(parts)
104
+
105
+
106
+ def _differences(a: Column, b: Column) -> List[str]:
107
+ changed = []
108
+ for label, x, y in (('type', (a.type or '').upper(), (b.type or '').upper()),
109
+ ('NOT NULL', a.not_null, b.not_null), ('default', a.default, b.default),
110
+ ('primary key', a.primary_key, b.primary_key), ('CHECK', a.options, b.options),
111
+ ('references', a.references, b.references)):
112
+ if x != y:
113
+ changed.append(label)
114
+ return changed
115
+
116
+
117
+ def diff(current_db, desired_db) -> List[Step]:
118
+ """The steps that turn the first database's schema into the second's."""
119
+ current, desired = _tables(current_db), _tables(desired_db)
120
+ steps: List[Step] = []
121
+ rebuilt = set()
122
+
123
+ for name in sorted(set(desired) - set(current)):
124
+ steps.append(Step('add-table', name, sql=[_create_sql(desired_db, name) + ';']))
125
+
126
+ for name in sorted(set(current) & set(desired)):
127
+ before, after = current[name].columns, desired[name].columns
128
+ added = [c for c in after if c not in before]
129
+ removed = [c for c in before if c not in after]
130
+ changed = {c: _differences(before[c], after[c]) for c in before
131
+ if c in after and _differences(before[c], after[c])}
132
+ addable = all(not after[c].primary_key and (not after[c].not_null or after[c].default is not None)
133
+ for c in added)
134
+ if not removed and not changed and addable:
135
+ for c in added:
136
+ steps.append(Step('add-column', f'{name}.{c}', after[c].type or '',
137
+ [f'ALTER TABLE "{name}" ADD COLUMN {_column_sql(after[c])};']))
138
+ continue
139
+ if not (added or removed or changed):
140
+ continue
141
+ common = ', '.join(f'"{c}"' for c in after if c in before)
142
+ create = re.sub(r'^\s*CREATE\s+TABLE\s+("?)' + re.escape(name) + r'\1',
143
+ f'CREATE TABLE "{name}__new"', _create_sql(desired_db, name), count=1, flags=re.I)
144
+ details = [f'+ {c}' for c in added] + [f'- {c}' for c in removed] + \
145
+ [f'{c}: {", ".join(d)}' for c, d in changed.items()]
146
+ warning = ''
147
+ if any('NOT NULL' in d and after[c].not_null for c, d in changed.items()) or \
148
+ any(after[c].not_null and after[c].default is None for c in added):
149
+ warning = 'fails if existing rows have no value for a NOT NULL column'
150
+ steps.append(Step('rebuild', name, '; '.join(details), [
151
+ create + ';',
152
+ f'INSERT INTO "{name}__new" ({common}) SELECT {common} FROM "{name}";',
153
+ f'DROP TABLE "{name}";',
154
+ f'ALTER TABLE "{name}__new" RENAME TO "{name}";',
155
+ ], loses_data=bool(removed) or any('type' in d for d in changed.values()), warning=warning))
156
+ rebuilt.add(name)
157
+
158
+ for name in sorted(set(current) - set(desired)):
159
+ steps.append(Step('drop-table', name, sql=[f'DROP TABLE "{name}";'], loses_data=True))
160
+
161
+ # Indexes: a rebuilt table lost its own, so they are created again.
162
+ before_idx, after_idx = _indexes(current_db), _indexes(desired_db)
163
+ for name, (table, sql) in sorted(before_idx.items()):
164
+ if table in desired and table not in rebuilt and after_idx.get(name, (None, None))[1] != sql:
165
+ steps.append(Step('drop-index', name, sql=[f'DROP INDEX "{name}";']))
166
+ for name, (table, sql) in sorted(after_idx.items()):
167
+ if before_idx.get(name, (None, None))[1] != sql or table in rebuilt:
168
+ steps.append(Step('add-index', name, sql=[sql + ';']))
169
+ return steps
170
+
171
+
172
+ def _schema_only(conn) -> str:
173
+ """The CREATE statements of a database, tables before indexes, without its rows."""
174
+ rows = conn.execute("SELECT type, sql FROM sqlite_master WHERE sql IS NOT NULL "
175
+ "AND name NOT LIKE 'sqlite_%' ORDER BY type = 'index', rowid").fetchall()
176
+ return '\n'.join(sql + ';' for _type, sql in rows)
177
+
178
+
179
+ def _signature(conn) -> tuple:
180
+ tables = _tables(conn)
181
+ return (tuple(sorted((n, tuple(sorted((c.name, (c.type or '').upper(), c.not_null, c.default, c.primary_key,
182
+ tuple(c.options or ()), c.references)
183
+ for c in t.columns.values()))) for n, t in tables.items())),
184
+ tuple(sorted((n, s) for n, (_t, s) in _indexes(conn).items())))
185
+
186
+
187
+ @dataclass
188
+ class Plan:
189
+ steps: List[Step]
190
+ up_sql: str
191
+ down_sql: str
192
+
193
+ @property
194
+ def loses_data(self) -> bool:
195
+ return any(s.loses_data for s in self.steps)
196
+
197
+
198
+ def make_plan(schema_file: Path, migrations_dir: Path) -> Plan:
199
+ if not schema_file.exists():
200
+ raise SchemaPlanError(f'{schema_file} does not exist: write the tables as they should be, '
201
+ f'e.g. CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL);')
202
+ migrations = _migration_scripts(migrations_dir)
203
+ current = _database(migrations)
204
+ desired = _database([(schema_file.name, schema_file.read_text(encoding='utf-8'))])
205
+ steps = diff(current, desired)
206
+ up = '\n'.join(s for step in steps for s in step.sql)
207
+ down = '\n'.join(s for step in diff(desired, current) for s in step.sql)
208
+ if steps:
209
+ # The plan must do what it says: applied, it yields schema.sql's schema.
210
+ # Checked on the structure only — what existing rows may break is the
211
+ # step's warning, shown in the plan, not a reason to hide the plan.
212
+ check = _database([('migrations (schema only)', _schema_only(current)), ('plan', up)])
213
+ if _signature(check) != _signature(desired):
214
+ raise SchemaPlanError('the generated migration does not reproduce schema.sql — '
215
+ 'write this change as a migration by hand (quantum migrate create)')
216
+ return Plan(steps, up, down)
217
+
218
+
219
+ def next_version(migrations_dir: Path) -> str:
220
+ numbers = [int(m.group(1)) for f in migrations_dir.glob('V*.sql')
221
+ for m in [re.match(r'V(\d+)_', f.name)] if m]
222
+ return f'V{(max(numbers) + 1 if numbers else 1):03d}'
223
+
224
+
225
+ def write_plan(plan: Plan, migrations_dir: Path, name: str) -> Path:
226
+ migrations_dir.mkdir(parents=True, exist_ok=True)
227
+ slug = re.sub(r'[^a-z0-9_]+', '_', name.lower()).strip('_') or 'schema'
228
+ version = next_version(migrations_dir)
229
+ path = migrations_dir / f'{version}_{slug}.sql'
230
+ header = '-- Generated by `quantum migrate plan` from schema.sql (DB-10)\n'
231
+ summary = '\n'.join(f'-- {s.line()}' for s in plan.steps)
232
+ path.write_text(f'{header}{summary}\n\n{plan.up_sql}\n', encoding='utf-8')
233
+ (migrations_dir / f'{version}_{slug}.down.sql').write_text(
234
+ f'{header}-- rollback\n\n{plan.down_sql}\n', encoding='utf-8')
235
+ return path
236
+
237
+
238
+ def run_plan(project: Path, schema: Optional[str], write: Optional[str], yes: bool,
239
+ allow_data_loss: bool, ask=input, interactive: bool = True) -> int:
240
+ """The `quantum migrate plan` command. Returns the exit code."""
241
+ from quantum.cli.commands.migrate import _paths_migrations
242
+ migrations_dir = project / _paths_migrations(project)
243
+ schema_file = project / (schema or 'schema.sql')
244
+ try:
245
+ plan = make_plan(schema_file, migrations_dir)
246
+ except SchemaPlanError as exc:
247
+ print(f'[ERROR] {exc}')
248
+ return 1
249
+ if not plan.steps:
250
+ print(f'Nothing to do: the migrations already produce {schema_file.name}.')
251
+ return 0
252
+ print(f'Plan: {schema_file.name} vs. the migrations in {migrations_dir.name}/\n')
253
+ for step in plan.steps:
254
+ print(' ' + step.line())
255
+ print('\nSQL:\n' + plan.up_sql)
256
+ if not write:
257
+ print('\nNothing written. `quantum migrate plan --write <name>` saves it as a migration.')
258
+ return 0
259
+ if plan.loses_data and not allow_data_loss:
260
+ print('\n[REFUSED] This plan loses data (marked !!). Check it, then run again with '
261
+ '--allow-data-loss.')
262
+ return 1
263
+ if not yes:
264
+ if not interactive:
265
+ print('\n[REFUSED] Confirm with --yes (not a terminal: nobody to ask).')
266
+ return 1
267
+ answer = ask(f'\nWrite migration {next_version(migrations_dir)}_{write}? [y/N] ').strip().lower()
268
+ if answer not in ('y', 'yes'):
269
+ print('Nothing written.')
270
+ return 1
271
+ path = write_plan(plan, migrations_dir, write)
272
+ print(f'Written: {path} (and {path.stem}.down.sql). Apply with `quantum migrate up`.')
273
+ return 0
@@ -1896,6 +1896,24 @@ class UIPagerNode(QuantumNode, UILayoutMixin):
1896
1896
  return [] if self.for_query else ["ui:pager needs for="]
1897
1897
 
1898
1898
 
1899
+ class UIHistoryNode(QuantumNode, UILayoutMixin):
1900
+ """<ui:history table="t" key="{row.id}" datasource="db"> — a row's change history (DB-11)."""
1901
+
1902
+ def __init__(self):
1903
+ self.table: Optional[str] = None
1904
+ self.key: Optional[str] = None
1905
+ self.datasource: Optional[str] = None
1906
+ self.limit: int = 50
1907
+ self.children: List[QuantumNode] = []
1908
+ self._init_layout_attrs()
1909
+
1910
+ def to_dict(self) -> Dict[str, Any]:
1911
+ return {"type": "ui_history", "table": self.table, "key": self.key}
1912
+
1913
+ def validate(self) -> List[str]:
1914
+ return []
1915
+
1916
+
1899
1917
  def is_ui_node(node) -> bool:
1900
1918
  """A ui:* element (UI-1): markup, drawn by a renderer, never executed."""
1901
1919
  return type(node).__module__ == __name__
@@ -1908,6 +1926,6 @@ CORE_TAGS = frozenset({
1908
1926
  'window', 'hbox', 'vbox', 'grid', 'panel', 'section', 'scrollbox', 'spacer', 'rule',
1909
1927
  'header', 'footer', 'card', 'card-header', 'card-body', 'card-footer', 'tabpanel', 'tab',
1910
1928
  'text', 'badge', 'alert', 'link', 'image', 'progress',
1911
- 'table', 'column', 'list', 'item', 'pager',
1929
+ 'table', 'column', 'list', 'item', 'pager', 'history',
1912
1930
  'form', 'formitem', 'input', 'checkbox', 'switch', 'radio', 'select', 'option', 'button',
1913
1931
  })
@@ -28,7 +28,7 @@ from .ast_nodes import (
28
28
  UIToastNode, UIToastContainerNode,
29
29
  UICarouselNode, UISlideNode,
30
30
  UIStepperNode, UIStepNode,
31
- UICalendarNode, UIDatePickerNode, UIPagerNode,
31
+ UICalendarNode, UIDatePickerNode, UIPagerNode, UIHistoryNode,
32
32
  )
33
33
  from quantum.core.features.theming.src import UIThemeNode, UIColorNode
34
34
  from quantum.core.ast_nodes import TextNode
@@ -82,6 +82,7 @@ class UIParser:
82
82
  'table': '_parse_ui_table',
83
83
  'column': '_parse_ui_column',
84
84
  'pager': '_parse_ui_pager',
85
+ 'history': '_parse_ui_history',
85
86
  'list': '_parse_ui_list',
86
87
  'item': '_parse_ui_item',
87
88
  'image': '_parse_ui_image',
@@ -550,6 +551,21 @@ class UIParser:
550
551
  self._apply_layout_attrs(node, element)
551
552
  return node
552
553
 
554
+ def _parse_ui_history(self, element: ET.Element) -> UIHistoryNode:
555
+ node = UIHistoryNode()
556
+ node.table = element.get('table')
557
+ node.key = element.get('key')
558
+ node.datasource = element.get('datasource')
559
+ missing = [a for a in ('table', 'key', 'datasource') if not element.get(a)]
560
+ if missing:
561
+ raise UIParseError(f'<ui:history> needs {", ".join(a + "=" for a in missing)}')
562
+ limit = element.get('limit')
563
+ if limit is not None and not limit.isdigit():
564
+ raise UIParseError(f'<ui:history limit="{limit}">: a number of entries')
565
+ node.limit = int(limit) if limit is not None else 50
566
+ self._apply_layout_attrs(node, element)
567
+ return node
568
+
553
569
  def _parse_ui_list(self, element: ET.Element) -> UIListNode:
554
570
  node = UIListNode()
555
571
  node.source = element.get('source')
@@ -122,9 +122,12 @@ class ActionHandler:
122
122
  context.set_variable(key, value)
123
123
 
124
124
  # 4. Execute action body
125
+ from quantum.runtime import history
126
+ history_token = history.enter(getattr(action, 'name', None), context.session_vars) # M22
125
127
  try:
126
128
  redirect_info = self._execute_action_body(action, context)
127
129
  finally:
130
+ history.leave(history_token)
128
131
  # M12: the /_dev panel shows the action's variables (no-op off debug).
129
132
  from quantum.runtime.dev_panel import note_scopes
130
133
  note_scopes(context, 'action')
@@ -86,6 +86,7 @@ class DatabaseService:
86
86
  'port': local_cfg.get('port', 5432),
87
87
  'username': local_cfg.get('username', ''),
88
88
  'password': local_cfg.get('password', ''),
89
+ 'history': bool(local_cfg.get('history')), # M22 (DB-11)
89
90
  }
90
91
 
91
92
  # DB-7: a datasource is declared in quantum.config.yaml, or it does not
@@ -277,6 +278,13 @@ class DatabaseService:
277
278
  # Convert :param syntax to database-specific placeholder
278
279
  prepared_sql, prepared_params = self._prepare_query(sql, params, db_type)
279
280
 
281
+ # M22 (DB-11): with history on, an action's write records the rows it touches.
282
+ capture = None
283
+ if config.get('history') and db_type == 'sqlite':
284
+ from quantum.runtime import history
285
+ capture = history.begin(conn, sql, params,
286
+ lambda q, p: self._prepare_query(q, p, db_type))
287
+
280
288
  # Execute query
281
289
  cursor.execute(prepared_sql, prepared_params)
282
290
 
@@ -309,6 +317,10 @@ class DatabaseService:
309
317
  # Non-SELECT query (INSERT, UPDATE, DELETE). Commit now ONLY
310
318
  # when standalone; inside a transaction the commit is deferred
311
319
  # to commit_transaction so a later failure can roll it back.
320
+ if capture is not None:
321
+ # Before the commit: the write and its history go together.
322
+ from quantum.runtime import history
323
+ history.finish(conn, capture, getattr(cursor, 'lastrowid', None))
312
324
  if not in_transaction:
313
325
  conn.commit()
314
326
  data = []
@@ -0,0 +1,203 @@
1
+ """Change history, per datasource (M22, DB-11).
2
+
3
+ With `history: true` on a datasource, every write an action makes to it —
4
+ INSERT, UPDATE, DELETE, including the cells of an editable table (UI-13) — is
5
+ recorded in a `quantum_history` table of that same database: when, who (the
6
+ session's user), which action, which row, and the row before and after, as
7
+ JSON. The record is written on the same connection, before the commit, so a
8
+ write and its history commit or roll back together.
9
+
10
+ Only writes made by actions are recorded: page statements do not write in a
11
+ well-formed app, and migrations are not changes to records. A write whose
12
+ shape is not a single-table INSERT/UPDATE/DELETE is still recorded, with its
13
+ SQL and no before/after — never skipped silently.
14
+
15
+ `<ui:history table="posts" key="{post.id}" datasource="db">` shows a row's
16
+ history on any page.
17
+ """
18
+
19
+ import contextvars
20
+ import json
21
+ import re
22
+ from datetime import datetime
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ TABLE = 'quantum_history'
26
+
27
+ CREATE_SQL = (f'CREATE TABLE IF NOT EXISTS {TABLE} ('
28
+ 'id INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, user TEXT, action TEXT, '
29
+ 'table_name TEXT NOT NULL, row_key TEXT, op TEXT NOT NULL, before TEXT, after TEXT, '
30
+ 'sql TEXT)')
31
+
32
+ _ACTION = contextvars.ContextVar('quantum_history_action', default=None)
33
+
34
+ _WRITE = re.compile(r'^\s*(INSERT\s+(?:OR\s+\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+"?(\w+)"?', re.I)
35
+ _WHERE = re.compile(r'\bWHERE\b(.*)$', re.I | re.S)
36
+
37
+
38
+ def enter(action: str, session_vars: Optional[dict]):
39
+ """Mark the writes that follow as made by this action, for this user."""
40
+ session = session_vars or {}
41
+ user = session.get('userName') or session.get('userId')
42
+ return _ACTION.set({'action': action, 'user': None if user is None else str(user)})
43
+
44
+
45
+ def leave(token) -> None:
46
+ _ACTION.reset(token)
47
+
48
+
49
+ def current() -> Optional[dict]:
50
+ return _ACTION.get()
51
+
52
+
53
+ def parse_write(sql: str):
54
+ """(op, table, where) of a single-table write, or None when it is not one."""
55
+ m = _WRITE.match(sql)
56
+ if not m:
57
+ return None
58
+ op = {'INSERT': 'insert', 'UPDATE': 'update', 'DELETE': 'delete'}[m.group(1).split()[0].upper()]
59
+ where = _WHERE.search(sql)
60
+ return op, m.group(2), (where.group(1).strip().rstrip(';') if where else None)
61
+
62
+
63
+ class Capture:
64
+ """One write being recorded: the rows before, then the rows after."""
65
+
66
+ def __init__(self, op: str, table: str, sql: str, key: Optional[str], before: List[dict], shaped: bool):
67
+ self.op, self.table, self.sql, self.key = op, table, sql, key
68
+ self.before, self.shaped = before, shaped
69
+
70
+
71
+ def _rows(cursor) -> List[dict]:
72
+ columns = [d[0] for d in cursor.description or []]
73
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
74
+
75
+
76
+ def _primary_key(conn, table: str) -> Optional[str]:
77
+ keys = [row[1] for row in conn.execute(f'PRAGMA table_info("{table}")') if row[5]]
78
+ return keys[0] if len(keys) == 1 else None
79
+
80
+
81
+ def begin(conn, sql: str, params, prepare) -> Optional[Capture]:
82
+ """Before a write: which rows it will touch, as they are. None when nothing is recorded."""
83
+ state = current()
84
+ if state is None:
85
+ return None
86
+ shape = parse_write(sql)
87
+ if shape is None:
88
+ table = re.search(r'\b(?:INTO|UPDATE|FROM)\s+"?(\w+)"?', sql, re.I)
89
+ if not table or table.group(1) == TABLE:
90
+ return None
91
+ return Capture('write', table.group(1), sql, None, [], shaped=False)
92
+ op, table, where = shape
93
+ if table == TABLE:
94
+ return None
95
+ key = _primary_key(conn, table)
96
+ before: List[dict] = []
97
+ if op in ('update', 'delete'):
98
+ query, args = prepare(f'SELECT * FROM "{table}"' + (f' WHERE {where}' if where else ''), params)
99
+ cursor = conn.cursor()
100
+ cursor.execute(query, args)
101
+ before = _rows(cursor)
102
+ cursor.close()
103
+ return Capture(op, table, sql, key, before, shaped=True)
104
+
105
+
106
+ def finish(conn, capture: Optional[Capture], last_row_id: Optional[int]) -> None:
107
+ """After the write, before the commit: record each row it changed."""
108
+ if capture is None:
109
+ return
110
+ state = current() or {}
111
+ conn.execute(CREATE_SQL)
112
+ records = []
113
+ if not capture.shaped:
114
+ records.append((None, None, None))
115
+ elif capture.op == 'insert':
116
+ row = None
117
+ if last_row_id:
118
+ cursor = conn.execute(f'SELECT * FROM "{capture.table}" WHERE rowid = ?', (last_row_id,))
119
+ found = _rows(cursor)
120
+ row = found[0] if found else None
121
+ key = row.get(capture.key) if (row and capture.key) else last_row_id
122
+ records.append((key, None, row))
123
+ else:
124
+ for before in capture.before:
125
+ key = before.get(capture.key) if capture.key else None
126
+ after = None
127
+ if capture.op == 'update' and capture.key:
128
+ cursor = conn.execute(f'SELECT * FROM "{capture.table}" WHERE "{capture.key}" = ?', (key,))
129
+ found = _rows(cursor)
130
+ after = found[0] if found else None
131
+ records.append((key, before, after))
132
+ now = datetime.now().isoformat(timespec='seconds')
133
+ for key, before, after in records:
134
+ conn.execute(
135
+ f'INSERT INTO {TABLE} (at, user, action, table_name, row_key, op, before, after, sql) '
136
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
137
+ (now, state.get('user'), state.get('action'), capture.table,
138
+ None if key is None else str(key), capture.op,
139
+ None if before is None else json.dumps(before, default=str, ensure_ascii=False),
140
+ None if after is None else json.dumps(after, default=str, ensure_ascii=False),
141
+ None if capture.shaped else capture.sql))
142
+
143
+
144
+ def changes(before: Optional[dict], after: Optional[dict]) -> List[tuple]:
145
+ """(column, old, new) of what an update changed."""
146
+ if not before or not after:
147
+ return []
148
+ return [(c, before.get(c), after.get(c)) for c in after if before.get(c) != after.get(c)]
149
+
150
+
151
+ def read(conn, table: str, key: Any, limit: int = 50) -> List[dict]:
152
+ """A row's history, newest first; empty when nothing was ever recorded."""
153
+ exists = conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (TABLE,)).fetchone()
154
+ if not exists:
155
+ return []
156
+ cursor = conn.execute(
157
+ f'SELECT at, user, action, op, before, after, sql FROM {TABLE} '
158
+ 'WHERE table_name = ? AND row_key = ? ORDER BY id DESC LIMIT ?', (table, str(key), limit))
159
+ result = []
160
+ for at, user, action, op, before, after, sql in cursor.fetchall():
161
+ result.append({'at': at, 'user': user, 'action': action, 'op': op,
162
+ 'before': json.loads(before) if before else None,
163
+ 'after': json.loads(after) if after else None, 'sql': sql})
164
+ return result
165
+
166
+
167
+ def entries_for(renderer, node) -> List[Dict[str, str]]:
168
+ """<ui:history>: the row's entries as text — when, who, action, change (web and console)."""
169
+ from pathlib import Path
170
+ from quantum.runtime.db_schema import SchemaError, connect_readonly
171
+ config = getattr(renderer, 'config', None) or {}
172
+ sources = config.get('datasources') or {}
173
+ if node.datasource not in sources:
174
+ raise ValueError(f'<ui:history datasource="{node.datasource}">: not declared in quantum.config.yaml')
175
+ if not sources[node.datasource].get('history'):
176
+ raise ValueError(f'<ui:history>: datasource "{node.datasource}" does not record history — '
177
+ f'add `history: true` to it (DB-11)')
178
+ key = renderer._apply_databinding(node.key) if '{' in (node.key or '') else node.key
179
+ try:
180
+ conn = connect_readonly(sources[node.datasource], Path.cwd())
181
+ except SchemaError as exc:
182
+ raise ValueError(f'<ui:history>: {exc}') from exc
183
+ try:
184
+ entries = read(conn, node.table, key, node.limit)
185
+ finally:
186
+ conn.close()
187
+ lines = []
188
+ for entry in entries:
189
+ if entry['op'] == 'insert':
190
+ change = 'created'
191
+ elif entry['op'] == 'delete':
192
+ change = 'deleted'
193
+ elif entry['op'] == 'update':
194
+ change = '; '.join(f'{c}: {old} → {new}' for c, old, new in changes(entry['before'], entry['after'])) or 'no change'
195
+ else:
196
+ change = entry.get('sql') or entry['op']
197
+ lines.append({'at': entry['at'].replace('T', ' '), 'user': entry['user'] or '—',
198
+ 'action': entry['action'] or '—', 'change': change})
199
+ return lines
200
+
201
+
202
+ HEADERS = (('at', 'When'), ('user', 'Who'), ('action', 'Action'), ('change', 'Change'))
203
+