quantum-framework 0.9.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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -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 +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,591 @@
1
+ """
2
+ Quantum expression evaluator.
3
+
4
+ One evaluator, built on `ast.parse` with a node whitelist, to replace the two
5
+ parallel hand-rolled ones (6 `_evaluate_*` methods in runtime/component.py, 4
6
+ more in runtime/renderer.py) and the six `eval()` calls they rely on. See
7
+ FRAMEWORK_PLAN.md Fase 2.1.
8
+
9
+ Two things this fixes by construction:
10
+
11
+ - **Security.** `eval()` with an emptied `__builtins__` is not a sandbox — it
12
+ is bypassable through attribute traversal
13
+ (`().__class__.__mro__[1].__subclasses__()`). A whitelist of AST node types
14
+ cannot be talked out of its own grammar. This closes P0.1 of
15
+ PUBLIC_RELEASE_PLAN.md.
16
+ - **Coercion.** `{a + b}` where the values arrived as strings — from an LLM
17
+ tool call, a form field, a query parameter — concatenated instead of adding.
18
+ Numeric-looking operands are coerced for arithmetic and comparison.
19
+
20
+ The grammar is deliberately small, and it was measured rather than guessed:
21
+ across 1,224 expressions in examples/*.q, 57% are a bare variable or a dotted
22
+ access, and the whole arithmetic/comparison/call surface is about 15%.
23
+ """
24
+
25
+ import ast
26
+ import math
27
+ import operator
28
+ import re
29
+ from typing import Any, Callable, Dict, Optional
30
+
31
+ from quantum.core.expression_stdlib import STDLIB
32
+
33
+
34
+ class ExpressionError(Exception):
35
+ """Raised when an expression cannot be evaluated."""
36
+
37
+
38
+ # `{n}`, `{n,}` and `{n,m}` are regex quantifiers, not Quantum expressions.
39
+ # They occur for real: examples/form_validation.q has pattern="\d{10,11}" and
40
+ # examples/python-scripting.q has r'[a-zA-Z]{2,}'. The old evaluator left them
41
+ # alone only by accident — it failed to evaluate them and handed the literal
42
+ # back. This evaluator would happily read `{10,11}` as the tuple (10, 11) and
43
+ # silently corrupt the validation pattern, so the case is excluded on purpose.
44
+ #
45
+ # The underlying gap is that Quantum has no escape for a literal brace. Until
46
+ # it has one, this heuristic is the seam, and it is deliberately narrow:
47
+ # digits and at most one comma, nothing else.
48
+ _REGEX_QUANTIFIER = re.compile(r'^\d+\s*(,\s*\d*)?$')
49
+
50
+
51
+ def is_regex_quantifier(content: str) -> bool:
52
+ """True when a `{...}` group should be left as literal text.
53
+
54
+ Note this also catches a bare `{5}` meant as the number 5 — ambiguous by
55
+ construction, and resolved in favour of not corrupting regexes. It matches
56
+ what the engine already did, so nothing changes for existing templates.
57
+ """
58
+ return bool(_REGEX_QUANTIFIER.match(content.strip()))
59
+
60
+
61
+ # Node types the evaluator accepts. Anything else is refused by construction —
62
+ # notably Lambda, comprehensions, walrus, imports, and any form of statement.
63
+ _ALLOWED_NODES = (
64
+ ast.Expression, ast.Constant, ast.Name, ast.Load,
65
+ ast.BinOp, ast.UnaryOp, ast.BoolOp, ast.Compare, ast.IfExp,
66
+ ast.Attribute, ast.Subscript, ast.Slice,
67
+ ast.Dict, ast.List, ast.Tuple, ast.Set,
68
+ ast.Call, ast.keyword,
69
+ # operators
70
+ ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow,
71
+ ast.USub, ast.UAdd, ast.Not,
72
+ ast.And, ast.Or,
73
+ ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn,
74
+ )
75
+
76
+ _BIN_OPS: Dict[type, Callable[[Any, Any], Any]] = {
77
+ ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
78
+ ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
79
+ ast.Mod: operator.mod, ast.Pow: operator.pow,
80
+ }
81
+
82
+ _CMP_OPS: Dict[type, Callable[[Any, Any], Any]] = {
83
+ ast.Eq: operator.eq, ast.NotEq: operator.ne,
84
+ ast.Lt: operator.lt, ast.LtE: operator.le,
85
+ ast.Gt: operator.gt, ast.GtE: operator.ge,
86
+ ast.In: lambda a, b: a in b, ast.NotIn: lambda a, b: a not in b,
87
+ }
88
+
89
+ _UNARY_OPS: Dict[type, Callable[[Any], Any]] = {
90
+ ast.USub: operator.neg, ast.UAdd: operator.pos, ast.Not: operator.not_,
91
+ }
92
+
93
+
94
+ def coerce_number(value: Any) -> Any:
95
+ """Turn a numeric-looking string into a number, leave everything else.
96
+
97
+ This is the difference between {a + b} adding and concatenating when the
98
+ values came from an LLM, a form post or a query parameter — all of which
99
+ deliver strings.
100
+ """
101
+ if isinstance(value, bool) or not isinstance(value, str):
102
+ return value
103
+ text = value.strip()
104
+ if not text:
105
+ return value
106
+ try:
107
+ return int(text)
108
+ except ValueError:
109
+ pass
110
+ try:
111
+ return float(text)
112
+ except ValueError:
113
+ return value
114
+
115
+
116
+ def _names_of(target: Any) -> list:
117
+ """The names a reader could plausibly have meant, for a suggestion."""
118
+ if isinstance(target, dict):
119
+ return [k for k in target if isinstance(k, str) and not k.startswith('_')]
120
+ return [a for a in dir(target) if not a.startswith('_')]
121
+
122
+
123
+ def _suggest(wanted: str, target: Any, limit: int = 3) -> str:
124
+ """`, did you mean x, y?` — or nothing when there is no near match.
125
+
126
+ The reason this exists: writing a real screen against Quantum, the two
127
+ failures that cost the most time were {p.name} (var= had been silently
128
+ discarded, so `p` did not exist) and {projects.recordCount} (the metadata
129
+ lives on `projects_result`). Both were resolvable in seconds from a list of
130
+ what WAS in scope, and neither message offered one. See DOGFOOD_NOTES.md.
131
+ """
132
+ import difflib
133
+ names = _names_of(target)
134
+ if not names:
135
+ return ""
136
+ close = difflib.get_close_matches(wanted, names, n=limit, cutoff=0.6)
137
+ if close:
138
+ return ", did you mean " + " or ".join(repr(c) for c in close) + "?"
139
+ # No near match: naming what IS available beats naming what is not, but
140
+ # only while the list is short enough to read.
141
+ if len(names) <= 8:
142
+ return " (in scope: " + ", ".join(sorted(names)) + ")"
143
+ return ""
144
+
145
+
146
+ # A template is evaluated inside a request. An expression that never returns
147
+ # is a hung worker, and these two do it with three characters:
148
+ #
149
+ # {9**9**9} -> does not return
150
+ # {10**100000000} -> does not return
151
+ # {'a' * 999999999} -> allocates ~1 GB and returns
152
+ #
153
+ # No dunder, no escape, no attacker — a typo in a template is enough, and there
154
+ # is no timeout anywhere above this to catch it. The caps are far above any
155
+ # real expression: 4096 bits is a 1233-digit number, and 10 MB of repeated
156
+ # string is already absurd for a page.
157
+ _MAX_POW_BITS = 4096
158
+ _MAX_REPEAT = 10_000_000
159
+
160
+
161
+ def _guard_pow(left: Any, right: Any) -> None:
162
+ """Recusa `a ** b` grande demais — inteiro OU float.
163
+
164
+ Exigia `isinstance` int nos DOIS lados, entao qualquer mistura passava
165
+ direto: `2.0 ** 100000000` e `2 ** 1e9` chegavam ao operador e
166
+ levantavam `OverflowError`, que nao e `ExpressionError` — subia pelo
167
+ handler da requisicao como um 500 sem explicacao, em vez da mensagem que
168
+ esta funcao existe para dar.
169
+ """
170
+ if isinstance(left, bool) or isinstance(right, bool):
171
+ return
172
+ if not isinstance(left, (int, float)) or not isinstance(right, (int, float)):
173
+ return
174
+ if right < 0 or left in (0, 1, -1):
175
+ return
176
+ try:
177
+ if isinstance(left, int) and isinstance(right, int):
178
+ # bits(left**right) ≈ right * bits(left)
179
+ bits = right * max(left.bit_length(), 1)
180
+ else:
181
+ base = abs(float(left))
182
+ if base <= 1.0:
183
+ return
184
+ bits = float(right) * math.log2(base)
185
+ except (OverflowError, ValueError):
186
+ # O proprio calculo do tamanho estourou: e grande demais.
187
+ bits = float('inf')
188
+
189
+ if bits > _MAX_POW_BITS:
190
+ raise ExpressionError(
191
+ f"{left}**{right} is too large to compute "
192
+ f"(over {_MAX_POW_BITS} bits); this would hang the request"
193
+ )
194
+
195
+
196
+ def _guard_repeat(left: Any, right: Any) -> None:
197
+ for seq, count in ((left, right), (right, left)):
198
+ if isinstance(seq, (str, bytes, list, tuple)) and isinstance(count, int) \
199
+ and not isinstance(count, bool):
200
+ if len(seq) * max(count, 0) > _MAX_REPEAT:
201
+ raise ExpressionError(
202
+ f"repeating a {len(seq)}-item sequence {count} times "
203
+ f"exceeds {_MAX_REPEAT}; this would exhaust memory"
204
+ )
205
+
206
+
207
+ def _both_numeric(left: Any, right: Any) -> bool:
208
+ return isinstance(left, (int, float)) and isinstance(right, (int, float)) \
209
+ and not isinstance(left, bool) and not isinstance(right, bool)
210
+
211
+
212
+
213
+ def _normalise_not(source: str) -> str:
214
+ """Rewrite JavaScript-style `!x` negation into Python's `not x`.
215
+
216
+ Real .q files use it ({!result.success} appears across the examples), so it
217
+ is part of the language surface whether or not it was ever specified. `!=`
218
+ is left alone, and `!` inside a string literal is not touched.
219
+ """
220
+ if '!' not in source:
221
+ return source
222
+
223
+ out = []
224
+ i = 0
225
+ quote = None
226
+ while i < len(source):
227
+ ch = source[i]
228
+ if quote:
229
+ out.append(ch)
230
+ if ch == quote and (i == 0 or source[i - 1] != "\\"):
231
+ quote = None
232
+ i += 1
233
+ continue
234
+ if ch in ('"', "'"):
235
+ quote = ch
236
+ out.append(ch)
237
+ i += 1
238
+ continue
239
+ if ch == '!' and i + 1 < len(source) and source[i + 1] != '=':
240
+ out.append('not ')
241
+ i += 1
242
+ continue
243
+ out.append(ch)
244
+ i += 1
245
+ return ''.join(out)
246
+
247
+
248
+ class ExpressionEvaluator:
249
+ """Evaluates Quantum `{...}` expressions against a variable context."""
250
+
251
+ def __init__(self, functions: Optional[Dict[str, Callable]] = None,
252
+ max_length: int = 2000,
253
+ function_resolver: Optional[Callable[[str], Optional[Callable]]] = None):
254
+ """
255
+ Args:
256
+ functions: extra callables available to expressions.
257
+ max_length: refuse expressions longer than this.
258
+ function_resolver: consulted for a name that is not in
259
+ `functions`. This is how a component's own q:function
260
+ becomes callable from `{somaTotal(itens)}` — the registry
261
+ is filled while the component executes, so it cannot be
262
+ handed over as a dict when the evaluator is built.
263
+ """
264
+ # Stdlib plus whatever the component registered (q:function).
265
+ self.functions: Dict[str, Callable] = dict(STDLIB)
266
+ if functions:
267
+ self.functions.update(functions)
268
+ self.max_length = max_length
269
+ self.function_resolver = function_resolver
270
+
271
+ # -- public API --------------------------------------------------------
272
+
273
+ def evaluate(self, expression: str, context: Dict[str, Any]) -> Any:
274
+ """Evaluate an expression, raising ExpressionError on any failure."""
275
+ if expression is None:
276
+ raise ExpressionError("expression is None")
277
+
278
+ source = expression.strip()
279
+ if not source:
280
+ raise ExpressionError("empty expression")
281
+ if len(source) > self.max_length:
282
+ raise ExpressionError(f"expression exceeds {self.max_length} characters")
283
+
284
+ source = _normalise_not(source)
285
+
286
+ try:
287
+ tree = ast.parse(source, mode='eval')
288
+ except SyntaxError as exc:
289
+ raise ExpressionError(f"invalid expression {source!r}: {exc.msg}") from exc
290
+
291
+ self._validate(tree, source)
292
+ try:
293
+ return self._eval(tree.body, context)
294
+ except RecursionError as exc:
295
+ # A guarda de profundidade cobre a EXPRESSAO; o contexto tambem
296
+ # pode ser fundo (um dict que referencia a si mesmo, por
297
+ # exemplo). Uma expressao de template nunca pode derrubar a
298
+ # requisicao com um erro que nao seja ExpressionError.
299
+ raise ExpressionError(
300
+ f"evaluating {source[:60]!r} ran out of stack") from exc
301
+
302
+ def try_evaluate(self, expression: str, context: Dict[str, Any],
303
+ default: Any = None) -> Any:
304
+ """Evaluate, returning `default` instead of raising.
305
+
306
+ For render paths, where a bad expression should not take down the page.
307
+ """
308
+ try:
309
+ return self.evaluate(expression, context)
310
+ except ExpressionError:
311
+ return default
312
+
313
+ # -- validation --------------------------------------------------------
314
+
315
+ # Profundidade maxima da arvore. `_eval` desce recursivamente, e o
316
+ # limite de recursao do Python e ~1000 quadros COMPARTILHADOS com tudo
317
+ # que ja esta na pilha (Flask, o executor, o renderer). `{-------...1}`
318
+ # com mil sinais levantava RecursionError — que nao e ExpressionError e
319
+ # portanto virava 500 — e ainda deixava o interpretador raspando o
320
+ # limite. Cem niveis e mais fundo do que qualquer expressao de template
321
+ # real e fica bem longe da borda.
322
+ MAX_DEPTH = 100
323
+
324
+ def _validate(self, tree: ast.AST, source: str):
325
+ self._check_depth(tree.body if isinstance(tree, ast.Expression)
326
+ else tree, source)
327
+ for node in ast.walk(tree):
328
+ if not isinstance(node, _ALLOWED_NODES):
329
+ raise ExpressionError(
330
+ f"{type(node).__name__} is not allowed in expressions "
331
+ f"({source!r})"
332
+ )
333
+ # Dunder traversal is the classic eval sandbox escape.
334
+ if isinstance(node, ast.Attribute) and node.attr.startswith('__'):
335
+ raise ExpressionError(
336
+ f"attribute {node.attr!r} is not accessible ({source!r})"
337
+ )
338
+ if isinstance(node, ast.Constant) and isinstance(node.value, str) \
339
+ and node.value.startswith('__'):
340
+ raise ExpressionError(f"dunder string is not allowed ({source!r})")
341
+
342
+ def _check_depth(self, root: ast.AST, source: str) -> None:
343
+ """Mede a profundidade sem recursao — medir recursivamente teria o
344
+ mesmo problema que a medicao existe para evitar."""
345
+ pilha = [(root, 1)]
346
+ while pilha:
347
+ node, nivel = pilha.pop()
348
+ if nivel > self.MAX_DEPTH:
349
+ raise ExpressionError(
350
+ f"expression is nested more than {self.MAX_DEPTH} levels "
351
+ f"deep ({source[:60]!r}...); this would exhaust the stack"
352
+ )
353
+ for filho in ast.iter_child_nodes(node):
354
+ pilha.append((filho, nivel + 1))
355
+
356
+ # -- evaluation --------------------------------------------------------
357
+
358
+ def _eval(self, node: ast.AST, ctx: Dict[str, Any]) -> Any:
359
+ if isinstance(node, ast.Constant):
360
+ return node.value
361
+
362
+ if isinstance(node, ast.Name):
363
+ return self._lookup(node.id, ctx)
364
+
365
+ if isinstance(node, ast.BinOp):
366
+ left = self._eval(node.left, ctx)
367
+ right = self._eval(node.right, ctx)
368
+ op = _BIN_OPS.get(type(node.op))
369
+ if op is None:
370
+ raise ExpressionError(f"unsupported operator {type(node.op).__name__}")
371
+
372
+ # String + string stays concatenation only when neither side is a
373
+ # number in disguise.
374
+ cl, cr = coerce_number(left), coerce_number(right)
375
+ if _both_numeric(cl, cr):
376
+ left, right = cl, cr
377
+
378
+ # Refuse what would not finish, before attempting it.
379
+ if isinstance(node.op, ast.Pow):
380
+ _guard_pow(left, right)
381
+ elif isinstance(node.op, ast.Mult):
382
+ _guard_repeat(left, right)
383
+
384
+ try:
385
+ return op(left, right)
386
+ except ZeroDivisionError:
387
+ raise ExpressionError("division by zero")
388
+ except TypeError as exc:
389
+ raise ExpressionError(f"cannot apply operator: {exc}") from exc
390
+ except (OverflowError, MemoryError) as exc:
391
+ # `1e300 ** 2` cabe na estimativa de bits e estoura assim
392
+ # mesmo, porque float tem expoente maximo. Sem isto o erro
393
+ # nao e ExpressionError e sobe como 500.
394
+ raise ExpressionError(
395
+ f"the result is too large to represent: {exc}") from exc
396
+
397
+ if isinstance(node, ast.UnaryOp):
398
+ operand = self._eval(node.operand, ctx)
399
+ op = _UNARY_OPS.get(type(node.op))
400
+ if op is None:
401
+ raise ExpressionError(f"unsupported unary {type(node.op).__name__}")
402
+ if isinstance(node.op, (ast.USub, ast.UAdd)):
403
+ operand = coerce_number(operand)
404
+ return op(operand)
405
+
406
+ if isinstance(node, ast.BoolOp):
407
+ values = [self._eval(v, ctx) for v in node.values]
408
+ if isinstance(node.op, ast.And):
409
+ result = True
410
+ for v in values:
411
+ if not v:
412
+ return v
413
+ result = v
414
+ return result
415
+ for v in values:
416
+ if v:
417
+ return v
418
+ return values[-1] if values else False
419
+
420
+ if isinstance(node, ast.Compare):
421
+ left = self._eval(node.left, ctx)
422
+ for op_node, comparator in zip(node.ops, node.comparators):
423
+ right = self._eval(comparator, ctx)
424
+ op = _CMP_OPS.get(type(op_node))
425
+ if op is None:
426
+ raise ExpressionError(f"unsupported comparison {type(op_node).__name__}")
427
+ cl, cr = coerce_number(left), coerce_number(right)
428
+ if _both_numeric(cl, cr):
429
+ left_cmp, right_cmp = cl, cr
430
+ else:
431
+ left_cmp, right_cmp = left, right
432
+ try:
433
+ if not op(left_cmp, right_cmp):
434
+ return False
435
+ except TypeError as exc:
436
+ raise ExpressionError(f"cannot compare: {exc}") from exc
437
+ left = right
438
+ return True
439
+
440
+ if isinstance(node, ast.IfExp):
441
+ return self._eval(node.body, ctx) if self._eval(node.test, ctx) \
442
+ else self._eval(node.orelse, ctx)
443
+
444
+ if isinstance(node, ast.Attribute):
445
+ # `session.userId` e escrito como acesso a atributo, mas o
446
+ # contexto guarda a variavel de escopo sob a CHAVE PLANA
447
+ # pontilhada 'session.userId' — nao existe objeto `session`.
448
+ # Sem isto, uma variavel de sessao PRESENTE levantava, e quem
449
+ # chamava traduzia isso em '' ou em False: `<q:set
450
+ # value="{session.userId}"/>` gravava vazio num INSERT, e
451
+ # `<q:if condition="{session.userRole == 'admin'}">` era False
452
+ # nos dois ramos. A documentacao ensina exatamente esses dois
453
+ # padroes.
454
+ plano = self._dotted_name(node)
455
+ if plano is not None and plano in ctx:
456
+ return ctx[plano]
457
+ return self._attribute(self._eval(node.value, ctx), node.attr)
458
+
459
+ if isinstance(node, ast.Subscript):
460
+ target = self._eval(node.value, ctx)
461
+ key = self._eval(node.slice, ctx) if not isinstance(node.slice, ast.Slice) \
462
+ else slice(
463
+ self._eval(node.slice.lower, ctx) if node.slice.lower else None,
464
+ self._eval(node.slice.upper, ctx) if node.slice.upper else None,
465
+ )
466
+ try:
467
+ if isinstance(target, dict):
468
+ return target.get(key)
469
+ return target[key]
470
+ except (IndexError, KeyError, TypeError) as exc:
471
+ raise ExpressionError(f"cannot index: {exc}") from exc
472
+
473
+ if isinstance(node, ast.Dict):
474
+ return {self._eval(k, ctx): self._eval(v, ctx)
475
+ for k, v in zip(node.keys, node.values)}
476
+
477
+ if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
478
+ values = [self._eval(e, ctx) for e in node.elts]
479
+ return set(values) if isinstance(node, ast.Set) else \
480
+ (tuple(values) if isinstance(node, ast.Tuple) else values)
481
+
482
+ if isinstance(node, ast.Call):
483
+ return self._call(node, ctx)
484
+
485
+ raise ExpressionError(f"cannot evaluate {type(node).__name__}")
486
+
487
+ # -- helpers -----------------------------------------------------------
488
+
489
+ @staticmethod
490
+ def _dotted_name(node: ast.Attribute):
491
+ """'session.userId' de um Attribute, ou None se a base nao for um nome.
492
+
493
+ So encadeamento puro de nomes vira chave plana: `a.b.c` sim,
494
+ `f(x).b` ou `itens[0].nome` nao — nesses a busca por chave seria
495
+ errada, e o acesso a atributo de verdade e que vale.
496
+ """
497
+ partes = []
498
+ atual = node
499
+ while isinstance(atual, ast.Attribute):
500
+ partes.append(atual.attr)
501
+ atual = atual.value
502
+ if not isinstance(atual, ast.Name):
503
+ return None
504
+ partes.append(atual.id)
505
+ return '.'.join(reversed(partes))
506
+
507
+ def _lookup(self, name: str, ctx: Dict[str, Any]) -> Any:
508
+ if name in ctx:
509
+ return ctx[name]
510
+ if name == 'true':
511
+ return True
512
+ if name == 'false':
513
+ return False
514
+ if name == 'null':
515
+ return None
516
+ raise ExpressionError(
517
+ f"variable {name!r} is not defined{_suggest(name, ctx)}"
518
+ )
519
+
520
+ def _attribute(self, target: Any, attr: str) -> Any:
521
+ if isinstance(target, dict):
522
+ if attr in target:
523
+ return target[attr]
524
+ raise ExpressionError(
525
+ f"key {attr!r} not found{_suggest(attr, target)}"
526
+ )
527
+ if isinstance(target, (list, tuple, str)) and attr == 'length':
528
+ return len(target)
529
+ if hasattr(target, attr):
530
+ valor = getattr(target, attr)
531
+ # Um atributo NUNCA pode entregar um callable.
532
+ #
533
+ # `_call` so aceita funcao com nome (bloqueia `x.metodo()`) e ha
534
+ # guarda de dunder sobre a AST — mas ambos eram contornados assim:
535
+ #
536
+ # max([alvo], key='{0.__class__.__base__}'.format)
537
+ #
538
+ # `'...'.format` e um ACESSO A ATRIBUTO que devolve um metodo
539
+ # ligado; passado como `key=` para uma funcao da stdlib, ela o
540
+ # INVOCA, e str.format faz travessia de atributo em tempo de
541
+ # execucao — onde a guarda de dunder, que olha o texto, nao
542
+ # alcanca. Reproduzido: a expressao acima executou uma @property
543
+ # do objeto do contexto.
544
+ #
545
+ # Um template le dado (`user.nome`), nao pega metodo para passar
546
+ # adiante. Recusar callable fecha a classe inteira de gadget, nao
547
+ # so o `format`.
548
+ if callable(valor) and not isinstance(valor, type):
549
+ raise ExpressionError(
550
+ f"attribute {attr!r} is a method, not data; expressions "
551
+ f"read values, they do not pass functions around"
552
+ )
553
+ return valor
554
+ if isinstance(target, (list, tuple)):
555
+ # The q:query case: `projects` is the rows and `projects_result`
556
+ # holds recordCount, so {projects.recordCount} is the natural
557
+ # first guess and it is wrong. Say so instead of going quiet.
558
+ raise ExpressionError(
559
+ f"{type(target).__name__} of {len(target)} items has no "
560
+ f"{attr!r}; use .length for the count, or the query's "
561
+ f"<name>_result for its metadata"
562
+ )
563
+ raise ExpressionError(
564
+ f"attribute {attr!r} not found on {type(target).__name__}"
565
+ f"{_suggest(attr, target)}"
566
+ )
567
+
568
+ def _call(self, node: ast.Call, ctx: Dict[str, Any]) -> Any:
569
+ if not isinstance(node.func, ast.Name):
570
+ raise ExpressionError("only named function calls are allowed")
571
+ fn = self.functions.get(node.func.id)
572
+ if fn is None and self.function_resolver is not None:
573
+ # A q:function declared by the component being executed.
574
+ # {countElements(myArray)} used to resolve to nothing at all: the
575
+ # placeholder was left in the text and the surrounding q:set then
576
+ # failed with "could not convert string to float:
577
+ # '{sumNumbers(myNumbers)}'".
578
+ fn = self.function_resolver(node.func.id)
579
+ if fn is None:
580
+ raise ExpressionError(
581
+ f"function {node.func.id!r} is not defined"
582
+ f"{_suggest(node.func.id, self.functions)}"
583
+ )
584
+ args = [self._eval(a, ctx) for a in node.args]
585
+ kwargs = {kw.arg: self._eval(kw.value, ctx) for kw in node.keywords}
586
+ try:
587
+ return fn(*args, **kwargs)
588
+ except ExpressionError:
589
+ raise
590
+ except Exception as exc: # noqa: BLE001
591
+ raise ExpressionError(f"{node.func.id}() failed: {exc}") from exc
@@ -0,0 +1,30 @@
1
+ """
2
+ Agents Feature - AI Agents with Tool Use
3
+
4
+ This module provides AST nodes for the q:agent feature,
5
+ enabling autonomous AI agents that can use tools to complete tasks.
6
+
7
+ Multi-Agent Systems:
8
+ - AgentTeamNode: Team of collaborating agents
9
+ - AgentHandoffNode: Built-in handoff tool for agent delegation
10
+ """
11
+
12
+ from .ast_node import (
13
+ AgentNode,
14
+ AgentInstructionNode,
15
+ AgentToolNode,
16
+ AgentToolParamNode,
17
+ AgentExecuteNode,
18
+ AgentTeamNode,
19
+ AgentHandoffNode,
20
+ )
21
+
22
+ __all__ = [
23
+ 'AgentNode',
24
+ 'AgentInstructionNode',
25
+ 'AgentToolNode',
26
+ 'AgentToolParamNode',
27
+ 'AgentExecuteNode',
28
+ 'AgentTeamNode',
29
+ 'AgentHandoffNode',
30
+ ]