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,187 @@
1
+ """Attribute types (design 5.4): base types and parameterized type specifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import unicodedata
6
+ from enum import StrEnum
7
+ from typing import NamedTuple, Self
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, JsonValue, field_validator, model_validator
10
+
11
+ #: Canonical value as stored in Record and Link content (design 6.1): JSON-compatible.
12
+ CanonicalValue = JsonValue
13
+
14
+ #: ``integer`` is a signed 64-bit integer (design 5.4).
15
+ INT64_MIN = -(2**63)
16
+ INT64_MAX = 2**63 - 1
17
+
18
+ #: Integers inside ``json`` values must be exactly representable as IEEE 754 doubles
19
+ #: so that every JSON implementation reads the same number (I-JSON, RFC 7493).
20
+ JSON_SAFE_INTEGER_MAX = 2**53 - 1
21
+
22
+ #: Prefix and pattern of Kind ids; repeated here so the type system does not depend on
23
+ #: the model package (the model package validates the same shape).
24
+ KIND_ID_PATTERN = r"^knd_[0-7][0-9A-HJKMNP-TV-Z]{25}$"
25
+
26
+
27
+ class AttributeType(StrEnum):
28
+ """Base types of the Lorebase type system."""
29
+
30
+ STRING = "string"
31
+ TEXT = "text"
32
+ URI = "uri"
33
+ ENUM = "enum"
34
+ INTEGER = "integer"
35
+ DECIMAL = "decimal"
36
+ FLOAT = "float"
37
+ BOOLEAN = "boolean"
38
+ DATE = "date"
39
+ TIMESTAMP = "timestamp"
40
+ DURATION = "duration"
41
+ JSON = "json"
42
+ GEO_POINT = "geo_point"
43
+ REF = "ref"
44
+ ARRAY = "array"
45
+
46
+
47
+ #: Types whose values are text in canonical form.
48
+ TEXTUAL_TYPES = frozenset(
49
+ {AttributeType.STRING, AttributeType.TEXT, AttributeType.URI, AttributeType.REF}
50
+ )
51
+
52
+ #: Types with a total order usable by ``min`` and ``max`` constraints.
53
+ ORDERED_TYPES = frozenset(
54
+ {
55
+ AttributeType.INTEGER,
56
+ AttributeType.DECIMAL,
57
+ AttributeType.FLOAT,
58
+ AttributeType.DATE,
59
+ AttributeType.TIMESTAMP,
60
+ AttributeType.DURATION,
61
+ }
62
+ )
63
+
64
+ #: Types that cannot be array elements: arrays hold scalars only (AC-05).
65
+ NON_ELEMENT_TYPES = frozenset({AttributeType.ARRAY, AttributeType.JSON})
66
+
67
+
68
+ class GeoPoint(NamedTuple):
69
+ """Decoded ``geo_point`` value."""
70
+
71
+ lat: float
72
+ lon: float
73
+
74
+
75
+ class TypeSpec(BaseModel):
76
+ """A parameterized type: a base type plus the parameters that type requires.
77
+
78
+ - ``decimal`` requires ``precision`` (total digits, at least 1) and ``scale``
79
+ (fractional digits, ``0 <= scale <= precision``).
80
+ - ``enum`` requires ``enum_values``: a closed, non-empty list of distinct strings.
81
+ - ``array`` requires ``element``: a scalar type spec (no arrays of arrays, no json).
82
+ - ``ref`` requires ``kind_ref``: the id of the Kind whose Record keys it holds.
83
+
84
+ Other parameters must be absent. Constraints (``min``, ``max``, ``pattern`` and so on)
85
+ belong to the Attribute, not to the type (see ``Constraints``).
86
+ """
87
+
88
+ model_config = ConfigDict(frozen=True, extra="forbid")
89
+
90
+ name: AttributeType
91
+ precision: int | None = Field(default=None, ge=1)
92
+ scale: int | None = Field(default=None, ge=0)
93
+ enum_values: tuple[str, ...] | None = None
94
+ element: TypeSpec | None = None
95
+ kind_ref: str | None = Field(default=None, pattern=KIND_ID_PATTERN)
96
+
97
+ @field_validator("enum_values")
98
+ @classmethod
99
+ def _normalize_enum_values(cls, values: tuple[str, ...] | None) -> tuple[str, ...] | None:
100
+ if values is None:
101
+ return None
102
+ normalized = tuple(unicodedata.normalize("NFC", v) for v in values)
103
+ if not normalized:
104
+ raise ValueError("enum_values must not be empty")
105
+ if any(v == "" for v in normalized):
106
+ raise ValueError("enum values must be non-empty strings")
107
+ if len(set(normalized)) != len(normalized):
108
+ raise ValueError("enum values must be distinct")
109
+ return normalized
110
+
111
+ @model_validator(mode="after")
112
+ def _check_parameters(self) -> Self:
113
+ name = self.name
114
+ if name is AttributeType.DECIMAL:
115
+ if self.precision is None or self.scale is None:
116
+ raise ValueError("decimal requires precision and scale")
117
+ if self.scale > self.precision:
118
+ raise ValueError("decimal scale must not exceed precision")
119
+ elif self.precision is not None or self.scale is not None:
120
+ raise ValueError(f"{name} does not take precision or scale")
121
+
122
+ if name is AttributeType.ENUM:
123
+ if self.enum_values is None:
124
+ raise ValueError("enum requires a non-empty enum_values list")
125
+ elif self.enum_values is not None:
126
+ raise ValueError(f"{name} does not take enum_values")
127
+
128
+ if name is AttributeType.ARRAY:
129
+ if self.element is None:
130
+ raise ValueError("array requires an element type")
131
+ if self.element.name in NON_ELEMENT_TYPES:
132
+ raise ValueError(f"array elements must be scalar, not {self.element.name}")
133
+ elif self.element is not None:
134
+ raise ValueError(f"{name} does not take an element type")
135
+
136
+ if name is AttributeType.REF:
137
+ if self.kind_ref is None:
138
+ raise ValueError("ref requires kind_ref")
139
+ elif self.kind_ref is not None:
140
+ raise ValueError(f"{name} does not take kind_ref")
141
+ return self
142
+
143
+ # Convenience constructors -------------------------------------------------
144
+
145
+ @classmethod
146
+ def of(cls, name: AttributeType | str) -> TypeSpec:
147
+ """Spec of a base type without parameters."""
148
+ return cls(name=AttributeType(name))
149
+
150
+ @classmethod
151
+ def decimal(cls, precision: int, scale: int) -> TypeSpec:
152
+ return cls(name=AttributeType.DECIMAL, precision=precision, scale=scale)
153
+
154
+ @classmethod
155
+ def enum(cls, *values: str) -> TypeSpec:
156
+ return cls(name=AttributeType.ENUM, enum_values=tuple(values))
157
+
158
+ @classmethod
159
+ def array(cls, element: TypeSpec) -> TypeSpec:
160
+ return cls(name=AttributeType.ARRAY, element=element)
161
+
162
+ @classmethod
163
+ def ref(cls, kind_id: str) -> TypeSpec:
164
+ return cls(name=AttributeType.REF, kind_ref=kind_id)
165
+
166
+ # Introspection ------------------------------------------------------------
167
+
168
+ @property
169
+ def is_array(self) -> bool:
170
+ return self.name is AttributeType.ARRAY
171
+
172
+ @property
173
+ def scalar(self) -> TypeSpec:
174
+ """The element spec for arrays, the spec itself otherwise."""
175
+ if self.element is not None:
176
+ return self.element
177
+ return self
178
+
179
+ def describe(self) -> str:
180
+ """Human-readable form, for example ``decimal(12,2)`` or ``array<string>``."""
181
+ if self.name is AttributeType.DECIMAL:
182
+ return f"decimal({self.precision},{self.scale})"
183
+ if self.name is AttributeType.ARRAY and self.element is not None:
184
+ return f"array<{self.element.describe()}>"
185
+ if self.name is AttributeType.REF:
186
+ return f"ref({self.kind_ref})"
187
+ return str(self.name)
@@ -0,0 +1,49 @@
1
+ """Schema and data validation (invariant I1)."""
2
+
3
+ from lorebase_core.validate.changes import (
4
+ DataChange,
5
+ LinkDelete,
6
+ LinkUpsert,
7
+ RecordDelete,
8
+ RecordUpsert,
9
+ )
10
+ from lorebase_core.validate.changeset import (
11
+ ChangesetReport,
12
+ IndexView,
13
+ evaluate_changeset,
14
+ validate_changeset,
15
+ )
16
+ from lorebase_core.validate.data_rules import check_values, validate_link, validate_record
17
+ from lorebase_core.validate.issues import Issue, IssueCode, PathPart, Severity, has_errors
18
+ from lorebase_core.validate.schema_rules import (
19
+ DEFAULT_RESERVED_ATTRIBUTE_NAMES,
20
+ KEYABLE_TYPES,
21
+ SchemaRules,
22
+ operation_sensitivity_issues,
23
+ validate_schema,
24
+ )
25
+
26
+ __all__ = [
27
+ "DEFAULT_RESERVED_ATTRIBUTE_NAMES",
28
+ "KEYABLE_TYPES",
29
+ "ChangesetReport",
30
+ "DataChange",
31
+ "IndexView",
32
+ "Issue",
33
+ "IssueCode",
34
+ "LinkDelete",
35
+ "LinkUpsert",
36
+ "PathPart",
37
+ "RecordDelete",
38
+ "RecordUpsert",
39
+ "SchemaRules",
40
+ "Severity",
41
+ "check_values",
42
+ "evaluate_changeset",
43
+ "has_errors",
44
+ "operation_sensitivity_issues",
45
+ "validate_changeset",
46
+ "validate_link",
47
+ "validate_record",
48
+ "validate_schema",
49
+ ]
@@ -0,0 +1,70 @@
1
+ """Data changes validated as one batch: Record and Link upserts and deletes.
2
+
3
+ An upsert carries the complete new content of the Record or Link.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated, Literal
9
+
10
+ from pydantic import Field, field_validator
11
+
12
+ from lorebase_core.model.base import Model
13
+ from lorebase_core.model.ids import KindId, RelationId
14
+ from lorebase_core.model.record import Link, LinkIdentity, Record, RecordIdentity
15
+ from lorebase_core.types import normalize_key
16
+
17
+
18
+ class RecordUpsert(Model):
19
+ op: Literal["upsert_record"] = "upsert_record"
20
+ record: Record
21
+
22
+ @property
23
+ def identity(self) -> RecordIdentity:
24
+ return self.record.identity
25
+
26
+
27
+ class RecordDelete(Model):
28
+ op: Literal["delete_record"] = "delete_record"
29
+ kind: KindId
30
+ key: str
31
+
32
+ @field_validator("key")
33
+ @classmethod
34
+ def _normalize_key(cls, key: str) -> str:
35
+ return normalize_key(key)
36
+
37
+ @property
38
+ def identity(self) -> RecordIdentity:
39
+ return RecordIdentity(self.kind, self.key)
40
+
41
+
42
+ class LinkUpsert(Model):
43
+ op: Literal["upsert_link"] = "upsert_link"
44
+ link: Link
45
+
46
+ @property
47
+ def identity(self) -> LinkIdentity:
48
+ return self.link.identity
49
+
50
+
51
+ class LinkDelete(Model):
52
+ op: Literal["delete_link"] = "delete_link"
53
+ relation: RelationId
54
+ from_key: str = Field(alias="from")
55
+ to_key: str = Field(alias="to")
56
+
57
+ @field_validator("from_key", "to_key")
58
+ @classmethod
59
+ def _normalize_key(cls, key: str) -> str:
60
+ return normalize_key(key)
61
+
62
+ @property
63
+ def identity(self) -> LinkIdentity:
64
+ return LinkIdentity(self.relation, self.from_key, self.to_key)
65
+
66
+
67
+ DataChange = Annotated[
68
+ RecordUpsert | RecordDelete | LinkUpsert | LinkDelete,
69
+ Field(discriminator="op"),
70
+ ]