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,81 @@
|
|
|
1
|
+
"""Per-table analysis: normalized names, column analyses and the key choice."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from lorebase_core.infer.columns import ColumnAnalysis, analyze_column
|
|
8
|
+
from lorebase_core.infer.keys import KeyChoice, choose_key, is_minted
|
|
9
|
+
from lorebase_core.infer.names import normalize, singular, strip_prefixes, strip_suffix
|
|
10
|
+
from lorebase_core.infer.options import InferenceOptions
|
|
11
|
+
from lorebase_core.infer.profile import DeclaredForeignKey, TableProfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TableAnalysis:
|
|
16
|
+
"""What inference knows about one table."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
snake: str
|
|
20
|
+
singular: str
|
|
21
|
+
columns: tuple[ColumnAnalysis, ...]
|
|
22
|
+
rows: int
|
|
23
|
+
values_read: int
|
|
24
|
+
key: KeyChoice | None
|
|
25
|
+
foreign_keys: tuple[DeclaredForeignKey, ...]
|
|
26
|
+
comment: str | None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def sampled(self) -> bool:
|
|
30
|
+
return self.rows > self.values_read
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def key_column(self) -> ColumnAnalysis | None:
|
|
34
|
+
"""The single key column (also for minted keys), or ``None``."""
|
|
35
|
+
if self.key is None or self.key.single is None:
|
|
36
|
+
return None
|
|
37
|
+
return self.columns[self.key.single]
|
|
38
|
+
|
|
39
|
+
def column(self, name: str) -> ColumnAnalysis | None:
|
|
40
|
+
return next((c for c in self.columns if c.name == name), None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def analyze_table(profile: TableProfile, options: InferenceOptions) -> TableAnalysis:
|
|
44
|
+
"""Analyze every column of ``profile`` and choose its key."""
|
|
45
|
+
snake = strip_prefixes(normalize(profile.name), options.strip_table_prefixes)
|
|
46
|
+
single = singular(snake) if options.singular_kind_names else snake
|
|
47
|
+
columns = tuple(
|
|
48
|
+
analyze_column(column, index, options) for index, column in enumerate(profile.columns)
|
|
49
|
+
)
|
|
50
|
+
values_read = max((c.count for c in columns), default=0)
|
|
51
|
+
rows = max(profile.row_count or 0, values_read)
|
|
52
|
+
sampled = rows > values_read
|
|
53
|
+
reference_like = frozenset(
|
|
54
|
+
c.index for c in columns if strip_suffix(c.snake, options.foreign_key_suffixes) is not None
|
|
55
|
+
)
|
|
56
|
+
names = list(dict.fromkeys((snake, single)))
|
|
57
|
+
key = choose_key(
|
|
58
|
+
names, columns, profile.primary_key, options, sampled=sampled, reference_like=reference_like
|
|
59
|
+
)
|
|
60
|
+
if key is not None and not key.declared and is_minted(key, columns, options):
|
|
61
|
+
key = KeyChoice(
|
|
62
|
+
columns=key.columns,
|
|
63
|
+
confidence=key.confidence,
|
|
64
|
+
reasons=(
|
|
65
|
+
*key.reasons,
|
|
66
|
+
f"{columns[key.columns[0]].name!r} holds generated ids: Records use minted keys",
|
|
67
|
+
),
|
|
68
|
+
candidates=key.candidates,
|
|
69
|
+
minted=True,
|
|
70
|
+
)
|
|
71
|
+
return TableAnalysis(
|
|
72
|
+
name=profile.name,
|
|
73
|
+
snake=snake,
|
|
74
|
+
singular=single,
|
|
75
|
+
columns=columns,
|
|
76
|
+
rows=rows,
|
|
77
|
+
values_read=values_read,
|
|
78
|
+
key=key,
|
|
79
|
+
foreign_keys=profile.foreign_keys,
|
|
80
|
+
comment=profile.comment,
|
|
81
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Three-way merge of Revisions (design 6.8 to 6.10): schema first, then data, then consistency."""
|
|
2
|
+
|
|
3
|
+
from lorebase_core.merge.conflicts import (
|
|
4
|
+
CHOICES,
|
|
5
|
+
GROUPED_KINDS,
|
|
6
|
+
MACHINE_RESOLVABLE,
|
|
7
|
+
AutoResolution,
|
|
8
|
+
Conflict,
|
|
9
|
+
ConflictKind,
|
|
10
|
+
Resolution,
|
|
11
|
+
ResolutionChoice,
|
|
12
|
+
ResolutionError,
|
|
13
|
+
Side,
|
|
14
|
+
UnresolvedConflictsError,
|
|
15
|
+
conflict_fingerprint,
|
|
16
|
+
)
|
|
17
|
+
from lorebase_core.merge.engine import (
|
|
18
|
+
IndexViewFactory,
|
|
19
|
+
LineageLoader,
|
|
20
|
+
MergeAttempt,
|
|
21
|
+
MergedTree,
|
|
22
|
+
MergeInput,
|
|
23
|
+
MergeOutcome,
|
|
24
|
+
MergeResult,
|
|
25
|
+
ProvenanceLoader,
|
|
26
|
+
apply_resolutions,
|
|
27
|
+
merge,
|
|
28
|
+
)
|
|
29
|
+
from lorebase_core.merge.index import TreeIndexView
|
|
30
|
+
from lorebase_core.merge.objects import (
|
|
31
|
+
DOMAIN_KEY,
|
|
32
|
+
InMemoryObjectStore,
|
|
33
|
+
MissingObjectError,
|
|
34
|
+
ObjectStore,
|
|
35
|
+
SchemaLoader,
|
|
36
|
+
load_schema,
|
|
37
|
+
put_schema,
|
|
38
|
+
schema_from_documents,
|
|
39
|
+
tree_schema_loader,
|
|
40
|
+
)
|
|
41
|
+
from lorebase_core.merge.policies import (
|
|
42
|
+
DEFAULT_ATOMIC_PROPERTIES,
|
|
43
|
+
ORIGIN_LABEL_PREFIXES,
|
|
44
|
+
ChangeStamp,
|
|
45
|
+
MergePolicy,
|
|
46
|
+
PolicyName,
|
|
47
|
+
origin_labels,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"CHOICES",
|
|
52
|
+
"DEFAULT_ATOMIC_PROPERTIES",
|
|
53
|
+
"DOMAIN_KEY",
|
|
54
|
+
"GROUPED_KINDS",
|
|
55
|
+
"MACHINE_RESOLVABLE",
|
|
56
|
+
"ORIGIN_LABEL_PREFIXES",
|
|
57
|
+
"AutoResolution",
|
|
58
|
+
"ChangeStamp",
|
|
59
|
+
"Conflict",
|
|
60
|
+
"ConflictKind",
|
|
61
|
+
"InMemoryObjectStore",
|
|
62
|
+
"IndexViewFactory",
|
|
63
|
+
"LineageLoader",
|
|
64
|
+
"MergeAttempt",
|
|
65
|
+
"MergeInput",
|
|
66
|
+
"MergeOutcome",
|
|
67
|
+
"MergePolicy",
|
|
68
|
+
"MergeResult",
|
|
69
|
+
"MergedTree",
|
|
70
|
+
"MissingObjectError",
|
|
71
|
+
"ObjectStore",
|
|
72
|
+
"PolicyName",
|
|
73
|
+
"ProvenanceLoader",
|
|
74
|
+
"Resolution",
|
|
75
|
+
"ResolutionChoice",
|
|
76
|
+
"ResolutionError",
|
|
77
|
+
"SchemaLoader",
|
|
78
|
+
"Side",
|
|
79
|
+
"TreeIndexView",
|
|
80
|
+
"UnresolvedConflictsError",
|
|
81
|
+
"apply_resolutions",
|
|
82
|
+
"conflict_fingerprint",
|
|
83
|
+
"load_schema",
|
|
84
|
+
"merge",
|
|
85
|
+
"origin_labels",
|
|
86
|
+
"put_schema",
|
|
87
|
+
"schema_from_documents",
|
|
88
|
+
"tree_schema_loader",
|
|
89
|
+
]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Conflict taxonomy, fingerprints and resolutions (design 6.9, 6.10).
|
|
2
|
+
|
|
3
|
+
Every Conflict has a stable code (``DV1`` to ``XS11``) and a fingerprint computed from its
|
|
4
|
+
code, location and the hashes of the three sides. Resolutions are matched to Conflicts by
|
|
5
|
+
fingerprint, so a merge recomputed against a moved target re-applies the resolutions of the
|
|
6
|
+
Conflicts that did not change.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from typing import Self
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue, model_validator
|
|
17
|
+
|
|
18
|
+
from lorebase_core.canonical import canonical_bytes
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CHOICES",
|
|
22
|
+
"GROUPED_KINDS",
|
|
23
|
+
"MACHINE_RESOLVABLE",
|
|
24
|
+
"AutoResolution",
|
|
25
|
+
"Conflict",
|
|
26
|
+
"ConflictKind",
|
|
27
|
+
"Resolution",
|
|
28
|
+
"ResolutionChoice",
|
|
29
|
+
"ResolutionError",
|
|
30
|
+
"Side",
|
|
31
|
+
"UnresolvedConflictsError",
|
|
32
|
+
"conflict_fingerprint",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConflictKind(StrEnum):
|
|
37
|
+
"""Stable Conflict codes (design 6.9). ``DV`` data, ``SC`` schema, ``XS`` cross-cutting."""
|
|
38
|
+
|
|
39
|
+
ATTRIBUTE_VALUE = "DV1"
|
|
40
|
+
DELETE_MODIFY = "DV2"
|
|
41
|
+
ADD_ADD = "DV3"
|
|
42
|
+
SCHEMA_PROPERTY = "SC1"
|
|
43
|
+
SCHEMA_DELETE_MODIFY = "SC2"
|
|
44
|
+
NAMING = "SC3"
|
|
45
|
+
ORPHANED_DATA = "XS1"
|
|
46
|
+
ORPHANED_VALUE = "XS2"
|
|
47
|
+
TYPE_COERCION = "XS3"
|
|
48
|
+
MISSING_VALUE = "XS4"
|
|
49
|
+
CONSTRAINT = "XS5"
|
|
50
|
+
CARDINALITY = "XS6"
|
|
51
|
+
REQUIRED_RELATION = "XS7"
|
|
52
|
+
UNIQUENESS = "XS8"
|
|
53
|
+
DANGLING_LINK = "XS9"
|
|
54
|
+
TRAIT_CONFORMANCE = "XS10"
|
|
55
|
+
BROKEN_REFERENCE = "XS11"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Side(StrEnum):
|
|
59
|
+
BASE = "base"
|
|
60
|
+
OURS = "ours"
|
|
61
|
+
THEIRS = "theirs"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ResolutionChoice(StrEnum):
|
|
65
|
+
"""How a Conflict is resolved; the meaning per Conflict kind is listed in ``CHOICES``.
|
|
66
|
+
|
|
67
|
+
- ``ours``, ``theirs``, ``base``: take that side's value, element or state.
|
|
68
|
+
- ``custom``: a value given by the resolver (``Resolution.value`` or, for grouped
|
|
69
|
+
Conflicts, ``Resolution.values`` keyed by member key).
|
|
70
|
+
- ``drop``: drop the data that breaks the merged schema (orphaned data or values, Records
|
|
71
|
+
with uncastable values, Records without a required Link, dangling Links).
|
|
72
|
+
- ``restore``: restore what the other side removed (a Kind, Relation or Attribute, a type
|
|
73
|
+
before its change, a deleted Record, a referenced schema element).
|
|
74
|
+
- ``relax``: relax the schema rule that the data breaks (required, constraint,
|
|
75
|
+
cardinality, ``required_from``, uniqueness, Trait implementation).
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
OURS = "ours"
|
|
79
|
+
THEIRS = "theirs"
|
|
80
|
+
BASE = "base"
|
|
81
|
+
CUSTOM = "custom"
|
|
82
|
+
DROP = "drop"
|
|
83
|
+
RESTORE = "restore"
|
|
84
|
+
RELAX = "relax"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_C = ResolutionChoice
|
|
88
|
+
_PICK = (_C.OURS, _C.THEIRS, _C.BASE, _C.CUSTOM)
|
|
89
|
+
|
|
90
|
+
#: Choices accepted per Conflict kind.
|
|
91
|
+
CHOICES: dict[ConflictKind, tuple[ResolutionChoice, ...]] = {
|
|
92
|
+
ConflictKind.ATTRIBUTE_VALUE: _PICK,
|
|
93
|
+
ConflictKind.DELETE_MODIFY: _PICK,
|
|
94
|
+
ConflictKind.ADD_ADD: _PICK,
|
|
95
|
+
ConflictKind.SCHEMA_PROPERTY: _PICK,
|
|
96
|
+
ConflictKind.SCHEMA_DELETE_MODIFY: _PICK,
|
|
97
|
+
ConflictKind.NAMING: (_C.CUSTOM,),
|
|
98
|
+
ConflictKind.ORPHANED_DATA: (_C.DROP, _C.RESTORE),
|
|
99
|
+
ConflictKind.ORPHANED_VALUE: (_C.DROP, _C.RESTORE),
|
|
100
|
+
ConflictKind.TYPE_COERCION: (_C.CUSTOM, _C.DROP, _C.RESTORE),
|
|
101
|
+
ConflictKind.MISSING_VALUE: (_C.CUSTOM, _C.RELAX),
|
|
102
|
+
ConflictKind.CONSTRAINT: (_C.CUSTOM, _C.RELAX),
|
|
103
|
+
ConflictKind.CARDINALITY: (_C.CUSTOM, _C.RELAX),
|
|
104
|
+
ConflictKind.REQUIRED_RELATION: (_C.CUSTOM, _C.DROP, _C.RELAX),
|
|
105
|
+
ConflictKind.UNIQUENESS: (_C.CUSTOM, _C.RELAX),
|
|
106
|
+
ConflictKind.DANGLING_LINK: (_C.DROP, _C.RESTORE),
|
|
107
|
+
ConflictKind.TRAIT_CONFORMANCE: (_C.CUSTOM, _C.RELAX),
|
|
108
|
+
ConflictKind.BROKEN_REFERENCE: (_C.CUSTOM, _C.RESTORE),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#: Kinds a ``MergePolicy`` can resolve without a person (``prefer`` and ``cascade_dangling``;
|
|
112
|
+
#: ``authority`` and ``newest_wins`` for the Attribute Conflicts ``DV1`` and ``DV3``).
|
|
113
|
+
MACHINE_RESOLVABLE = frozenset(
|
|
114
|
+
{
|
|
115
|
+
ConflictKind.ATTRIBUTE_VALUE,
|
|
116
|
+
ConflictKind.DELETE_MODIFY,
|
|
117
|
+
ConflictKind.ADD_ADD,
|
|
118
|
+
ConflictKind.SCHEMA_PROPERTY,
|
|
119
|
+
ConflictKind.SCHEMA_DELETE_MODIFY,
|
|
120
|
+
ConflictKind.DANGLING_LINK,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
#: Kinds reported as one Conflict over many members, with a count and a sample of keys.
|
|
125
|
+
GROUPED_KINDS = frozenset(
|
|
126
|
+
{
|
|
127
|
+
ConflictKind.ORPHANED_DATA,
|
|
128
|
+
ConflictKind.ORPHANED_VALUE,
|
|
129
|
+
ConflictKind.TYPE_COERCION,
|
|
130
|
+
ConflictKind.MISSING_VALUE,
|
|
131
|
+
ConflictKind.CONSTRAINT,
|
|
132
|
+
ConflictKind.CARDINALITY,
|
|
133
|
+
ConflictKind.REQUIRED_RELATION,
|
|
134
|
+
ConflictKind.UNIQUENESS,
|
|
135
|
+
ConflictKind.DANGLING_LINK,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def conflict_fingerprint(
|
|
141
|
+
kind: ConflictKind,
|
|
142
|
+
partition: str,
|
|
143
|
+
key: str | None,
|
|
144
|
+
attribute: str | None,
|
|
145
|
+
hashes: tuple[bytes | None, bytes | None, bytes | None],
|
|
146
|
+
members: Iterable[str] = (),
|
|
147
|
+
) -> str:
|
|
148
|
+
"""``sha256(code, partition, key, attribute, base_hash, ours_hash, theirs_hash)``.
|
|
149
|
+
|
|
150
|
+
Grouped Conflicts also hash their sorted member keys, so a group that gains or loses
|
|
151
|
+
members is a different Conflict.
|
|
152
|
+
"""
|
|
153
|
+
digest = hashlib.sha256()
|
|
154
|
+
head: list[JsonValue] = [
|
|
155
|
+
kind.value,
|
|
156
|
+
partition,
|
|
157
|
+
key,
|
|
158
|
+
attribute,
|
|
159
|
+
*(None if h is None else h.hex() for h in hashes),
|
|
160
|
+
]
|
|
161
|
+
digest.update(canonical_bytes(head))
|
|
162
|
+
for member in sorted(members):
|
|
163
|
+
digest.update(b"\x00")
|
|
164
|
+
digest.update(member.encode("utf-8"))
|
|
165
|
+
return digest.hexdigest()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class _Frozen(BaseModel):
|
|
169
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Conflict(_Frozen):
|
|
173
|
+
"""One Conflict of a merge attempt.
|
|
174
|
+
|
|
175
|
+
``partition`` and ``key`` locate the element (for schema Conflicts the partition is
|
|
176
|
+
``schema`` and the key the element id); ``attribute`` is an Attribute id for data
|
|
177
|
+
Conflicts and a dotted property path for schema Conflicts. ``base``, ``ours`` and
|
|
178
|
+
``theirs`` hold the competing values (``None`` means absent) and the ``*_hash`` fields the
|
|
179
|
+
hashes of the elements they come from. Grouped Conflicts carry ``count`` members and a
|
|
180
|
+
``sample`` of their keys.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
kind: ConflictKind
|
|
184
|
+
fingerprint: str
|
|
185
|
+
partition: str
|
|
186
|
+
key: str | None = None
|
|
187
|
+
attribute: str | None = None
|
|
188
|
+
base: JsonValue = None
|
|
189
|
+
ours: JsonValue = None
|
|
190
|
+
theirs: JsonValue = None
|
|
191
|
+
base_hash: str | None = None
|
|
192
|
+
ours_hash: str | None = None
|
|
193
|
+
theirs_hash: str | None = None
|
|
194
|
+
count: int = Field(default=1, ge=1)
|
|
195
|
+
sample: tuple[str, ...] = ()
|
|
196
|
+
message: str = ""
|
|
197
|
+
details: dict[str, JsonValue] = Field(default_factory=dict)
|
|
198
|
+
choices: tuple[ResolutionChoice, ...] = ()
|
|
199
|
+
machine_resolvable: bool = False
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def grouped(self) -> bool:
|
|
203
|
+
return self.kind in GROUPED_KINDS
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Resolution(_Frozen):
|
|
207
|
+
"""A resolver's decision for the Conflict with ``fingerprint``.
|
|
208
|
+
|
|
209
|
+
``custom`` takes ``value`` (``None`` means absent), or ``values`` keyed by member key for
|
|
210
|
+
grouped Conflicts.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
fingerprint: str
|
|
214
|
+
choice: ResolutionChoice
|
|
215
|
+
value: JsonValue = None
|
|
216
|
+
values: dict[str, JsonValue] | None = None
|
|
217
|
+
resolver: str = ""
|
|
218
|
+
|
|
219
|
+
@model_validator(mode="after")
|
|
220
|
+
def _check_custom(self) -> Self:
|
|
221
|
+
if self.choice is not ResolutionChoice.CUSTOM and (
|
|
222
|
+
self.value is not None or self.values is not None
|
|
223
|
+
):
|
|
224
|
+
raise ValueError("only custom resolutions carry a value")
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def value_for(self, member: str | None) -> JsonValue:
|
|
228
|
+
"""The custom value for ``member``: its entry in ``values``, else ``value``."""
|
|
229
|
+
if self.values is not None and member is not None and member in self.values:
|
|
230
|
+
return self.values[member]
|
|
231
|
+
return self.value
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class AutoResolution(_Frozen):
|
|
235
|
+
"""A Conflict resolved by the engine or by a policy, recorded for Lineage and review.
|
|
236
|
+
|
|
237
|
+
``details`` say why a Lineage-based policy decided: the origins matched by ``authority``,
|
|
238
|
+
the timestamps compared by ``newest_wins``, and the provenance entries they come from.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
kind: ConflictKind
|
|
242
|
+
fingerprint: str
|
|
243
|
+
partition: str
|
|
244
|
+
key: str | None = None
|
|
245
|
+
attribute: str | None = None
|
|
246
|
+
choice: ResolutionChoice
|
|
247
|
+
resolver: str
|
|
248
|
+
count: int = Field(default=1, ge=1)
|
|
249
|
+
sample: tuple[str, ...] = ()
|
|
250
|
+
details: dict[str, JsonValue] = Field(default_factory=dict)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ResolutionError(ValueError):
|
|
254
|
+
"""A resolution that does not fit the merge attempt."""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class UnresolvedConflictsError(ResolutionError):
|
|
258
|
+
"""Raised when resolutions leave Conflicts of the attempt open."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, fingerprints: Iterable[str]) -> None:
|
|
261
|
+
self.fingerprints = tuple(sorted(fingerprints))
|
|
262
|
+
super().__init__(f"{len(self.fingerprints)} conflict(s) left unresolved")
|