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.
Files changed (94) hide show
  1. lorebase_core/__init__.py +1 -0
  2. lorebase_core/canonical/__init__.py +35 -0
  3. lorebase_core/canonical/hashing.py +128 -0
  4. lorebase_core/canonical/jcs.py +162 -0
  5. lorebase_core/expr/__init__.py +118 -0
  6. lorebase_core/expr/ast.py +443 -0
  7. lorebase_core/expr/checker.py +428 -0
  8. lorebase_core/expr/compiler.py +87 -0
  9. lorebase_core/expr/context.py +82 -0
  10. lorebase_core/expr/derivations.py +306 -0
  11. lorebase_core/expr/environment.py +176 -0
  12. lorebase_core/expr/errors.py +98 -0
  13. lorebase_core/expr/evaluator.py +414 -0
  14. lorebase_core/expr/functions.py +898 -0
  15. lorebase_core/expr/limits.py +52 -0
  16. lorebase_core/expr/operations.py +479 -0
  17. lorebase_core/expr/ops.py +243 -0
  18. lorebase_core/expr/resolver.py +96 -0
  19. lorebase_core/expr/rows.py +148 -0
  20. lorebase_core/expr/sensitivity.py +337 -0
  21. lorebase_core/expr/types.py +227 -0
  22. lorebase_core/expr/validation.py +51 -0
  23. lorebase_core/expr/values.py +169 -0
  24. lorebase_core/expr/visibility.py +363 -0
  25. lorebase_core/infer/__init__.py +36 -0
  26. lorebase_core/infer/columns.py +621 -0
  27. lorebase_core/infer/engine.py +546 -0
  28. lorebase_core/infer/keys.py +164 -0
  29. lorebase_core/infer/names.py +145 -0
  30. lorebase_core/infer/options.py +290 -0
  31. lorebase_core/infer/profile.py +54 -0
  32. lorebase_core/infer/proposal.py +122 -0
  33. lorebase_core/infer/references.py +314 -0
  34. lorebase_core/infer/suggestions.py +731 -0
  35. lorebase_core/infer/tables.py +81 -0
  36. lorebase_core/merge/__init__.py +89 -0
  37. lorebase_core/merge/conflicts.py +262 -0
  38. lorebase_core/merge/consistency.py +440 -0
  39. lorebase_core/merge/data_merge.py +522 -0
  40. lorebase_core/merge/engine.py +476 -0
  41. lorebase_core/merge/index.py +100 -0
  42. lorebase_core/merge/objects.py +204 -0
  43. lorebase_core/merge/policies.py +322 -0
  44. lorebase_core/merge/resolve.py +321 -0
  45. lorebase_core/merge/schema_merge.py +429 -0
  46. lorebase_core/merge/state.py +443 -0
  47. lorebase_core/model/__init__.py +245 -0
  48. lorebase_core/model/attribute.py +72 -0
  49. lorebase_core/model/base.py +61 -0
  50. lorebase_core/model/derivation.py +95 -0
  51. lorebase_core/model/domain.py +71 -0
  52. lorebase_core/model/embedding.py +193 -0
  53. lorebase_core/model/ids.py +78 -0
  54. lorebase_core/model/kind.py +85 -0
  55. lorebase_core/model/lineage.py +114 -0
  56. lorebase_core/model/operation.py +161 -0
  57. lorebase_core/model/paths.py +141 -0
  58. lorebase_core/model/record.py +127 -0
  59. lorebase_core/model/relation.py +83 -0
  60. lorebase_core/model/schema.py +219 -0
  61. lorebase_core/model/source.py +204 -0
  62. lorebase_core/model/trait.py +37 -0
  63. lorebase_core/model/vault.py +43 -0
  64. lorebase_core/model/yaml_schema.py +1058 -0
  65. lorebase_core/query/__init__.py +97 -0
  66. lorebase_core/query/dsl.py +227 -0
  67. lorebase_core/query/resolve.py +794 -0
  68. lorebase_core/revision/__init__.py +59 -0
  69. lorebase_core/revision/ancestry.py +272 -0
  70. lorebase_core/revision/changeset.py +62 -0
  71. lorebase_core/revision/revision.py +121 -0
  72. lorebase_core/tree/__init__.py +76 -0
  73. lorebase_core/tree/diff.py +119 -0
  74. lorebase_core/tree/hamt.py +333 -0
  75. lorebase_core/tree/node.py +280 -0
  76. lorebase_core/tree/params.py +33 -0
  77. lorebase_core/tree/root.py +70 -0
  78. lorebase_core/tree/store.py +98 -0
  79. lorebase_core/types/__init__.py +71 -0
  80. lorebase_core/types/casts.py +271 -0
  81. lorebase_core/types/constraints.py +299 -0
  82. lorebase_core/types/encoding.py +538 -0
  83. lorebase_core/types/errors.py +41 -0
  84. lorebase_core/types/matching.py +40 -0
  85. lorebase_core/types/types.py +187 -0
  86. lorebase_core/validate/__init__.py +49 -0
  87. lorebase_core/validate/changes.py +70 -0
  88. lorebase_core/validate/changeset.py +551 -0
  89. lorebase_core/validate/data_rules.py +291 -0
  90. lorebase_core/validate/issues.py +123 -0
  91. lorebase_core/validate/schema_rules.py +1304 -0
  92. lorebase_core-0.2.3.dist-info/METADATA +9 -0
  93. lorebase_core-0.2.3.dist-info/RECORD +94 -0
  94. lorebase_core-0.2.3.dist-info/WHEEL +4 -0
