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,54 @@
1
+ """
2
+ `${VAR}` in quantum.config.yaml (CFG-1).
3
+
4
+ datasources:
5
+ db:
6
+ password: ${DB_PASSWORD}
7
+ host: ${DB_HOST:-localhost}
8
+ port: ${DB_PORT:5432} # ':' works as well as ':-'
9
+
10
+ A config file is the place people put secrets by reference, and every loader
11
+ used to read `${DB_PASSWORD}` as the literal password. There were four loaders
12
+ (the CLI, the web server, the service container, migrations); they all go
13
+ through expand_env now.
14
+ """
15
+
16
+ import os
17
+ import re
18
+ from typing import Any
19
+
20
+ _REFERENCE = re.compile(r'\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-?([^}]*))?\}')
21
+
22
+
23
+ class ConfigEnvError(ValueError):
24
+ """A `${VAR}` with no value and no default."""
25
+
26
+
27
+ def expand_env(value: Any, path: str = '') -> Any:
28
+ """Replace `${NAME}` and `${NAME:-default}` in every string of a loaded config.
29
+
30
+ `$$` is a literal `$`. A variable that is not set and has no default is an
31
+ error naming the variable and where it was used — an empty password or
32
+ database path would otherwise fail much later, somewhere else.
33
+ """
34
+ if isinstance(value, dict):
35
+ return {k: expand_env(v, f'{path}.{k}' if path else str(k)) for k, v in value.items()}
36
+ if isinstance(value, list):
37
+ return [expand_env(v, f'{path}[{i}]') for i, v in enumerate(value)]
38
+ if not isinstance(value, str) or '$' not in value:
39
+ return value
40
+
41
+ def substitute(match):
42
+ if match.group(0) == '$$':
43
+ return '$'
44
+ name, default = match.group(1), match.group(2)
45
+ if name in os.environ:
46
+ return os.environ[name]
47
+ if default is not None:
48
+ return default
49
+ raise ConfigEnvError(
50
+ f"quantum.config.yaml: {path or 'value'} uses ${{{name}}}, but the "
51
+ f"environment variable {name} is not set. Set it, or give a default: "
52
+ f"${{{name}:-value}}")
53
+
54
+ return _REFERENCE.sub(substitute, value)
@@ -0,0 +1,8 @@
1
+ """Documentation pages that error messages point to (one place, one spelling)."""
2
+
3
+ DOCS = 'https://danielgregorio.github.io/quantum'
4
+
5
+ # EXEC-*: the order in which a page's parts run. The traps — a statement in
6
+ # the markup, a variable the action cannot see, a guard reading the page —
7
+ # all come from that order, so their messages send the reader here.
8
+ HOW_A_PAGE_RUNS = f'{DOCS}/guide/how-a-page-runs'
@@ -0,0 +1,87 @@
1
+ """
2
+ Make an unresolved `{expression}` say something.
3
+
4
+ FRAMEWORK_PLAN.md Fase 2.2. Both databinding paths catch every exception and
5
+ hand back the literal placeholder, which is the right thing to RENDER — a page
6
+ should not 500 because one field is missing — but it threw the diagnosis away.
7
+ The evaluator already knew that `p` was not defined, or that `projects` is a
8
+ list with no `recordCount`; nobody was told.
9
+
10
+ That silence was the single biggest cost measured in Fase 4: six of the eleven
11
+ frictions found while writing one real screen were the same disease, and three
12
+ of the four bugs produced no message at all. The time went into discovering
13
+ what was wrong, not into fixing it. See DOGFOOD_NOTES.md.
14
+
15
+ Two things this has to get right to be worth having:
16
+
17
+ - **Not flood.** A `q:loop` over 1,000 rows evaluates the same broken
18
+ expression 1,000 times. Each distinct problem is reported once.
19
+ - **Not cry wolf.** A missing `session.`/`form.` variable is a documented
20
+ contract, not a mistake — templates render before login on purpose. Those
21
+ stay quiet, or the log becomes noise nobody reads.
22
+ """
23
+
24
+ import logging
25
+ import re
26
+ import threading
27
+
28
+ logger = logging.getLogger('quantum.databinding')
29
+
30
+ # Scopes whose absence is normal: a template renders before login, before a
31
+ # form is posted, before a request has query parameters.
32
+ QUIET_SCOPES = ('session', 'application', 'request', 'cookie', 'form', 'query')
33
+
34
+
35
+ def root_scope(expression: str) -> str:
36
+ """The leading scope name of an expression, if it has one."""
37
+ return expression.split('.')[0].split('[')[0].strip()
38
+
39
+
40
+ def is_absent_scope(expression: str) -> bool:
41
+ """Whether an unresolved expression is an ABSENT SCOPED VARIABLE.
42
+
43
+ Those resolve to '' rather than failing — a template renders before login
44
+ on purpose. Both databinding paths have to agree on this or the same
45
+ {form.email} renders as '' in one pass and as literal text in the other,
46
+ which is exactly what happened: the runtime honoured the contract and the
47
+ renderer leaked the placeholder onto the page.
48
+ """
49
+ return root_scope(expression) in QUIET_SCOPES
50
+
51
+
52
+ # `{"product": "Laptop", "price": 999}` — a JSON object written inside a
53
+ # value, not an expression that failed. Quantum has no escape for a brace, so
54
+ # a JSON literal in a .q file is read as databinding, complained about once
55
+ # per object, and then left alone (which is why it still works). Ten objects
56
+ # meant ten warnings pointing at nothing the author can fix.
57
+ _JSON_OBJECT_BODY = re.compile(r'^\s*(["\'])(?:(?!\1).)*\1\s*:', re.DOTALL)
58
+
59
+
60
+ def looks_like_json_object(expression: str) -> bool:
61
+ """Whether this is the inside of a JSON object, not an expression."""
62
+ return bool(_JSON_OBJECT_BODY.match(expression))
63
+
64
+
65
+ _seen = set()
66
+ _lock = threading.Lock()
67
+
68
+
69
+ def report_unresolved(expression: str, error: BaseException) -> None:
70
+ """Log why a `{expression}` did not resolve, once per distinct problem."""
71
+ if is_absent_scope(expression) or looks_like_json_object(expression):
72
+ return
73
+
74
+ reason = str(error) or type(error).__name__
75
+ key = (expression, reason)
76
+ with _lock:
77
+ if key in _seen:
78
+ return
79
+ _seen.add(key)
80
+
81
+ logger.warning("{%s} did not resolve: %s", expression, reason)
82
+
83
+
84
+ def reset() -> None:
85
+ """Forget what has been reported. For tests, and for a reloading server."""
86
+ with _lock:
87
+ _seen.clear()
@@ -0,0 +1,208 @@
1
+ """
2
+ Standard functions available inside Quantum `{...}` expressions.
3
+
4
+ Deliberately small. Every entry here is a function that was either already
5
+ needed by real .q files or whose absence caused a concrete bug:
6
+
7
+ - `now()` / `dateAdd()` / `dateFormat()` — their absence is why
8
+ `session.sessionExpiry` had to be hardcoded to make login work at all
9
+ (AUDIT_FIX_PLAN.md Fase 1). A framework that cannot express "24 hours from
10
+ now" in its own language forces every app to hardcode a date.
11
+ - `len` / `first` / `last` — `.length` already worked on arrays via attribute
12
+ access; these make the same thing available as a call.
13
+
14
+ Functions are looked up by name from expressions, so anything added here
15
+ becomes language surface. That is the reason to keep it short.
16
+ """
17
+
18
+ import math
19
+ import re
20
+ from decimal import Decimal, ROUND_HALF_UP
21
+ from datetime import datetime, timedelta
22
+ from typing import Any, Iterable, Optional
23
+
24
+ _UNITS = {
25
+ 's': 'seconds', 'sec': 'seconds', 'second': 'seconds', 'seconds': 'seconds',
26
+ 'n': 'minutes', 'min': 'minutes', 'minute': 'minutes', 'minutes': 'minutes',
27
+ 'h': 'hours', 'hour': 'hours', 'hours': 'hours',
28
+ 'd': 'days', 'day': 'days', 'days': 'days',
29
+ 'w': 'weeks', 'week': 'weeks', 'weeks': 'weeks',
30
+ }
31
+
32
+
33
+ # -- dates -----------------------------------------------------------------
34
+
35
+ def now() -> datetime:
36
+ """Current local datetime."""
37
+ return datetime.now()
38
+
39
+
40
+ def date_add(unit: str, amount: int, start: Optional[Any] = None) -> datetime:
41
+ """Add an interval to a datetime (defaults to now).
42
+
43
+ dateAdd('h', 24) -> 24 hours from now
44
+ """
45
+ key = _UNITS.get(str(unit).lower())
46
+ if key is None:
47
+ raise ValueError(
48
+ f"unknown date unit {unit!r}; use s/n/h/d/w "
49
+ f"(seconds/minutes/hours/days/weeks)"
50
+ )
51
+ base = _as_datetime(start) if start is not None else datetime.now()
52
+ return base + timedelta(**{key: int(amount)})
53
+
54
+
55
+ def date_format(value: Any, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
56
+ """Format a datetime (or ISO string) with a strftime pattern."""
57
+ return _as_datetime(value).strftime(fmt)
58
+
59
+
60
+ def date_diff(unit: str, start: Any, end: Any) -> int:
61
+ """Whole units between two datetimes."""
62
+ key = _UNITS.get(str(unit).lower())
63
+ if key is None:
64
+ raise ValueError(f"unknown date unit {unit!r}")
65
+ delta = _as_datetime(end) - _as_datetime(start)
66
+ seconds = delta.total_seconds()
67
+ per = {'seconds': 1, 'minutes': 60, 'hours': 3600,
68
+ 'days': 86400, 'weeks': 604800}[key]
69
+ return int(seconds // per)
70
+
71
+
72
+ def _as_datetime(value: Any) -> datetime:
73
+ if isinstance(value, datetime):
74
+ return value
75
+ if isinstance(value, str):
76
+ return datetime.fromisoformat(value)
77
+ raise ValueError(f"cannot read {value!r} as a datetime")
78
+
79
+
80
+ # -- strings ---------------------------------------------------------------
81
+
82
+ def _upper(v: Any) -> str: return str(v).upper()
83
+ def _lower(v: Any) -> str: return str(v).lower()
84
+ def _trim(v: Any) -> str: return str(v).strip()
85
+ def _replace(v: Any, old: str, new: str) -> str: return str(v).replace(old, new)
86
+ def _split(v: Any, sep: str = ",") -> list: return str(v).split(sep)
87
+ def _contains(v: Any, needle: Any) -> bool: return str(needle) in str(v)
88
+
89
+
90
+ def _slugify(v: Any) -> str:
91
+ """'Olá, Mundo!' -> 'ola-mundo': for URLs made from a title (EXPR-9)."""
92
+ import unicodedata
93
+ texto = unicodedata.normalize('NFKD', str(v)).encode('ascii', 'ignore').decode('ascii')
94
+ return re.sub(r'[^a-z0-9]+', '-', texto.lower()).strip('-')
95
+
96
+
97
+ # -- collections -----------------------------------------------------------
98
+
99
+ def _len(v: Any) -> int:
100
+ try:
101
+ return len(v)
102
+ except TypeError:
103
+ return 0
104
+
105
+
106
+ def _first(v: Iterable) -> Any:
107
+ items = list(v)
108
+ return items[0] if items else None
109
+
110
+
111
+ def _last(v: Iterable) -> Any:
112
+ items = list(v)
113
+ return items[-1] if items else None
114
+
115
+
116
+ def _join(v: Iterable, sep: str = ", ") -> str:
117
+ return sep.join(str(i) for i in v)
118
+
119
+
120
+ def _sort(v: Iterable, reverse: bool = False) -> list:
121
+ return sorted(v, reverse=reverse)
122
+
123
+
124
+ # -- numbers ---------------------------------------------------------------
125
+
126
+ def _round(v: Any, digits: int = 0) -> Any:
127
+ # EXPR-9: half away from zero, as in ColdFusion and spreadsheets. Python's
128
+ # round() is banker's rounding over binary floats: round(2.5) was 2 and
129
+ # round(0.125, 2) was 0.12. Decimal over the number's text avoids both.
130
+ casas = int(digits)
131
+ passo = Decimal(1).scaleb(-casas)
132
+ result = Decimal(str(v)).quantize(passo, rounding=ROUND_HALF_UP)
133
+ return int(result) if casas <= 0 else float(result)
134
+
135
+
136
+ def _ceil(v: Any) -> int:
137
+ return math.ceil(float(v))
138
+
139
+
140
+ def _floor(v: Any) -> int:
141
+ return math.floor(float(v))
142
+
143
+
144
+ # -- passwords ---------------------------------------------------------------
145
+ #
146
+ # Authentication is Core (decision D4), and a login is "look the user up, then
147
+ # check the password". Without these, a .q could not check a password against
148
+ # a stored hash without q:python — so every login example just set
149
+ # session.authenticated=true for whoever submitted the form (gap AUTH-1).
150
+
151
+ def hash_password(password: Any) -> str:
152
+ """bcrypt hash of a password, for storing. Never store the password itself."""
153
+ import bcrypt
154
+ if password is None or password == '':
155
+ raise ValueError("hashPassword: the password is empty")
156
+ return bcrypt.hashpw(str(password).encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
157
+
158
+
159
+ def verify_password(password: Any, hashed: Any) -> bool:
160
+ """True when `password` matches a hash made by hashPassword.
161
+
162
+ Fail-closed: an empty password, a missing hash or a malformed hash is
163
+ False, never an error — so `q:if condition="verifyPassword(senha, h)"`
164
+ cannot be tricked into the true branch by bad data.
165
+ """
166
+ import bcrypt
167
+ if not password or not hashed or not isinstance(hashed, str):
168
+ return False
169
+ try:
170
+ return bcrypt.checkpw(str(password).encode('utf-8'), hashed.encode('utf-8'))
171
+ except (ValueError, TypeError):
172
+ return False
173
+
174
+
175
+ STDLIB = {
176
+ # passwords
177
+ 'hashPassword': hash_password,
178
+ 'verifyPassword': verify_password,
179
+ # dates
180
+ 'now': now,
181
+ 'dateAdd': date_add,
182
+ 'dateFormat': date_format,
183
+ 'dateDiff': date_diff,
184
+ # strings
185
+ 'upper': _upper,
186
+ 'lower': _lower,
187
+ 'trim': _trim,
188
+ 'replace': _replace,
189
+ 'split': _split,
190
+ 'contains': _contains,
191
+ 'slugify': _slugify,
192
+ # collections
193
+ 'len': _len,
194
+ 'first': _first,
195
+ 'last': _last,
196
+ 'join': _join,
197
+ 'sort': _sort,
198
+ # numbers
199
+ 'round': _round,
200
+ 'ceil': _ceil,
201
+ 'floor': _floor,
202
+ 'abs': abs,
203
+ 'min': min,
204
+ 'max': max,
205
+ 'int': int,
206
+ 'float': float,
207
+ 'str': str,
208
+ }