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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|