@@ -0,0 +1,443 @@
1
+ """Expression syntax: parse CEL source with cel-python and build a small typed-independent AST.
2
+
3
+ cel-python provides the grammar (the CEL specification's, as a Lark LALR parser) with
4
+ positions; the engine converts its parse tree into the nodes below so that checking and
5
+ evaluation do not depend on the parser's internals.
6
+
7
+ Supported syntax: literals (int, double, string, bytes, bool, null), identifiers, field
8
+ selection, indexing, global and method calls, list and map literals, unary ``-`` and ``!``,
9
+ arithmetic, comparisons, ``in``, ``&&``, ``||``, ``?:`` and the macros ``has``, ``all``,
10
+ ``exists``, ``exists_one``, ``map`` (two arguments), ``filter`` and ``count``. Unsigned
11
+ integers and message construction (``Type{field: value}``) are not supported.
12
+
13
+ ``$step[n]`` in Operation steps refers to the Record of an earlier step. ``$`` is not CEL
14
+ syntax, so it is rewritten to the reserved identifier ``_step`` before parsing (same length,
15
+ so positions are unchanged); text inside string literals is left alone.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass
21
+
22
+ from celpy.celparser import CELParser
23
+ from celpy.evaluation import celbytes, celstr
24
+ from lark import Lark, Token, Tree
25
+ from lark.exceptions import (
26
+ LarkError,
27
+ UnexpectedCharacters,
28
+ UnexpectedEOF,
29
+ UnexpectedInput,
30
+ UnexpectedToken,
31
+ )
32
+
33
+ from lorebase_core.expr.errors import ExprError, Position, limit_error, syntax_error
34
+ from lorebase_core.expr.limits import ExprLimits
35
+ from lorebase_core.expr.types import (
36
+ BOOL,
37
+ BYTES,
38
+ DOUBLE,
39
+ INT,
40
+ NULL,
41
+ ExprType,
42
+ string_values,
43
+ )
44
+ from lorebase_core.types import INT64_MAX, INT64_MIN
45
+
46
+ #: Source form and identifier of step references (see the module docstring).
47
+ STEP_SYNTAX = "$step"
48
+ STEP_VARIABLE = "_step"
49
+
50
+ #: Comprehension macros: ``target.macro(variable, body)``.
51
+ MACROS = frozenset({"all", "exists", "exists_one", "map", "filter", "count"})
52
+
53
+ _PASS_THROUGH = frozenset(
54
+ {
55
+ "expr",
56
+ "conditionalor",
57
+ "conditionaland",
58
+ "relation",
59
+ "addition",
60
+ "multiplication",
61
+ "unary",
62
+ "member",
63
+ "primary",
64
+ "paren_expr",
65
+ }
66
+ )
67
+ _RELATIONS = {
68
+ "relation_lt": "<",
69
+ "relation_le": "<=",
70
+ "relation_gt": ">",
71
+ "relation_ge": ">=",
72
+ "relation_eq": "==",
73
+ "relation_ne": "!=",
74
+ "relation_in": "in",
75
+ }
76
+ _ARITHMETIC = {
77
+ "addition_add": "+",
78
+ "addition_sub": "-",
79
+ "multiplication_mul": "*",
80
+ "multiplication_div": "/",
81
+ "multiplication_mod": "%",
82
+ }
83
+
84
+
85
+ @dataclass(frozen=True, eq=False, slots=True)
86
+ class Node:
87
+ pos: Position
88
+
89
+
90
+ @dataclass(frozen=True, eq=False, slots=True)
91
+ class Literal(Node):
92
+ value: object
93
+ type: ExprType
94
+
95
+
96
+ @dataclass(frozen=True, eq=False, slots=True)
97
+ class Ident(Node):
98
+ name: str
99
+
100
+
101
+ @dataclass(frozen=True, eq=False, slots=True)
102
+ class Select(Node):
103
+ """``operand.field``; with ``test`` it is ``has(operand.field)``."""
104
+
105
+ operand: Node
106
+ field: str
107
+ test: bool = False
108
+
109
+
110
+ @dataclass(frozen=True, eq=False, slots=True)
111
+ class Index(Node):
112
+ operand: Node
113
+ index: Node
114
+
115
+
116
+ @dataclass(frozen=True, eq=False, slots=True)
117
+ class Call(Node):
118
+ """``function(args)``, or ``target.function(args)`` when ``target`` is set."""
119
+
120
+ function: str
121
+ target: Node | None
122
+ args: tuple[Node, ...]
123
+
124
+
125
+ @dataclass(frozen=True, eq=False, slots=True)
126
+ class Comprehension(Node):
127
+ """``target.macro(variable, body)``."""
128
+
129
+ macro: str
130
+ target: Node
131
+ variable: str
132
+ body: Node
133
+
134
+
135
+ @dataclass(frozen=True, eq=False, slots=True)
136
+ class ListLit(Node):
137
+ items: tuple[Node, ...]
138
+
139
+
140
+ @dataclass(frozen=True, eq=False, slots=True)
141
+ class MapLit(Node):
142
+ entries: tuple[tuple[Node, Node], ...]
143
+
144
+
145
+ @dataclass(frozen=True, eq=False, slots=True)
146
+ class Unary(Node):
147
+ op: str
148
+ operand: Node
149
+
150
+
151
+ @dataclass(frozen=True, eq=False, slots=True)
152
+ class Binary(Node):
153
+ op: str
154
+ left: Node
155
+ right: Node
156
+
157
+
158
+ @dataclass(frozen=True, eq=False, slots=True)
159
+ class Conditional(Node):
160
+ condition: Node
161
+ then: Node
162
+ otherwise: Node
163
+
164
+
165
+ def rewrite_step_references(source: str) -> str:
166
+ """Replace ``$step`` outside string literals by ``_step`` (see the module docstring)."""
167
+ out: list[str] = []
168
+ index = 0
169
+ quote: str | None = None
170
+ length = len(source)
171
+ while index < length:
172
+ char = source[index]
173
+ if quote is not None:
174
+ if char == "\\":
175
+ out.append(source[index : index + 2])
176
+ index += 2
177
+ continue
178
+ if source.startswith(quote, index):
179
+ out.append(quote)
180
+ index += len(quote)
181
+ quote = None
182
+ continue
183
+ out.append(char)
184
+ index += 1
185
+ continue
186
+ if char in "\"'":
187
+ quote = char * 3 if source.startswith(char * 3, index) else char
188
+ out.append(quote)
189
+ index += len(quote)
190
+ continue
191
+ if source.startswith(STEP_SYNTAX, index):
192
+ out.append(STEP_VARIABLE)
193
+ index += len(STEP_SYNTAX)
194
+ continue
195
+ out.append(char)
196
+ index += 1
197
+ return "".join(out)
198
+
199
+
200
+ def parse(source: str, limits: ExprLimits) -> Node:
201
+ """Parse ``source`` into an AST. Raises ``ExprError`` (``syntax`` or ``limit_exceeded``)."""
202
+ if len(source) > limits.max_source_length:
203
+ raise limit_error(
204
+ f"expression is longer than {limits.max_source_length} characters"
205
+ ).located(source=source)
206
+ text = rewrite_step_references(source)
207
+ if not text.strip():
208
+ raise syntax_error("expression is empty").located(source=source)
209
+ try:
210
+ tree = _lark().parse(text)
211
+ except UnexpectedInput as error:
212
+ message, position = _syntax_problem(text, error)
213
+ raise syntax_error(message, position).located(source=source) from None
214
+ except LarkError as error:
215
+ raise syntax_error(f"syntax error: {error}").located(source=source) from None
216
+ except RecursionError:
217
+ raise limit_error(f"expression nests deeper than {limits.max_depth} levels").located(
218
+ source=source
219
+ ) from None
220
+ try:
221
+ return _Builder(limits.max_depth).build(tree, 0, Position(1, 1))
222
+ except ExprError as error:
223
+ raise error.located(source=source) from None
224
+
225
+
226
+ def _lark() -> Lark:
227
+ """cel-python's CEL grammar (loaded once by its ``CELParser``); used directly so that
228
+ parse errors keep the offending token and parsing holds no per-call state."""
229
+ CELParser()
230
+ parser = CELParser.CEL_PARSER
231
+ assert parser is not None
232
+ return parser
233
+
234
+
235
+ def _end(text: str) -> Position:
236
+ lines = text.splitlines() or [""]
237
+ return Position(len(lines), len(lines[-1]) + 1)
238
+
239
+
240
+ def _syntax_problem(text: str, error: UnexpectedInput) -> tuple[str, Position]:
241
+ if isinstance(error, UnexpectedEOF) or (
242
+ isinstance(error, UnexpectedToken) and error.token.type == "$END"
243
+ ):
244
+ return "syntax error: unexpected end of expression", _end(text)
245
+ position = Position(error.line, error.column)
246
+ if isinstance(error, UnexpectedCharacters):
247
+ found = error.char
248
+ elif isinstance(error, UnexpectedToken):
249
+ found = str(error.token.value)
250
+ else:
251
+ found = text.splitlines()[error.line - 1][error.column - 1 :][:1]
252
+ if found == "$":
253
+ return f"syntax error: unexpected '$' ({STEP_SYNTAX}[n] is the only $ form)", position
254
+ return f"syntax error: unexpected {found!r}", position
255
+
256
+
257
+ class _Builder:
258
+ """Converts the cel-python parse tree into AST nodes, bounding the nesting depth."""
259
+
260
+ def __init__(self, max_depth: int) -> None:
261
+ self.max_depth = max_depth
262
+
263
+ def build(self, item: Tree[Token] | Token, depth: int, parent: Position) -> Node:
264
+ if isinstance(item, Token):
265
+ raise syntax_error(f"unexpected token {item.value!r}", _token_position(item, parent))
266
+ tree = item
267
+ while (
268
+ tree.data in _PASS_THROUGH
269
+ and len(tree.children) == 1
270
+ and isinstance(tree.children[0], Tree)
271
+ ):
272
+ tree = tree.children[0]
273
+ position = _tree_position(tree, parent)
274
+ if depth >= self.max_depth:
275
+ raise limit_error(f"expression nests deeper than {self.max_depth} levels", position)
276
+ handler = getattr(self, f"_{tree.data}", None)
277
+ if handler is None:
278
+ raise syntax_error(f"unsupported syntax ({tree.data})", position)
279
+ node: Node = handler(tree, depth + 1, position)
280
+ return node
281
+
282
+ def _expr(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
283
+ condition, then, otherwise = tree.children
284
+ return Conditional(
285
+ pos,
286
+ self.build(condition, depth, pos),
287
+ self.build(then, depth, pos),
288
+ self.build(otherwise, depth, pos),
289
+ )
290
+
291
+ def _conditionalor(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
292
+ left, right = tree.children
293
+ return Binary(pos, "||", self.build(left, depth, pos), self.build(right, depth, pos))
294
+
295
+ def _conditionaland(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
296
+ left, right = tree.children
297
+ return Binary(pos, "&&", self.build(left, depth, pos), self.build(right, depth, pos))
298
+
299
+ def _operator(
300
+ self, tree: Tree[Token], depth: int, pos: Position, table: dict[str, str]
301
+ ) -> Node:
302
+ operator, right = tree.children
303
+ assert isinstance(operator, Tree)
304
+ (left,) = operator.children
305
+ op = table[operator.data]
306
+ right_node = self.build(right, depth, pos)
307
+ return Binary(_tree_position(operator, pos), op, self.build(left, depth, pos), right_node)
308
+
309
+ def _relation(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
310
+ return self._operator(tree, depth, pos, _RELATIONS)
311
+
312
+ def _addition(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
313
+ return self._operator(tree, depth, pos, _ARITHMETIC)
314
+
315
+ def _multiplication(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
316
+ return self._operator(tree, depth, pos, _ARITHMETIC)
317
+
318
+ def _unary(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
319
+ operator, operand = tree.children
320
+ assert isinstance(operator, Tree)
321
+ op = "!" if operator.data == "unary_not" else "-"
322
+ return Unary(pos, op, self.build(operand, depth, pos))
323
+
324
+ def _member_dot(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
325
+ operand, name = tree.children
326
+ assert isinstance(name, Token)
327
+ return Select(_token_position(name, pos), self.build(operand, depth, pos), str(name.value))
328
+
329
+ def _member_dot_arg(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
330
+ operand, name, *rest = tree.children
331
+ assert isinstance(name, Token)
332
+ function = str(name.value)
333
+ name_pos = _token_position(name, pos)
334
+ target = self.build(operand, depth, pos)
335
+ args = self._arguments(rest, depth, pos)
336
+ if function in MACROS:
337
+ if len(args) != 2 or not isinstance(args[0], Ident):
338
+ raise syntax_error(
339
+ f"{function}() takes a variable name and an expression, "
340
+ f"for example list.{function}(x, x > 0)",
341
+ name_pos,
342
+ )
343
+ return Comprehension(name_pos, function, target, args[0].name, args[1])
344
+ return Call(name_pos, function, target, args)
345
+
346
+ def _member_index(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
347
+ operand, index = tree.children
348
+ return Index(pos, self.build(operand, depth, pos), self.build(index, depth, pos))
349
+
350
+ def _member_object(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
351
+ raise syntax_error("message construction (Type{field: value}) is not supported", pos)
352
+
353
+ def _dot_ident(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
354
+ return self._ident(tree, depth, pos)
355
+
356
+ def _dot_ident_arg(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
357
+ return self._ident_arg(tree, depth, pos)
358
+
359
+ def _ident(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
360
+ (name,) = tree.children
361
+ assert isinstance(name, Token)
362
+ return Ident(_token_position(name, pos), str(name.value))
363
+
364
+ def _ident_arg(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
365
+ name, *rest = tree.children
366
+ assert isinstance(name, Token)
367
+ function = str(name.value)
368
+ args = self._arguments(rest, depth, pos)
369
+ if function == "has":
370
+ if len(args) != 1 or not isinstance(args[0], Select) or args[0].test:
371
+ raise syntax_error("has() takes one field selection, for example has(x.f)", pos)
372
+ selection = args[0]
373
+ return Select(selection.pos, selection.operand, selection.field, test=True)
374
+ return Call(pos, function, None, args)
375
+
376
+ def _arguments(
377
+ self, rest: list[Tree[Token] | Token], depth: int, pos: Position
378
+ ) -> tuple[Node, ...]:
379
+ if not rest:
380
+ return ()
381
+ (exprlist,) = rest
382
+ assert isinstance(exprlist, Tree)
383
+ return tuple(self.build(child, depth, pos) for child in exprlist.children)
384
+
385
+ def _list_lit(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
386
+ return ListLit(pos, self._arguments(list(tree.children), depth, pos))
387
+
388
+ def _map_lit(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
389
+ if not tree.children:
390
+ return MapLit(pos, ())
391
+ (inits,) = tree.children
392
+ assert isinstance(inits, Tree)
393
+ items = [self.build(child, depth, pos) for child in inits.children]
394
+ return MapLit(pos, tuple(zip(items[0::2], items[1::2], strict=True)))
395
+
396
+ def _literal(self, tree: Tree[Token], depth: int, pos: Position) -> Node:
397
+ (token,) = tree.children
398
+ assert isinstance(token, Token)
399
+ kind = token.type
400
+ text = str(token.value)
401
+ if kind == "INT_LIT":
402
+ negative = text.startswith("-")
403
+ digits = text.lstrip("-")
404
+ value = int(digits, 16) if digits[:2].lower() == "0x" else int(digits, 10)
405
+ value = -value if negative else value
406
+ if not INT64_MIN <= value <= INT64_MAX:
407
+ raise syntax_error(f"integer literal {text} is outside the int64 range", pos)
408
+ return Literal(pos, value, INT)
409
+ if kind == "UINT_LIT":
410
+ raise syntax_error("unsigned integers are not supported; use an int literal", pos)
411
+ if kind == "FLOAT_LIT":
412
+ return Literal(pos, float(text), DOUBLE)
413
+ if kind in ("STRING_LIT", "MLSTRING_LIT"):
414
+ try:
415
+ string = str(celstr(token))
416
+ except ValueError as error:
417
+ raise syntax_error(f"invalid string literal: {error}", pos) from None
418
+ return Literal(pos, string, string_values(frozenset({string}), enum=False))
419
+ if kind == "BYTES_LIT":
420
+ try:
421
+ data = bytes(celbytes(token))
422
+ except ValueError as error:
423
+ raise syntax_error(f"invalid bytes literal: {error}", pos) from None
424
+ return Literal(pos, data, BYTES)
425
+ if kind == "BOOL_LIT":
426
+ return Literal(pos, text == "true", BOOL)
427
+ if kind == "NULL_LIT":
428
+ return Literal(pos, None, NULL)
429
+ raise syntax_error(f"unsupported literal {text!r}", pos)
430
+
431
+
432
+ def _tree_position(tree: Tree[Token], fallback: Position) -> Position:
433
+ line = getattr(tree.meta, "line", None)
434
+ column = getattr(tree.meta, "column", None)
435
+ if line is None or column is None:
436
+ return fallback
437
+ return Position(line, column)
438
+
439
+
440
+ def _token_position(token: Token, fallback: Position) -> Position:
441
+ if token.line is None or token.column is None:
442
+ return fallback
443
+ return Position(token.line, token.column)