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