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,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
|
+
]
|