lorebase-core 0.2.3__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.
- lorebase_core/__init__.py +1 -0
- lorebase_core/canonical/__init__.py +35 -0
- lorebase_core/canonical/hashing.py +128 -0
- lorebase_core/canonical/jcs.py +162 -0
- lorebase_core/expr/__init__.py +118 -0
- lorebase_core/expr/ast.py +443 -0
- lorebase_core/expr/checker.py +428 -0
- lorebase_core/expr/compiler.py +87 -0
- lorebase_core/expr/context.py +82 -0
- lorebase_core/expr/derivations.py +306 -0
- lorebase_core/expr/environment.py +176 -0
- lorebase_core/expr/errors.py +98 -0
- lorebase_core/expr/evaluator.py +414 -0
- lorebase_core/expr/functions.py +898 -0
- lorebase_core/expr/limits.py +52 -0
- lorebase_core/expr/operations.py +479 -0
- lorebase_core/expr/ops.py +243 -0
- lorebase_core/expr/resolver.py +96 -0
- lorebase_core/expr/rows.py +148 -0
- lorebase_core/expr/sensitivity.py +337 -0
- lorebase_core/expr/types.py +227 -0
- lorebase_core/expr/validation.py +51 -0
- lorebase_core/expr/values.py +169 -0
- lorebase_core/expr/visibility.py +363 -0
- lorebase_core/infer/__init__.py +36 -0
- lorebase_core/infer/columns.py +621 -0
- lorebase_core/infer/engine.py +546 -0
- lorebase_core/infer/keys.py +164 -0
- lorebase_core/infer/names.py +145 -0
- lorebase_core/infer/options.py +290 -0
- lorebase_core/infer/profile.py +54 -0
- lorebase_core/infer/proposal.py +122 -0
- lorebase_core/infer/references.py +314 -0
- lorebase_core/infer/suggestions.py +731 -0
- lorebase_core/infer/tables.py +81 -0
- lorebase_core/merge/__init__.py +89 -0
- lorebase_core/merge/conflicts.py +262 -0
- lorebase_core/merge/consistency.py +440 -0
- lorebase_core/merge/data_merge.py +522 -0
- lorebase_core/merge/engine.py +476 -0
- lorebase_core/merge/index.py +100 -0
- lorebase_core/merge/objects.py +204 -0
- lorebase_core/merge/policies.py +322 -0
- lorebase_core/merge/resolve.py +321 -0
- lorebase_core/merge/schema_merge.py +429 -0
- lorebase_core/merge/state.py +443 -0
- lorebase_core/model/__init__.py +245 -0
- lorebase_core/model/attribute.py +72 -0
- lorebase_core/model/base.py +61 -0
- lorebase_core/model/derivation.py +95 -0
- lorebase_core/model/domain.py +71 -0
- lorebase_core/model/embedding.py +193 -0
- lorebase_core/model/ids.py +78 -0
- lorebase_core/model/kind.py +85 -0
- lorebase_core/model/lineage.py +114 -0
- lorebase_core/model/operation.py +161 -0
- lorebase_core/model/paths.py +141 -0
- lorebase_core/model/record.py +127 -0
- lorebase_core/model/relation.py +83 -0
- lorebase_core/model/schema.py +219 -0
- lorebase_core/model/source.py +204 -0
- lorebase_core/model/trait.py +37 -0
- lorebase_core/model/vault.py +43 -0
- lorebase_core/model/yaml_schema.py +1058 -0
- lorebase_core/query/__init__.py +97 -0
- lorebase_core/query/dsl.py +227 -0
- lorebase_core/query/resolve.py +794 -0
- lorebase_core/revision/__init__.py +59 -0
- lorebase_core/revision/ancestry.py +272 -0
- lorebase_core/revision/changeset.py +62 -0
- lorebase_core/revision/revision.py +121 -0
- lorebase_core/tree/__init__.py +76 -0
- lorebase_core/tree/diff.py +119 -0
- lorebase_core/tree/hamt.py +333 -0
- lorebase_core/tree/node.py +280 -0
- lorebase_core/tree/params.py +33 -0
- lorebase_core/tree/root.py +70 -0
- lorebase_core/tree/store.py +98 -0
- lorebase_core/types/__init__.py +71 -0
- lorebase_core/types/casts.py +271 -0
- lorebase_core/types/constraints.py +299 -0
- lorebase_core/types/encoding.py +538 -0
- lorebase_core/types/errors.py +41 -0
- lorebase_core/types/matching.py +40 -0
- lorebase_core/types/types.py +187 -0
- lorebase_core/validate/__init__.py +49 -0
- lorebase_core/validate/changes.py +70 -0
- lorebase_core/validate/changeset.py +551 -0
- lorebase_core/validate/data_rules.py +291 -0
- lorebase_core/validate/issues.py +123 -0
- lorebase_core/validate/schema_rules.py +1304 -0
- lorebase_core-0.2.3.dist-info/METADATA +9 -0
- lorebase_core-0.2.3.dist-info/RECORD +94 -0
- lorebase_core-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
"""The Lorebase function library: signatures for the checker, implementations for evaluation.
|
|
2
|
+
|
|
3
|
+
Global functions
|
|
4
|
+
``now()``, ``today()``; ``record(ref)``, ``links(ref, "relation")`` (Operations only);
|
|
5
|
+
``size(x)``, ``count(list)``; conversions ``int``, ``double``, ``decimal``, ``string``,
|
|
6
|
+
``date``, ``timestamp``, ``duration`` (``"1h30m"`` or ISO 8601 ``"P1DT2H"``); durations
|
|
7
|
+
``days(n)``, ``hours(n)``, ``minutes(n)``, ``seconds(n)``; ``days_between(a, b)``;
|
|
8
|
+
math ``abs``, ``round(x, places)`` (half up), ``floor``, ``ceil``, ``min``, ``max``
|
|
9
|
+
(two values or a list), ``sum(list)``; ``matches(text, regex)``.
|
|
10
|
+
Methods
|
|
11
|
+
strings: ``size``, ``contains``, ``startsWith``, ``endsWith``, ``matches`` (RE2,
|
|
12
|
+
partial match), ``lower``, ``upper``, ``trim``, ``replace``, ``split``, ``substring``,
|
|
13
|
+
``indexOf``; lists: ``size``, ``join``; dates and timestamps: ``getFullYear``,
|
|
14
|
+
``getMonth`` (0-based), ``getDate`` (1-based), ``getDayOfMonth`` (0-based),
|
|
15
|
+
``getDayOfWeek`` (0 is Sunday), ``getDayOfYear`` (0-based); timestamps and durations:
|
|
16
|
+
``getHours``, ``getMinutes``, ``getSeconds`` (timestamps in UTC; durations in total).
|
|
17
|
+
|
|
18
|
+
Regular expressions use RE2 (linear time). Literal patterns are validated when compiling.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
import re
|
|
25
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from datetime import UTC, date, datetime, timedelta
|
|
28
|
+
from decimal import ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP, Context, Decimal
|
|
29
|
+
from typing import Protocol
|
|
30
|
+
|
|
31
|
+
import re2 # type: ignore[import-untyped]
|
|
32
|
+
|
|
33
|
+
from lorebase_core.expr import ops
|
|
34
|
+
from lorebase_core.expr.ast import Call, Literal
|
|
35
|
+
from lorebase_core.expr.environment import Environment
|
|
36
|
+
from lorebase_core.expr.errors import ExprError, evaluation_error, type_error
|
|
37
|
+
from lorebase_core.expr.limits import ExprLimits
|
|
38
|
+
from lorebase_core.expr.types import (
|
|
39
|
+
BOOL,
|
|
40
|
+
DATE,
|
|
41
|
+
DECIMAL,
|
|
42
|
+
DOUBLE,
|
|
43
|
+
DURATION,
|
|
44
|
+
DYN,
|
|
45
|
+
INT,
|
|
46
|
+
NUMERIC,
|
|
47
|
+
ORDERED,
|
|
48
|
+
STRING,
|
|
49
|
+
TIMESTAMP,
|
|
50
|
+
ExprType,
|
|
51
|
+
TypeKind,
|
|
52
|
+
join,
|
|
53
|
+
list_of,
|
|
54
|
+
record_of,
|
|
55
|
+
)
|
|
56
|
+
from lorebase_core.expr.values import RecordValue, Ref, type_name
|
|
57
|
+
from lorebase_core.types import AttributeType, EncodingError, TypeSpec, decode_value, encode_value
|
|
58
|
+
from lorebase_core.types.encoding import (
|
|
59
|
+
float_text,
|
|
60
|
+
format_date,
|
|
61
|
+
format_duration,
|
|
62
|
+
format_timestamp,
|
|
63
|
+
to_decimal,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_TIMESTAMP_SPEC = TypeSpec.of(AttributeType.TIMESTAMP)
|
|
67
|
+
_DATE_SPEC = TypeSpec.of(AttributeType.DATE)
|
|
68
|
+
_DURATION_SPEC = TypeSpec.of(AttributeType.DURATION)
|
|
69
|
+
_INT_TEXT = re.compile(r"[+-]?[0-9]+")
|
|
70
|
+
_CEL_DURATION = re.compile(r"(-)?((?:[0-9]+(?:\.[0-9]+)?(?:h|ms|m|s|us|ns))+)")
|
|
71
|
+
_CEL_DURATION_PART = re.compile(r"([0-9]+(?:\.[0-9]+)?)(h|ms|m|s|us|ns)")
|
|
72
|
+
_MICROSECONDS = {
|
|
73
|
+
"h": Decimal(3_600_000_000),
|
|
74
|
+
"m": Decimal(60_000_000),
|
|
75
|
+
"s": Decimal(1_000_000),
|
|
76
|
+
"ms": Decimal(1000),
|
|
77
|
+
"us": Decimal(1),
|
|
78
|
+
"ns": Decimal("0.001"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Runtime(Protocol):
|
|
83
|
+
"""What implementations need from the evaluator."""
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def now(self) -> datetime: ...
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def limits(self) -> ExprLimits: ...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def decimals(self) -> Context: ...
|
|
93
|
+
|
|
94
|
+
def load_record(self, ref: Ref) -> RecordValue: ...
|
|
95
|
+
|
|
96
|
+
def load_links(self, ref: Ref, name: str) -> list[RecordValue]: ...
|
|
97
|
+
|
|
98
|
+
def text(self, value: str) -> str:
|
|
99
|
+
"""``value`` if within ``max_string_length``; raises otherwise."""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def items(self, value: list[object]) -> list[object]:
|
|
103
|
+
"""``value`` if within ``max_list_size``; raises otherwise."""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
def regex(self, pattern: str) -> Callable[[str], bool]: ...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class CallSite:
|
|
111
|
+
"""A call being type checked: argument types (the target first for methods)."""
|
|
112
|
+
|
|
113
|
+
node: Call
|
|
114
|
+
args: tuple[ExprType, ...]
|
|
115
|
+
environment: Environment
|
|
116
|
+
|
|
117
|
+
def literal(self, index: int) -> object:
|
|
118
|
+
"""Value of argument ``index`` (target excluded) when it is a literal, else ``None``."""
|
|
119
|
+
if index < len(self.node.args) and isinstance(self.node.args[index], Literal):
|
|
120
|
+
literal = self.node.args[index]
|
|
121
|
+
assert isinstance(literal, Literal)
|
|
122
|
+
return literal.value
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def describe(self) -> str:
|
|
126
|
+
return ", ".join(self.environment.describe(t) for t in self.args)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Check = Callable[[CallSite], ExprType]
|
|
130
|
+
Run = Callable[[Runtime, Sequence[object]], object]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Function:
|
|
135
|
+
name: str
|
|
136
|
+
member: bool
|
|
137
|
+
check: Check
|
|
138
|
+
run: Run
|
|
139
|
+
records: bool = False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def compile_regex(pattern: str) -> Callable[[str], bool]:
|
|
143
|
+
"""An RE2 search function; raises ``ValueError`` for an invalid pattern."""
|
|
144
|
+
options = re2.Options()
|
|
145
|
+
options.log_errors = False
|
|
146
|
+
try:
|
|
147
|
+
compiled = re2.compile(pattern, options)
|
|
148
|
+
except re2.error as error:
|
|
149
|
+
detail = error.args[0] if error.args else error
|
|
150
|
+
if isinstance(detail, bytes):
|
|
151
|
+
detail = detail.decode("utf-8", "replace")
|
|
152
|
+
raise ValueError(f"invalid regular expression: {detail}") from None
|
|
153
|
+
|
|
154
|
+
def search(text: str) -> bool:
|
|
155
|
+
return compiled.search(text) is not None
|
|
156
|
+
|
|
157
|
+
return search
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Signatures -----------------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
Result = ExprType | Callable[[tuple[ExprType, ...]], ExprType]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def signatures(name: str, *overloads: tuple[tuple[TypeKind | None, ...], Result]) -> Check:
|
|
166
|
+
"""A checker accepting the listed parameter kinds (``None`` accepts any type)."""
|
|
167
|
+
|
|
168
|
+
def check(site: CallSite) -> ExprType:
|
|
169
|
+
results: list[ExprType] = []
|
|
170
|
+
arities = {len(params) for params, _ in overloads}
|
|
171
|
+
for params, result in overloads:
|
|
172
|
+
if len(params) != len(site.args):
|
|
173
|
+
continue
|
|
174
|
+
if all(
|
|
175
|
+
p is None or a.kind is TypeKind.DYN or a.kind is p
|
|
176
|
+
for p, a in zip(params, site.args, strict=True)
|
|
177
|
+
):
|
|
178
|
+
results.append(result(site.args) if callable(result) else result)
|
|
179
|
+
if not results:
|
|
180
|
+
if len(site.args) not in arities:
|
|
181
|
+
expected = " or ".join(str(n) for n in sorted(arities))
|
|
182
|
+
raise type_error(
|
|
183
|
+
f"{name}() takes {expected} argument(s), got {len(site.args)}", site.node.pos
|
|
184
|
+
)
|
|
185
|
+
raise type_error(f"no overload of {name}() accepts ({site.describe()})", site.node.pos)
|
|
186
|
+
if all(r == results[0] for r in results):
|
|
187
|
+
return results[0]
|
|
188
|
+
return DYN
|
|
189
|
+
|
|
190
|
+
return check
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _first(args: tuple[ExprType, ...]) -> ExprType:
|
|
194
|
+
return args[0].plain()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
K = TypeKind
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Implementations ------------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _size(rt: Runtime, args: Sequence[object]) -> object:
|
|
204
|
+
(value,) = args
|
|
205
|
+
if isinstance(value, str | bytes | list | dict):
|
|
206
|
+
return len(value)
|
|
207
|
+
raise evaluation_error(f"size() is not defined for {type_name(value)}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _to_int(rt: Runtime, args: Sequence[object]) -> object:
|
|
211
|
+
(value,) = args
|
|
212
|
+
if isinstance(value, bool):
|
|
213
|
+
raise evaluation_error("int() does not convert bools")
|
|
214
|
+
if isinstance(value, int):
|
|
215
|
+
return value
|
|
216
|
+
if isinstance(value, float):
|
|
217
|
+
if not math.isfinite(value):
|
|
218
|
+
raise evaluation_error("int() of a non-finite double")
|
|
219
|
+
return ops.check_int(math.trunc(value))
|
|
220
|
+
if isinstance(value, Decimal):
|
|
221
|
+
return ops.check_int(int(value))
|
|
222
|
+
if isinstance(value, str):
|
|
223
|
+
if not _INT_TEXT.fullmatch(value.strip()):
|
|
224
|
+
raise evaluation_error(f"int() cannot parse {value!r}")
|
|
225
|
+
return ops.check_int(int(value))
|
|
226
|
+
if isinstance(value, datetime):
|
|
227
|
+
return math.floor(value.timestamp())
|
|
228
|
+
raise evaluation_error(f"int() is not defined for {type_name(value)}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _to_double(rt: Runtime, args: Sequence[object]) -> object:
|
|
232
|
+
(value,) = args
|
|
233
|
+
if isinstance(value, bool) or not isinstance(value, int | float | Decimal | str):
|
|
234
|
+
raise evaluation_error(f"double() is not defined for {type_name(value)}")
|
|
235
|
+
try:
|
|
236
|
+
result = float(to_decimal(value)) if isinstance(value, str) else float(value)
|
|
237
|
+
except (EncodingError, OverflowError) as error:
|
|
238
|
+
raise evaluation_error(f"double() failed: {error}") from None
|
|
239
|
+
if not math.isfinite(result):
|
|
240
|
+
raise evaluation_error("double() result is not finite")
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _to_decimal(rt: Runtime, args: Sequence[object]) -> object:
|
|
245
|
+
(value,) = args
|
|
246
|
+
try:
|
|
247
|
+
return to_decimal(value)
|
|
248
|
+
except EncodingError as error:
|
|
249
|
+
raise evaluation_error(f"decimal() failed: {error.message}") from None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _to_string(rt: Runtime, args: Sequence[object]) -> object:
|
|
253
|
+
(value,) = args
|
|
254
|
+
if isinstance(value, str):
|
|
255
|
+
return value
|
|
256
|
+
if isinstance(value, bool):
|
|
257
|
+
return "true" if value else "false"
|
|
258
|
+
if isinstance(value, int):
|
|
259
|
+
return str(value)
|
|
260
|
+
if isinstance(value, float):
|
|
261
|
+
return float_text(value)
|
|
262
|
+
if isinstance(value, Decimal):
|
|
263
|
+
return format(value, "f")
|
|
264
|
+
if isinstance(value, datetime):
|
|
265
|
+
return format_timestamp(value)
|
|
266
|
+
if isinstance(value, date):
|
|
267
|
+
return format_date(value)
|
|
268
|
+
if isinstance(value, timedelta):
|
|
269
|
+
return format_duration(value)
|
|
270
|
+
if isinstance(value, bytes):
|
|
271
|
+
try:
|
|
272
|
+
return value.decode("utf-8")
|
|
273
|
+
except UnicodeDecodeError:
|
|
274
|
+
raise evaluation_error("string() of bytes that are not UTF-8") from None
|
|
275
|
+
if isinstance(value, Ref | RecordValue):
|
|
276
|
+
return value.key
|
|
277
|
+
raise evaluation_error(f"string() is not defined for {type_name(value)}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _decode(spec: TypeSpec, value: str, what: str) -> object:
|
|
281
|
+
try:
|
|
282
|
+
return decode_value(spec, encode_value(spec, value))
|
|
283
|
+
except EncodingError as error:
|
|
284
|
+
raise evaluation_error(f"{what}() cannot parse {value!r}: {error.message}") from None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _to_date(rt: Runtime, args: Sequence[object]) -> object:
|
|
288
|
+
(value,) = args
|
|
289
|
+
if isinstance(value, datetime):
|
|
290
|
+
return value.astimezone(UTC).date()
|
|
291
|
+
if isinstance(value, date):
|
|
292
|
+
return value
|
|
293
|
+
if isinstance(value, str):
|
|
294
|
+
return _decode(_DATE_SPEC, value, "date")
|
|
295
|
+
raise evaluation_error(f"date() is not defined for {type_name(value)}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _to_timestamp(rt: Runtime, args: Sequence[object]) -> object:
|
|
299
|
+
(value,) = args
|
|
300
|
+
if isinstance(value, datetime):
|
|
301
|
+
return value
|
|
302
|
+
if isinstance(value, date):
|
|
303
|
+
return datetime(value.year, value.month, value.day, tzinfo=UTC)
|
|
304
|
+
if isinstance(value, str):
|
|
305
|
+
return _decode(_TIMESTAMP_SPEC, value, "timestamp")
|
|
306
|
+
raise evaluation_error(f"timestamp() is not defined for {type_name(value)}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def parse_duration(text: str) -> timedelta:
|
|
310
|
+
"""CEL duration text (``"1h30m"``, ``"-1.5s"``) or ISO 8601 (``"P1DT2H"``)."""
|
|
311
|
+
stripped = text.strip()
|
|
312
|
+
if stripped.lstrip("-").startswith("P"):
|
|
313
|
+
value = _decode(_DURATION_SPEC, stripped, "duration")
|
|
314
|
+
assert isinstance(value, timedelta)
|
|
315
|
+
return value
|
|
316
|
+
match = _CEL_DURATION.fullmatch(stripped)
|
|
317
|
+
if match is None:
|
|
318
|
+
raise evaluation_error(f"duration() cannot parse {text!r}")
|
|
319
|
+
micros = sum(
|
|
320
|
+
(
|
|
321
|
+
Decimal(amount) * _MICROSECONDS[unit]
|
|
322
|
+
for amount, unit in _CEL_DURATION_PART.findall(text)
|
|
323
|
+
),
|
|
324
|
+
Decimal(0),
|
|
325
|
+
)
|
|
326
|
+
if micros != micros.to_integral_value():
|
|
327
|
+
raise evaluation_error("durations are precise to the microsecond")
|
|
328
|
+
total = int(micros)
|
|
329
|
+
try:
|
|
330
|
+
return timedelta(microseconds=-total if match.group(1) else total)
|
|
331
|
+
except OverflowError:
|
|
332
|
+
raise evaluation_error("duration is out of range") from None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _to_duration(rt: Runtime, args: Sequence[object]) -> object:
|
|
336
|
+
(value,) = args
|
|
337
|
+
if isinstance(value, timedelta):
|
|
338
|
+
return value
|
|
339
|
+
if isinstance(value, str):
|
|
340
|
+
return parse_duration(value)
|
|
341
|
+
raise evaluation_error(f"duration() is not defined for {type_name(value)}")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _unit(unit: str) -> Run:
|
|
345
|
+
def run(rt: Runtime, args: Sequence[object]) -> object:
|
|
346
|
+
(value,) = args
|
|
347
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
348
|
+
raise evaluation_error(f"{unit}() takes an int")
|
|
349
|
+
try:
|
|
350
|
+
return timedelta(**{unit: value})
|
|
351
|
+
except OverflowError:
|
|
352
|
+
raise evaluation_error(f"{unit}({value}) is out of range") from None
|
|
353
|
+
|
|
354
|
+
return run
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _days_between(rt: Runtime, args: Sequence[object]) -> object:
|
|
358
|
+
start, end = args
|
|
359
|
+
if isinstance(start, datetime) and isinstance(end, datetime):
|
|
360
|
+
return (end - start).days
|
|
361
|
+
if (
|
|
362
|
+
isinstance(start, date)
|
|
363
|
+
and isinstance(end, date)
|
|
364
|
+
and not isinstance(start, datetime)
|
|
365
|
+
and not isinstance(end, datetime)
|
|
366
|
+
):
|
|
367
|
+
return (end - start).days
|
|
368
|
+
raise evaluation_error(
|
|
369
|
+
f"days_between() takes two dates or two timestamps, got {type_name(start)} "
|
|
370
|
+
f"and {type_name(end)}"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _abs(rt: Runtime, args: Sequence[object]) -> object:
|
|
375
|
+
(value,) = args
|
|
376
|
+
if isinstance(value, bool) or not ops.is_number(value):
|
|
377
|
+
raise evaluation_error(f"abs() is not defined for {type_name(value)}")
|
|
378
|
+
if isinstance(value, int):
|
|
379
|
+
return ops.check_int(abs(value))
|
|
380
|
+
if isinstance(value, Decimal):
|
|
381
|
+
return rt.decimals.abs(value)
|
|
382
|
+
assert isinstance(value, float)
|
|
383
|
+
return abs(value)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _rounding(rounding: str, name: str) -> Run:
|
|
387
|
+
def run(rt: Runtime, args: Sequence[object]) -> object:
|
|
388
|
+
value, places = (args[0], args[1]) if len(args) == 2 else (args[0], 0)
|
|
389
|
+
if isinstance(places, bool) or not isinstance(places, int) or places < 0:
|
|
390
|
+
raise evaluation_error(f"{name}() places must be a non-negative int")
|
|
391
|
+
if isinstance(value, bool) or not ops.is_number(value):
|
|
392
|
+
raise evaluation_error(f"{name}() is not defined for {type_name(value)}")
|
|
393
|
+
if isinstance(value, int):
|
|
394
|
+
return value
|
|
395
|
+
exact = value if isinstance(value, Decimal) else to_decimal(value)
|
|
396
|
+
try:
|
|
397
|
+
rounded = exact.quantize(
|
|
398
|
+
Decimal(1).scaleb(-places), rounding=rounding, context=rt.decimals
|
|
399
|
+
)
|
|
400
|
+
except ArithmeticError:
|
|
401
|
+
raise evaluation_error(f"{name}() needs more digits than decimal precision") from None
|
|
402
|
+
return rounded if isinstance(value, Decimal) else float(rounded)
|
|
403
|
+
|
|
404
|
+
return run
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _extreme(name: str, pick: Callable[[int], bool]) -> Run:
|
|
408
|
+
def run(rt: Runtime, args: Sequence[object]) -> object:
|
|
409
|
+
values: Sequence[object] = args
|
|
410
|
+
if len(args) == 1:
|
|
411
|
+
(only,) = args
|
|
412
|
+
if not isinstance(only, list):
|
|
413
|
+
raise evaluation_error(f"{name}() of one argument takes a list")
|
|
414
|
+
values = only
|
|
415
|
+
if not values:
|
|
416
|
+
raise evaluation_error(f"{name}() of an empty list")
|
|
417
|
+
best = values[0]
|
|
418
|
+
for value in values[1:]:
|
|
419
|
+
if pick(ops.compare(value, best)):
|
|
420
|
+
best = value
|
|
421
|
+
return best
|
|
422
|
+
|
|
423
|
+
return run
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _sum(rt: Runtime, args: Sequence[object]) -> object:
|
|
427
|
+
(values,) = args
|
|
428
|
+
if not isinstance(values, list):
|
|
429
|
+
raise evaluation_error(f"sum() takes a list, got {type_name(values)}")
|
|
430
|
+
total: object = 0
|
|
431
|
+
for value in values:
|
|
432
|
+
total = ops.arithmetic("+", total, value, rt.decimals)
|
|
433
|
+
return total
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _text(value: object, what: str) -> str:
|
|
437
|
+
if not isinstance(value, str):
|
|
438
|
+
raise evaluation_error(f"{what} needs a string, got {type_name(value)}")
|
|
439
|
+
return value
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _string_method(name: str, body: Callable[[Runtime, str, list[str]], object]) -> Run:
|
|
443
|
+
def run(rt: Runtime, args: Sequence[object]) -> object:
|
|
444
|
+
target = _text(args[0], f"{name}()")
|
|
445
|
+
rest = [_text(arg, f"{name}() argument") for arg in args[1:]]
|
|
446
|
+
return body(rt, target, rest)
|
|
447
|
+
|
|
448
|
+
return run
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _matches(rt: Runtime, args: Sequence[object]) -> object:
|
|
452
|
+
text = _text(args[0], "matches()")
|
|
453
|
+
pattern = _text(args[1], "matches() pattern")
|
|
454
|
+
return rt.regex(pattern)(text)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _substring(rt: Runtime, args: Sequence[object]) -> object:
|
|
458
|
+
text = _text(args[0], "substring()")
|
|
459
|
+
bounds = list(args[1:])
|
|
460
|
+
if any(isinstance(b, bool) or not isinstance(b, int) for b in bounds):
|
|
461
|
+
raise evaluation_error("substring() takes int positions")
|
|
462
|
+
start = bounds[0]
|
|
463
|
+
end = bounds[1] if len(bounds) > 1 else len(text)
|
|
464
|
+
assert isinstance(start, int)
|
|
465
|
+
assert isinstance(end, int)
|
|
466
|
+
if not 0 <= start <= end <= len(text):
|
|
467
|
+
raise evaluation_error(f"substring({start}, {end}) is out of range for size {len(text)}")
|
|
468
|
+
return text[start:end]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _join(rt: Runtime, args: Sequence[object]) -> object:
|
|
472
|
+
items = args[0]
|
|
473
|
+
separator = _text(args[1], "join() separator") if len(args) > 1 else ""
|
|
474
|
+
if not isinstance(items, list):
|
|
475
|
+
raise evaluation_error(f"join() needs a list, got {type_name(items)}")
|
|
476
|
+
return rt.text(separator.join(_text(item, "join() element") for item in items))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _split(rt: Runtime, target: str, rest: list[str]) -> object:
|
|
480
|
+
if not rest[0]:
|
|
481
|
+
raise evaluation_error("split() needs a non-empty separator")
|
|
482
|
+
return rt.items(list(target.split(rest[0])))
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _date_part(name: str, part: Callable[[date], int]) -> Run:
|
|
486
|
+
def run(rt: Runtime, args: Sequence[object]) -> object:
|
|
487
|
+
(value,) = args
|
|
488
|
+
if isinstance(value, datetime):
|
|
489
|
+
return part(value.astimezone(UTC))
|
|
490
|
+
if isinstance(value, date):
|
|
491
|
+
return part(value)
|
|
492
|
+
raise evaluation_error(f"{name}() is not defined for {type_name(value)}")
|
|
493
|
+
|
|
494
|
+
return run
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _time_part(name: str, unit_seconds: int, modulo: int) -> Run:
|
|
498
|
+
def run(rt: Runtime, args: Sequence[object]) -> object:
|
|
499
|
+
(value,) = args
|
|
500
|
+
if isinstance(value, datetime):
|
|
501
|
+
utc = value.astimezone(UTC)
|
|
502
|
+
seconds = utc.hour * 3600 + utc.minute * 60 + utc.second
|
|
503
|
+
return (seconds // unit_seconds) % modulo
|
|
504
|
+
if isinstance(value, timedelta):
|
|
505
|
+
micros = value // timedelta(microseconds=1)
|
|
506
|
+
whole = abs(micros) // (unit_seconds * 1_000_000)
|
|
507
|
+
return -whole if micros < 0 else whole
|
|
508
|
+
raise evaluation_error(f"{name}() is not defined for {type_name(value)}")
|
|
509
|
+
|
|
510
|
+
return run
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _record(rt: Runtime, args: Sequence[object]) -> object:
|
|
514
|
+
return rt.load_record(ops.ref_key(args[0], "record() argument"))
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _links(rt: Runtime, args: Sequence[object]) -> object:
|
|
518
|
+
name = _text(args[1], "links() relation")
|
|
519
|
+
return rt.load_links(ops.ref_key(args[0], "links() argument"), name)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# Special checks -------------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _check_record(site: CallSite) -> ExprType:
|
|
526
|
+
if len(site.args) != 1:
|
|
527
|
+
raise type_error("record() takes one argument", site.node.pos)
|
|
528
|
+
(arg,) = site.args
|
|
529
|
+
if arg.kind is K.DYN:
|
|
530
|
+
return DYN
|
|
531
|
+
if arg.kind is not K.REF or arg.name is None:
|
|
532
|
+
raise type_error(
|
|
533
|
+
"record() takes a Record reference (a ref parameter, an attribute of type ref "
|
|
534
|
+
f"or $step[n]), got {site.describe()}",
|
|
535
|
+
site.node.pos,
|
|
536
|
+
)
|
|
537
|
+
return record_of(arg.name)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _check_links(site: CallSite) -> ExprType:
|
|
541
|
+
if len(site.args) != 2:
|
|
542
|
+
raise type_error("links() takes a Record reference and a Relation name", site.node.pos)
|
|
543
|
+
ref, _ = site.args
|
|
544
|
+
name = site.literal(1)
|
|
545
|
+
if not isinstance(name, str):
|
|
546
|
+
raise type_error("links() needs a Relation name as a string literal", site.node.pos)
|
|
547
|
+
found = site.environment.schema.link_by_api_name(name)
|
|
548
|
+
if found is None:
|
|
549
|
+
raise type_error(f"unknown Relation {name!r}", site.node.pos)
|
|
550
|
+
relation, direction = found
|
|
551
|
+
if ref.kind is K.REF and ref.name is not None and relation.endpoint(direction) != ref.name:
|
|
552
|
+
raise type_error(
|
|
553
|
+
f"Relation {name!r} does not start at Kind {site.environment.kind_name(ref.name)}",
|
|
554
|
+
site.node.pos,
|
|
555
|
+
)
|
|
556
|
+
if ref.kind not in (K.REF, K.DYN):
|
|
557
|
+
raise type_error(f"links() takes a Record reference, got {site.describe()}", site.node.pos)
|
|
558
|
+
return list_of(record_of(relation.far_endpoint(direction)))
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _check_matches(site: CallSite) -> ExprType:
|
|
562
|
+
check = signatures("matches", ((K.STRING, K.STRING), BOOL))
|
|
563
|
+
result = check(site)
|
|
564
|
+
pattern = site.literal(len(site.node.args) - 1)
|
|
565
|
+
if isinstance(pattern, str):
|
|
566
|
+
try:
|
|
567
|
+
compile_regex(pattern)
|
|
568
|
+
except ValueError as error:
|
|
569
|
+
raise type_error(str(error), site.node.pos) from None
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _check_extreme(name: str) -> Check:
|
|
574
|
+
def check(site: CallSite) -> ExprType:
|
|
575
|
+
if len(site.args) == 1:
|
|
576
|
+
(items,) = site.args
|
|
577
|
+
if items.kind is K.DYN:
|
|
578
|
+
return DYN
|
|
579
|
+
if items.kind is not K.LIST:
|
|
580
|
+
raise type_error(f"{name}() of one argument takes a list", site.node.pos)
|
|
581
|
+
element = items.element
|
|
582
|
+
if element.kind not in NUMERIC | ORDERED | {K.DYN}:
|
|
583
|
+
raise type_error(f"{name}() needs orderable elements", site.node.pos)
|
|
584
|
+
return element.plain()
|
|
585
|
+
if len(site.args) != 2:
|
|
586
|
+
raise type_error(f"{name}() takes a list or two values", site.node.pos)
|
|
587
|
+
left, right = site.args
|
|
588
|
+
if K.DYN in (left.kind, right.kind):
|
|
589
|
+
return DYN
|
|
590
|
+
if left.is_numeric and right.is_numeric:
|
|
591
|
+
return join(left, right)
|
|
592
|
+
if left.kind is right.kind and left.kind in ORDERED:
|
|
593
|
+
return left.plain()
|
|
594
|
+
raise type_error(f"{name}() cannot compare {site.describe()}", site.node.pos)
|
|
595
|
+
|
|
596
|
+
return check
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _check_sum(site: CallSite) -> ExprType:
|
|
600
|
+
if len(site.args) != 1:
|
|
601
|
+
raise type_error("sum() takes one list", site.node.pos)
|
|
602
|
+
(items,) = site.args
|
|
603
|
+
if items.kind is K.DYN:
|
|
604
|
+
return DYN
|
|
605
|
+
if items.kind is not K.LIST or items.element.kind not in NUMERIC | {K.DYN}:
|
|
606
|
+
raise type_error(f"sum() takes a list of numbers, got {site.describe()}", site.node.pos)
|
|
607
|
+
return items.element
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _check_join(site: CallSite) -> ExprType:
|
|
611
|
+
if len(site.args) not in (1, 2):
|
|
612
|
+
raise type_error("join() takes an optional separator", site.node.pos)
|
|
613
|
+
items = site.args[0]
|
|
614
|
+
separator_ok = len(site.args) == 1 or site.args[1].kind in (K.STRING, K.DYN)
|
|
615
|
+
if items.kind is K.LIST and items.element.kind in (K.STRING, K.DYN) and separator_ok:
|
|
616
|
+
return STRING
|
|
617
|
+
if items.kind is K.DYN:
|
|
618
|
+
return STRING
|
|
619
|
+
raise type_error(f"join() needs a list of strings, got {site.describe()}", site.node.pos)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _check_count(site: CallSite) -> ExprType:
|
|
623
|
+
if len(site.args) != 1:
|
|
624
|
+
raise type_error("count() takes one list", site.node.pos)
|
|
625
|
+
if site.args[0].kind not in (K.LIST, K.MAP, K.DYN):
|
|
626
|
+
raise type_error(f"count() takes a list, got {site.describe()}", site.node.pos)
|
|
627
|
+
return INT
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
_SIZED = ((K.STRING,), INT), ((K.BYTES,), INT), ((K.LIST,), INT), ((K.MAP,), INT)
|
|
631
|
+
_NUMBER_IN_OUT = ((K.INT,), INT), ((K.DOUBLE,), DOUBLE), ((K.DECIMAL,), DECIMAL)
|
|
632
|
+
_DATE_LIKE = ((K.DATE,), INT), ((K.TIMESTAMP,), INT)
|
|
633
|
+
_TIME_LIKE = ((K.TIMESTAMP,), INT), ((K.DURATION,), INT)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _library() -> dict[tuple[str, bool], Function]:
|
|
637
|
+
entries = [
|
|
638
|
+
Function("now", False, signatures("now", ((), TIMESTAMP)), lambda rt, _: rt.now),
|
|
639
|
+
Function(
|
|
640
|
+
"today",
|
|
641
|
+
False,
|
|
642
|
+
signatures("today", ((), DATE)),
|
|
643
|
+
lambda rt, _: rt.now.astimezone(UTC).date(),
|
|
644
|
+
),
|
|
645
|
+
Function("record", False, _check_record, _record, records=True),
|
|
646
|
+
Function("links", False, _check_links, _links, records=True),
|
|
647
|
+
Function("size", False, signatures("size", *_SIZED), _size),
|
|
648
|
+
Function("size", True, signatures("size", *_SIZED), _size),
|
|
649
|
+
Function("count", False, _check_count, _size),
|
|
650
|
+
Function(
|
|
651
|
+
"int",
|
|
652
|
+
False,
|
|
653
|
+
signatures(
|
|
654
|
+
"int",
|
|
655
|
+
((K.INT,), INT),
|
|
656
|
+
((K.DOUBLE,), INT),
|
|
657
|
+
((K.DECIMAL,), INT),
|
|
658
|
+
((K.STRING,), INT),
|
|
659
|
+
((K.TIMESTAMP,), INT),
|
|
660
|
+
),
|
|
661
|
+
_to_int,
|
|
662
|
+
),
|
|
663
|
+
Function(
|
|
664
|
+
"double",
|
|
665
|
+
False,
|
|
666
|
+
signatures(
|
|
667
|
+
"double",
|
|
668
|
+
((K.INT,), DOUBLE),
|
|
669
|
+
((K.DOUBLE,), DOUBLE),
|
|
670
|
+
((K.DECIMAL,), DOUBLE),
|
|
671
|
+
((K.STRING,), DOUBLE),
|
|
672
|
+
),
|
|
673
|
+
_to_double,
|
|
674
|
+
),
|
|
675
|
+
Function(
|
|
676
|
+
"decimal",
|
|
677
|
+
False,
|
|
678
|
+
signatures(
|
|
679
|
+
"decimal",
|
|
680
|
+
((K.INT,), DECIMAL),
|
|
681
|
+
((K.DOUBLE,), DECIMAL),
|
|
682
|
+
((K.DECIMAL,), DECIMAL),
|
|
683
|
+
((K.STRING,), DECIMAL),
|
|
684
|
+
),
|
|
685
|
+
_to_decimal,
|
|
686
|
+
),
|
|
687
|
+
Function(
|
|
688
|
+
"string",
|
|
689
|
+
False,
|
|
690
|
+
signatures(
|
|
691
|
+
"string",
|
|
692
|
+
*[
|
|
693
|
+
((kind,), STRING)
|
|
694
|
+
for kind in (
|
|
695
|
+
K.STRING,
|
|
696
|
+
K.BOOL,
|
|
697
|
+
K.INT,
|
|
698
|
+
K.DOUBLE,
|
|
699
|
+
K.DECIMAL,
|
|
700
|
+
K.TIMESTAMP,
|
|
701
|
+
K.DATE,
|
|
702
|
+
K.DURATION,
|
|
703
|
+
K.BYTES,
|
|
704
|
+
K.REF,
|
|
705
|
+
K.RECORD,
|
|
706
|
+
)
|
|
707
|
+
],
|
|
708
|
+
),
|
|
709
|
+
_to_string,
|
|
710
|
+
),
|
|
711
|
+
Function(
|
|
712
|
+
"date",
|
|
713
|
+
False,
|
|
714
|
+
signatures("date", ((K.STRING,), DATE), ((K.TIMESTAMP,), DATE), ((K.DATE,), DATE)),
|
|
715
|
+
_to_date,
|
|
716
|
+
),
|
|
717
|
+
Function(
|
|
718
|
+
"timestamp",
|
|
719
|
+
False,
|
|
720
|
+
signatures(
|
|
721
|
+
"timestamp",
|
|
722
|
+
((K.STRING,), TIMESTAMP),
|
|
723
|
+
((K.DATE,), TIMESTAMP),
|
|
724
|
+
((K.TIMESTAMP,), TIMESTAMP),
|
|
725
|
+
),
|
|
726
|
+
_to_timestamp,
|
|
727
|
+
),
|
|
728
|
+
Function(
|
|
729
|
+
"duration",
|
|
730
|
+
False,
|
|
731
|
+
signatures("duration", ((K.STRING,), DURATION), ((K.DURATION,), DURATION)),
|
|
732
|
+
_to_duration,
|
|
733
|
+
),
|
|
734
|
+
*[
|
|
735
|
+
Function(unit, False, signatures(unit, ((K.INT,), DURATION)), _unit(unit))
|
|
736
|
+
for unit in ("days", "hours", "minutes", "seconds")
|
|
737
|
+
],
|
|
738
|
+
Function(
|
|
739
|
+
"days_between",
|
|
740
|
+
False,
|
|
741
|
+
signatures("days_between", ((K.DATE, K.DATE), INT), ((K.TIMESTAMP, K.TIMESTAMP), INT)),
|
|
742
|
+
_days_between,
|
|
743
|
+
),
|
|
744
|
+
Function("abs", False, signatures("abs", *_NUMBER_IN_OUT), _abs),
|
|
745
|
+
Function(
|
|
746
|
+
"round",
|
|
747
|
+
False,
|
|
748
|
+
signatures(
|
|
749
|
+
"round",
|
|
750
|
+
*_NUMBER_IN_OUT,
|
|
751
|
+
((K.INT, K.INT), INT),
|
|
752
|
+
((K.DOUBLE, K.INT), DOUBLE),
|
|
753
|
+
((K.DECIMAL, K.INT), DECIMAL),
|
|
754
|
+
),
|
|
755
|
+
_rounding(ROUND_HALF_UP, "round"),
|
|
756
|
+
),
|
|
757
|
+
Function(
|
|
758
|
+
"floor", False, signatures("floor", *_NUMBER_IN_OUT), _rounding(ROUND_FLOOR, "floor")
|
|
759
|
+
),
|
|
760
|
+
Function(
|
|
761
|
+
"ceil", False, signatures("ceil", *_NUMBER_IN_OUT), _rounding(ROUND_CEILING, "ceil")
|
|
762
|
+
),
|
|
763
|
+
Function("min", False, _check_extreme("min"), _extreme("min", lambda c: c < 0)),
|
|
764
|
+
Function("max", False, _check_extreme("max"), _extreme("max", lambda c: c > 0)),
|
|
765
|
+
Function("sum", False, _check_sum, _sum),
|
|
766
|
+
Function("matches", False, _check_matches, _matches),
|
|
767
|
+
Function("matches", True, _check_matches, _matches),
|
|
768
|
+
Function(
|
|
769
|
+
"contains",
|
|
770
|
+
True,
|
|
771
|
+
signatures("contains", ((K.STRING, K.STRING), BOOL)),
|
|
772
|
+
_string_method("contains", lambda rt, s, a: a[0] in s),
|
|
773
|
+
),
|
|
774
|
+
Function(
|
|
775
|
+
"startsWith",
|
|
776
|
+
True,
|
|
777
|
+
signatures("startsWith", ((K.STRING, K.STRING), BOOL)),
|
|
778
|
+
_string_method("startsWith", lambda rt, s, a: s.startswith(a[0])),
|
|
779
|
+
),
|
|
780
|
+
Function(
|
|
781
|
+
"endsWith",
|
|
782
|
+
True,
|
|
783
|
+
signatures("endsWith", ((K.STRING, K.STRING), BOOL)),
|
|
784
|
+
_string_method("endsWith", lambda rt, s, a: s.endswith(a[0])),
|
|
785
|
+
),
|
|
786
|
+
Function(
|
|
787
|
+
"lower",
|
|
788
|
+
True,
|
|
789
|
+
signatures("lower", ((K.STRING,), STRING)),
|
|
790
|
+
_string_method("lower", lambda rt, s, a: s.lower()),
|
|
791
|
+
),
|
|
792
|
+
Function(
|
|
793
|
+
"upper",
|
|
794
|
+
True,
|
|
795
|
+
signatures("upper", ((K.STRING,), STRING)),
|
|
796
|
+
_string_method("upper", lambda rt, s, a: s.upper()),
|
|
797
|
+
),
|
|
798
|
+
Function(
|
|
799
|
+
"trim",
|
|
800
|
+
True,
|
|
801
|
+
signatures("trim", ((K.STRING,), STRING)),
|
|
802
|
+
_string_method("trim", lambda rt, s, a: s.strip()),
|
|
803
|
+
),
|
|
804
|
+
Function(
|
|
805
|
+
"replace",
|
|
806
|
+
True,
|
|
807
|
+
signatures("replace", ((K.STRING, K.STRING, K.STRING), STRING)),
|
|
808
|
+
_string_method("replace", lambda rt, s, a: rt.text(s.replace(a[0], a[1]))),
|
|
809
|
+
),
|
|
810
|
+
Function(
|
|
811
|
+
"split",
|
|
812
|
+
True,
|
|
813
|
+
signatures("split", ((K.STRING, K.STRING), list_of(STRING))),
|
|
814
|
+
_string_method("split", _split),
|
|
815
|
+
),
|
|
816
|
+
Function(
|
|
817
|
+
"substring",
|
|
818
|
+
True,
|
|
819
|
+
signatures(
|
|
820
|
+
"substring", ((K.STRING, K.INT), STRING), ((K.STRING, K.INT, K.INT), STRING)
|
|
821
|
+
),
|
|
822
|
+
_substring,
|
|
823
|
+
),
|
|
824
|
+
Function(
|
|
825
|
+
"indexOf",
|
|
826
|
+
True,
|
|
827
|
+
signatures("indexOf", ((K.STRING, K.STRING), INT)),
|
|
828
|
+
_string_method("indexOf", lambda rt, s, a: s.find(a[0])),
|
|
829
|
+
),
|
|
830
|
+
Function("join", True, _check_join, _join),
|
|
831
|
+
Function(
|
|
832
|
+
"getFullYear",
|
|
833
|
+
True,
|
|
834
|
+
signatures("getFullYear", *_DATE_LIKE),
|
|
835
|
+
_date_part("getFullYear", lambda d: d.year),
|
|
836
|
+
),
|
|
837
|
+
Function(
|
|
838
|
+
"getMonth",
|
|
839
|
+
True,
|
|
840
|
+
signatures("getMonth", *_DATE_LIKE),
|
|
841
|
+
_date_part("getMonth", lambda d: d.month - 1),
|
|
842
|
+
),
|
|
843
|
+
Function(
|
|
844
|
+
"getDate",
|
|
845
|
+
True,
|
|
846
|
+
signatures("getDate", *_DATE_LIKE),
|
|
847
|
+
_date_part("getDate", lambda d: d.day),
|
|
848
|
+
),
|
|
849
|
+
Function(
|
|
850
|
+
"getDayOfMonth",
|
|
851
|
+
True,
|
|
852
|
+
signatures("getDayOfMonth", *_DATE_LIKE),
|
|
853
|
+
_date_part("getDayOfMonth", lambda d: d.day - 1),
|
|
854
|
+
),
|
|
855
|
+
Function(
|
|
856
|
+
"getDayOfWeek",
|
|
857
|
+
True,
|
|
858
|
+
signatures("getDayOfWeek", *_DATE_LIKE),
|
|
859
|
+
_date_part("getDayOfWeek", lambda d: (d.weekday() + 1) % 7),
|
|
860
|
+
),
|
|
861
|
+
Function(
|
|
862
|
+
"getDayOfYear",
|
|
863
|
+
True,
|
|
864
|
+
signatures("getDayOfYear", *_DATE_LIKE),
|
|
865
|
+
_date_part("getDayOfYear", lambda d: d.timetuple().tm_yday - 1),
|
|
866
|
+
),
|
|
867
|
+
Function(
|
|
868
|
+
"getHours", True, signatures("getHours", *_TIME_LIKE), _time_part("getHours", 3600, 24)
|
|
869
|
+
),
|
|
870
|
+
Function(
|
|
871
|
+
"getMinutes",
|
|
872
|
+
True,
|
|
873
|
+
signatures("getMinutes", *_TIME_LIKE),
|
|
874
|
+
_time_part("getMinutes", 60, 60),
|
|
875
|
+
),
|
|
876
|
+
Function(
|
|
877
|
+
"getSeconds",
|
|
878
|
+
True,
|
|
879
|
+
signatures("getSeconds", *_TIME_LIKE),
|
|
880
|
+
_time_part("getSeconds", 1, 60),
|
|
881
|
+
),
|
|
882
|
+
]
|
|
883
|
+
return {(f.name, f.member): f for f in entries}
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
#: Functions by ``(name, member)``.
|
|
887
|
+
LIBRARY: Mapping[tuple[str, bool], Function] = _library()
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def lookup(name: str, member: bool) -> Function | None:
|
|
891
|
+
return LIBRARY.get((name, member))
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def raise_with(error: ExprError, node: Call) -> ExprError:
|
|
895
|
+
"""``error`` positioned at ``node`` unless it carries a position already."""
|
|
896
|
+
if error.position is None:
|
|
897
|
+
error.position = node.pos
|
|
898
|
+
return error
|