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,321 @@
|
|
|
1
|
+
"""From collected Conflict groups to reported Conflicts or merge directives (design 6.10).
|
|
2
|
+
|
|
3
|
+
A Conflict whose fingerprint has a resolution turns into directives for the next run of the
|
|
4
|
+
merge (schema overrides, ignored side changes, data overrides). Conflicts found by re-checking
|
|
5
|
+
the merged state are reported again when the applied resolution did not make them go away.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pydantic import JsonValue
|
|
11
|
+
|
|
12
|
+
from lorebase_core.merge.conflicts import (
|
|
13
|
+
GROUPED_KINDS,
|
|
14
|
+
ConflictKind,
|
|
15
|
+
Resolution,
|
|
16
|
+
ResolutionChoice,
|
|
17
|
+
ResolutionError,
|
|
18
|
+
Side,
|
|
19
|
+
)
|
|
20
|
+
from lorebase_core.merge.consistency import relaxed_cardinality
|
|
21
|
+
from lorebase_core.merge.objects import partition_element
|
|
22
|
+
from lorebase_core.merge.schema_merge import SchemaMerge, at_path
|
|
23
|
+
from lorebase_core.merge.state import (
|
|
24
|
+
MISSING,
|
|
25
|
+
SIDES,
|
|
26
|
+
Context,
|
|
27
|
+
Directives,
|
|
28
|
+
Group,
|
|
29
|
+
Payload,
|
|
30
|
+
Stage,
|
|
31
|
+
Value,
|
|
32
|
+
check_choice,
|
|
33
|
+
other_side,
|
|
34
|
+
)
|
|
35
|
+
from lorebase_core.model import Cardinality
|
|
36
|
+
from lorebase_core.tree import PARTITION_SCHEMA, kind_partition, link_key, tree_get
|
|
37
|
+
|
|
38
|
+
__all__ = ["emit_groups"]
|
|
39
|
+
|
|
40
|
+
_C = ResolutionChoice
|
|
41
|
+
_K = ConflictKind
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def emit_groups(ctx: Context, schema: SchemaMerge) -> None:
|
|
45
|
+
"""Report the collected Conflicts, or turn the resolved ones into directives."""
|
|
46
|
+
ordered = sorted(
|
|
47
|
+
ctx.groups.values(),
|
|
48
|
+
key=lambda g: (g.kind.value, g.partition, g.key or "", g.attribute or "", g.stage.value),
|
|
49
|
+
)
|
|
50
|
+
for group in ordered:
|
|
51
|
+
members = sorted(group.members) if group.kind in GROUPED_KINDS else None
|
|
52
|
+
conflict = ctx.conflict(
|
|
53
|
+
group.kind,
|
|
54
|
+
group.partition,
|
|
55
|
+
group.key,
|
|
56
|
+
group.attribute,
|
|
57
|
+
group.values,
|
|
58
|
+
ctx.element_hashes(group.element),
|
|
59
|
+
members=members,
|
|
60
|
+
message=group.message,
|
|
61
|
+
details=group.details,
|
|
62
|
+
)
|
|
63
|
+
resolution = ctx.resolutions.get(conflict.fingerprint)
|
|
64
|
+
directives = None
|
|
65
|
+
if resolution is not None:
|
|
66
|
+
check_choice(conflict, resolution)
|
|
67
|
+
directives = _directives(ctx, schema, group, resolution)
|
|
68
|
+
if directives is None:
|
|
69
|
+
ctx.report(conflict)
|
|
70
|
+
if members is not None:
|
|
71
|
+
ctx.members[conflict.fingerprint] = tuple(members)
|
|
72
|
+
continue
|
|
73
|
+
ctx.out.absorb(directives)
|
|
74
|
+
if group.stage is Stage.CHECK and ctx.directives.covers(directives):
|
|
75
|
+
ctx.report(conflict)
|
|
76
|
+
if members is not None:
|
|
77
|
+
ctx.members[conflict.fingerprint] = tuple(members)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _directives(
|
|
81
|
+
ctx: Context, schema: SchemaMerge, group: Group, resolution: Resolution
|
|
82
|
+
) -> Directives | None:
|
|
83
|
+
"""Directives that carry out ``resolution``; ``None`` when it does not apply."""
|
|
84
|
+
kind, choice = group.kind, resolution.choice
|
|
85
|
+
if group.partition == PARTITION_SCHEMA:
|
|
86
|
+
return _schema_directives(ctx, schema, group, resolution)
|
|
87
|
+
element = partition_element(group.partition)
|
|
88
|
+
if element is None:
|
|
89
|
+
return None
|
|
90
|
+
directives = Directives()
|
|
91
|
+
attribute = group.attribute
|
|
92
|
+
members = sorted(group.members)
|
|
93
|
+
if choice is _C.CUSTOM and kind is not _K.CARDINALITY and kind is not _K.REQUIRED_RELATION:
|
|
94
|
+
if attribute is None:
|
|
95
|
+
return None
|
|
96
|
+
for member in members:
|
|
97
|
+
if kind is _K.UNIQUENESS and (
|
|
98
|
+
resolution.values is None or member not in resolution.values
|
|
99
|
+
):
|
|
100
|
+
continue
|
|
101
|
+
directives.values[(group.partition, member, attribute)] = _value(
|
|
102
|
+
resolution.value_for(member)
|
|
103
|
+
)
|
|
104
|
+
return directives
|
|
105
|
+
if kind in (_K.ORPHANED_DATA, _K.ORPHANED_VALUE, _K.TYPE_COERCION) and choice is _C.RESTORE:
|
|
106
|
+
return _restore_schema(ctx, group, element)
|
|
107
|
+
if choice is _C.DROP:
|
|
108
|
+
return _drop(ctx, group, members)
|
|
109
|
+
if choice is _C.RELAX:
|
|
110
|
+
return _relax(ctx, schema, group, element)
|
|
111
|
+
if kind is _K.CARDINALITY and choice is _C.CUSTOM:
|
|
112
|
+
kept = resolution.value if isinstance(resolution.value, list) else []
|
|
113
|
+
keep = {tuple(pair) for pair in kept if isinstance(pair, list)}
|
|
114
|
+
for member in members:
|
|
115
|
+
detail = group.members[member]
|
|
116
|
+
links = detail.get("links") if isinstance(detail, dict) else None
|
|
117
|
+
for pair in links if isinstance(links, list) else []:
|
|
118
|
+
if isinstance(pair, list) and tuple(pair) not in keep:
|
|
119
|
+
directives.states[(group.partition, link_key(str(pair[0]), str(pair[1])))] = (
|
|
120
|
+
None
|
|
121
|
+
)
|
|
122
|
+
return directives
|
|
123
|
+
if kind is _K.REQUIRED_RELATION and choice is _C.CUSTOM:
|
|
124
|
+
for member in members:
|
|
125
|
+
target = resolution.value_for(member)
|
|
126
|
+
if not isinstance(target, str):
|
|
127
|
+
raise ResolutionError("required relation resolutions name the Record to link to")
|
|
128
|
+
directives.states[(group.partition, link_key(member, target))] = {
|
|
129
|
+
"relation": element,
|
|
130
|
+
"from": member,
|
|
131
|
+
"to": target,
|
|
132
|
+
"values": {},
|
|
133
|
+
}
|
|
134
|
+
return directives
|
|
135
|
+
if kind is _K.DANGLING_LINK and choice is _C.RESTORE:
|
|
136
|
+
for member in members:
|
|
137
|
+
detail = group.members[member]
|
|
138
|
+
missing = detail.get("missing") if isinstance(detail, dict) else None
|
|
139
|
+
for target in missing if isinstance(missing, list) else []:
|
|
140
|
+
if not isinstance(target, list) or len(target) != 2:
|
|
141
|
+
continue
|
|
142
|
+
location = (kind_partition(str(target[0])), str(target[1]))
|
|
143
|
+
restored = _latest_version(ctx, location)
|
|
144
|
+
if restored is None:
|
|
145
|
+
return None
|
|
146
|
+
directives.states[location] = restored
|
|
147
|
+
return directives
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _value(value: JsonValue) -> Value:
|
|
152
|
+
return MISSING if value is None else value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _drop(ctx: Context, group: Group, members: list[str]) -> Directives | None:
|
|
156
|
+
directives = Directives()
|
|
157
|
+
if group.kind in (_K.ORPHANED_DATA, _K.ORPHANED_VALUE) and group.stage is Stage.DECISION:
|
|
158
|
+
return directives
|
|
159
|
+
if group.kind is _K.ORPHANED_VALUE or (
|
|
160
|
+
group.kind is _K.DANGLING_LINK and group.attribute is not None
|
|
161
|
+
):
|
|
162
|
+
assert group.attribute is not None
|
|
163
|
+
for member in members:
|
|
164
|
+
directives.values[(group.partition, member, group.attribute)] = MISSING
|
|
165
|
+
return directives
|
|
166
|
+
if group.kind is _K.REQUIRED_RELATION:
|
|
167
|
+
element = partition_element(group.partition)
|
|
168
|
+
relation = ctx.schemas[Side.OURS], ctx.schemas[Side.THEIRS]
|
|
169
|
+
from_kind = next(
|
|
170
|
+
(s.relations[element].from_kind for s in relation if s and element in s.relations),
|
|
171
|
+
None,
|
|
172
|
+
)
|
|
173
|
+
if from_kind is None:
|
|
174
|
+
return None
|
|
175
|
+
for member in members:
|
|
176
|
+
directives.states[(kind_partition(from_kind), member)] = None
|
|
177
|
+
return directives
|
|
178
|
+
for member in members:
|
|
179
|
+
directives.states[(group.partition, member)] = None
|
|
180
|
+
return directives
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _restore_schema(ctx: Context, group: Group, element: str) -> Directives | None:
|
|
184
|
+
"""Restore what the schema side removed or retyped, keeping the data side's version."""
|
|
185
|
+
data_side = group.data_side
|
|
186
|
+
if data_side is None:
|
|
187
|
+
return None
|
|
188
|
+
schema_side = other_side(data_side)
|
|
189
|
+
document = ctx.element_document(data_side, element)
|
|
190
|
+
if document is None:
|
|
191
|
+
return None
|
|
192
|
+
directives = Directives()
|
|
193
|
+
if group.kind is _K.ORPHANED_DATA:
|
|
194
|
+
directives.elements[element] = document
|
|
195
|
+
directives.ignored_partitions.add((schema_side, group.partition))
|
|
196
|
+
return directives
|
|
197
|
+
attribute = group.attribute
|
|
198
|
+
definition = at_path(document, ("attributes", attribute or ""))
|
|
199
|
+
if attribute is None or not isinstance(definition, dict):
|
|
200
|
+
return None
|
|
201
|
+
if group.kind is _K.ORPHANED_VALUE:
|
|
202
|
+
directives.properties[(element, ("attributes", attribute))] = definition
|
|
203
|
+
else:
|
|
204
|
+
directives.properties[(element, ("attributes", attribute, "type"))] = definition.get("type")
|
|
205
|
+
directives.ignored_attributes.add((schema_side, group.partition, attribute))
|
|
206
|
+
return directives
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _relax(ctx: Context, schema: SchemaMerge, group: Group, element: str) -> Directives | None:
|
|
210
|
+
directives = Directives()
|
|
211
|
+
attribute = group.attribute
|
|
212
|
+
if group.kind is _K.CARDINALITY:
|
|
213
|
+
relation = schema.schema.relations.get(element)
|
|
214
|
+
if relation is None or attribute not in ("from", "to"):
|
|
215
|
+
return None
|
|
216
|
+
relaxed: Cardinality = relaxed_cardinality(relation.cardinality, attribute)
|
|
217
|
+
directives.properties[(element, ("cardinality",))] = relaxed.value
|
|
218
|
+
return directives
|
|
219
|
+
if group.kind is _K.REQUIRED_RELATION:
|
|
220
|
+
directives.properties[(element, ("required_from",))] = False
|
|
221
|
+
return directives
|
|
222
|
+
if group.kind is _K.TRAIT_CONFORMANCE:
|
|
223
|
+
trait = _trait_of(group)
|
|
224
|
+
if trait is None:
|
|
225
|
+
return None
|
|
226
|
+
directives.trait_removals.add((element, trait))
|
|
227
|
+
return directives
|
|
228
|
+
if attribute is None:
|
|
229
|
+
return None
|
|
230
|
+
path: tuple[str, ...] = ("attributes", attribute)
|
|
231
|
+
if group.kind is _K.MISSING_VALUE:
|
|
232
|
+
directives.properties[(element, (*path, "required"))] = False
|
|
233
|
+
elif group.kind is _K.UNIQUENESS:
|
|
234
|
+
directives.properties[(element, (*path, "unique"))] = False
|
|
235
|
+
elif group.kind is _K.CONSTRAINT:
|
|
236
|
+
base = at_path(ctx.element_document(Side.BASE, element), (*path, "constraints"))
|
|
237
|
+
directives.properties[(element, (*path, "constraints"))] = base
|
|
238
|
+
else:
|
|
239
|
+
return None
|
|
240
|
+
return directives
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _trait_of(group: Group) -> str | None:
|
|
244
|
+
trait = group.details.get("trait")
|
|
245
|
+
if isinstance(trait, str):
|
|
246
|
+
return trait
|
|
247
|
+
for detail in group.members.values():
|
|
248
|
+
if isinstance(detail, dict) and isinstance(detail.get("trait"), str):
|
|
249
|
+
return str(detail["trait"])
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _schema_directives(
|
|
254
|
+
ctx: Context, schema: SchemaMerge, group: Group, resolution: Resolution
|
|
255
|
+
) -> Directives | None:
|
|
256
|
+
directives = Directives()
|
|
257
|
+
key = group.key
|
|
258
|
+
if key is None:
|
|
259
|
+
return None
|
|
260
|
+
raw_path = group.details.get("path")
|
|
261
|
+
path: tuple[str | int, ...] = (
|
|
262
|
+
tuple(p for p in raw_path if isinstance(p, str | int)) if isinstance(raw_path, list) else ()
|
|
263
|
+
)
|
|
264
|
+
choice = resolution.choice
|
|
265
|
+
if group.kind is _K.TRAIT_CONFORMANCE:
|
|
266
|
+
trait = _trait_of(group)
|
|
267
|
+
if trait is None:
|
|
268
|
+
return None
|
|
269
|
+
if choice is _C.RELAX:
|
|
270
|
+
directives.trait_removals.add((key, trait))
|
|
271
|
+
elif isinstance(resolution.value, dict):
|
|
272
|
+
directives.trait_mappings[(key, trait)] = dict(resolution.value)
|
|
273
|
+
else:
|
|
274
|
+
raise ResolutionError("a custom Trait conformance resolution is an attribute map")
|
|
275
|
+
return directives
|
|
276
|
+
if group.kind is _K.BROKEN_REFERENCE:
|
|
277
|
+
if choice is _C.CUSTOM:
|
|
278
|
+
if not isinstance(resolution.value, dict):
|
|
279
|
+
raise ResolutionError("a custom broken reference resolution is an element")
|
|
280
|
+
directives.elements[key] = dict(resolution.value)
|
|
281
|
+
return directives
|
|
282
|
+
reference = group.details.get("reference")
|
|
283
|
+
return _restore_reference(ctx, schema, reference) if isinstance(reference, str) else None
|
|
284
|
+
if not path:
|
|
285
|
+
return None
|
|
286
|
+
if choice is _C.CUSTOM:
|
|
287
|
+
directives.properties[(key, path)] = _value(resolution.value)
|
|
288
|
+
return directives
|
|
289
|
+
index = {_C.BASE: 0, _C.OURS: 1, _C.THEIRS: 2}[choice]
|
|
290
|
+
directives.properties[(key, path)] = _value(group.values[index])
|
|
291
|
+
return directives
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _restore_reference(ctx: Context, schema: SchemaMerge, reference: str) -> Directives | None:
|
|
295
|
+
"""Restore a removed schema element, or a removed Attribute of a merged element."""
|
|
296
|
+
directives = Directives()
|
|
297
|
+
for side in (*SIDES, Side.BASE):
|
|
298
|
+
document = ctx.element_document(side, reference)
|
|
299
|
+
if document is not None:
|
|
300
|
+
directives.elements[reference] = document
|
|
301
|
+
return directives
|
|
302
|
+
for side in (*SIDES, Side.BASE):
|
|
303
|
+
for owner, digest in sorted(ctx.schema_hashes[side].items()):
|
|
304
|
+
if owner not in schema.hashes:
|
|
305
|
+
continue
|
|
306
|
+
definition = at_path(ctx.document(digest), ("attributes", reference))
|
|
307
|
+
if isinstance(definition, dict):
|
|
308
|
+
directives.properties[(owner, ("attributes", reference))] = definition
|
|
309
|
+
return directives
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _latest_version(ctx: Context, location: tuple[str, str]) -> Payload | None:
|
|
314
|
+
"""The version of a Record kept by ours, else theirs, else the base."""
|
|
315
|
+
for side in (*SIDES, Side.BASE):
|
|
316
|
+
root = ctx.roots[side]
|
|
317
|
+
digest = tree_get(ctx.store, root, *location)
|
|
318
|
+
document = ctx.document(digest)
|
|
319
|
+
if document is not None:
|
|
320
|
+
return document
|
|
321
|
+
return None
|