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
+ """Shared state of one merge run: sides, directives from resolutions, Conflict collection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass, field, fields
8
+ from enum import Enum
9
+ from typing import Any, Final
10
+
11
+ from pydantic import JsonValue
12
+
13
+ from lorebase_core.canonical import ObjectType
14
+ from lorebase_core.merge.conflicts import (
15
+ CHOICES,
16
+ MACHINE_RESOLVABLE,
17
+ AutoResolution,
18
+ Conflict,
19
+ ConflictKind,
20
+ Resolution,
21
+ ResolutionChoice,
22
+ ResolutionError,
23
+ Side,
24
+ conflict_fingerprint,
25
+ )
26
+ from lorebase_core.merge.objects import ObjectStore, SchemaLoader, load_object
27
+ from lorebase_core.merge.policies import (
28
+ ChangeStamp,
29
+ MergePolicy,
30
+ PolicyDecision,
31
+ PolicyName,
32
+ PolicyRule,
33
+ decide_authority,
34
+ decide_newest,
35
+ )
36
+ from lorebase_core.model import Schema
37
+ from lorebase_core.tree import NodeStore, TreeParams
38
+ from lorebase_core.validate import SchemaRules
39
+
40
+ __all__ = [
41
+ "MISSING",
42
+ "SIDES",
43
+ "Context",
44
+ "Directives",
45
+ "Group",
46
+ "LineageLookup",
47
+ "Missing",
48
+ "Payload",
49
+ "Stage",
50
+ "Value",
51
+ "as_json",
52
+ "check_choice",
53
+ "other_side",
54
+ "same",
55
+ ]
56
+
57
+
58
+ class Missing(Enum):
59
+ """Marker for an absent value (JSON ``null`` and absent are the same)."""
60
+
61
+ MISSING = "missing"
62
+
63
+ def __repr__(self) -> str:
64
+ return "MISSING"
65
+
66
+
67
+ MISSING: Final = Missing.MISSING
68
+
69
+ Value = JsonValue | Missing
70
+ Payload = dict[str, JsonValue]
71
+ SIDES: Final = (Side.OURS, Side.THEIRS)
72
+ _ORDER: Final = (Side.BASE, Side.OURS, Side.THEIRS)
73
+ _PREFERABLE = frozenset(
74
+ {
75
+ ConflictKind.ATTRIBUTE_VALUE,
76
+ ConflictKind.DELETE_MODIFY,
77
+ ConflictKind.ADD_ADD,
78
+ ConflictKind.SCHEMA_PROPERTY,
79
+ ConflictKind.SCHEMA_DELETE_MODIFY,
80
+ }
81
+ )
82
+ #: Conflicts on one Attribute: the only ones Lineage-based policies decide.
83
+ _ATTRIBUTE_KINDS = frozenset({ConflictKind.ATTRIBUTE_VALUE, ConflictKind.ADD_ADD})
84
+
85
+ LineageLookup = Callable[[Side, str, str], Mapping[str, ChangeStamp]]
86
+ """``(side, partition, key) -> {attribute id: change stamp}`` of the last change of each
87
+ Attribute of an element on ``ours`` or ``theirs``."""
88
+
89
+
90
+ def same(a: Value, b: Value) -> bool:
91
+ """Structural equality that keeps JSON types apart (``1``, ``1.0`` and ``True`` differ)."""
92
+ if type(a) is not type(b):
93
+ return False
94
+ if isinstance(a, dict):
95
+ assert isinstance(b, dict)
96
+ return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)
97
+ if isinstance(a, list):
98
+ assert isinstance(b, list)
99
+ return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b, strict=True))
100
+ return a == b
101
+
102
+
103
+ def as_json(value: Value) -> JsonValue:
104
+ return None if value is MISSING else value
105
+
106
+
107
+ class Stage(Enum):
108
+ """When a grouped Conflict is found.
109
+
110
+ ``DECISION`` Conflicts are settled by their resolution. ``CHECK`` Conflicts are found by
111
+ re-checking the merged state; they are reported again while a resolution leaves the
112
+ state invalid.
113
+ """
114
+
115
+ DECISION = "decision"
116
+ CHECK = "check"
117
+
118
+
119
+ @dataclass
120
+ class Directives:
121
+ """Changes to the merge derived from resolutions of cross-cutting and schema Conflicts.
122
+
123
+ They apply on the next run of the merge: schema element and property overrides, sides
124
+ whose changes are ignored for a partition or an Attribute, and data state overrides.
125
+ """
126
+
127
+ elements: dict[str, Payload | None] = field(default_factory=dict)
128
+ properties: dict[tuple[str, tuple[str | int, ...]], Value] = field(default_factory=dict)
129
+ trait_removals: set[tuple[str, str]] = field(default_factory=set)
130
+ trait_mappings: dict[tuple[str, str], Payload] = field(default_factory=dict)
131
+ ignored_partitions: set[tuple[Side, str]] = field(default_factory=set)
132
+ ignored_attributes: set[tuple[Side, str, str]] = field(default_factory=set)
133
+ states: dict[tuple[str, str], Payload | None] = field(default_factory=dict)
134
+ values: dict[tuple[str, str, str], Value] = field(default_factory=dict)
135
+
136
+ def _parts(self) -> list[Any]:
137
+ return [getattr(self, f.name) for f in fields(self)]
138
+
139
+ def covers(self, other: Directives) -> bool:
140
+ """Whether every directive of ``other`` is already part of this set."""
141
+ for mine, theirs in zip(self._parts(), other._parts(), strict=True):
142
+ if isinstance(mine, set):
143
+ assert isinstance(theirs, set)
144
+ if not theirs <= mine:
145
+ return False
146
+ continue
147
+ assert isinstance(mine, dict)
148
+ assert isinstance(theirs, dict)
149
+ for key, value in theirs.items():
150
+ if key not in mine or not _same_directive(mine[key], value):
151
+ return False
152
+ return True
153
+
154
+ def absorb(self, other: Directives) -> bool:
155
+ """Add the directives of ``other``; return whether anything changed."""
156
+ if self.covers(other):
157
+ return False
158
+ for mine, theirs in zip(self._parts(), other._parts(), strict=True):
159
+ if isinstance(mine, set):
160
+ assert isinstance(theirs, set)
161
+ mine |= theirs
162
+ else:
163
+ assert isinstance(mine, dict)
164
+ assert isinstance(theirs, dict)
165
+ mine.update(theirs)
166
+ return True
167
+
168
+ def copy(self) -> Directives:
169
+ return Directives(**{f.name: copy.copy(getattr(self, f.name)) for f in fields(self)})
170
+
171
+
172
+ def _same_directive(a: object, b: object) -> bool:
173
+ if isinstance(a, Missing) or isinstance(b, Missing):
174
+ return a is b
175
+ return same(a, b) # type: ignore[arg-type]
176
+
177
+
178
+ @dataclass
179
+ class Group:
180
+ """A Conflict being collected: grouped Conflicts gather members during the run."""
181
+
182
+ kind: ConflictKind
183
+ partition: str
184
+ key: str | None
185
+ attribute: str | None
186
+ stage: Stage
187
+ #: Schema element whose three versions identify the Conflict (hashes and values).
188
+ element: str | None
189
+ values: tuple[JsonValue, JsonValue, JsonValue] = (None, None, None)
190
+ message: str = ""
191
+ details: dict[str, JsonValue] = field(default_factory=dict)
192
+ members: dict[str, JsonValue] = field(default_factory=dict)
193
+ #: The side whose data conflicts with the other side's schema change, if any.
194
+ data_side: Side | None = None
195
+
196
+
197
+ @dataclass
198
+ class Context:
199
+ """Inputs and outputs of one run of the merge pipeline."""
200
+
201
+ store: NodeStore
202
+ objects: ObjectStore
203
+ policy: MergePolicy
204
+ params: TreeParams
205
+ schema_loader: SchemaLoader
206
+ roots: dict[Side, bytes | None]
207
+ schemas: dict[Side, Schema | None]
208
+ resolutions: Mapping[str, Resolution]
209
+ directives: Directives
210
+ virtual: bool = False
211
+ #: Options of the static checks of the merged schema.
212
+ rules: SchemaRules = field(default_factory=SchemaRules)
213
+ #: Lineage of the sides, read by the ``authority`` and ``newest_wins`` policies.
214
+ lineage: LineageLookup | None = None
215
+ conflicts: list[Conflict] = field(default_factory=list)
216
+ auto: list[AutoResolution] = field(default_factory=list)
217
+ out: Directives = field(default_factory=Directives)
218
+ groups: dict[tuple[object, ...], Group] = field(default_factory=dict)
219
+ #: Partitions whose element has an open schema Conflict: no cross-cutting Conflicts.
220
+ blocked: set[str] = field(default_factory=set)
221
+ #: Partitions whose element has an open delete/modify Conflict: no data Conflicts either.
222
+ frozen: set[str] = field(default_factory=set)
223
+ #: Elements with an open data Conflict: left out of the consistency checks.
224
+ open_keys: set[tuple[str, str]] = field(default_factory=set)
225
+ #: Schema elements with an open schema Conflict: no static checks on them.
226
+ open_elements: set[str] = field(default_factory=set)
227
+ #: Full member keys of the grouped Conflicts reported, by fingerprint.
228
+ members: dict[str, tuple[str, ...]] = field(default_factory=dict)
229
+ #: Static schema problems already present on ours or theirs: ``(code, path)``.
230
+ known_issues: set[tuple[object, tuple[object, ...]]] = field(default_factory=set)
231
+ schema_hashes: dict[Side, dict[str, bytes]] = field(default_factory=dict)
232
+ _documents: dict[bytes, Payload] = field(default_factory=dict)
233
+
234
+ # Objects --------------------------------------------------------------------
235
+
236
+ def document(self, digest: bytes | None) -> Payload | None:
237
+ """The payload of an object, cached for the run."""
238
+ if digest is None:
239
+ return None
240
+ payload = self._documents.get(digest)
241
+ if payload is None:
242
+ payload = self._documents[digest] = load_object(self.objects, digest)
243
+ return payload
244
+
245
+ def put(self, object_type: ObjectType, payload: Payload) -> bytes:
246
+ digest = self.objects.put(object_type, payload)
247
+ self._documents.setdefault(digest, payload)
248
+ return digest
249
+
250
+ def element_hashes(self, key: str | None) -> tuple[bytes | None, bytes | None, bytes | None]:
251
+ if key is None:
252
+ return (None, None, None)
253
+ return (
254
+ self.schema_hashes[Side.BASE].get(key),
255
+ self.schema_hashes[Side.OURS].get(key),
256
+ self.schema_hashes[Side.THEIRS].get(key),
257
+ )
258
+
259
+ def element_document(self, side: Side, key: str) -> Payload | None:
260
+ return self.document(self.schema_hashes[side].get(key))
261
+
262
+ # Conflicts ------------------------------------------------------------------
263
+
264
+ def conflict(
265
+ self,
266
+ kind: ConflictKind,
267
+ partition: str,
268
+ key: str | None,
269
+ attribute: str | None,
270
+ values: tuple[JsonValue, JsonValue, JsonValue],
271
+ hashes: tuple[bytes | None, bytes | None, bytes | None],
272
+ *,
273
+ members: list[str] | None = None,
274
+ message: str = "",
275
+ details: dict[str, JsonValue] | None = None,
276
+ ) -> Conflict:
277
+ ordered = sorted(members or ())
278
+ return Conflict(
279
+ kind=kind,
280
+ fingerprint=conflict_fingerprint(kind, partition, key, attribute, hashes, ordered),
281
+ partition=partition,
282
+ key=key,
283
+ attribute=attribute,
284
+ base=values[0],
285
+ ours=values[1],
286
+ theirs=values[2],
287
+ base_hash=None if hashes[0] is None else hashes[0].hex(),
288
+ ours_hash=None if hashes[1] is None else hashes[1].hex(),
289
+ theirs_hash=None if hashes[2] is None else hashes[2].hex(),
290
+ count=max(1, len(ordered)),
291
+ sample=tuple(ordered[: self.policy.sample_size]),
292
+ message=message,
293
+ details=details or {},
294
+ choices=CHOICES[kind],
295
+ machine_resolvable=kind in MACHINE_RESOLVABLE,
296
+ )
297
+
298
+ def decide(
299
+ self,
300
+ conflict: Conflict,
301
+ element: str | None,
302
+ attribute: str | None,
303
+ *,
304
+ attribute_authority: str | None = None,
305
+ ) -> Resolution | None:
306
+ """The resolution of ``conflict``: given by a resolver, else by policy, else none.
307
+
308
+ Policy rules are tried in the order of ``MergePolicy.rules``; the first that decides
309
+ resolves the Conflict and is recorded as an automatic resolution with its reasons.
310
+ """
311
+ resolution = self.resolutions.get(conflict.fingerprint)
312
+ if resolution is not None:
313
+ check_choice(conflict, resolution)
314
+ return resolution
315
+ if self.virtual or conflict.kind not in _PREFERABLE:
316
+ return None
317
+ rules = self.policy.rules(element, attribute, attribute_authority=attribute_authority)
318
+ for rule in rules:
319
+ decision = self._apply(rule, conflict)
320
+ if decision is None:
321
+ continue
322
+ choice = ResolutionChoice(decision.side.value)
323
+ resolver = decision.name.resolver
324
+ self.record_auto(conflict, choice, resolver, details=decision.details)
325
+ return Resolution(fingerprint=conflict.fingerprint, choice=choice, resolver=resolver)
326
+ return None
327
+
328
+ def _apply(self, rule: PolicyRule, conflict: Conflict) -> PolicyDecision | None:
329
+ if rule.name is PolicyName.PREFER:
330
+ assert rule.side is not None
331
+ return PolicyDecision(PolicyName.PREFER, rule.side)
332
+ if conflict.kind not in _ATTRIBUTE_KINDS:
333
+ return None
334
+ ours = self.lineage_entry(Side.OURS, conflict)
335
+ theirs = self.lineage_entry(Side.THEIRS, conflict)
336
+ if rule.name is PolicyName.AUTHORITY:
337
+ return decide_authority(rule.ranking, ours, theirs)
338
+ return decide_newest(ours, theirs)
339
+
340
+ def lineage_entry(self, side: Side, conflict: Conflict) -> ChangeStamp | None:
341
+ """The stamp of ``side``'s last change of the Conflict's Attribute."""
342
+ if self.lineage is None or conflict.key is None or conflict.attribute is None:
343
+ return None
344
+ return self.lineage(side, conflict.partition, conflict.key).get(conflict.attribute)
345
+
346
+ def record_auto(
347
+ self,
348
+ conflict: Conflict,
349
+ choice: ResolutionChoice,
350
+ resolver: str,
351
+ *,
352
+ details: Mapping[str, JsonValue] | None = None,
353
+ ) -> None:
354
+ if self.virtual:
355
+ return
356
+ self.auto.append(
357
+ AutoResolution(
358
+ kind=conflict.kind,
359
+ fingerprint=conflict.fingerprint,
360
+ partition=conflict.partition,
361
+ key=conflict.key,
362
+ attribute=conflict.attribute,
363
+ choice=choice,
364
+ resolver=resolver,
365
+ count=conflict.count,
366
+ sample=conflict.sample,
367
+ details=dict(details or {}),
368
+ )
369
+ )
370
+
371
+ def report(self, conflict: Conflict) -> None:
372
+ if not self.virtual:
373
+ self.conflicts.append(conflict)
374
+
375
+ def group(
376
+ self,
377
+ kind: ConflictKind,
378
+ partition: str,
379
+ key: str | None,
380
+ attribute: str | None,
381
+ *,
382
+ stage: Stage,
383
+ element: str | None,
384
+ message: str,
385
+ data_side: Side | None = None,
386
+ details: dict[str, JsonValue] | None = None,
387
+ ) -> Group:
388
+ """The Conflict group for this location, created on first use."""
389
+ group_key = (kind, partition, key, attribute, stage)
390
+ found = self.groups.get(group_key)
391
+ if found is None:
392
+ found = self.groups[group_key] = Group(
393
+ kind=kind,
394
+ partition=partition,
395
+ key=key,
396
+ attribute=attribute,
397
+ stage=stage,
398
+ element=element,
399
+ values=self.element_values(element, attribute),
400
+ message=message,
401
+ details=dict(details or {}),
402
+ data_side=data_side,
403
+ )
404
+ return found
405
+
406
+ def property_values(self, element: str, name: str) -> tuple[JsonValue, JsonValue, JsonValue]:
407
+ """The three versions of one top-level property of a schema element."""
408
+ values = [
409
+ None if document is None else document.get(name)
410
+ for document in (self.element_document(side, element) for side in _ORDER)
411
+ ]
412
+ return (values[0], values[1], values[2])
413
+
414
+ def element_values(
415
+ self, element: str | None, attribute: str | None
416
+ ) -> tuple[JsonValue, JsonValue, JsonValue]:
417
+ """The three versions of an element's Attribute definition, or of the element."""
418
+ if element is None:
419
+ return (None, None, None)
420
+ values: list[JsonValue] = []
421
+ for side in (Side.BASE, Side.OURS, Side.THEIRS):
422
+ document = self.element_document(side, element)
423
+ if document is None:
424
+ values.append(None)
425
+ continue
426
+ attributes = document.get("attributes")
427
+ if attribute is not None and isinstance(attributes, dict):
428
+ values.append(attributes.get(attribute))
429
+ else:
430
+ values.append(document.get("api_name"))
431
+ return (values[0], values[1], values[2])
432
+
433
+
434
+ def other_side(side: Side) -> Side:
435
+ return Side.THEIRS if side is Side.OURS else Side.OURS
436
+
437
+
438
+ def check_choice(conflict: Conflict, resolution: Resolution) -> None:
439
+ if resolution.choice not in CHOICES[conflict.kind]:
440
+ raise ResolutionError(
441
+ f"{resolution.choice} does not resolve a {conflict.kind} conflict "
442
+ f"({conflict.fingerprint})"
443
+ )
@@ -0,0 +1,245 @@
1
+ """Schema element models, Records, Links and Lineage entries (design 5)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from lorebase_core.model.attribute import Attribute, SearchWeight, Sensitivity
8
+ from lorebase_core.model.base import (
9
+ ElementStatus,
10
+ Model,
11
+ NameStyle,
12
+ SchemaElement,
13
+ is_valid_api_name,
14
+ snake_case,
15
+ )
16
+ from lorebase_core.model.derivation import (
17
+ AggregateDerivation,
18
+ AggregateFunction,
19
+ Derivation,
20
+ DerivationDefinition,
21
+ ExpressionDerivation,
22
+ PathDerivation,
23
+ PathStep,
24
+ )
25
+ from lorebase_core.model.domain import Domain, DomainExample, DomainSlug
26
+ from lorebase_core.model.ids import (
27
+ AttributeId,
28
+ DerivationId,
29
+ IdPrefix,
30
+ KindId,
31
+ OperationId,
32
+ ProvenanceId,
33
+ RelationId,
34
+ SourceId,
35
+ TraitId,
36
+ id_prefix,
37
+ is_ulid,
38
+ new_id,
39
+ new_ulid,
40
+ )
41
+ from lorebase_core.model.kind import BuiltinOperation, Kind, KindSearch, TraitImplementation
42
+ from lorebase_core.model.lineage import (
43
+ ExtractionOrigin,
44
+ ImportOrigin,
45
+ LineageMap,
46
+ MergeResolutionOrigin,
47
+ OperationOrigin,
48
+ Origin,
49
+ OriginType,
50
+ ProvenanceEntry,
51
+ SchemaMigrationOrigin,
52
+ SourceSyncOrigin,
53
+ )
54
+ from lorebase_core.model.operation import (
55
+ CreateLinkStep,
56
+ CreateRecordStep,
57
+ DeleteLinkStep,
58
+ DeleteRecordStep,
59
+ Expression,
60
+ Operation,
61
+ OperationParameter,
62
+ OperationStep,
63
+ Precondition,
64
+ RateLimit,
65
+ ReviewMode,
66
+ ReviewPolicy,
67
+ UpdateRecordStep,
68
+ builtin_link_operation_names,
69
+ builtin_operation_names,
70
+ )
71
+ from lorebase_core.model.paths import PathHop, PathRelation
72
+ from lorebase_core.model.record import (
73
+ Link,
74
+ LinkIdentity,
75
+ Record,
76
+ RecordIdentity,
77
+ format_ref,
78
+ parse_ref,
79
+ record_key,
80
+ render_key,
81
+ )
82
+ from lorebase_core.model.relation import (
83
+ Cardinality,
84
+ LinkDirection,
85
+ OnDelete,
86
+ Relation,
87
+ RelationBacking,
88
+ )
89
+ from lorebase_core.model.schema import Schema
90
+ from lorebase_core.model.source import (
91
+ SECRET_REFERENCE_PREFIX,
92
+ AttributeMapping,
93
+ HttpOptions,
94
+ HttpPagination,
95
+ MergePolicy,
96
+ PaginationType,
97
+ RelationMapping,
98
+ Source,
99
+ SourceType,
100
+ SyncMode,
101
+ TargetBranchPolicy,
102
+ Transform,
103
+ TransformName,
104
+ )
105
+ from lorebase_core.model.trait import Trait, TraitAttribute
106
+ from lorebase_core.model.vault import (
107
+ VAULT_KEY,
108
+ looks_like_reference,
109
+ vault_reference,
110
+ vault_token,
111
+ )
112
+ from lorebase_core.model.yaml_schema import (
113
+ SCHEMA_FORMAT,
114
+ SchemaYamlError,
115
+ SchemaYamlProblem,
116
+ dump_schema,
117
+ load_schema,
118
+ locate_issues,
119
+ schema_from_document,
120
+ schema_to_document,
121
+ validate_schema_yaml,
122
+ yaml_path,
123
+ )
124
+
125
+ if TYPE_CHECKING:
126
+ from lorebase_core.validate.issues import Issue
127
+ from lorebase_core.validate.schema_rules import SchemaRules
128
+
129
+
130
+ def validate_schema(schema: Schema, rules: SchemaRules | None = None) -> list[Issue]:
131
+ """Static validation of a Schema; see ``lorebase_core.validate.schema_rules``."""
132
+ from lorebase_core.validate.schema_rules import validate_schema as _validate_schema
133
+
134
+ return _validate_schema(schema, rules)
135
+
136
+
137
+ __all__ = [
138
+ "SCHEMA_FORMAT",
139
+ "SECRET_REFERENCE_PREFIX",
140
+ "VAULT_KEY",
141
+ "AggregateDerivation",
142
+ "AggregateFunction",
143
+ "Attribute",
144
+ "AttributeId",
145
+ "AttributeMapping",
146
+ "BuiltinOperation",
147
+ "Cardinality",
148
+ "CreateLinkStep",
149
+ "CreateRecordStep",
150
+ "DeleteLinkStep",
151
+ "DeleteRecordStep",
152
+ "Derivation",
153
+ "DerivationDefinition",
154
+ "DerivationId",
155
+ "Domain",
156
+ "DomainExample",
157
+ "DomainSlug",
158
+ "ElementStatus",
159
+ "Expression",
160
+ "ExpressionDerivation",
161
+ "ExtractionOrigin",
162
+ "HttpOptions",
163
+ "HttpPagination",
164
+ "IdPrefix",
165
+ "ImportOrigin",
166
+ "Kind",
167
+ "KindId",
168
+ "KindSearch",
169
+ "LineageMap",
170
+ "Link",
171
+ "LinkDirection",
172
+ "LinkIdentity",
173
+ "MergePolicy",
174
+ "MergeResolutionOrigin",
175
+ "Model",
176
+ "NameStyle",
177
+ "OnDelete",
178
+ "Operation",
179
+ "OperationId",
180
+ "OperationOrigin",
181
+ "OperationParameter",
182
+ "OperationStep",
183
+ "Origin",
184
+ "OriginType",
185
+ "PaginationType",
186
+ "PathDerivation",
187
+ "PathHop",
188
+ "PathRelation",
189
+ "PathStep",
190
+ "Precondition",
191
+ "ProvenanceEntry",
192
+ "ProvenanceId",
193
+ "RateLimit",
194
+ "Record",
195
+ "RecordIdentity",
196
+ "Relation",
197
+ "RelationBacking",
198
+ "RelationId",
199
+ "RelationMapping",
200
+ "ReviewMode",
201
+ "ReviewPolicy",
202
+ "Schema",
203
+ "SchemaElement",
204
+ "SchemaMigrationOrigin",
205
+ "SchemaYamlError",
206
+ "SchemaYamlProblem",
207
+ "SearchWeight",
208
+ "Sensitivity",
209
+ "Source",
210
+ "SourceId",
211
+ "SourceSyncOrigin",
212
+ "SourceType",
213
+ "SyncMode",
214
+ "TargetBranchPolicy",
215
+ "Trait",
216
+ "TraitAttribute",
217
+ "TraitId",
218
+ "TraitImplementation",
219
+ "Transform",
220
+ "TransformName",
221
+ "UpdateRecordStep",
222
+ "builtin_link_operation_names",
223
+ "builtin_operation_names",
224
+ "dump_schema",
225
+ "format_ref",
226
+ "id_prefix",
227
+ "is_ulid",
228
+ "is_valid_api_name",
229
+ "load_schema",
230
+ "locate_issues",
231
+ "looks_like_reference",
232
+ "new_id",
233
+ "new_ulid",
234
+ "parse_ref",
235
+ "record_key",
236
+ "render_key",
237
+ "schema_from_document",
238
+ "schema_to_document",
239
+ "snake_case",
240
+ "validate_schema",
241
+ "validate_schema_yaml",
242
+ "vault_reference",
243
+ "vault_token",
244
+ "yaml_path",
245
+ ]