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,97 @@
|
|
|
1
|
+
"""Query DSL (design 8.2): request models and their validation against a Schema."""
|
|
2
|
+
|
|
3
|
+
from lorebase_core.query.dsl import (
|
|
4
|
+
Aggregate,
|
|
5
|
+
AllOf,
|
|
6
|
+
AnyOf,
|
|
7
|
+
AttributeCondition,
|
|
8
|
+
CountCondition,
|
|
9
|
+
DerivedCondition,
|
|
10
|
+
Include,
|
|
11
|
+
MatchCondition,
|
|
12
|
+
NotCondition,
|
|
13
|
+
Operator,
|
|
14
|
+
OrderBy,
|
|
15
|
+
Predicate,
|
|
16
|
+
Query,
|
|
17
|
+
ViaCondition,
|
|
18
|
+
)
|
|
19
|
+
from lorebase_core.query.resolve import (
|
|
20
|
+
KEY_FIELD,
|
|
21
|
+
OPERATORS,
|
|
22
|
+
TITLE_FIELD,
|
|
23
|
+
AggregatePlan,
|
|
24
|
+
Comparison,
|
|
25
|
+
Condition,
|
|
26
|
+
Conjunction,
|
|
27
|
+
Disjunction,
|
|
28
|
+
FieldRef,
|
|
29
|
+
FieldSource,
|
|
30
|
+
FullText,
|
|
31
|
+
Hop,
|
|
32
|
+
IncludePlan,
|
|
33
|
+
LinkCount,
|
|
34
|
+
LinkExists,
|
|
35
|
+
Negation,
|
|
36
|
+
Ordering,
|
|
37
|
+
QueryError,
|
|
38
|
+
QueryIssue,
|
|
39
|
+
QueryLimits,
|
|
40
|
+
QueryPlan,
|
|
41
|
+
Target,
|
|
42
|
+
condition_kinds,
|
|
43
|
+
plan_kinds,
|
|
44
|
+
resolve_field,
|
|
45
|
+
resolve_hop,
|
|
46
|
+
resolve_query,
|
|
47
|
+
resolve_where,
|
|
48
|
+
target_of_kind,
|
|
49
|
+
target_of_trait,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"KEY_FIELD",
|
|
54
|
+
"OPERATORS",
|
|
55
|
+
"TITLE_FIELD",
|
|
56
|
+
"Aggregate",
|
|
57
|
+
"AggregatePlan",
|
|
58
|
+
"AllOf",
|
|
59
|
+
"AnyOf",
|
|
60
|
+
"AttributeCondition",
|
|
61
|
+
"Comparison",
|
|
62
|
+
"Condition",
|
|
63
|
+
"Conjunction",
|
|
64
|
+
"CountCondition",
|
|
65
|
+
"DerivedCondition",
|
|
66
|
+
"Disjunction",
|
|
67
|
+
"FieldRef",
|
|
68
|
+
"FieldSource",
|
|
69
|
+
"FullText",
|
|
70
|
+
"Hop",
|
|
71
|
+
"Include",
|
|
72
|
+
"IncludePlan",
|
|
73
|
+
"LinkCount",
|
|
74
|
+
"LinkExists",
|
|
75
|
+
"MatchCondition",
|
|
76
|
+
"Negation",
|
|
77
|
+
"NotCondition",
|
|
78
|
+
"Operator",
|
|
79
|
+
"OrderBy",
|
|
80
|
+
"Ordering",
|
|
81
|
+
"Predicate",
|
|
82
|
+
"Query",
|
|
83
|
+
"QueryError",
|
|
84
|
+
"QueryIssue",
|
|
85
|
+
"QueryLimits",
|
|
86
|
+
"QueryPlan",
|
|
87
|
+
"Target",
|
|
88
|
+
"ViaCondition",
|
|
89
|
+
"condition_kinds",
|
|
90
|
+
"plan_kinds",
|
|
91
|
+
"resolve_field",
|
|
92
|
+
"resolve_hop",
|
|
93
|
+
"resolve_query",
|
|
94
|
+
"resolve_where",
|
|
95
|
+
"target_of_kind",
|
|
96
|
+
"target_of_trait",
|
|
97
|
+
]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""The JSON query DSL (design 8.2): request models only, no schema knowledge.
|
|
2
|
+
|
|
3
|
+
A query names a Kind (or a Trait, which expands to the Kinds implementing it), an optional
|
|
4
|
+
`where` predicate, the fields to return, Links to include, the ordering and a page size:
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"kind": "Part",
|
|
8
|
+
"where": {"and": [
|
|
9
|
+
{"attr": "category", "op": "eq", "value": "bearing"},
|
|
10
|
+
{"via": "supplied_by", "where": {"attr": "country", "op": "in", "value": ["IT", "DE"]}},
|
|
11
|
+
{"count": "supplied_by", "op": "gte", "value": 2}
|
|
12
|
+
]},
|
|
13
|
+
"select": ["key", "title", "category"],
|
|
14
|
+
"include": {"supplied_by": {"select": ["key", "title"], "limit": 5}},
|
|
15
|
+
"order_by": [{"attr": "category"}, {"attr": "key", "dir": "desc"}],
|
|
16
|
+
"limit": 50,
|
|
17
|
+
"aggregate": {"group_by": "category"}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Predicates are JSON objects told apart by their leading member:
|
|
21
|
+
|
|
22
|
+
- `{"and": [...]}`, `{"or": [...]}`, `{"not": {...}}`: boolean composition;
|
|
23
|
+
- `{"attr": name, "op": op, "value": v}`: compare an Attribute (or the pseudo fields `key` and
|
|
24
|
+
`title`); `{"derived": name, ...}` does the same for a derived Attribute;
|
|
25
|
+
- `{"via": relation, "where": {...}}`: a Link of the Relation (forward or inverse api name)
|
|
26
|
+
exists, optionally to a Record matching `where`;
|
|
27
|
+
- `{"count": relation, "op": op, "value": n, "where": {...}}`: compare the number of such Links;
|
|
28
|
+
- `{"match": "text"}`: full-text match on the Record's search vector.
|
|
29
|
+
|
|
30
|
+
Validation against a Schema lives in `lorebase_core.query.resolve`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from enum import StrEnum
|
|
36
|
+
from typing import Annotated, Any, Literal, Self
|
|
37
|
+
|
|
38
|
+
from pydantic import (
|
|
39
|
+
BaseModel,
|
|
40
|
+
ConfigDict,
|
|
41
|
+
Discriminator,
|
|
42
|
+
Field,
|
|
43
|
+
JsonValue,
|
|
44
|
+
Tag,
|
|
45
|
+
model_validator,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"Aggregate",
|
|
50
|
+
"AllOf",
|
|
51
|
+
"AnyOf",
|
|
52
|
+
"AttributeCondition",
|
|
53
|
+
"CountCondition",
|
|
54
|
+
"DerivedCondition",
|
|
55
|
+
"Include",
|
|
56
|
+
"MatchCondition",
|
|
57
|
+
"NotCondition",
|
|
58
|
+
"Operator",
|
|
59
|
+
"OrderBy",
|
|
60
|
+
"Predicate",
|
|
61
|
+
"Query",
|
|
62
|
+
"ViaCondition",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class QueryModel(BaseModel):
|
|
67
|
+
"""Frozen request model; unknown members are rejected."""
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(frozen=True, extra="forbid", populate_by_name=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Operator(StrEnum):
|
|
73
|
+
"""Comparison operators; which ones apply depends on the Attribute type."""
|
|
74
|
+
|
|
75
|
+
EQ = "eq"
|
|
76
|
+
NE = "ne"
|
|
77
|
+
LT = "lt"
|
|
78
|
+
LTE = "lte"
|
|
79
|
+
GT = "gt"
|
|
80
|
+
GTE = "gte"
|
|
81
|
+
IN = "in"
|
|
82
|
+
BETWEEN = "between"
|
|
83
|
+
CONTAINS = "contains"
|
|
84
|
+
PREFIX = "prefix"
|
|
85
|
+
EXISTS = "exists"
|
|
86
|
+
MATCH = "match"
|
|
87
|
+
NEAR = "near"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AttributeCondition(QueryModel):
|
|
91
|
+
"""Compare an Attribute, `key` or `title` with a value."""
|
|
92
|
+
|
|
93
|
+
attr: str = Field(min_length=1)
|
|
94
|
+
op: Operator
|
|
95
|
+
value: JsonValue = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DerivedCondition(QueryModel):
|
|
99
|
+
"""Compare a derived Attribute with a value."""
|
|
100
|
+
|
|
101
|
+
derived: str = Field(min_length=1)
|
|
102
|
+
op: Operator
|
|
103
|
+
value: JsonValue = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ViaCondition(QueryModel):
|
|
107
|
+
"""A Link of `via` exists, to a Record matching `where` when given."""
|
|
108
|
+
|
|
109
|
+
via: str = Field(min_length=1)
|
|
110
|
+
where: Predicate | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CountCondition(QueryModel):
|
|
114
|
+
"""Compare the number of Links of `count` (to Records matching `where`) with a value."""
|
|
115
|
+
|
|
116
|
+
count: str = Field(min_length=1)
|
|
117
|
+
op: Operator
|
|
118
|
+
value: JsonValue
|
|
119
|
+
where: Predicate | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class MatchCondition(QueryModel):
|
|
123
|
+
"""Full-text match of the Record (title and searchable Attributes)."""
|
|
124
|
+
|
|
125
|
+
match: str = Field(min_length=1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AllOf(QueryModel):
|
|
129
|
+
items: list[Predicate] = Field(alias="and", min_length=1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AnyOf(QueryModel):
|
|
133
|
+
items: list[Predicate] = Field(alias="or", min_length=1)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NotCondition(QueryModel):
|
|
137
|
+
item: Predicate = Field(alias="not")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_TAGS = ("and", "or", "not", "via", "count", "match", "derived", "attr")
|
|
141
|
+
_TAG_OF_MODEL: dict[type[QueryModel], str] = {}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _predicate_tag(value: Any) -> str | None:
|
|
145
|
+
"""Discriminate predicates by their leading member (raw JSON) or by model class."""
|
|
146
|
+
if isinstance(value, dict):
|
|
147
|
+
for tag in _TAGS:
|
|
148
|
+
if tag in value:
|
|
149
|
+
return tag
|
|
150
|
+
return None
|
|
151
|
+
return _TAG_OF_MODEL.get(type(value))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
Predicate = Annotated[
|
|
155
|
+
Annotated[AllOf, Tag("and")]
|
|
156
|
+
| Annotated[AnyOf, Tag("or")]
|
|
157
|
+
| Annotated[NotCondition, Tag("not")]
|
|
158
|
+
| Annotated[ViaCondition, Tag("via")]
|
|
159
|
+
| Annotated[CountCondition, Tag("count")]
|
|
160
|
+
| Annotated[MatchCondition, Tag("match")]
|
|
161
|
+
| Annotated[DerivedCondition, Tag("derived")]
|
|
162
|
+
| Annotated[AttributeCondition, Tag("attr")],
|
|
163
|
+
Discriminator(
|
|
164
|
+
_predicate_tag,
|
|
165
|
+
custom_error_type="predicate",
|
|
166
|
+
custom_error_message="a predicate needs one of the members " + ", ".join(_TAGS),
|
|
167
|
+
),
|
|
168
|
+
]
|
|
169
|
+
"""Any predicate of the DSL."""
|
|
170
|
+
|
|
171
|
+
_TAG_OF_MODEL.update(
|
|
172
|
+
{
|
|
173
|
+
AllOf: "and",
|
|
174
|
+
AnyOf: "or",
|
|
175
|
+
NotCondition: "not",
|
|
176
|
+
ViaCondition: "via",
|
|
177
|
+
CountCondition: "count",
|
|
178
|
+
MatchCondition: "match",
|
|
179
|
+
DerivedCondition: "derived",
|
|
180
|
+
AttributeCondition: "attr",
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class OrderBy(QueryModel):
|
|
186
|
+
"""One ordering term: an Attribute, `key` or `title`. Absent values sort last."""
|
|
187
|
+
|
|
188
|
+
attr: str = Field(min_length=1)
|
|
189
|
+
dir: Literal["asc", "desc"] = "asc"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Include(QueryModel):
|
|
193
|
+
"""Neighbors to return with each Record, through one Relation."""
|
|
194
|
+
|
|
195
|
+
select: list[str] | None = None
|
|
196
|
+
limit: int | None = Field(default=None, ge=1)
|
|
197
|
+
where: Predicate | None = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Aggregate(QueryModel):
|
|
201
|
+
"""Count the matching Records per Kind, and per value of `group_by` when given."""
|
|
202
|
+
|
|
203
|
+
group_by: str | None = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Query(QueryModel):
|
|
207
|
+
"""A query over the Records of one Kind or of the Kinds implementing a Trait."""
|
|
208
|
+
|
|
209
|
+
kind: str | None = None
|
|
210
|
+
trait: str | None = None
|
|
211
|
+
where: Predicate | None = None
|
|
212
|
+
select: list[str] | None = None
|
|
213
|
+
include: dict[str, Include] = Field(default_factory=dict)
|
|
214
|
+
order_by: list[OrderBy] = Field(default_factory=list)
|
|
215
|
+
limit: int | None = Field(default=None, ge=0)
|
|
216
|
+
cursor: str | None = None
|
|
217
|
+
aggregate: Aggregate | None = None
|
|
218
|
+
|
|
219
|
+
@model_validator(mode="after")
|
|
220
|
+
def _one_target(self) -> Self:
|
|
221
|
+
if (self.kind is None) == (self.trait is None):
|
|
222
|
+
raise ValueError("a query names exactly one of kind or trait")
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
for _model in (ViaCondition, CountCondition, AllOf, AnyOf, NotCondition, Include, Query):
|
|
227
|
+
_model.model_rebuild()
|