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