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,291 @@
|
|
|
1
|
+
"""Data validation of single Records and Links against a Schema (invariant I1).
|
|
2
|
+
|
|
3
|
+
These rules need no access to other data: types, canonical form, constraints, required
|
|
4
|
+
Attributes, keys and Trait conformance. Rules that need existing data (uniqueness,
|
|
5
|
+
cardinality, referential integrity) live in ``changeset``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
from pydantic import JsonValue
|
|
14
|
+
|
|
15
|
+
from lorebase_core.model.attribute import Attribute
|
|
16
|
+
from lorebase_core.model.ids import is_ulid
|
|
17
|
+
from lorebase_core.model.kind import Kind
|
|
18
|
+
from lorebase_core.model.record import Link, Record, record_key
|
|
19
|
+
from lorebase_core.model.relation import RelationBacking
|
|
20
|
+
from lorebase_core.model.schema import Schema
|
|
21
|
+
from lorebase_core.model.vault import VAULT_KEY, looks_like_reference, vault_token
|
|
22
|
+
from lorebase_core.types import (
|
|
23
|
+
CastError,
|
|
24
|
+
ConstraintName,
|
|
25
|
+
EncodingError,
|
|
26
|
+
cast,
|
|
27
|
+
check_constraints,
|
|
28
|
+
encode_value,
|
|
29
|
+
is_canonical,
|
|
30
|
+
)
|
|
31
|
+
from lorebase_core.validate.issues import Issue, IssueCode, PathPart
|
|
32
|
+
|
|
33
|
+
CONSTRAINT_CODES: dict[ConstraintName, IssueCode] = {
|
|
34
|
+
ConstraintName.MIN: IssueCode.VALUE_MIN,
|
|
35
|
+
ConstraintName.MAX: IssueCode.VALUE_MAX,
|
|
36
|
+
ConstraintName.MIN_LENGTH: IssueCode.VALUE_MIN_LENGTH,
|
|
37
|
+
ConstraintName.MAX_LENGTH: IssueCode.VALUE_MAX_LENGTH,
|
|
38
|
+
ConstraintName.PATTERN: IssueCode.VALUE_PATTERN,
|
|
39
|
+
ConstraintName.ENUM_VALUES: IssueCode.VALUE_ENUM_VALUES,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def holds_reference(value: object) -> bool:
|
|
44
|
+
"""Whether a vault reference (or anything shaped like one) is nested inside `value`, at
|
|
45
|
+
any depth of its objects and arrays."""
|
|
46
|
+
stack: list[object] = [value]
|
|
47
|
+
while stack:
|
|
48
|
+
current = stack.pop()
|
|
49
|
+
if isinstance(current, dict):
|
|
50
|
+
if VAULT_KEY in current:
|
|
51
|
+
return True
|
|
52
|
+
stack.extend(current.values())
|
|
53
|
+
elif isinstance(current, list):
|
|
54
|
+
stack.extend(current)
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RecordExistence(Protocol):
|
|
59
|
+
"""The part of ``IndexView`` needed to check Link endpoints."""
|
|
60
|
+
|
|
61
|
+
def record_exists(self, kind_id: str, key: str) -> bool: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_values(
|
|
65
|
+
attributes: Mapping[str, Attribute],
|
|
66
|
+
values: Mapping[str, JsonValue],
|
|
67
|
+
path: tuple[PathPart, ...],
|
|
68
|
+
*,
|
|
69
|
+
kind: str | None = None,
|
|
70
|
+
relation: str | None = None,
|
|
71
|
+
key: str | None = None,
|
|
72
|
+
) -> list[Issue]:
|
|
73
|
+
"""Type, canonical form, constraint and required checks of a value map."""
|
|
74
|
+
issues: list[Issue] = []
|
|
75
|
+
|
|
76
|
+
def report(code: IssueCode, attribute_id: str, message: str, **details: JsonValue) -> None:
|
|
77
|
+
issues.append(
|
|
78
|
+
Issue(
|
|
79
|
+
code=code,
|
|
80
|
+
path=(*path, "values", attribute_id),
|
|
81
|
+
message=message,
|
|
82
|
+
kind=kind,
|
|
83
|
+
relation=relation,
|
|
84
|
+
key=key,
|
|
85
|
+
attribute=attribute_id,
|
|
86
|
+
details=details,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for attribute_id, value in values.items():
|
|
91
|
+
attribute = attributes.get(attribute_id)
|
|
92
|
+
if attribute is None:
|
|
93
|
+
report(
|
|
94
|
+
IssueCode.VALUE_ATTRIBUTE_UNKNOWN, attribute_id, f"unknown attribute {attribute_id}"
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
name = attribute.api_name
|
|
98
|
+
if attribute.is_derived:
|
|
99
|
+
report(
|
|
100
|
+
IssueCode.VALUE_DERIVED,
|
|
101
|
+
attribute_id,
|
|
102
|
+
f"{name!r} is derived; derived values are never stored",
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
if looks_like_reference(value):
|
|
106
|
+
# The store mints references and checks they belong to the Record (13.5); the
|
|
107
|
+
# value behind one was validated when it was written.
|
|
108
|
+
if not attribute.in_vault or vault_token(value) is None:
|
|
109
|
+
report(
|
|
110
|
+
IssueCode.VALUE_VAULT_REFERENCE,
|
|
111
|
+
attribute_id,
|
|
112
|
+
f"{name!r} does not take a vault reference",
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
if isinstance(value, dict | list) and holds_reference(value):
|
|
116
|
+
# Only a vault Attribute holds a reference, and only as its whole value: one
|
|
117
|
+
# nested in another value would be stored as it is and read back as a token.
|
|
118
|
+
report(
|
|
119
|
+
IssueCode.VALUE_VAULT_REFERENCE,
|
|
120
|
+
attribute_id,
|
|
121
|
+
f"{name!r} holds a vault reference inside its value",
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
canonical = encode_value(attribute.type, value)
|
|
126
|
+
except EncodingError as exc:
|
|
127
|
+
report(IssueCode.VALUE_TYPE, attribute_id, f"{name!r}: {exc}", reason=exc.reason.value)
|
|
128
|
+
continue
|
|
129
|
+
if canonical != value:
|
|
130
|
+
report(
|
|
131
|
+
IssueCode.VALUE_NOT_CANONICAL,
|
|
132
|
+
attribute_id,
|
|
133
|
+
f"{name!r} is not in canonical form; expected {canonical!r}",
|
|
134
|
+
canonical=canonical,
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
for violation in check_constraints(attribute.type, attribute.constraints, canonical):
|
|
138
|
+
details: dict[str, JsonValue] = {"constraint": violation.constraint.value}
|
|
139
|
+
if violation.index is not None:
|
|
140
|
+
details["index"] = violation.index
|
|
141
|
+
report(
|
|
142
|
+
CONSTRAINT_CODES[violation.constraint],
|
|
143
|
+
attribute_id,
|
|
144
|
+
f"{name!r}: {violation.message}",
|
|
145
|
+
**details,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
for attribute in attributes.values():
|
|
149
|
+
if attribute.required and not attribute.is_derived and values.get(attribute.id) is None:
|
|
150
|
+
report(IssueCode.VALUE_REQUIRED, attribute.id, f"{attribute.api_name!r} is required")
|
|
151
|
+
return issues
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _check_key(kind: Kind, record: Record, path: tuple[PathPart, ...]) -> list[Issue]:
|
|
155
|
+
if not kind.has_natural_key:
|
|
156
|
+
if is_ulid(record.key):
|
|
157
|
+
return []
|
|
158
|
+
return [
|
|
159
|
+
Issue(
|
|
160
|
+
code=IssueCode.RECORD_KEY_INVALID,
|
|
161
|
+
path=(*path, "key"),
|
|
162
|
+
message=f"Kind {kind.api_name!r} has no primary key; Record keys are minted ULIDs",
|
|
163
|
+
kind=kind.id,
|
|
164
|
+
key=record.key,
|
|
165
|
+
)
|
|
166
|
+
]
|
|
167
|
+
for attribute_id in kind.primary_key:
|
|
168
|
+
attribute = kind.attributes.get(attribute_id)
|
|
169
|
+
value = record.values.get(attribute_id)
|
|
170
|
+
if attribute is None or value is None or not is_canonical(attribute.type, value):
|
|
171
|
+
return []
|
|
172
|
+
expected = record_key(kind, record.values)
|
|
173
|
+
if expected is None or expected == record.key:
|
|
174
|
+
return []
|
|
175
|
+
return [
|
|
176
|
+
Issue(
|
|
177
|
+
code=IssueCode.RECORD_KEY_MISMATCH,
|
|
178
|
+
path=(*path, "key"),
|
|
179
|
+
message=f"key {record.key!r} does not match the primary key values ({expected!r})",
|
|
180
|
+
kind=kind.id,
|
|
181
|
+
key=record.key,
|
|
182
|
+
details={"expected": expected},
|
|
183
|
+
)
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _check_traits(
|
|
188
|
+
schema: Schema, kind: Kind, record: Record, path: tuple[PathPart, ...]
|
|
189
|
+
) -> list[Issue]:
|
|
190
|
+
issues: list[Issue] = []
|
|
191
|
+
for implementation in kind.traits:
|
|
192
|
+
trait = schema.traits.get(implementation.trait_id)
|
|
193
|
+
if trait is None:
|
|
194
|
+
continue
|
|
195
|
+
for trait_attribute in trait.attributes.values():
|
|
196
|
+
attribute_id = implementation.attribute_map.get(trait_attribute.id)
|
|
197
|
+
attribute = kind.attributes.get(attribute_id) if attribute_id is not None else None
|
|
198
|
+
label = f"{trait.api_name}.{trait_attribute.api_name}"
|
|
199
|
+
if attribute is None:
|
|
200
|
+
continue
|
|
201
|
+
value = record.values.get(attribute.id)
|
|
202
|
+
message: str | None = None
|
|
203
|
+
if value is None:
|
|
204
|
+
if trait_attribute.required:
|
|
205
|
+
message = f"{label} is required by Trait {trait.api_name!r}"
|
|
206
|
+
elif is_canonical(attribute.type, value):
|
|
207
|
+
try:
|
|
208
|
+
cast(attribute.type, trait_attribute.type, value)
|
|
209
|
+
except CastError as exc:
|
|
210
|
+
message = f"{label}: {exc.message}"
|
|
211
|
+
if message is not None:
|
|
212
|
+
issues.append(
|
|
213
|
+
Issue(
|
|
214
|
+
code=IssueCode.RECORD_TRAIT_CONFORMANCE,
|
|
215
|
+
path=(*path, "values", attribute.id),
|
|
216
|
+
message=message,
|
|
217
|
+
kind=kind.id,
|
|
218
|
+
key=record.key,
|
|
219
|
+
attribute=attribute.id,
|
|
220
|
+
details={"trait": trait.id, "trait_attribute": trait_attribute.id},
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
return issues
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def validate_record(
|
|
227
|
+
schema: Schema, record: Record, *, path: tuple[PathPart, ...] = ()
|
|
228
|
+
) -> list[Issue]:
|
|
229
|
+
"""Validate one Record against its Kind: key, types, constraints, required, Traits."""
|
|
230
|
+
kind = schema.kinds.get(record.kind)
|
|
231
|
+
if kind is None:
|
|
232
|
+
return [
|
|
233
|
+
Issue(
|
|
234
|
+
code=IssueCode.RECORD_KIND_UNKNOWN,
|
|
235
|
+
path=(*path, "kind"),
|
|
236
|
+
message=f"unknown Kind {record.kind}",
|
|
237
|
+
kind=record.kind,
|
|
238
|
+
key=record.key,
|
|
239
|
+
)
|
|
240
|
+
]
|
|
241
|
+
issues = _check_key(kind, record, path)
|
|
242
|
+
issues.extend(check_values(kind.attributes, record.values, path, kind=kind.id, key=record.key))
|
|
243
|
+
issues.extend(_check_traits(schema, kind, record, path))
|
|
244
|
+
return issues
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def validate_link(
|
|
248
|
+
schema: Schema,
|
|
249
|
+
link: Link,
|
|
250
|
+
index_view: RecordExistence | None = None,
|
|
251
|
+
*,
|
|
252
|
+
path: tuple[PathPart, ...] = (),
|
|
253
|
+
) -> list[Issue]:
|
|
254
|
+
"""Validate one Link against its Relation; with ``index_view``, also check its endpoints."""
|
|
255
|
+
relation = schema.relations.get(link.relation)
|
|
256
|
+
if relation is None:
|
|
257
|
+
return [
|
|
258
|
+
Issue(
|
|
259
|
+
code=IssueCode.LINK_RELATION_UNKNOWN,
|
|
260
|
+
path=(*path, "relation"),
|
|
261
|
+
message=f"unknown Relation {link.relation}",
|
|
262
|
+
relation=link.relation,
|
|
263
|
+
)
|
|
264
|
+
]
|
|
265
|
+
if relation.backing is RelationBacking.FOREIGN_KEY:
|
|
266
|
+
return [
|
|
267
|
+
Issue(
|
|
268
|
+
code=IssueCode.LINK_FOREIGN_KEY_BACKED,
|
|
269
|
+
path=(*path, "relation"),
|
|
270
|
+
message=f"Links of {relation.api_name!r} derive from a foreign key attribute",
|
|
271
|
+
relation=relation.id,
|
|
272
|
+
)
|
|
273
|
+
]
|
|
274
|
+
issues = check_values(relation.attributes, link.values, path, relation=relation.id)
|
|
275
|
+
if index_view is not None:
|
|
276
|
+
for end, kind_id, key in (
|
|
277
|
+
("from", relation.from_kind, link.from_key),
|
|
278
|
+
("to", relation.to_kind, link.to_key),
|
|
279
|
+
):
|
|
280
|
+
if not index_view.record_exists(kind_id, key):
|
|
281
|
+
issues.append(
|
|
282
|
+
Issue(
|
|
283
|
+
code=IssueCode.LINK_ENDPOINT_MISSING,
|
|
284
|
+
path=(*path, end),
|
|
285
|
+
message=f"{end} Record {key!r} does not exist",
|
|
286
|
+
kind=kind_id,
|
|
287
|
+
relation=relation.id,
|
|
288
|
+
key=key,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
return issues
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Validation issues: structured findings of schema and data validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Severity(StrEnum):
|
|
12
|
+
ERROR = "error"
|
|
13
|
+
WARNING = "warning"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IssueCode(StrEnum):
|
|
17
|
+
"""Stable issue codes. ``schema.*`` come from static validation, the others from data."""
|
|
18
|
+
|
|
19
|
+
# Schema: names and ids
|
|
20
|
+
SCHEMA_API_NAME_INVALID = "schema.api_name_invalid"
|
|
21
|
+
SCHEMA_API_NAME_DUPLICATE = "schema.api_name_duplicate"
|
|
22
|
+
SCHEMA_API_NAME_RESERVED = "schema.api_name_reserved"
|
|
23
|
+
SCHEMA_ID_DUPLICATE = "schema.id_duplicate"
|
|
24
|
+
SCHEMA_DESCRIPTION_MISSING = "schema.description_missing"
|
|
25
|
+
# Schema: Kinds and Attributes
|
|
26
|
+
SCHEMA_PRIMARY_KEY_UNKNOWN = "schema.primary_key_unknown"
|
|
27
|
+
SCHEMA_PRIMARY_KEY_INVALID = "schema.primary_key_invalid"
|
|
28
|
+
SCHEMA_TITLE_ATTRIBUTE_MISSING = "schema.title_attribute_missing"
|
|
29
|
+
SCHEMA_TITLE_ATTRIBUTE_UNKNOWN = "schema.title_attribute_unknown"
|
|
30
|
+
SCHEMA_TYPE_REFERENCE_UNKNOWN = "schema.type_reference_unknown"
|
|
31
|
+
SCHEMA_CONSTRAINT_INVALID = "schema.constraint_invalid"
|
|
32
|
+
SCHEMA_DEFAULT_INVALID = "schema.default_invalid"
|
|
33
|
+
SCHEMA_DEFAULT_CONFLICT = "schema.default_conflict"
|
|
34
|
+
SCHEMA_ATTRIBUTE_FLAG_INVALID = "schema.attribute_flag_invalid"
|
|
35
|
+
SCHEMA_AUTHORITY_UNKNOWN = "schema.authority_unknown"
|
|
36
|
+
SCHEMA_EMBEDDING_TEMPLATE_INVALID = "schema.embedding_template_invalid"
|
|
37
|
+
SCHEMA_VAULT_INVALID = "schema.vault_invalid"
|
|
38
|
+
SCHEMA_SENSITIVE_ATTRIBUTE = "schema.sensitive_attribute"
|
|
39
|
+
SCHEMA_SENSITIVITY_INHERITED = "schema.sensitivity_inherited"
|
|
40
|
+
# Schema: Relations
|
|
41
|
+
SCHEMA_RELATION_ENDPOINT_UNKNOWN = "schema.relation_endpoint_unknown"
|
|
42
|
+
SCHEMA_FOREIGN_KEY_INVALID = "schema.foreign_key_invalid"
|
|
43
|
+
# Schema: Traits
|
|
44
|
+
SCHEMA_TRAIT_UNKNOWN = "schema.trait_unknown"
|
|
45
|
+
SCHEMA_TRAIT_DUPLICATE = "schema.trait_duplicate"
|
|
46
|
+
SCHEMA_TRAIT_MAPPING_MISSING = "schema.trait_mapping_missing"
|
|
47
|
+
SCHEMA_TRAIT_MAPPING_INVALID = "schema.trait_mapping_invalid"
|
|
48
|
+
SCHEMA_TRAIT_MAPPING_TYPE = "schema.trait_mapping_type"
|
|
49
|
+
SCHEMA_TRAIT_MAPPING_REQUIRED = "schema.trait_mapping_required"
|
|
50
|
+
# Schema: Operations, Derivations, Sources
|
|
51
|
+
SCHEMA_EXPRESSIONS_DISABLED = "schema.expressions_disabled"
|
|
52
|
+
SCHEMA_EXPRESSION_INVALID = "schema.expression_invalid"
|
|
53
|
+
SCHEMA_OPERATION_REFERENCE_UNKNOWN = "schema.operation_reference_unknown"
|
|
54
|
+
SCHEMA_OPERATION_INVALID = "schema.operation_invalid"
|
|
55
|
+
SCHEMA_DERIVATION_REFERENCE_UNKNOWN = "schema.derivation_reference_unknown"
|
|
56
|
+
SCHEMA_DERIVATION_INVALID = "schema.derivation_invalid"
|
|
57
|
+
SCHEMA_DERIVATION_CYCLE = "schema.derivation_cycle"
|
|
58
|
+
SCHEMA_SOURCE_REFERENCE_UNKNOWN = "schema.source_reference_unknown"
|
|
59
|
+
SCHEMA_SOURCE_INVALID = "schema.source_invalid"
|
|
60
|
+
|
|
61
|
+
# Data: values (Records and Links)
|
|
62
|
+
VALUE_ATTRIBUTE_UNKNOWN = "value.attribute_unknown"
|
|
63
|
+
VALUE_DERIVED = "value.derived"
|
|
64
|
+
VALUE_TYPE = "value.type"
|
|
65
|
+
VALUE_NOT_CANONICAL = "value.not_canonical"
|
|
66
|
+
VALUE_REQUIRED = "value.required"
|
|
67
|
+
VALUE_MIN = "value.min"
|
|
68
|
+
VALUE_MAX = "value.max"
|
|
69
|
+
VALUE_MIN_LENGTH = "value.min_length"
|
|
70
|
+
VALUE_MAX_LENGTH = "value.max_length"
|
|
71
|
+
VALUE_PATTERN = "value.pattern"
|
|
72
|
+
VALUE_ENUM_VALUES = "value.enum_values"
|
|
73
|
+
VALUE_UNIQUE = "value.unique"
|
|
74
|
+
VALUE_REFERENCE_MISSING = "value.reference_missing"
|
|
75
|
+
VALUE_VAULT_REFERENCE = "value.vault_reference"
|
|
76
|
+
VALUE_VAULT_UNAVAILABLE = "value.vault_unavailable"
|
|
77
|
+
VALUE_VAULT_ERASED = "value.vault_erased"
|
|
78
|
+
VALUE_INVALID = "value.invalid"
|
|
79
|
+
"""A value of a sensitive Attribute is invalid; how is not told to callers without the
|
|
80
|
+
clearance (Keeper replaces the specific code with this one)."""
|
|
81
|
+
# Data: Records
|
|
82
|
+
RECORD_KIND_UNKNOWN = "record.kind_unknown"
|
|
83
|
+
RECORD_KEY_INVALID = "record.key_invalid"
|
|
84
|
+
RECORD_KEY_MISMATCH = "record.key_mismatch"
|
|
85
|
+
RECORD_TRAIT_CONFORMANCE = "record.trait_conformance"
|
|
86
|
+
RECORD_NOT_FOUND = "record.not_found"
|
|
87
|
+
RECORD_DELETE_RESTRICTED = "record.delete_restricted"
|
|
88
|
+
RECORD_REQUIRED_RELATION = "record.required_relation"
|
|
89
|
+
# Data: Links
|
|
90
|
+
LINK_RELATION_UNKNOWN = "link.relation_unknown"
|
|
91
|
+
LINK_FOREIGN_KEY_BACKED = "link.foreign_key_backed"
|
|
92
|
+
LINK_ENDPOINT_MISSING = "link.endpoint_missing"
|
|
93
|
+
LINK_CARDINALITY = "link.cardinality"
|
|
94
|
+
LINK_NOT_FOUND = "link.not_found"
|
|
95
|
+
# Data: change sets
|
|
96
|
+
CHANGESET_DUPLICATE_CHANGE = "changeset.duplicate_change"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
PathPart = str | int
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Issue(BaseModel):
|
|
103
|
+
"""One finding. ``path`` locates it inside the schema or the change set."""
|
|
104
|
+
|
|
105
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
106
|
+
|
|
107
|
+
code: IssueCode
|
|
108
|
+
severity: Severity = Severity.ERROR
|
|
109
|
+
path: tuple[PathPart, ...] = ()
|
|
110
|
+
message: str
|
|
111
|
+
kind: str | None = None
|
|
112
|
+
relation: str | None = None
|
|
113
|
+
key: str | None = None
|
|
114
|
+
attribute: str | None = None
|
|
115
|
+
details: dict[str, JsonValue] = Field(default_factory=dict)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def is_error(self) -> bool:
|
|
119
|
+
return self.severity is Severity.ERROR
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def has_errors(issues: Iterable[Issue]) -> bool:
|
|
123
|
+
return any(issue.is_error for issue in issues)
|