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,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,148 @@
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
+ from datetime import datetime, timedelta
19
+ from typing import Any, Iterable, Optional
20
+
21
+ _UNITS = {
22
+ 's': 'seconds', 'sec': 'seconds', 'second': 'seconds', 'seconds': 'seconds',
23
+ 'n': 'minutes', 'min': 'minutes', 'minute': 'minutes', 'minutes': 'minutes',
24
+ 'h': 'hours', 'hour': 'hours', 'hours': 'hours',
25
+ 'd': 'days', 'day': 'days', 'days': 'days',
26
+ 'w': 'weeks', 'week': 'weeks', 'weeks': 'weeks',
27
+ }
28
+
29
+
30
+ # -- dates -----------------------------------------------------------------
31
+
32
+ def now() -> datetime:
33
+ """Current local datetime."""
34
+ return datetime.now()
35
+
36
+
37
+ def date_add(unit: str, amount: int, start: Optional[Any] = None) -> datetime:
38
+ """Add an interval to a datetime (defaults to now).
39
+
40
+ dateAdd('h', 24) -> 24 hours from now
41
+ """
42
+ key = _UNITS.get(str(unit).lower())
43
+ if key is None:
44
+ raise ValueError(
45
+ f"unknown date unit {unit!r}; use s/n/h/d/w "
46
+ f"(seconds/minutes/hours/days/weeks)"
47
+ )
48
+ base = _as_datetime(start) if start is not None else datetime.now()
49
+ return base + timedelta(**{key: int(amount)})
50
+
51
+
52
+ def date_format(value: Any, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
53
+ """Format a datetime (or ISO string) with a strftime pattern."""
54
+ return _as_datetime(value).strftime(fmt)
55
+
56
+
57
+ def date_diff(unit: str, start: Any, end: Any) -> int:
58
+ """Whole units between two datetimes."""
59
+ key = _UNITS.get(str(unit).lower())
60
+ if key is None:
61
+ raise ValueError(f"unknown date unit {unit!r}")
62
+ delta = _as_datetime(end) - _as_datetime(start)
63
+ seconds = delta.total_seconds()
64
+ per = {'seconds': 1, 'minutes': 60, 'hours': 3600,
65
+ 'days': 86400, 'weeks': 604800}[key]
66
+ return int(seconds // per)
67
+
68
+
69
+ def _as_datetime(value: Any) -> datetime:
70
+ if isinstance(value, datetime):
71
+ return value
72
+ if isinstance(value, str):
73
+ return datetime.fromisoformat(value)
74
+ raise ValueError(f"cannot read {value!r} as a datetime")
75
+
76
+
77
+ # -- strings ---------------------------------------------------------------
78
+
79
+ def _upper(v: Any) -> str: return str(v).upper()
80
+ def _lower(v: Any) -> str: return str(v).lower()
81
+ def _trim(v: Any) -> str: return str(v).strip()
82
+ def _replace(v: Any, old: str, new: str) -> str: return str(v).replace(old, new)
83
+ def _split(v: Any, sep: str = ",") -> list: return str(v).split(sep)
84
+ def _contains(v: Any, needle: Any) -> bool: return str(needle) in str(v)
85
+
86
+
87
+ # -- collections -----------------------------------------------------------
88
+
89
+ def _len(v: Any) -> int:
90
+ try:
91
+ return len(v)
92
+ except TypeError:
93
+ return 0
94
+
95
+
96
+ def _first(v: Iterable) -> Any:
97
+ items = list(v)
98
+ return items[0] if items else None
99
+
100
+
101
+ def _last(v: Iterable) -> Any:
102
+ items = list(v)
103
+ return items[-1] if items else None
104
+
105
+
106
+ def _join(v: Iterable, sep: str = ", ") -> str:
107
+ return sep.join(str(i) for i in v)
108
+
109
+
110
+ def _sort(v: Iterable, reverse: bool = False) -> list:
111
+ return sorted(v, reverse=reverse)
112
+
113
+
114
+ # -- numbers ---------------------------------------------------------------
115
+
116
+ def _round(v: Any, digits: int = 0) -> Any:
117
+ result = round(float(v), int(digits))
118
+ return int(result) if digits == 0 else result
119
+
120
+
121
+ STDLIB = {
122
+ # dates
123
+ 'now': now,
124
+ 'dateAdd': date_add,
125
+ 'dateFormat': date_format,
126
+ 'dateDiff': date_diff,
127
+ # strings
128
+ 'upper': _upper,
129
+ 'lower': _lower,
130
+ 'trim': _trim,
131
+ 'replace': _replace,
132
+ 'split': _split,
133
+ 'contains': _contains,
134
+ # collections
135
+ 'len': _len,
136
+ 'first': _first,
137
+ 'last': _last,
138
+ 'join': _join,
139
+ 'sort': _sort,
140
+ # numbers
141
+ 'round': _round,
142
+ 'abs': abs,
143
+ 'min': min,
144
+ 'max': max,
145
+ 'int': int,
146
+ 'float': float,
147
+ 'str': str,
148
+ }