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,306 @@
|
|
|
1
|
+
"""Derivations (design 5.8): compile ``expression`` Derivations and aggregate filters, evaluate
|
|
2
|
+
them over one Record, and tell which derived Attributes and Relations they read.
|
|
3
|
+
|
|
4
|
+
Recomputing derived values when Records change is the materializer's job; this module only
|
|
5
|
+
computes one value from one Record (whose values may include other derived values).
|
|
6
|
+
``derivation_inputs`` lists the derived Attributes a Derivation may read, for cycle detection
|
|
7
|
+
and evaluation order; ``links_read`` lists the ``links.<relation>`` fields an expression reads.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from dataclasses import dataclass, fields
|
|
14
|
+
|
|
15
|
+
from pydantic import JsonValue
|
|
16
|
+
|
|
17
|
+
from lorebase_core.expr.ast import Ident, Node, Select, parse
|
|
18
|
+
from lorebase_core.expr.compiler import CompiledExpression, compile, run
|
|
19
|
+
from lorebase_core.expr.context import EvaluationContext, activation
|
|
20
|
+
from lorebase_core.expr.environment import LINKS, LinkField, link_fields, record_environment
|
|
21
|
+
from lorebase_core.expr.errors import ExprError, ExprErrorCode, PathPart
|
|
22
|
+
from lorebase_core.expr.limits import ExprLimits
|
|
23
|
+
from lorebase_core.expr.ops import require_bool
|
|
24
|
+
from lorebase_core.expr.types import BOOL, from_type_spec
|
|
25
|
+
from lorebase_core.expr.values import encode_result
|
|
26
|
+
from lorebase_core.model.derivation import AggregateDerivation, Derivation, ExpressionDerivation
|
|
27
|
+
from lorebase_core.model.record import Record
|
|
28
|
+
from lorebase_core.model.schema import Schema
|
|
29
|
+
from lorebase_core.types import TypeSpec
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, eq=False)
|
|
33
|
+
class CompiledDerivation:
|
|
34
|
+
"""An ``expression`` Derivation (``expression`` and ``target`` set) or an aggregate with a
|
|
35
|
+
``filter`` over the Records it aggregates."""
|
|
36
|
+
|
|
37
|
+
derivation: Derivation
|
|
38
|
+
expression: CompiledExpression | None = None
|
|
39
|
+
target: TypeSpec | None = None
|
|
40
|
+
filter: CompiledExpression | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _compile(
|
|
44
|
+
schema: Schema, derivation: Derivation, limits: ExprLimits, errors: list[ExprError] | None
|
|
45
|
+
) -> CompiledDerivation:
|
|
46
|
+
definition = derivation.definition
|
|
47
|
+
|
|
48
|
+
def fail(error: ExprError, path: tuple[PathPart, ...]) -> None:
|
|
49
|
+
error.located(path=path)
|
|
50
|
+
if errors is None:
|
|
51
|
+
raise error
|
|
52
|
+
errors.append(error)
|
|
53
|
+
|
|
54
|
+
if isinstance(definition, ExpressionDerivation):
|
|
55
|
+
kind = schema.kinds.get(definition.kind)
|
|
56
|
+
attribute = kind.attributes.get(definition.attribute) if kind is not None else None
|
|
57
|
+
if kind is None or attribute is None:
|
|
58
|
+
if errors is None:
|
|
59
|
+
raise ExprError(ExprErrorCode.TYPE, "the Derivation's target does not exist")
|
|
60
|
+
return CompiledDerivation(derivation)
|
|
61
|
+
if LINKS in {a.api_name for a in kind.attributes.values()}:
|
|
62
|
+
fail(
|
|
63
|
+
ExprError(
|
|
64
|
+
ExprErrorCode.TYPE, f"attribute name {LINKS!r} is reserved in expressions"
|
|
65
|
+
),
|
|
66
|
+
("definition", "kind"),
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
program = compile(
|
|
70
|
+
definition.expression,
|
|
71
|
+
record_environment(schema, kind.id),
|
|
72
|
+
expected=from_type_spec(attribute.type),
|
|
73
|
+
nullable=True,
|
|
74
|
+
limits=limits,
|
|
75
|
+
)
|
|
76
|
+
except ExprError as error:
|
|
77
|
+
fail(error, ("definition", "expression"))
|
|
78
|
+
return CompiledDerivation(derivation)
|
|
79
|
+
return CompiledDerivation(derivation, expression=program, target=attribute.type)
|
|
80
|
+
if isinstance(definition, AggregateDerivation) and definition.filter is not None:
|
|
81
|
+
relation = schema.link_relation(definition.relation)
|
|
82
|
+
if relation is None:
|
|
83
|
+
if errors is None:
|
|
84
|
+
raise ExprError(ExprErrorCode.TYPE, "the aggregated Relation does not exist")
|
|
85
|
+
return CompiledDerivation(derivation)
|
|
86
|
+
far = relation.far_endpoint(definition.direction)
|
|
87
|
+
try:
|
|
88
|
+
program = compile(
|
|
89
|
+
definition.filter,
|
|
90
|
+
record_environment(schema, far),
|
|
91
|
+
expected=BOOL,
|
|
92
|
+
limits=limits,
|
|
93
|
+
)
|
|
94
|
+
except ExprError as error:
|
|
95
|
+
fail(error, ("definition", "filter"))
|
|
96
|
+
return CompiledDerivation(derivation)
|
|
97
|
+
return CompiledDerivation(derivation, filter=program)
|
|
98
|
+
return CompiledDerivation(derivation)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def compile_derivation(
|
|
102
|
+
schema: Schema, derivation: Derivation, limits: ExprLimits | None = None
|
|
103
|
+
) -> CompiledDerivation:
|
|
104
|
+
"""Compile the expressions of ``derivation``; raises the first ``ExprError``."""
|
|
105
|
+
return _compile(schema, derivation, limits or ExprLimits(), None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def derivation_expression_errors(
|
|
109
|
+
schema: Schema, derivation: Derivation, limits: ExprLimits | None = None
|
|
110
|
+
) -> list[ExprError]:
|
|
111
|
+
"""Every expression problem of ``derivation`` (paths inside the element)."""
|
|
112
|
+
errors: list[ExprError] = []
|
|
113
|
+
_compile(schema, derivation, limits or ExprLimits(), errors)
|
|
114
|
+
return errors
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def evaluate_derivation(
|
|
118
|
+
compiled: CompiledDerivation, record: Record, context: EvaluationContext
|
|
119
|
+
) -> JsonValue:
|
|
120
|
+
"""The canonical value an ``expression`` Derivation computes for ``record``.
|
|
121
|
+
|
|
122
|
+
``None`` means no value. Raises ``ExprError`` when evaluation fails or the result does
|
|
123
|
+
not fit the target Attribute (``expression.value_invalid``).
|
|
124
|
+
"""
|
|
125
|
+
if compiled.expression is None or compiled.target is None:
|
|
126
|
+
raise ExprError(ExprErrorCode.EVALUATION, "not an expression Derivation")
|
|
127
|
+
program = compiled.expression
|
|
128
|
+
variables = activation(program.environment, record=record)
|
|
129
|
+
value = run(
|
|
130
|
+
program,
|
|
131
|
+
variables,
|
|
132
|
+
now=context.now,
|
|
133
|
+
resolver=context.resolver,
|
|
134
|
+
limits=context.limits,
|
|
135
|
+
)
|
|
136
|
+
return encode_result(compiled.target, value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def matches_filter(
|
|
140
|
+
compiled: CompiledDerivation, record: Record, context: EvaluationContext
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""Whether a Record at the far end of an aggregate passes its ``filter`` (true if none)."""
|
|
143
|
+
if compiled.filter is None:
|
|
144
|
+
return True
|
|
145
|
+
variables = activation(compiled.filter.environment, record=record)
|
|
146
|
+
value = run(
|
|
147
|
+
compiled.filter,
|
|
148
|
+
variables,
|
|
149
|
+
now=context.now,
|
|
150
|
+
resolver=context.resolver,
|
|
151
|
+
limits=context.limits,
|
|
152
|
+
)
|
|
153
|
+
return require_bool(value, "filter")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Dependencies ------------------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _nodes(node: Node) -> Iterator[Node]:
|
|
160
|
+
"""``node`` and every node below it."""
|
|
161
|
+
yield node
|
|
162
|
+
for item in fields(node):
|
|
163
|
+
value = getattr(node, item.name)
|
|
164
|
+
children = value if isinstance(value, tuple) else (value,)
|
|
165
|
+
for child in children:
|
|
166
|
+
for part in child if isinstance(child, tuple) else (child,):
|
|
167
|
+
if isinstance(part, Node):
|
|
168
|
+
yield from _nodes(part)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class _Names:
|
|
173
|
+
"""Identifiers, selected fields and ``links.<name>`` fields of an expression."""
|
|
174
|
+
|
|
175
|
+
idents: frozenset[str]
|
|
176
|
+
fields: frozenset[str]
|
|
177
|
+
links: frozenset[str]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _names(root: Node) -> _Names:
|
|
181
|
+
idents: set[str] = set()
|
|
182
|
+
selected: set[str] = set()
|
|
183
|
+
links: set[str] = set()
|
|
184
|
+
for node in _nodes(root):
|
|
185
|
+
if isinstance(node, Ident):
|
|
186
|
+
idents.add(node.name)
|
|
187
|
+
elif isinstance(node, Select):
|
|
188
|
+
selected.add(node.field)
|
|
189
|
+
if isinstance(node.operand, Ident) and node.operand.name == LINKS:
|
|
190
|
+
links.add(node.field)
|
|
191
|
+
return _Names(frozenset(idents), frozenset(selected), frozenset(links))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def links_read(program: CompiledExpression) -> list[LinkField]:
|
|
195
|
+
"""The ``links.<relation>`` fields a Record expression reads, in name order."""
|
|
196
|
+
names = _names(program.root).links
|
|
197
|
+
available = program.environment.link_fields
|
|
198
|
+
return [available[name] for name in sorted(names) if name in available]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _record_inputs(schema: Schema, kind_id: str, source: str, limits: ExprLimits) -> set[str]:
|
|
202
|
+
"""Derived Attributes an expression over a Record of ``kind_id`` may read: its own derived
|
|
203
|
+
Attributes named in it, and derived Attributes of Records one hop away (through a
|
|
204
|
+
``links.<relation>`` field) whose name appears in it. Conservative by design."""
|
|
205
|
+
try:
|
|
206
|
+
names = _names(parse(source, limits))
|
|
207
|
+
except ExprError:
|
|
208
|
+
return set() # reported by expression validation
|
|
209
|
+
words = names.idents | names.fields
|
|
210
|
+
inputs: set[str] = set()
|
|
211
|
+
kind = schema.kinds.get(kind_id)
|
|
212
|
+
if kind is not None:
|
|
213
|
+
inputs.update(
|
|
214
|
+
a.id for a in kind.attributes.values() if a.is_derived and a.api_name in names.idents
|
|
215
|
+
)
|
|
216
|
+
available = link_fields(schema, kind_id)
|
|
217
|
+
for name in names.links:
|
|
218
|
+
field = available.get(name)
|
|
219
|
+
far = (
|
|
220
|
+
None
|
|
221
|
+
if field is None
|
|
222
|
+
else schema.kinds.get(field.relation.far_endpoint(field.direction))
|
|
223
|
+
)
|
|
224
|
+
if far is not None:
|
|
225
|
+
inputs.update(
|
|
226
|
+
a.id for a in far.attributes.values() if a.is_derived and a.api_name in words
|
|
227
|
+
)
|
|
228
|
+
return inputs
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def derivation_inputs(
|
|
232
|
+
schema: Schema, derivation: Derivation, limits: ExprLimits | None = None
|
|
233
|
+
) -> frozenset[str]:
|
|
234
|
+
"""Ids of the derived Attributes ``derivation`` may read (a superset, never fewer).
|
|
235
|
+
|
|
236
|
+
An ``expression`` Derivation reads its Record and the Records one hop away; an aggregate
|
|
237
|
+
reads the ``source_attribute`` and the ``filter`` of the Records it aggregates. Path
|
|
238
|
+
Derivations read nothing derived.
|
|
239
|
+
"""
|
|
240
|
+
limits = limits or ExprLimits()
|
|
241
|
+
definition = derivation.definition
|
|
242
|
+
if isinstance(definition, ExpressionDerivation):
|
|
243
|
+
return frozenset(_record_inputs(schema, definition.kind, definition.expression, limits))
|
|
244
|
+
if not isinstance(definition, AggregateDerivation):
|
|
245
|
+
return frozenset()
|
|
246
|
+
relation = schema.link_relation(definition.relation)
|
|
247
|
+
if relation is None:
|
|
248
|
+
return frozenset()
|
|
249
|
+
far_id = relation.far_endpoint(definition.direction)
|
|
250
|
+
far = schema.kinds.get(far_id)
|
|
251
|
+
inputs: set[str] = set()
|
|
252
|
+
if far is not None and definition.source_attribute is not None:
|
|
253
|
+
source = far.attributes.get(definition.source_attribute)
|
|
254
|
+
if source is not None and source.is_derived:
|
|
255
|
+
inputs.add(source.id)
|
|
256
|
+
if definition.filter is not None:
|
|
257
|
+
inputs.update(_record_inputs(schema, far_id, definition.filter, limits))
|
|
258
|
+
return frozenset(inputs)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _expression_reads(
|
|
262
|
+
schema: Schema, kind_id: str, source: str, limits: ExprLimits
|
|
263
|
+
) -> set[tuple[str, str]]:
|
|
264
|
+
"""`(Kind id, Attribute id)` of every Attribute an expression over a Record of `kind_id`
|
|
265
|
+
may read: its own Attributes named in it and those of Records one hop away (through a
|
|
266
|
+
``links.<relation>`` field) whose name appears in it. A superset, never fewer."""
|
|
267
|
+
try:
|
|
268
|
+
names = _names(parse(source, limits))
|
|
269
|
+
except ExprError:
|
|
270
|
+
return set() # reported by expression validation
|
|
271
|
+
words = names.idents | names.fields
|
|
272
|
+
reads: set[tuple[str, str]] = set()
|
|
273
|
+
kind = schema.kinds.get(kind_id)
|
|
274
|
+
if kind is not None:
|
|
275
|
+
reads.update((kind_id, a.id) for a in kind.attributes.values() if a.api_name in words)
|
|
276
|
+
available = link_fields(schema, kind_id)
|
|
277
|
+
for name in names.links:
|
|
278
|
+
field = available.get(name)
|
|
279
|
+
far_id = None if field is None else field.relation.far_endpoint(field.direction)
|
|
280
|
+
far = None if far_id is None else schema.kinds.get(far_id)
|
|
281
|
+
if far is not None:
|
|
282
|
+
reads.update((far.id, a.id) for a in far.attributes.values() if a.api_name in words)
|
|
283
|
+
return reads
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def derivation_reads(
|
|
287
|
+
schema: Schema, derivation: Derivation, limits: ExprLimits | None = None
|
|
288
|
+
) -> frozenset[tuple[str, str]]:
|
|
289
|
+
"""`(Kind id, Attribute id)` of every Attribute, stored or derived, a Derivation may read
|
|
290
|
+
(a superset): what its value can tell, for sensitivity (`schema.sensitivity_inherited`)."""
|
|
291
|
+
limits = limits or ExprLimits()
|
|
292
|
+
definition = derivation.definition
|
|
293
|
+
if isinstance(definition, ExpressionDerivation):
|
|
294
|
+
return frozenset(_expression_reads(schema, definition.kind, definition.expression, limits))
|
|
295
|
+
if not isinstance(definition, AggregateDerivation):
|
|
296
|
+
return frozenset()
|
|
297
|
+
relation = schema.link_relation(definition.relation)
|
|
298
|
+
if relation is None:
|
|
299
|
+
return frozenset()
|
|
300
|
+
far_id = relation.far_endpoint(definition.direction)
|
|
301
|
+
reads: set[tuple[str, str]] = set()
|
|
302
|
+
if definition.source_attribute is not None:
|
|
303
|
+
reads.add((far_id, definition.source_attribute))
|
|
304
|
+
if definition.filter is not None:
|
|
305
|
+
reads.update(_expression_reads(schema, far_id, definition.filter, limits))
|
|
306
|
+
return frozenset(reads)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Typed environments: the names an expression may use, derived from the Schema.
|
|
2
|
+
|
|
3
|
+
- Operation expressions (preconditions, review policy, step values): ``params.<name>`` and
|
|
4
|
+
each parameter by its bare name, ``principal.id`` and ``principal.subjects``, the
|
|
5
|
+
functions ``record(ref)`` and ``links(ref, "relation")``; in steps also ``$step[n]``, the
|
|
6
|
+
Record of an earlier step.
|
|
7
|
+
- Record expressions (Derivations, aggregate filters): the Record's Attributes by api name,
|
|
8
|
+
``key``, and ``links.<relation>``: the Records one hop away, by forward or inverse
|
|
9
|
+
Relation name (or path Derivation name).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from lorebase_core.expr.types import (
|
|
18
|
+
DOUBLE,
|
|
19
|
+
DYN,
|
|
20
|
+
GEO_POINT,
|
|
21
|
+
STRING,
|
|
22
|
+
ExprType,
|
|
23
|
+
TypeKind,
|
|
24
|
+
from_type_spec,
|
|
25
|
+
list_of,
|
|
26
|
+
object_of,
|
|
27
|
+
record_of,
|
|
28
|
+
ref_of,
|
|
29
|
+
)
|
|
30
|
+
from lorebase_core.model.operation import (
|
|
31
|
+
CreateRecordStep,
|
|
32
|
+
DeleteRecordStep,
|
|
33
|
+
Operation,
|
|
34
|
+
UpdateRecordStep,
|
|
35
|
+
)
|
|
36
|
+
from lorebase_core.model.relation import LinkDirection, Relation
|
|
37
|
+
from lorebase_core.model.schema import Schema
|
|
38
|
+
from lorebase_core.types import TypeSpec
|
|
39
|
+
|
|
40
|
+
#: Names an Operation environment defines itself; parameters cannot use them.
|
|
41
|
+
PARAMS = "params"
|
|
42
|
+
PRINCIPAL = "principal"
|
|
43
|
+
#: Names a Record environment defines itself; Attributes cannot use them (``key`` is
|
|
44
|
+
#: reserved for Attributes by schema validation already).
|
|
45
|
+
KEY = "key"
|
|
46
|
+
LINKS = "links"
|
|
47
|
+
#: Fields of a ``record(Kind)`` value besides its Attributes.
|
|
48
|
+
RECORD_KEY = "key"
|
|
49
|
+
RECORD_REF = "ref"
|
|
50
|
+
|
|
51
|
+
PRINCIPAL_FIELDS: Mapping[str, ExprType] = {"id": STRING, "subjects": list_of(STRING)}
|
|
52
|
+
GEO_POINT_FIELDS: Mapping[str, ExprType] = {"lat": DOUBLE, "lon": DOUBLE}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class LinkField:
|
|
57
|
+
"""A ``links.<name>`` field: the Relation and the direction it is traversed in."""
|
|
58
|
+
|
|
59
|
+
relation: Relation
|
|
60
|
+
direction: LinkDirection
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, eq=False)
|
|
64
|
+
class Environment:
|
|
65
|
+
"""Declared names and types. ``subject`` is the Kind id whose Attributes are variables;
|
|
66
|
+
``parameters`` are the Operation parameter types (to decode parameter values)."""
|
|
67
|
+
|
|
68
|
+
schema: Schema
|
|
69
|
+
variables: Mapping[str, ExprType]
|
|
70
|
+
objects: Mapping[str, Mapping[str, ExprType]] = field(default_factory=dict)
|
|
71
|
+
subject: str | None = None
|
|
72
|
+
records: bool = False
|
|
73
|
+
step_types: tuple[ExprType | None, ...] | None = None
|
|
74
|
+
link_fields: Mapping[str, LinkField] = field(default_factory=dict)
|
|
75
|
+
parameters: Mapping[str, TypeSpec] = field(default_factory=dict)
|
|
76
|
+
_record_fields: dict[str, dict[str, ExprType]] = field(
|
|
77
|
+
default_factory=dict, repr=False, compare=False
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def kind_name(self, kind_id: str) -> str:
|
|
81
|
+
kind = self.schema.kinds.get(kind_id)
|
|
82
|
+
return kind.api_name if kind is not None else kind_id
|
|
83
|
+
|
|
84
|
+
def describe(self, expr_type: ExprType) -> str:
|
|
85
|
+
return expr_type.describe(self.kind_name)
|
|
86
|
+
|
|
87
|
+
def record_fields(self, kind_id: str) -> dict[str, ExprType]:
|
|
88
|
+
"""Fields of ``record(kind)`` values: Attributes by api name, ``key`` and ``ref``."""
|
|
89
|
+
cached = self._record_fields.get(kind_id)
|
|
90
|
+
if cached is not None:
|
|
91
|
+
return cached
|
|
92
|
+
fields: dict[str, ExprType] = {}
|
|
93
|
+
kind = self.schema.kinds.get(kind_id)
|
|
94
|
+
if kind is not None:
|
|
95
|
+
fields = {a.api_name: from_type_spec(a.type) for a in kind.attributes.values()}
|
|
96
|
+
fields[RECORD_KEY] = STRING
|
|
97
|
+
fields[RECORD_REF] = ref_of(kind_id)
|
|
98
|
+
self._record_fields[kind_id] = fields
|
|
99
|
+
return fields
|
|
100
|
+
|
|
101
|
+
def fields_of(self, expr_type: ExprType) -> Mapping[str, ExprType] | None:
|
|
102
|
+
"""Fields of a record or object type; ``None`` for other types."""
|
|
103
|
+
if expr_type.kind is TypeKind.RECORD and expr_type.name is not None:
|
|
104
|
+
return self.record_fields(expr_type.name)
|
|
105
|
+
if expr_type.kind is TypeKind.OBJECT and expr_type.name is not None:
|
|
106
|
+
if expr_type.name == GEO_POINT:
|
|
107
|
+
return GEO_POINT_FIELDS
|
|
108
|
+
return self.objects.get(expr_type.name)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def operation_environment(
|
|
113
|
+
schema: Schema, operation: Operation, *, step_index: int | None = None
|
|
114
|
+
) -> Environment:
|
|
115
|
+
"""Environment of an Operation's expressions; with ``step_index``, of that step's."""
|
|
116
|
+
parameters = {p.name: from_type_spec(p.type) for p in operation.parameters}
|
|
117
|
+
variables: dict[str, ExprType] = dict(parameters)
|
|
118
|
+
variables[PARAMS] = object_of(PARAMS)
|
|
119
|
+
variables[PRINCIPAL] = object_of(PRINCIPAL)
|
|
120
|
+
step_types: tuple[ExprType | None, ...] | None = None
|
|
121
|
+
if step_index is not None:
|
|
122
|
+
step_types = tuple(_step_type(schema, step) for step in operation.steps[:step_index])
|
|
123
|
+
return Environment(
|
|
124
|
+
schema=schema,
|
|
125
|
+
variables=variables,
|
|
126
|
+
objects={PARAMS: parameters, PRINCIPAL: PRINCIPAL_FIELDS},
|
|
127
|
+
records=True,
|
|
128
|
+
step_types=step_types,
|
|
129
|
+
parameters={p.name: p.type for p in operation.parameters},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _step_type(schema: Schema, step: object) -> ExprType | None:
|
|
134
|
+
"""Type of ``$step[n]``: the Record reference, ``dyn`` for an unknown Kind (reported by
|
|
135
|
+
schema validation), ``None`` for Link steps."""
|
|
136
|
+
if isinstance(step, CreateRecordStep | UpdateRecordStep | DeleteRecordStep):
|
|
137
|
+
return ref_of(step.kind) if step.kind in schema.kinds else DYN
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def record_environment(schema: Schema, kind_id: str) -> Environment:
|
|
142
|
+
"""Environment of expressions over one Record of ``kind_id`` (Derivations, filters)."""
|
|
143
|
+
kind = schema.kinds.get(kind_id)
|
|
144
|
+
variables: dict[str, ExprType] = {}
|
|
145
|
+
if kind is not None:
|
|
146
|
+
variables = {a.api_name: from_type_spec(a.type) for a in kind.attributes.values()}
|
|
147
|
+
links = link_fields(schema, kind_id)
|
|
148
|
+
variables[KEY] = STRING
|
|
149
|
+
variables[LINKS] = object_of(LINKS)
|
|
150
|
+
link_types = {
|
|
151
|
+
name: list_of(record_of(f.relation.far_endpoint(f.direction))) for name, f in links.items()
|
|
152
|
+
}
|
|
153
|
+
return Environment(
|
|
154
|
+
schema=schema,
|
|
155
|
+
variables=variables,
|
|
156
|
+
objects={LINKS: link_types},
|
|
157
|
+
subject=kind_id,
|
|
158
|
+
link_fields=links,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def link_fields(schema: Schema, kind_id: str) -> dict[str, LinkField]:
|
|
163
|
+
"""Relation names usable from a Record of ``kind_id``: forward names of Relations that
|
|
164
|
+
start at the Kind, inverse names of Relations that end there; path Derivations count as
|
|
165
|
+
Relations (their virtual Relation, design 5.8)."""
|
|
166
|
+
fields: dict[str, LinkField] = {}
|
|
167
|
+
relations = [
|
|
168
|
+
*schema.relations.values(),
|
|
169
|
+
*(path.relation for path in schema.path_relations().values()),
|
|
170
|
+
]
|
|
171
|
+
for relation in relations:
|
|
172
|
+
if relation.to_kind == kind_id and relation.inverse_api_name:
|
|
173
|
+
fields[relation.inverse_api_name] = LinkField(relation, LinkDirection.INVERSE)
|
|
174
|
+
if relation.from_kind == kind_id:
|
|
175
|
+
fields[relation.api_name] = LinkField(relation, LinkDirection.FORWARD)
|
|
176
|
+
return fields
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Errors of the expression engine: one exception type with a stable code and a position."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
PathPart = str | int
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExprErrorCode(StrEnum):
|
|
12
|
+
"""Stable codes; ``syntax`` and ``type`` are static, the others come from evaluation."""
|
|
13
|
+
|
|
14
|
+
SYNTAX = "expression.syntax"
|
|
15
|
+
TYPE = "expression.type"
|
|
16
|
+
EVALUATION = "expression.evaluation"
|
|
17
|
+
LIMIT_EXCEEDED = "expression.limit_exceeded"
|
|
18
|
+
VALUE_INVALID = "expression.value_invalid"
|
|
19
|
+
RECORD_NOT_FOUND = "record.not_found"
|
|
20
|
+
RECORD_EXISTS = "record.exists"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Position:
|
|
25
|
+
"""1-based line and column of a node in the expression source."""
|
|
26
|
+
|
|
27
|
+
line: int
|
|
28
|
+
column: int
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
return f"{self.line}:{self.column}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExprError(Exception):
|
|
35
|
+
"""A problem with an expression: compiling it, or evaluating it.
|
|
36
|
+
|
|
37
|
+
``path`` locates the expression inside its schema element (for example
|
|
38
|
+
``("preconditions", 0, "expression")``); it is empty until a caller sets it.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
code: ExprErrorCode,
|
|
44
|
+
message: str,
|
|
45
|
+
*,
|
|
46
|
+
position: Position | None = None,
|
|
47
|
+
source: str | None = None,
|
|
48
|
+
path: tuple[PathPart, ...] = (),
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
self.code = code
|
|
52
|
+
self.message = message
|
|
53
|
+
self.position = position
|
|
54
|
+
self.source = source
|
|
55
|
+
self.path = path
|
|
56
|
+
|
|
57
|
+
def located(
|
|
58
|
+
self, *, source: str | None = None, path: tuple[PathPart, ...] | None = None
|
|
59
|
+
) -> ExprError:
|
|
60
|
+
"""This error with the expression source and path filled in where they are missing."""
|
|
61
|
+
if source is not None and self.source is None:
|
|
62
|
+
self.source = source
|
|
63
|
+
if path is not None and not self.path:
|
|
64
|
+
self.path = path
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def describe(self) -> str:
|
|
68
|
+
"""``line:column: message`` when the position is known, the message otherwise."""
|
|
69
|
+
if self.position is None:
|
|
70
|
+
return self.message
|
|
71
|
+
return f"{self.position}: {self.message}"
|
|
72
|
+
|
|
73
|
+
def snippet(self) -> str:
|
|
74
|
+
"""The source line of the error with a caret under the column (empty if unknown)."""
|
|
75
|
+
if self.position is None or self.source is None:
|
|
76
|
+
return ""
|
|
77
|
+
lines = self.source.splitlines() or [""]
|
|
78
|
+
line = lines[min(self.position.line, len(lines)) - 1]
|
|
79
|
+
return f"{line}\n{' ' * (self.position.column - 1)}^"
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
return self.describe()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def syntax_error(message: str, position: Position | None = None) -> ExprError:
|
|
86
|
+
return ExprError(ExprErrorCode.SYNTAX, message, position=position)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def type_error(message: str, position: Position | None = None) -> ExprError:
|
|
90
|
+
return ExprError(ExprErrorCode.TYPE, message, position=position)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def evaluation_error(message: str, position: Position | None = None) -> ExprError:
|
|
94
|
+
return ExprError(ExprErrorCode.EVALUATION, message, position=position)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def limit_error(message: str, position: Position | None = None) -> ExprError:
|
|
98
|
+
return ExprError(ExprErrorCode.LIMIT_EXCEEDED, message, position=position)
|