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,169 @@
|
|
|
1
|
+
"""Runtime values of evaluation and their conversion from and to canonical values.
|
|
2
|
+
|
|
3
|
+
``int`` is int, ``double`` float, ``decimal`` Decimal, ``timestamp`` an aware datetime in
|
|
4
|
+
UTC, ``date`` date, ``duration`` timedelta, lists are lists and maps dicts. Schema values
|
|
5
|
+
use the classes below. An absent Attribute reads as ``None`` (null).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import date, datetime, timedelta
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
|
|
15
|
+
from pydantic import JsonValue
|
|
16
|
+
|
|
17
|
+
from lorebase_core.expr.errors import ExprError, ExprErrorCode, evaluation_error
|
|
18
|
+
from lorebase_core.model.kind import Kind
|
|
19
|
+
from lorebase_core.model.record import format_ref
|
|
20
|
+
from lorebase_core.types import (
|
|
21
|
+
AttributeType,
|
|
22
|
+
EncodingError,
|
|
23
|
+
GeoPoint,
|
|
24
|
+
TypeSpec,
|
|
25
|
+
decode_value,
|
|
26
|
+
encode_value,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class Ref:
|
|
32
|
+
"""A Record reference: Kind id and key."""
|
|
33
|
+
|
|
34
|
+
kind: str
|
|
35
|
+
key: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ObjectValue:
|
|
40
|
+
"""A value of an object type (``params``, ``principal``)."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
fields: Mapping[str, object]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(eq=False, slots=True)
|
|
47
|
+
class RecordValue:
|
|
48
|
+
"""A loaded Record. Attribute values are decoded on first access."""
|
|
49
|
+
|
|
50
|
+
kind: Kind
|
|
51
|
+
key: str
|
|
52
|
+
values: Mapping[str, JsonValue]
|
|
53
|
+
_decoded: dict[str, object] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def ref(self) -> Ref:
|
|
57
|
+
return Ref(self.kind.id, self.key)
|
|
58
|
+
|
|
59
|
+
def has(self, name: str) -> bool:
|
|
60
|
+
attribute = self.kind.attribute_by_api_name(name)
|
|
61
|
+
return attribute is not None and self.values.get(attribute.id) is not None
|
|
62
|
+
|
|
63
|
+
def get(self, name: str) -> object:
|
|
64
|
+
"""Decoded value of the Attribute called ``name`` (``None`` when absent)."""
|
|
65
|
+
if name in self._decoded:
|
|
66
|
+
return self._decoded[name]
|
|
67
|
+
attribute = self.kind.attribute_by_api_name(name)
|
|
68
|
+
if attribute is None:
|
|
69
|
+
raise evaluation_error(f"Kind {self.kind.api_name} has no attribute {name!r}")
|
|
70
|
+
value = to_runtime(attribute.type, self.values.get(attribute.id))
|
|
71
|
+
self._decoded[name] = value
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return f"RecordValue({format_ref(self.kind.api_name, self.key)})"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(eq=False, slots=True)
|
|
79
|
+
class LinksValue:
|
|
80
|
+
"""``links`` of the subject Record; fields are resolved lazily by the evaluator."""
|
|
81
|
+
|
|
82
|
+
kind: Kind
|
|
83
|
+
key: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def to_runtime(spec: TypeSpec, canonical: object) -> object:
|
|
87
|
+
"""Runtime value of a canonical value of ``spec`` (``None`` stays ``None``)."""
|
|
88
|
+
if canonical is None:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
value = decode_value(spec, canonical)
|
|
92
|
+
except EncodingError as error:
|
|
93
|
+
raise evaluation_error(f"stored value is invalid: {error.message}") from None
|
|
94
|
+
return _wrap(spec, value)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _wrap(spec: TypeSpec, value: object) -> object:
|
|
98
|
+
if spec.name is AttributeType.REF:
|
|
99
|
+
assert spec.kind_ref is not None
|
|
100
|
+
assert isinstance(value, str)
|
|
101
|
+
return Ref(spec.kind_ref, value)
|
|
102
|
+
if spec.name is AttributeType.ARRAY and isinstance(value, list):
|
|
103
|
+
assert spec.element is not None
|
|
104
|
+
element = spec.element
|
|
105
|
+
return [_wrap(element, item) for item in value]
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def to_raw(value: object) -> object:
|
|
110
|
+
"""A runtime value as input for ``encode_value`` (Record references become keys)."""
|
|
111
|
+
if isinstance(value, Ref):
|
|
112
|
+
return value.key
|
|
113
|
+
if isinstance(value, RecordValue):
|
|
114
|
+
return value.key
|
|
115
|
+
if isinstance(value, list):
|
|
116
|
+
return [to_raw(item) for item in value]
|
|
117
|
+
if isinstance(value, dict):
|
|
118
|
+
return {str(k): to_raw(v) for k, v in value.items()}
|
|
119
|
+
if isinstance(value, ObjectValue):
|
|
120
|
+
return {k: to_raw(v) for k, v in value.fields.items()}
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def encode_result(spec: TypeSpec, value: object) -> JsonValue:
|
|
125
|
+
"""Encode an evaluation result for an Attribute of type ``spec``.
|
|
126
|
+
|
|
127
|
+
Raises ``ExprError`` with code ``expression.value_invalid`` when the value does not fit.
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(value, Ref) and spec.name is AttributeType.REF and value.kind != spec.kind_ref:
|
|
130
|
+
raise ExprError(ExprErrorCode.VALUE_INVALID, "reference to a Record of another Kind")
|
|
131
|
+
try:
|
|
132
|
+
return encode_value(spec, to_raw(value))
|
|
133
|
+
except EncodingError as error:
|
|
134
|
+
raise ExprError(ExprErrorCode.VALUE_INVALID, error.message) from None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def type_name(value: object) -> str:
|
|
138
|
+
"""CEL name of the type of a runtime value, for error messages."""
|
|
139
|
+
if value is None:
|
|
140
|
+
return "null"
|
|
141
|
+
if isinstance(value, bool):
|
|
142
|
+
return "bool"
|
|
143
|
+
if isinstance(value, int):
|
|
144
|
+
return "int"
|
|
145
|
+
if isinstance(value, float):
|
|
146
|
+
return "double"
|
|
147
|
+
if isinstance(value, Decimal):
|
|
148
|
+
return "decimal"
|
|
149
|
+
if isinstance(value, str):
|
|
150
|
+
return "string"
|
|
151
|
+
if isinstance(value, bytes):
|
|
152
|
+
return "bytes"
|
|
153
|
+
if isinstance(value, datetime):
|
|
154
|
+
return "timestamp"
|
|
155
|
+
if isinstance(value, date):
|
|
156
|
+
return "date"
|
|
157
|
+
if isinstance(value, timedelta):
|
|
158
|
+
return "duration"
|
|
159
|
+
if isinstance(value, list):
|
|
160
|
+
return "list"
|
|
161
|
+
if isinstance(value, dict):
|
|
162
|
+
return "map"
|
|
163
|
+
if isinstance(value, Ref):
|
|
164
|
+
return "ref"
|
|
165
|
+
if isinstance(value, RecordValue):
|
|
166
|
+
return "record"
|
|
167
|
+
if isinstance(value, GeoPoint):
|
|
168
|
+
return "geo_point"
|
|
169
|
+
return "object"
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""What a reader limited to some Kinds sees of a schema (design 13.3, Kind-scoped invisibility).
|
|
2
|
+
|
|
3
|
+
`visible_schema(schema, kinds)` is the schema as a caller whose grants are narrowed to the
|
|
4
|
+
Kinds `kinds` (api names) sees it: the other Kinds are not there at all, and neither is
|
|
5
|
+
anything that would tell about them. Reads resolve names against this schema, so a name the
|
|
6
|
+
caller may not read fails exactly like a name that does not exist.
|
|
7
|
+
|
|
8
|
+
Left out, besides the invisible Kinds:
|
|
9
|
+
|
|
10
|
+
- Relations with an invisible end, and path Derivations with a step left out;
|
|
11
|
+
- Derivations computing an Attribute of an invisible Kind, aggregating over a Relation left
|
|
12
|
+
out, or reading (an expression's `links.<name>` fields, an aggregate's source Attribute and
|
|
13
|
+
filter) anything left out, transitively; the derived Attributes they compute go with them;
|
|
14
|
+
- Attributes of type `ref` (or arrays of `ref`) to an invisible Kind;
|
|
15
|
+
- Trait implementations mapping an Attribute left out, and Traits implemented by nothing left
|
|
16
|
+
in, or declaring a reference to an invisible Kind;
|
|
17
|
+
- Operations whose parameters, steps or expressions name anything left out;
|
|
18
|
+
- Sources writing an invisible Kind or a Relation or Attribute left out;
|
|
19
|
+
- Kinds keyed by a reference to an invisible Kind, and Kinds whose titles, full-text index
|
|
20
|
+
or embeddings come from an Attribute left out (a derived searchable or title Attribute
|
|
21
|
+
counting Records of an invisible Kind): their keys, stored titles and search vectors would
|
|
22
|
+
tell about it, so they are invisible too.
|
|
23
|
+
|
|
24
|
+
Elements are conservative supersets: an element that may tell about an invisible Kind is left
|
|
25
|
+
out, even when it would not in practice.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from collections.abc import Collection
|
|
32
|
+
|
|
33
|
+
from lorebase_core.expr.ast import Ident, Node, Select, parse
|
|
34
|
+
from lorebase_core.expr.derivations import _names, _nodes, derivation_reads
|
|
35
|
+
from lorebase_core.expr.environment import LINKS, link_fields
|
|
36
|
+
from lorebase_core.expr.errors import ExprError
|
|
37
|
+
from lorebase_core.expr.limits import ExprLimits
|
|
38
|
+
from lorebase_core.model.attribute import Attribute
|
|
39
|
+
from lorebase_core.model.derivation import (
|
|
40
|
+
AggregateDerivation,
|
|
41
|
+
Derivation,
|
|
42
|
+
ExpressionDerivation,
|
|
43
|
+
PathDerivation,
|
|
44
|
+
)
|
|
45
|
+
from lorebase_core.model.kind import Kind
|
|
46
|
+
from lorebase_core.model.operation import CreateLinkStep, DeleteLinkStep, Operation
|
|
47
|
+
from lorebase_core.model.relation import Relation
|
|
48
|
+
from lorebase_core.model.schema import Schema
|
|
49
|
+
from lorebase_core.model.source import Source
|
|
50
|
+
from lorebase_core.model.trait import Trait
|
|
51
|
+
from lorebase_core.types import TypeSpec
|
|
52
|
+
|
|
53
|
+
_WORD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def visible_schema(
|
|
57
|
+
schema: Schema, kinds: Collection[str], limits: ExprLimits | None = None
|
|
58
|
+
) -> Schema:
|
|
59
|
+
"""`schema` as seen by a reader of the Kinds named `kinds` (api names) only."""
|
|
60
|
+
limits = limits or ExprLimits()
|
|
61
|
+
shown = {k.id for k in schema.kinds.values() if k.api_name in kinds}
|
|
62
|
+
if len(shown) == len(schema.kinds):
|
|
63
|
+
return schema
|
|
64
|
+
while True:
|
|
65
|
+
relations, paths, dropped, derivations = _project(schema, shown, limits)
|
|
66
|
+
# A Kind whose keys hold keys of a Kind left out, or whose titles, full-text index or
|
|
67
|
+
# embeddings are computed from something left out, would tell about it through them:
|
|
68
|
+
# it is left out too.
|
|
69
|
+
tainted = {
|
|
70
|
+
kind_id
|
|
71
|
+
for kind_id in shown
|
|
72
|
+
if _indexes_hidden(schema.kinds[kind_id], dropped)
|
|
73
|
+
or _keys_hidden(schema.kinds[kind_id], shown)
|
|
74
|
+
}
|
|
75
|
+
if not tainted:
|
|
76
|
+
break
|
|
77
|
+
shown -= tainted
|
|
78
|
+
sources = {
|
|
79
|
+
s.id for s in schema.sources.values() if _source_visible(s, shown, relations, dropped)
|
|
80
|
+
}
|
|
81
|
+
kept_kinds = {
|
|
82
|
+
kind_id: _kind(kind, dropped, sources)
|
|
83
|
+
for kind_id, kind in schema.kinds.items()
|
|
84
|
+
if kind_id in shown
|
|
85
|
+
}
|
|
86
|
+
kept_relations = {
|
|
87
|
+
rid: relation.model_copy(
|
|
88
|
+
update={"attributes": _attributes(relation.attributes, dropped, sources)}
|
|
89
|
+
)
|
|
90
|
+
for rid, relation in schema.relations.items()
|
|
91
|
+
if rid in relations
|
|
92
|
+
}
|
|
93
|
+
# An implementation mapping a Trait Attribute to an Attribute left out goes with it.
|
|
94
|
+
implementations = {
|
|
95
|
+
kind_id: tuple(
|
|
96
|
+
t for t in kind.traits if not any(a in dropped for a in t.attribute_map.values())
|
|
97
|
+
)
|
|
98
|
+
for kind_id, kind in kept_kinds.items()
|
|
99
|
+
}
|
|
100
|
+
implemented = {t.trait_id for found in implementations.values() for t in found}
|
|
101
|
+
traits = {
|
|
102
|
+
tid: trait
|
|
103
|
+
for tid, trait in schema.traits.items()
|
|
104
|
+
if (tid in implemented or not schema.kinds_implementing(tid))
|
|
105
|
+
and not any(_refs_hidden(a.type, shown) for a in trait.attributes.values())
|
|
106
|
+
}
|
|
107
|
+
kept_kinds = {
|
|
108
|
+
kind_id: kind.model_copy(
|
|
109
|
+
update={"traits": tuple(t for t in implementations[kind_id] if t.trait_id in traits)}
|
|
110
|
+
)
|
|
111
|
+
for kind_id, kind in kept_kinds.items()
|
|
112
|
+
}
|
|
113
|
+
hidden_words = _hidden_words(schema, shown, relations | paths, dropped)
|
|
114
|
+
operations = {
|
|
115
|
+
oid: operation
|
|
116
|
+
for oid, operation in schema.operations.items()
|
|
117
|
+
if _operation_visible(operation, shown, relations, dropped, hidden_words)
|
|
118
|
+
}
|
|
119
|
+
return schema.model_copy(
|
|
120
|
+
update={
|
|
121
|
+
"kinds": kept_kinds,
|
|
122
|
+
"relations": kept_relations,
|
|
123
|
+
"traits": traits,
|
|
124
|
+
"operations": operations,
|
|
125
|
+
"derivations": {
|
|
126
|
+
d: schema.derivations[d] for d in schema.derivations if d in derivations
|
|
127
|
+
},
|
|
128
|
+
"sources": {s: schema.sources[s] for s in schema.sources if s in sources},
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _project(
|
|
134
|
+
schema: Schema, shown: Collection[str], limits: ExprLimits
|
|
135
|
+
) -> tuple[set[str], set[str], set[str], set[str]]:
|
|
136
|
+
"""Relations, path Derivations, left out Attributes and Derivations for the Kinds
|
|
137
|
+
`shown` (ids)."""
|
|
138
|
+
relations = {
|
|
139
|
+
r.id for r in schema.relations.values() if r.from_kind in shown and r.to_kind in shown
|
|
140
|
+
}
|
|
141
|
+
paths = {
|
|
142
|
+
path.id
|
|
143
|
+
for path in schema.path_relations().values()
|
|
144
|
+
if all(step.relation.id in relations for step in path.steps)
|
|
145
|
+
}
|
|
146
|
+
dropped = _hidden_attributes(schema, shown)
|
|
147
|
+
derivations = _visible_derivations(schema, shown, relations | paths, dropped, limits)
|
|
148
|
+
for kind in schema.kinds.values():
|
|
149
|
+
dropped |= {
|
|
150
|
+
a.id
|
|
151
|
+
for a in kind.attributes.values()
|
|
152
|
+
if a.derivation_id is not None and a.derivation_id not in derivations
|
|
153
|
+
}
|
|
154
|
+
return relations, paths, dropped, derivations
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _indexes_hidden(kind: Kind, dropped: Collection[str]) -> bool:
|
|
158
|
+
"""Whether a left out Attribute of `kind` feeds its titles, full-text index or
|
|
159
|
+
embeddings."""
|
|
160
|
+
template = kind.search.embedding_template
|
|
161
|
+
words = set(_WORD.findall(template)) if template else set()
|
|
162
|
+
return any(
|
|
163
|
+
a.id in dropped and (a.searchable or a.id == kind.title_attribute or a.api_name in words)
|
|
164
|
+
for a in kind.attributes.values()
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _refs_hidden(spec: TypeSpec, shown: Collection[str]) -> bool:
|
|
169
|
+
"""Whether a type holds keys of a Kind not in `shown`."""
|
|
170
|
+
if spec.kind_ref is not None and spec.kind_ref not in shown:
|
|
171
|
+
return True
|
|
172
|
+
return spec.element is not None and _refs_hidden(spec.element, shown)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _keys_hidden(kind: Kind, shown: Collection[str]) -> bool:
|
|
176
|
+
"""Whether a primary key Attribute of `kind` holds keys of a Kind not in `shown`."""
|
|
177
|
+
return any(
|
|
178
|
+
(a := kind.attributes.get(attribute_id)) is not None and _refs_hidden(a.type, shown)
|
|
179
|
+
for attribute_id in kind.primary_key
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _hidden_attributes(schema: Schema, shown: Collection[str]) -> set[str]:
|
|
184
|
+
"""Stored Attributes holding keys of Kinds not in `shown`."""
|
|
185
|
+
owners: list[Kind | Relation] = [*schema.kinds.values(), *schema.relations.values()]
|
|
186
|
+
return {
|
|
187
|
+
a.id for owner in owners for a in owner.attributes.values() if _refs_hidden(a.type, shown)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _expression_links(
|
|
192
|
+
schema: Schema, kind_id: str, source: str, limits: ExprLimits
|
|
193
|
+
) -> set[str] | None:
|
|
194
|
+
"""Ids of the Relations (or path Derivations) an expression over a Record of `kind_id`
|
|
195
|
+
reads through `links.<name>`; None when it uses `links` otherwise (every one, then)."""
|
|
196
|
+
try:
|
|
197
|
+
root = parse(source, limits)
|
|
198
|
+
except ExprError:
|
|
199
|
+
return set() # not evaluated: expression validation reports it
|
|
200
|
+
names = _names(root)
|
|
201
|
+
if LINKS in names.idents and _bare_links(root):
|
|
202
|
+
return None
|
|
203
|
+
fields = link_fields(schema, kind_id)
|
|
204
|
+
return {fields[name].relation.id for name in names.links if name in fields}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _bare_links(root: Node) -> bool:
|
|
208
|
+
"""Whether `links` appears other than as `links.<name>`."""
|
|
209
|
+
selected = sum(
|
|
210
|
+
1
|
|
211
|
+
for node in _nodes(root)
|
|
212
|
+
if isinstance(node, Select)
|
|
213
|
+
and isinstance(node.operand, Ident)
|
|
214
|
+
and node.operand.name == LINKS
|
|
215
|
+
)
|
|
216
|
+
named = sum(1 for node in _nodes(root) if isinstance(node, Ident) and node.name == LINKS)
|
|
217
|
+
return named > selected
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _derivation_visible(
|
|
221
|
+
schema: Schema,
|
|
222
|
+
derivation: Derivation,
|
|
223
|
+
shown: Collection[str],
|
|
224
|
+
links: Collection[str],
|
|
225
|
+
dropped: Collection[str],
|
|
226
|
+
limits: ExprLimits,
|
|
227
|
+
) -> bool:
|
|
228
|
+
definition = derivation.definition
|
|
229
|
+
if isinstance(definition, PathDerivation):
|
|
230
|
+
return derivation.id in links
|
|
231
|
+
if definition.kind not in shown:
|
|
232
|
+
return False
|
|
233
|
+
if isinstance(definition, AggregateDerivation):
|
|
234
|
+
if definition.relation not in links:
|
|
235
|
+
return False
|
|
236
|
+
relation = schema.link_relation(definition.relation)
|
|
237
|
+
if relation is None:
|
|
238
|
+
return False
|
|
239
|
+
if definition.filter is not None:
|
|
240
|
+
far = relation.far_endpoint(definition.direction)
|
|
241
|
+
used = _expression_links(schema, far, definition.filter, limits)
|
|
242
|
+
if used is None or not used <= set(links):
|
|
243
|
+
return False
|
|
244
|
+
elif isinstance(definition, ExpressionDerivation):
|
|
245
|
+
used = _expression_links(schema, definition.kind, definition.expression, limits)
|
|
246
|
+
if used is None or not used <= set(links):
|
|
247
|
+
return False
|
|
248
|
+
reads = derivation_reads(schema, derivation, limits)
|
|
249
|
+
return all(kind in shown and attribute not in dropped for kind, attribute in reads)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _visible_derivations(
|
|
253
|
+
schema: Schema,
|
|
254
|
+
shown: Collection[str],
|
|
255
|
+
links: Collection[str],
|
|
256
|
+
dropped: set[str],
|
|
257
|
+
limits: ExprLimits,
|
|
258
|
+
) -> set[str]:
|
|
259
|
+
"""Ids of the Derivations left in; `dropped` gains the derived Attributes left out."""
|
|
260
|
+
visible = set(schema.derivations)
|
|
261
|
+
changed = True
|
|
262
|
+
while changed:
|
|
263
|
+
changed = False
|
|
264
|
+
for derivation_id in sorted(visible):
|
|
265
|
+
derivation = schema.derivations[derivation_id]
|
|
266
|
+
if _derivation_visible(schema, derivation, shown, links, dropped, limits):
|
|
267
|
+
continue
|
|
268
|
+
visible.discard(derivation_id)
|
|
269
|
+
target = derivation.target
|
|
270
|
+
if target is not None:
|
|
271
|
+
dropped.add(target[1])
|
|
272
|
+
changed = True
|
|
273
|
+
return visible
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _attributes(
|
|
277
|
+
attributes: dict[str, Attribute], dropped: Collection[str], sources: Collection[str]
|
|
278
|
+
) -> dict[str, Attribute]:
|
|
279
|
+
found: dict[str, Attribute] = {}
|
|
280
|
+
for attribute_id, attribute in attributes.items():
|
|
281
|
+
if attribute_id in dropped:
|
|
282
|
+
continue
|
|
283
|
+
if attribute.authority is not None and attribute.authority not in sources:
|
|
284
|
+
attribute = attribute.model_copy(update={"authority": None})
|
|
285
|
+
found[attribute_id] = attribute
|
|
286
|
+
return found
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _kind(kind: Kind, dropped: Collection[str], sources: Collection[str]) -> Kind:
|
|
290
|
+
attributes = _attributes(kind.attributes, dropped, sources)
|
|
291
|
+
return kind.model_copy(update={"attributes": attributes})
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _source_visible(
|
|
295
|
+
source: Source, shown: Collection[str], relations: Collection[str], dropped: Collection[str]
|
|
296
|
+
) -> bool:
|
|
297
|
+
return (
|
|
298
|
+
source.target_kind in shown
|
|
299
|
+
and all(m.relation in relations for m in source.relation_mapping)
|
|
300
|
+
and not any(a in dropped for a in source.attribute_mapping)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _hidden_words(
|
|
305
|
+
schema: Schema, shown: Collection[str], links: Collection[str], dropped: Collection[str]
|
|
306
|
+
) -> frozenset[str]:
|
|
307
|
+
"""Names of what is left out (Kinds, Relations both ways, path Derivations, Attributes)."""
|
|
308
|
+
words: set[str] = {k.api_name for k in schema.kinds.values() if k.id not in shown}
|
|
309
|
+
for relation in schema.relations.values():
|
|
310
|
+
if relation.id not in links:
|
|
311
|
+
words |= {relation.api_name, relation.inverse_api_name}
|
|
312
|
+
for path in schema.path_relations().values():
|
|
313
|
+
if path.id not in links:
|
|
314
|
+
words |= {path.relation.api_name, path.relation.inverse_api_name}
|
|
315
|
+
for kind in schema.kinds.values():
|
|
316
|
+
words |= {a.api_name for a in kind.attributes.values() if a.id in dropped}
|
|
317
|
+
return frozenset(w for w in words if w)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _operation_visible(
|
|
321
|
+
operation: Operation,
|
|
322
|
+
shown: Collection[str],
|
|
323
|
+
relations: Collection[str],
|
|
324
|
+
dropped: Collection[str],
|
|
325
|
+
hidden_words: Collection[str],
|
|
326
|
+
) -> bool:
|
|
327
|
+
if any(_refs_hidden(p.type, shown) for p in operation.parameters):
|
|
328
|
+
return False
|
|
329
|
+
expressions: list[str] = [p.expression for p in operation.preconditions]
|
|
330
|
+
if operation.review_policy.expression is not None:
|
|
331
|
+
expressions.append(operation.review_policy.expression)
|
|
332
|
+
for step in operation.steps:
|
|
333
|
+
if isinstance(step, CreateLinkStep | DeleteLinkStep):
|
|
334
|
+
if step.relation not in relations:
|
|
335
|
+
return False
|
|
336
|
+
expressions += [step.from_, step.to]
|
|
337
|
+
else:
|
|
338
|
+
if step.kind not in shown:
|
|
339
|
+
return False
|
|
340
|
+
target = getattr(step, "target", None)
|
|
341
|
+
if target is not None:
|
|
342
|
+
expressions.append(target)
|
|
343
|
+
values: dict[str, str] = getattr(step, "values", {})
|
|
344
|
+
if any(a in dropped for a in values):
|
|
345
|
+
return False
|
|
346
|
+
expressions += list(values.values())
|
|
347
|
+
return not any(set(_WORD.findall(e)) & set(hidden_words) for e in expressions)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def element_ids(schema: Schema) -> set[str]:
|
|
351
|
+
"""Ids of every schema element and of every Attribute of a Kind, Relation or Trait."""
|
|
352
|
+
ids = {element.id for element in schema.elements()}
|
|
353
|
+
owners: list[Kind | Relation | Trait] = [
|
|
354
|
+
*schema.kinds.values(),
|
|
355
|
+
*schema.relations.values(),
|
|
356
|
+
*schema.traits.values(),
|
|
357
|
+
]
|
|
358
|
+
for owner in owners:
|
|
359
|
+
ids |= set(owner.attributes)
|
|
360
|
+
return ids
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
__all__ = ["element_ids", "visible_schema"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Deterministic schema inference from structured sources (extraction tier 0, design 9.1).
|
|
2
|
+
|
|
3
|
+
``infer_schema`` turns ``TableProfile`` objects (columns with sampled or complete values, row
|
|
4
|
+
counts and whatever a catalog declares) into a ``SchemaProposal``: a draft ``Schema`` with Kinds,
|
|
5
|
+
typed Attributes with constraints, primary keys and Relations with cardinality, the evidence
|
|
6
|
+
behind each element and the issues a reviewer must settle. No model is called and nothing is
|
|
7
|
+
read or written: profilers (CSV, database catalogs) live outside the core.
|
|
8
|
+
|
|
9
|
+
Thresholds and naming rules are ``InferenceOptions``; see docs/inference.md.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from lorebase_core.infer.engine import infer_schema
|
|
13
|
+
from lorebase_core.infer.options import InferenceOptions
|
|
14
|
+
from lorebase_core.infer.profile import ColumnProfile, DeclaredForeignKey, TableProfile
|
|
15
|
+
from lorebase_core.infer.proposal import (
|
|
16
|
+
Evidence,
|
|
17
|
+
InferenceIssue,
|
|
18
|
+
InferenceIssueCode,
|
|
19
|
+
SchemaProposal,
|
|
20
|
+
TableOutcome,
|
|
21
|
+
TableRole,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ColumnProfile",
|
|
26
|
+
"DeclaredForeignKey",
|
|
27
|
+
"Evidence",
|
|
28
|
+
"InferenceIssue",
|
|
29
|
+
"InferenceIssueCode",
|
|
30
|
+
"InferenceOptions",
|
|
31
|
+
"SchemaProposal",
|
|
32
|
+
"TableOutcome",
|
|
33
|
+
"TableProfile",
|
|
34
|
+
"TableRole",
|
|
35
|
+
"infer_schema",
|
|
36
|
+
]
|