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