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 @@
1
+ __version__ = "0.2.3"
@@ -0,0 +1,35 @@
1
+ """Canonical JSON (RFC 8785 with Lorebase rules) and content hashing."""
2
+
3
+ from lorebase_core.canonical.hashing import (
4
+ HASH_SCHEME,
5
+ HASH_SIZE,
6
+ NODE_HASH_TYPE,
7
+ REVISION_HASH_TYPE,
8
+ ObjectType,
9
+ canonical_bytes,
10
+ from_hex,
11
+ hex,
12
+ node_hash,
13
+ normalize,
14
+ object_hash,
15
+ revision_hash,
16
+ )
17
+ from lorebase_core.canonical.jcs import CanonicalError, format_number, jcs_bytes
18
+
19
+ __all__ = [
20
+ "HASH_SCHEME",
21
+ "HASH_SIZE",
22
+ "NODE_HASH_TYPE",
23
+ "REVISION_HASH_TYPE",
24
+ "CanonicalError",
25
+ "ObjectType",
26
+ "canonical_bytes",
27
+ "format_number",
28
+ "from_hex",
29
+ "hex",
30
+ "jcs_bytes",
31
+ "node_hash",
32
+ "normalize",
33
+ "object_hash",
34
+ "revision_hash",
35
+ ]
@@ -0,0 +1,128 @@
1
+ """Lorebase canonical form and content hashing (design 6.1, AC-09)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import unicodedata
7
+ from collections.abc import Mapping, Sequence
8
+ from enum import StrEnum
9
+
10
+ from lorebase_core.canonical.jcs import CanonicalError, jcs_bytes
11
+
12
+ __all__ = [
13
+ "HASH_SCHEME",
14
+ "HASH_SIZE",
15
+ "NODE_HASH_TYPE",
16
+ "REVISION_HASH_TYPE",
17
+ "ObjectType",
18
+ "canonical_bytes",
19
+ "from_hex",
20
+ "hex",
21
+ "node_hash",
22
+ "normalize",
23
+ "object_hash",
24
+ "revision_hash",
25
+ ]
26
+
27
+ HASH_SCHEME = "lb1"
28
+ """Version tag of the hashing scheme. Changing any hashing rule requires a new tag."""
29
+
30
+ HASH_SIZE = hashlib.sha256().digest_size
31
+ """Size in bytes of every Lorebase hash."""
32
+
33
+ NODE_HASH_TYPE = "node"
34
+ REVISION_HASH_TYPE = "revision"
35
+
36
+ _SEPARATOR = b"\x00"
37
+
38
+
39
+ class ObjectType(StrEnum):
40
+ """Types of content-addressed objects (design 6.1)."""
41
+
42
+ SCHEMA_KIND = "schema.kind"
43
+ SCHEMA_RELATION = "schema.relation"
44
+ SCHEMA_TRAIT = "schema.trait"
45
+ SCHEMA_OPERATION = "schema.operation"
46
+ SCHEMA_DERIVATION = "schema.derivation"
47
+ SCHEMA_SOURCE = "schema.source"
48
+ SCHEMA_DOMAIN = "schema.domain"
49
+ RECORD = "record"
50
+ LINK = "link"
51
+
52
+
53
+ _HASHED_TYPES = frozenset({*(t.value for t in ObjectType), REVISION_HASH_TYPE})
54
+
55
+
56
+ def normalize(value: object) -> object:
57
+ """Apply the Lorebase value rules before RFC 8785 serialization.
58
+
59
+ Strings (including object keys) are NFC-normalized and object members whose
60
+ value is ``None`` are omitted, since null and absent are the same. ``None``
61
+ inside arrays is kept, because array positions are significant.
62
+ Integers and decimals of Attribute values are expected to arrive already
63
+ encoded as canonical strings; timestamps are taken as given.
64
+ """
65
+ if isinstance(value, str):
66
+ return unicodedata.normalize("NFC", value)
67
+ if isinstance(value, Mapping):
68
+ result: dict[str, object] = {}
69
+ for key, item in value.items():
70
+ if not isinstance(key, str):
71
+ raise CanonicalError(f"object keys must be strings, got {type(key).__name__}")
72
+ if item is None:
73
+ continue
74
+ nfc_key = unicodedata.normalize("NFC", key)
75
+ if nfc_key in result:
76
+ raise CanonicalError(f"duplicate object key after NFC normalization: {nfc_key!r}")
77
+ result[nfc_key] = normalize(item)
78
+ return result
79
+ if isinstance(value, Sequence) and not isinstance(value, bytes | bytearray):
80
+ return [normalize(item) for item in value]
81
+ return value
82
+
83
+
84
+ def canonical_bytes(value: object) -> bytes:
85
+ """Return the Lorebase canonical JSON bytes of ``value`` (RFC 8785 plus Lorebase rules)."""
86
+ return jcs_bytes(normalize(value))
87
+
88
+
89
+ def object_hash(object_type: str, value: object) -> bytes:
90
+ """Return ``sha256("lb1\\x00" + object_type + "\\x00" + canonical_bytes(value))``.
91
+
92
+ ``object_type`` is an :class:`ObjectType` or ``"revision"``.
93
+ """
94
+ if object_type not in _HASHED_TYPES:
95
+ raise ValueError(f"unknown object type: {object_type!r}")
96
+ return _scoped_hash(str(object_type), canonical_bytes(value))
97
+
98
+
99
+ def revision_hash(value: object) -> bytes:
100
+ """Hash a canonical Revision payload (design 6.4)."""
101
+ return _scoped_hash(REVISION_HASH_TYPE, canonical_bytes(value))
102
+
103
+
104
+ def node_hash(body: bytes) -> bytes:
105
+ """Hash an encoded tree node: ``sha256("lb1\\x00node\\x00" + body)`` (design 6.2)."""
106
+ return _scoped_hash(NODE_HASH_TYPE, body)
107
+
108
+
109
+ def hex(h: bytes) -> str:
110
+ """Render a hash as lowercase hex, the API representation."""
111
+ return h.hex()
112
+
113
+
114
+ def from_hex(text: str) -> bytes:
115
+ """Parse a lowercase hex hash produced by :func:`hex`."""
116
+ if len(text) != 2 * HASH_SIZE or text != text.lower():
117
+ raise ValueError(f"expected {2 * HASH_SIZE} lowercase hex characters")
118
+ return bytes.fromhex(text)
119
+
120
+
121
+ def _scoped_hash(scope: str, body: bytes) -> bytes:
122
+ digest = hashlib.sha256()
123
+ digest.update(HASH_SCHEME.encode("ascii"))
124
+ digest.update(_SEPARATOR)
125
+ digest.update(scope.encode("ascii"))
126
+ digest.update(_SEPARATOR)
127
+ digest.update(body)
128
+ return digest.digest()
@@ -0,0 +1,162 @@
1
+ """RFC 8785 JSON Canonicalization Scheme (JCS) serializer.
2
+
3
+ This module implements the plain RFC 8785 rules. The Lorebase value rules
4
+ (NFC strings, omitted nulls) are applied on top of it by
5
+ :func:`lorebase_core.canonical.canonical_bytes`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ import re
12
+ from collections.abc import Mapping, Sequence
13
+ from decimal import Decimal
14
+
15
+ __all__ = ["CanonicalError", "format_number", "jcs_bytes"]
16
+
17
+ # ECMAScript Number::toString switches to exponential notation outside
18
+ # 1e-7 < |x| < 1e21 (RFC 8785, section 3.2.2.3).
19
+ _MAX_PLAIN_EXPONENT = 21
20
+ _MIN_PLAIN_EXPONENT = -6
21
+
22
+ _ESCAPES = {
23
+ 0x08: "\\b",
24
+ 0x09: "\\t",
25
+ 0x0A: "\\n",
26
+ 0x0C: "\\f",
27
+ 0x0D: "\\r",
28
+ 0x22: '\\"',
29
+ 0x5C: "\\\\",
30
+ }
31
+ _FIRST_PRINTABLE = 0x20
32
+ #: Characters `_write_string` escapes; a string without any is written as it is.
33
+ _NEEDS_ESCAPE = re.compile(r'[\x00-\x1f"\\]')
34
+
35
+
36
+ class CanonicalError(ValueError):
37
+ """Raised when a value cannot be represented in canonical JSON."""
38
+
39
+
40
+ def jcs_bytes(value: object) -> bytes:
41
+ """Serialize a JSON-compatible value to RFC 8785 canonical UTF-8 bytes.
42
+
43
+ Accepted types: ``None``, ``bool``, ``int`` (only if its decimal text is
44
+ the canonical text of an IEEE 754 double), ``float`` (finite), ``str``, mappings with ``str``
45
+ keys, lists and tuples.
46
+ """
47
+ parts: list[str] = []
48
+ _write(value, parts)
49
+ try:
50
+ return "".join(parts).encode("utf-8")
51
+ except UnicodeEncodeError as exc:
52
+ raise CanonicalError("string contains a lone surrogate") from exc
53
+
54
+
55
+ def format_number(value: float) -> str:
56
+ """Format a number with the ECMAScript Number::toString rules (RFC 8785 3.2.2.3)."""
57
+ if isinstance(value, bool):
58
+ raise CanonicalError("booleans are not numbers")
59
+ if isinstance(value, int):
60
+ # RFC 8785 numbers are IEEE 754 doubles. An integer is accepted only when
61
+ # its decimal text is already the canonical text of its double, so no
62
+ # digit is silently changed; other integers must be passed as strings.
63
+ try:
64
+ text = format_number(float(value))
65
+ except OverflowError:
66
+ text = ""
67
+ if text != str(value):
68
+ raise CanonicalError(
69
+ f"integer {value} is not exactly representable; pass it as a string"
70
+ )
71
+ return text
72
+ if not math.isfinite(value):
73
+ raise CanonicalError("NaN and Infinity are not valid JSON numbers")
74
+ if value == 0:
75
+ return "0"
76
+ sign = "-" if value < 0 else ""
77
+ # repr() yields the shortest digit string that round-trips, which is the
78
+ # digit selection ECMAScript requires.
79
+ _, digit_tuple, exponent = Decimal(repr(abs(value))).as_tuple()
80
+ assert isinstance(exponent, int)
81
+ digits = "".join(str(d) for d in digit_tuple).rstrip("0")
82
+ exponent += len(digit_tuple) - len(digits)
83
+ k = len(digits)
84
+ n = exponent + k # value = 0.digits * 10**n
85
+ if k <= n <= _MAX_PLAIN_EXPONENT:
86
+ body = digits + "0" * (n - k)
87
+ elif 0 < n <= _MAX_PLAIN_EXPONENT:
88
+ body = digits[:n] + "." + digits[n:]
89
+ elif _MIN_PLAIN_EXPONENT < n <= 0:
90
+ body = "0." + "0" * (-n) + digits
91
+ else:
92
+ e = n - 1
93
+ e_text = f"+{e}" if e >= 0 else str(e)
94
+ mantissa = digits if k == 1 else digits[0] + "." + digits[1:]
95
+ body = f"{mantissa}e{e_text}"
96
+ return sign + body
97
+
98
+
99
+ def _write(value: object, out: list[str]) -> None:
100
+ if value is None:
101
+ out.append("null")
102
+ elif value is True:
103
+ out.append("true")
104
+ elif value is False:
105
+ out.append("false")
106
+ elif isinstance(value, int | float):
107
+ out.append(format_number(value))
108
+ elif isinstance(value, str):
109
+ _write_string(value, out)
110
+ elif isinstance(value, Mapping):
111
+ _write_object(value, out)
112
+ elif isinstance(value, Sequence) and not isinstance(value, bytes | bytearray):
113
+ out.append("[")
114
+ for index, item in enumerate(value):
115
+ if index:
116
+ out.append(",")
117
+ _write(item, out)
118
+ out.append("]")
119
+ else:
120
+ raise CanonicalError(f"unsupported type for canonical JSON: {type(value).__name__}")
121
+
122
+
123
+ def _write_object(value: Mapping[object, object], out: list[str]) -> None:
124
+ keys: list[str] = []
125
+ for key in value:
126
+ if not isinstance(key, str):
127
+ raise CanonicalError(f"object keys must be strings, got {type(key).__name__}")
128
+ keys.append(key)
129
+ keys.sort(key=_utf16_sort_key)
130
+ out.append("{")
131
+ for index, key in enumerate(keys):
132
+ if index:
133
+ out.append(",")
134
+ _write_string(key, out)
135
+ out.append(":")
136
+ _write(value[key], out)
137
+ out.append("}")
138
+
139
+
140
+ def _utf16_sort_key(key: str) -> bytes:
141
+ # Big-endian UTF-16 bytes compare exactly like sequences of UTF-16 code units.
142
+ try:
143
+ return key.encode("utf-16-be")
144
+ except UnicodeEncodeError as exc:
145
+ raise CanonicalError("object key contains a lone surrogate") from exc
146
+
147
+
148
+ def _write_string(value: str, out: list[str]) -> None:
149
+ if _NEEDS_ESCAPE.search(value) is None:
150
+ out.append(f'"{value}"')
151
+ return
152
+ out.append('"')
153
+ for char in value:
154
+ code = ord(char)
155
+ escaped = _ESCAPES.get(code)
156
+ if escaped is not None:
157
+ out.append(escaped)
158
+ elif code < _FIRST_PRINTABLE:
159
+ out.append(f"\\u{code:04x}")
160
+ else:
161
+ out.append(char)
162
+ out.append('"')
@@ -0,0 +1,118 @@
1
+ """Expression engine (design 5.7, 5.8, AC-06): CEL parsed by cel-python, type checked against
2
+ the Schema and evaluated in a bounded, deterministic sandbox without I/O.
3
+
4
+ - ``compile(expression, environment)`` parses and type checks; environments come from
5
+ ``operation_environment`` and ``record_environment``.
6
+ - ``compile_operation`` / ``compile_derivation`` compile every expression of an element;
7
+ ``derivation_inputs`` and ``links_read`` tell what a Derivation reads.
8
+ - ``evaluate_precondition``, ``check_preconditions``, ``evaluate_review_policy`` and
9
+ ``plan_steps`` evaluate Operations; ``evaluate_derivation`` and ``matches_filter``
10
+ evaluate Derivations. Records come from a caller-supplied ``RecordResolver``.
11
+ - ``compile_row_expression`` and ``evaluate_row_expression`` handle the ``expression`` of a
12
+ Source's Attribute mapping, over one row of the Source; ``RowExpressions`` compiles them
13
+ once over the Source's columns.
14
+ - ``ExprLimits`` bounds compiling and evaluation; ``ExprError`` reports problems with a
15
+ stable code, the position in the expression and the path in the schema element.
16
+ """
17
+
18
+ from lorebase_core.expr.compiler import CompiledExpression, compile, run
19
+ from lorebase_core.expr.context import EvaluationContext, Principal, activation
20
+ from lorebase_core.expr.derivations import (
21
+ CompiledDerivation,
22
+ compile_derivation,
23
+ derivation_expression_errors,
24
+ derivation_inputs,
25
+ derivation_reads,
26
+ evaluate_derivation,
27
+ links_read,
28
+ matches_filter,
29
+ )
30
+ from lorebase_core.expr.environment import (
31
+ Environment,
32
+ LinkField,
33
+ operation_environment,
34
+ record_environment,
35
+ )
36
+ from lorebase_core.expr.errors import ExprError, ExprErrorCode, Position
37
+ from lorebase_core.expr.limits import ExprLimits
38
+ from lorebase_core.expr.operations import (
39
+ RESERVED_PARAMETER_NAMES,
40
+ CompiledOperation,
41
+ CompiledStep,
42
+ PreconditionFailure,
43
+ StepPlan,
44
+ check_preconditions,
45
+ compile_operation,
46
+ evaluate_precondition,
47
+ evaluate_review_policy,
48
+ operation_expression_errors,
49
+ plan_steps,
50
+ )
51
+ from lorebase_core.expr.resolver import (
52
+ LinkedRecord,
53
+ LinksRequest,
54
+ MemoryResolver,
55
+ RecordNotLoaded,
56
+ RecordRequest,
57
+ RecordResolver,
58
+ )
59
+ from lorebase_core.expr.rows import (
60
+ RowExpressions,
61
+ compile_row_expression,
62
+ evaluate_row_expression,
63
+ row_environment,
64
+ row_value,
65
+ row_variables,
66
+ )
67
+ from lorebase_core.expr.types import ExprType, TypeKind, from_type_spec
68
+
69
+ __all__ = [
70
+ "RESERVED_PARAMETER_NAMES",
71
+ "CompiledDerivation",
72
+ "CompiledExpression",
73
+ "CompiledOperation",
74
+ "CompiledStep",
75
+ "Environment",
76
+ "EvaluationContext",
77
+ "ExprError",
78
+ "ExprErrorCode",
79
+ "ExprLimits",
80
+ "ExprType",
81
+ "LinkField",
82
+ "LinkedRecord",
83
+ "LinksRequest",
84
+ "MemoryResolver",
85
+ "Position",
86
+ "PreconditionFailure",
87
+ "Principal",
88
+ "RecordNotLoaded",
89
+ "RecordRequest",
90
+ "RecordResolver",
91
+ "RowExpressions",
92
+ "StepPlan",
93
+ "TypeKind",
94
+ "activation",
95
+ "check_preconditions",
96
+ "compile",
97
+ "compile_derivation",
98
+ "compile_operation",
99
+ "compile_row_expression",
100
+ "derivation_expression_errors",
101
+ "derivation_inputs",
102
+ "derivation_reads",
103
+ "evaluate_derivation",
104
+ "evaluate_precondition",
105
+ "evaluate_review_policy",
106
+ "evaluate_row_expression",
107
+ "from_type_spec",
108
+ "links_read",
109
+ "matches_filter",
110
+ "operation_environment",
111
+ "operation_expression_errors",
112
+ "plan_steps",
113
+ "record_environment",
114
+ "row_environment",
115
+ "row_value",
116
+ "row_variables",
117
+ "run",
118
+ ]