python-corekit 0.1.1__py3-none-any.whl → 0.3.0__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.
- corekit/api/__init__.py +18 -3
- corekit/api/application.py +275 -0
- corekit/api/lifespan.py +233 -0
- corekit/api/middleware.py +93 -0
- corekit/api/routers.py +109 -1
- corekit/concurrency/__init__.py +2 -2
- corekit/concurrency/decorators.py +32 -5
- corekit/concurrency/thread_local.py +2 -2
- corekit/concurrency/worker.py +74 -65
- corekit/config/loader.py +42 -5
- corekit/config/settings.py +11 -1
- corekit/connections/__init__.py +7 -1
- corekit/connections/connectable.py +45 -4
- corekit/connections/redis/connection.py +53 -10
- corekit/connections/sql/__init__.py +33 -4
- corekit/connections/sql/connection.py +56 -3
- corekit/connections/sql/fields/__init__.py +2 -2
- corekit/connections/sql/fields/jsonb.py +13 -6
- corekit/connections/sql/migration/__init__.py +9 -5
- corekit/connections/sql/migration/base.py +3 -3
- corekit/connections/sql/migration/operations.py +135 -44
- corekit/connections/sql/migration/registry.py +2 -2
- corekit/connections/sql/operations/__init__.py +24 -0
- corekit/connections/sql/operations/base.py +111 -0
- corekit/connections/sql/operations/statements.py +170 -0
- corekit/connections/sql/query.py +4 -62
- corekit/connections/sql/table.py +33 -29
- corekit/crypto/__init__.py +3 -1
- corekit/crypto/constants.py +2 -2
- corekit/crypto/hasher.py +9 -4
- corekit/data/__init__.py +8 -0
- corekit/data/dataset.py +8 -2
- corekit/data/expressions/__init__.py +10 -2
- corekit/data/expressions/comparison.py +142 -123
- corekit/data/expressions/expression.py +71 -98
- corekit/data/expressions/operator.py +39 -0
- corekit/data/expressions/target.py +21 -0
- corekit/data/record.py +147 -147
- corekit/data/stats.py +162 -157
- corekit/decorators/__init__.py +2 -2
- corekit/decorators/exception_handling.py +38 -9
- corekit/docker/watchdog.py +50 -31
- corekit/etl/__init__.py +2 -1
- corekit/etl/connection.py +46 -44
- corekit/etl/extract/extractor.py +6 -13
- corekit/etl/orchestrator.py +19 -2
- corekit/etl/schemas.py +2 -2
- corekit/etl/transform/transformer.py +4 -1
- corekit/events/publisher.py +1 -1
- corekit/events/reader.py +26 -21
- corekit/events/sse.py +4 -1
- corekit/events/websocket.py +27 -13
- corekit/exceptions/__init__.py +33 -0
- corekit/exceptions/base.py +139 -10
- corekit/exceptions/enum.py +17 -0
- corekit/exceptions/types.py +6 -6
- corekit/files/__init__.py +2 -4
- corekit/files/base.py +15 -2
- corekit/files/enum.py +0 -5
- corekit/files/json.py +16 -2
- corekit/http/__init__.py +51 -0
- corekit/http/api.py +24 -0
- corekit/http/client.py +100 -73
- corekit/http/exceptions.py +140 -0
- corekit/http/response.py +50 -1
- corekit/http/status.py +89 -0
- corekit/jobs/__init__.py +26 -0
- corekit/jobs/registry.py +87 -0
- corekit/jobs/runner.py +80 -0
- corekit/jobs/task.py +173 -0
- corekit/log_monitor/models.py +8 -2
- corekit/log_monitor/service.py +77 -38
- corekit/notifications/base.py +18 -10
- corekit/observability/__init__.py +12 -3
- corekit/observability/benchmarkable.py +23 -5
- corekit/observability/loggable.py +21 -0
- corekit/observability/request_context.py +188 -0
- corekit/observability/timing/timer.py +4 -2
- corekit/registry/__init__.py +12 -7
- corekit/registry/ordered.py +86 -0
- corekit/registry/registry.py +55 -14
- corekit/schemas/__init__.py +10 -0
- corekit/schemas/enum.py +70 -49
- corekit/schemas/models/arbitrary.py +11 -11
- corekit/schemas/pydantic/fields.py +35 -35
- corekit/schemas/types.py +45 -40
- corekit/serialization/__init__.py +24 -0
- corekit/serialization/pickle_file.py +61 -0
- corekit/serialization/serializable.py +22 -2
- corekit/serialization/serializer.py +10 -3
- corekit/utils/__init__.py +59 -5
- corekit/utils/coercion.py +118 -0
- corekit/utils/collections.py +124 -0
- corekit/utils/ids.py +61 -5
- corekit/utils/payload.py +112 -0
- corekit/utils/raise_exc.py +8 -8
- corekit/utils/text.py +56 -0
- corekit/utils/time.py +74 -21
- corekit/utils/validators.py +15 -15
- corekit/utils/void.py +8 -8
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/METADATA +103 -97
- python_corekit-0.3.0.dist-info/RECORD +145 -0
- corekit/constants.py +0 -45
- corekit/exceptions/http/exceptions.py +0 -37
- corekit/files/pickle.py +0 -12
- python_corekit-0.1.1.dist-info/RECORD +0 -125
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
from typing import Any, Iterable
|
|
2
2
|
|
|
3
3
|
from corekit.data.expressions.expression import Expression
|
|
4
|
+
from corekit.data.expressions.operator import Operator
|
|
5
|
+
from corekit.data.expressions.target import Target
|
|
6
|
+
|
|
7
|
+
UNSET = object()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnknownFieldError(AttributeError):
|
|
11
|
+
"""
|
|
12
|
+
Raised when an expression names a field the target does not have.
|
|
13
|
+
"""
|
|
4
14
|
|
|
5
15
|
|
|
6
16
|
class Comparison(Expression):
|
|
7
17
|
"""
|
|
8
18
|
Base for comparing two values (Equals, GreaterThan, IsIn, ...).
|
|
9
19
|
|
|
10
|
-
`left`, `right`, and `
|
|
11
|
-
|
|
12
|
-
tree and read out what each node represents without evaluating it.
|
|
13
|
-
`symbol` is a human-readable operator string; the class itself
|
|
20
|
+
`left`, `right`, and `operator` are public so a translator can walk the
|
|
21
|
+
tree and read what each node represents without evaluating it. The class itself
|
|
14
22
|
(Equals, GreaterThan, etc.) is the canonical, type-safe identifier of
|
|
15
23
|
*which* operation a node represents, so a translator can dispatch on
|
|
16
24
|
isinstance(node, GreaterThan) rather than string-matching on symbol.
|
|
17
25
|
"""
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
operator: Operator
|
|
20
28
|
|
|
21
29
|
def __init__(self, left: Any, right: Any) -> None:
|
|
22
30
|
self.left = left
|
|
@@ -25,154 +33,153 @@ class Comparison(Expression):
|
|
|
25
33
|
def __repr__(self) -> str:
|
|
26
34
|
return f"({self.left!r} {self.symbol} {self.right!r})"
|
|
27
35
|
|
|
36
|
+
@property
|
|
37
|
+
def symbol(self) -> str:
|
|
38
|
+
"""
|
|
39
|
+
The human-readable operator, e.g. ``>=``.
|
|
40
|
+
"""
|
|
41
|
+
return self.operator.symbol
|
|
42
|
+
|
|
28
43
|
@staticmethod
|
|
29
|
-
def
|
|
44
|
+
def resolve(value: Any, *args: Any, **kwargs: Any) -> Any:
|
|
30
45
|
"""
|
|
31
|
-
|
|
32
|
-
to be able to resolve the final value of complex expressions without
|
|
33
|
-
having to evaluate the entire tree.
|
|
46
|
+
Resolve one operand, passing everything through to the expression.
|
|
34
47
|
"""
|
|
35
48
|
if isinstance(value, ValueExpression):
|
|
36
|
-
return value.
|
|
49
|
+
return value.resolve(*args, **kwargs)
|
|
37
50
|
return value
|
|
38
51
|
|
|
39
52
|
def get_left(self, *args: Any, **kwargs: Any) -> Any:
|
|
40
|
-
return self._resolve_value(self.left, *args, **kwargs)
|
|
41
|
-
|
|
42
|
-
def get_right(self, *args, **kwargs) -> Any:
|
|
43
53
|
"""
|
|
44
|
-
|
|
54
|
+
Whatever is on the left of this comparison, resolved.
|
|
45
55
|
"""
|
|
46
|
-
return self.
|
|
56
|
+
return self.resolve(self.left, *args, **kwargs)
|
|
47
57
|
|
|
58
|
+
def get_right(self, *args: Any, **kwargs: Any) -> Any:
|
|
59
|
+
"""
|
|
60
|
+
Whatever is on the right of this comparison, resolved.
|
|
61
|
+
"""
|
|
62
|
+
return self.resolve(self.right, *args, **kwargs)
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
@property
|
|
65
|
+
def compares_fields(self) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Whether the right side names a field rather than holding a value.
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
A dialect that only compares a field to a value needs a different form
|
|
70
|
+
for this, so each renderer has to ask.
|
|
71
|
+
"""
|
|
72
|
+
return isinstance(self.right, FieldExpression)
|
|
54
73
|
|
|
55
|
-
def
|
|
56
|
-
|
|
74
|
+
def _reject_sql(self, reason: str) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Refuse a form SQL cannot mean the same way Python does.
|
|
77
|
+
"""
|
|
78
|
+
raise NotImplementedError(f"SQL cannot express `{self.symbol}`: {reason}")
|
|
57
79
|
|
|
58
|
-
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
59
|
-
return {self.get_left(*args, **kwargs): self.get_right(*args, **kwargs)}
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
class Equals(Comparison):
|
|
82
|
+
operator = Operator.EQUALS
|
|
63
83
|
|
|
84
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
85
|
+
return self.get_left(context, *args, **kwargs) == self.get_right(context, *args, **kwargs)
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
88
|
+
return self.get_left(context) == self.get_right(context)
|
|
67
89
|
|
|
68
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
69
|
-
return self.get_left(*args, **kwargs) != self.get_right(*args, **kwargs)
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
class NotEquals(Comparison):
|
|
92
|
+
operator = Operator.NOT_EQUALS
|
|
73
93
|
|
|
74
|
-
def
|
|
75
|
-
return
|
|
94
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
95
|
+
return self.get_left(context, *args, **kwargs) != self.get_right(context, *args, **kwargs)
|
|
76
96
|
|
|
77
|
-
def
|
|
78
|
-
|
|
97
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
98
|
+
left = self.get_left(context)
|
|
99
|
+
# Python treats None != value as True. SQL treats NULL != value as
|
|
100
|
+
# unknown and drops the row, so a null column must be included.
|
|
101
|
+
if self.right is None:
|
|
102
|
+
return left.is_not(None)
|
|
103
|
+
if isinstance(self.right, FieldExpression):
|
|
104
|
+
return left != self.get_right(context)
|
|
105
|
+
return (left != self.get_right(context)) | left.is_(None)
|
|
79
106
|
|
|
80
107
|
|
|
81
108
|
class LessThan(Comparison):
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
85
|
-
return self.get_left(*args, **kwargs) < self.get_right(*args, **kwargs)
|
|
109
|
+
operator = Operator.LESS_THAN
|
|
86
110
|
|
|
87
|
-
def
|
|
88
|
-
return
|
|
111
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
112
|
+
return self.get_left(context, *args, **kwargs) < self.get_right(context, *args, **kwargs)
|
|
89
113
|
|
|
90
|
-
def
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
94
|
-
return {"range": {self.get_left(*args, **kwargs): {"lt": self.get_right(*args, **kwargs)}}}
|
|
114
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
115
|
+
return self.get_left(context) < self.get_right(context)
|
|
95
116
|
|
|
96
117
|
|
|
97
118
|
class LessThanOrEquals(Comparison):
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
101
|
-
return self.get_left(*args, **kwargs) <= self.get_right(*args, **kwargs)
|
|
119
|
+
operator = Operator.LESS_THAN_OR_EQUALS
|
|
102
120
|
|
|
103
|
-
def
|
|
104
|
-
return
|
|
121
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
122
|
+
return self.get_left(context, *args, **kwargs) <= self.get_right(context, *args, **kwargs)
|
|
105
123
|
|
|
106
|
-
def
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
110
|
-
return {"range": {self.get_left(*args, **kwargs): {"lte": self.get_right(*args, **kwargs)}}}
|
|
124
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
125
|
+
return self.get_left(context) <= self.get_right(context)
|
|
111
126
|
|
|
112
127
|
|
|
113
128
|
class GreaterThan(Comparison):
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
117
|
-
return self.get_left(*args, **kwargs) > self.get_right(*args, **kwargs)
|
|
118
|
-
|
|
119
|
-
def to_sql(self, *args: Any, **kwargs: Any) -> str:
|
|
120
|
-
return f"{self.get_left(*args, **kwargs)} > {self.get_right(*args, **kwargs)}"
|
|
129
|
+
operator = Operator.GREATER_THAN
|
|
121
130
|
|
|
122
|
-
def
|
|
123
|
-
return
|
|
131
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
132
|
+
return self.get_left(context, *args, **kwargs) > self.get_right(context, *args, **kwargs)
|
|
124
133
|
|
|
125
|
-
def
|
|
126
|
-
return
|
|
134
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
135
|
+
return self.get_left(context) > self.get_right(context)
|
|
127
136
|
|
|
128
137
|
|
|
129
138
|
class GreaterThanOrEquals(Comparison):
|
|
130
|
-
|
|
139
|
+
operator = Operator.GREATER_THAN_OR_EQUALS
|
|
131
140
|
|
|
132
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
133
|
-
return self.get_left(*args, **kwargs) >= self.get_right(*args, **kwargs)
|
|
141
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
142
|
+
return self.get_left(context, *args, **kwargs) >= self.get_right(context, *args, **kwargs)
|
|
134
143
|
|
|
135
|
-
def
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
139
|
-
return {self.get_left(*args, **kwargs): {"$gte": self.get_right(*args, **kwargs)}}
|
|
140
|
-
|
|
141
|
-
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
142
|
-
return {"range": {self.get_left(*args, **kwargs): {"gte": self.get_right(*args, **kwargs)}}}
|
|
144
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
145
|
+
return self.get_left(context) >= self.get_right(context)
|
|
143
146
|
|
|
144
147
|
|
|
145
148
|
class IsIn(Comparison):
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
149
|
-
return self.get_left(*args, **kwargs) in self.get_right(*args, **kwargs)
|
|
149
|
+
operator = Operator.IS_IN
|
|
150
150
|
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
return f"{self.get_left(*args, **kwargs)} IN {self.get_right(*args, **kwargs)}"
|
|
151
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
152
|
+
return self.get_left(context, *args, **kwargs) in self.get_right(context, *args, **kwargs)
|
|
154
153
|
|
|
155
|
-
def
|
|
156
|
-
|
|
154
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
155
|
+
if self.compares_fields:
|
|
156
|
+
self._reject_sql("the right side is a field, and SQL IN needs a list of values")
|
|
157
|
+
return self.get_left(context).in_(self._values(context))
|
|
157
158
|
|
|
158
|
-
def
|
|
159
|
-
|
|
159
|
+
def _values(self, context: Any = None) -> list[Any]:
|
|
160
|
+
"""
|
|
161
|
+
The candidate values as a sorted list, so rendering is deterministic.
|
|
162
|
+
"""
|
|
163
|
+
operand = self.get_right(context)
|
|
164
|
+
if isinstance(operand, (set, frozenset)):
|
|
165
|
+
return sorted(operand, key=repr)
|
|
166
|
+
return list(operand)
|
|
160
167
|
|
|
161
168
|
|
|
162
169
|
class Contains(Comparison):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return self.get_right(*args, **kwargs) in self.get_left(*args, **kwargs)
|
|
170
|
+
"""
|
|
171
|
+
Substring or membership: the left side contains the right.
|
|
172
|
+
"""
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
return ""
|
|
174
|
+
operator = Operator.CONTAINS
|
|
170
175
|
|
|
171
|
-
def
|
|
172
|
-
return
|
|
176
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
177
|
+
return self.get_right(context, *args, **kwargs) in self.get_left(context, *args, **kwargs)
|
|
173
178
|
|
|
174
|
-
def
|
|
175
|
-
|
|
179
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
180
|
+
# Python is case-sensitive `in`. SQL LIKE is case-insensitive for ASCII
|
|
181
|
+
# on SQLite, so compiling this would match a different set of rows.
|
|
182
|
+
self._reject_sql("substring match is case-sensitive in Python and LIKE is not")
|
|
176
183
|
|
|
177
184
|
|
|
178
185
|
class ValueExpression(Expression):
|
|
@@ -180,8 +187,7 @@ class ValueExpression(Expression):
|
|
|
180
187
|
Expression representing a single value to be compared.
|
|
181
188
|
|
|
182
189
|
The resulting tree (Equals/And/Or/... nodes) is also an inspectable structure,
|
|
183
|
-
not just a callable.
|
|
184
|
-
(SQL, MongoDB, Elasticsearch, etc.)
|
|
190
|
+
not just a callable. SQL is emitted through ``to_sqlalchemy``.
|
|
185
191
|
"""
|
|
186
192
|
|
|
187
193
|
def __init__(self, value: Any) -> None:
|
|
@@ -214,39 +220,52 @@ class ValueExpression(Expression):
|
|
|
214
220
|
def contains(self, substring: Any) -> "Comparison":
|
|
215
221
|
return Contains(self, substring)
|
|
216
222
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def to_sql(self, *args: Any, **kwargs: Any) -> str:
|
|
221
|
-
return str(self.value)
|
|
222
|
-
|
|
223
|
-
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
224
|
-
return self.value
|
|
223
|
+
def resolve(self, context: Any = None, target: Target = Target.VALUE, *args: Any, **kwargs: Any) -> Any:
|
|
224
|
+
"""
|
|
225
|
+
Resolve this literal, which is its own value for every target.
|
|
225
226
|
|
|
226
|
-
|
|
227
|
+
The signature matches ``FieldExpression.resolve`` so a caller can resolve
|
|
228
|
+
either side of a comparison without knowing which it holds.
|
|
229
|
+
"""
|
|
227
230
|
return self.value
|
|
228
231
|
|
|
229
232
|
|
|
230
233
|
class FieldExpression(ValueExpression):
|
|
231
234
|
"""
|
|
232
235
|
A value expression representing a named field reference for building filter predicates.
|
|
233
|
-
|
|
236
|
+
|
|
237
|
+
FieldExpression resolves against anything that holds fields by name: a mapping,
|
|
238
|
+
a dataclass, a pydantic model, a plain object, or a SQLAlchemy model class.
|
|
234
239
|
"""
|
|
235
240
|
|
|
236
|
-
def __call__(self,
|
|
237
|
-
return bool(
|
|
241
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
242
|
+
return bool(self.resolve(context))
|
|
238
243
|
|
|
239
244
|
def __repr__(self) -> str:
|
|
240
245
|
return f"Field({self.value!r})"
|
|
241
246
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def to_sql(self, record: Any, *args: Any, **kwargs: Any) -> str:
|
|
246
|
-
return record[self.value]
|
|
247
|
+
def resolve(self, context: Any = None, target: Target = Target.VALUE, *args: Any, **kwargs: Any) -> Any:
|
|
248
|
+
"""
|
|
249
|
+
Resolve this field against ``context``.
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
Args:
|
|
252
|
+
context: Whatever holds the field -- a mapping, an object, or a model class.
|
|
253
|
+
target: ``NAME`` for the field's name, ``VALUE`` for what ``context`` holds for it.
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
Raises:
|
|
256
|
+
UnknownFieldError: If ``context`` has no such field.
|
|
257
|
+
"""
|
|
258
|
+
if target is Target.NAME:
|
|
259
|
+
return self.value
|
|
260
|
+
|
|
261
|
+
getter = getattr(context, "__getitem__", None)
|
|
262
|
+
if getter is not None:
|
|
263
|
+
try:
|
|
264
|
+
return getter(self.value)
|
|
265
|
+
except (KeyError, IndexError, TypeError):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
value = getattr(context, self.value, UNSET)
|
|
269
|
+
if value is UNSET:
|
|
270
|
+
raise UnknownFieldError(f"{context!r} has no field {self.value!r}")
|
|
271
|
+
return value
|
|
@@ -1,98 +1,71 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Expression:
|
|
5
|
-
"""
|
|
6
|
-
Base class for composable predicates. Subclasses implement __call__
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
10
|
-
raise NotImplementedError
|
|
11
|
-
|
|
12
|
-
def __and__(self, other: "Expression") -> "Expression":
|
|
13
|
-
return And(self, other)
|
|
14
|
-
|
|
15
|
-
def __or__(self, other: "Expression") -> "Expression":
|
|
16
|
-
return Or(self, other)
|
|
17
|
-
|
|
18
|
-
def __invert__(self) -> "Expression":
|
|
19
|
-
return Not(self)
|
|
20
|
-
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
self.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
def
|
|
68
|
-
return f"
|
|
69
|
-
|
|
70
|
-
def
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
74
|
-
return {
|
|
75
|
-
"bool": {
|
|
76
|
-
"should": [self.left.to_elasticsearch(*args, **kwargs), self.right.to_elasticsearch(*args, **kwargs)]
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class Not(Expression):
|
|
82
|
-
def __init__(self, expr: Expression) -> None:
|
|
83
|
-
self.expr = expr
|
|
84
|
-
|
|
85
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
86
|
-
return not self.expr(*args, **kwargs)
|
|
87
|
-
|
|
88
|
-
def __repr__(self) -> str:
|
|
89
|
-
return f"~{self.expr!r}"
|
|
90
|
-
|
|
91
|
-
def to_sql(self, *args: Any, **kwargs: Any) -> str:
|
|
92
|
-
return f"NOT ({self.expr.to_sql(*args, **kwargs)})"
|
|
93
|
-
|
|
94
|
-
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
95
|
-
return {"$not": self.expr.to_mongo(*args, **kwargs)}
|
|
96
|
-
|
|
97
|
-
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
98
|
-
return {"bool": {"must_not": self.expr.to_elasticsearch(*args, **kwargs)}}
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Expression:
|
|
5
|
+
"""
|
|
6
|
+
Base class for composable predicates. Subclasses implement __call__
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
def __and__(self, other: "Expression") -> "Expression":
|
|
13
|
+
return And(self, other)
|
|
14
|
+
|
|
15
|
+
def __or__(self, other: "Expression") -> "Expression":
|
|
16
|
+
return Or(self, other)
|
|
17
|
+
|
|
18
|
+
def __invert__(self) -> "Expression":
|
|
19
|
+
return Not(self)
|
|
20
|
+
|
|
21
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
22
|
+
"""
|
|
23
|
+
Compile to a SQLAlchemy clause against ``context``, a model class.
|
|
24
|
+
|
|
25
|
+
Values are bound as parameters rather than rendered into SQL text.
|
|
26
|
+
"""
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class And(Expression):
|
|
31
|
+
def __init__(self, left: Expression, right: Expression) -> None:
|
|
32
|
+
self.left = left
|
|
33
|
+
self.right = right
|
|
34
|
+
|
|
35
|
+
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
36
|
+
return bool(self.left(*args, **kwargs)) and bool(self.right(*args, **kwargs))
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"({self.left!r} & {self.right!r})"
|
|
40
|
+
|
|
41
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
42
|
+
return self.left.to_sqlalchemy(context) & self.right.to_sqlalchemy(context)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Or(Expression):
|
|
46
|
+
def __init__(self, left: Expression, right: Expression) -> None:
|
|
47
|
+
self.left = left
|
|
48
|
+
self.right = right
|
|
49
|
+
|
|
50
|
+
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
51
|
+
return bool(self.left(*args, **kwargs)) or bool(self.right(*args, **kwargs))
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return f"({self.left!r} | {self.right!r})"
|
|
55
|
+
|
|
56
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
57
|
+
return self.left.to_sqlalchemy(context) | self.right.to_sqlalchemy(context)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Not(Expression):
|
|
61
|
+
def __init__(self, expr: Expression) -> None:
|
|
62
|
+
self.expr = expr
|
|
63
|
+
|
|
64
|
+
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
65
|
+
return not self.expr(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return f"~{self.expr!r}"
|
|
69
|
+
|
|
70
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
71
|
+
return ~self.expr.to_sqlalchemy(context)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The operators a comparison can express.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from corekit.schemas.enum import ValidatingEnum
|
|
8
|
+
|
|
9
|
+
__all__ = ["Dialect", "Operator"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Dialect(NamedTuple):
|
|
13
|
+
"""
|
|
14
|
+
How one operator is spelled for humans.
|
|
15
|
+
|
|
16
|
+
SQL is compiled through SQLAlchemy, not spelled here. A backend that does
|
|
17
|
+
not exist yet should not get a dialect string until it has a renderer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
symbol: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Operator(ValidatingEnum):
|
|
24
|
+
"""
|
|
25
|
+
A comparison operator, identified by the comparison class that uses it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
EQUALS = Dialect("==")
|
|
29
|
+
NOT_EQUALS = Dialect("!=")
|
|
30
|
+
LESS_THAN = Dialect("<")
|
|
31
|
+
LESS_THAN_OR_EQUALS = Dialect("<=")
|
|
32
|
+
GREATER_THAN = Dialect(">")
|
|
33
|
+
GREATER_THAN_OR_EQUALS = Dialect(">=")
|
|
34
|
+
IS_IN = Dialect("is in")
|
|
35
|
+
CONTAINS = Dialect("contains")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def symbol(self) -> str:
|
|
39
|
+
return self.value.symbol
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
How an operand should be resolved.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from corekit.schemas.enum import StringEnum
|
|
6
|
+
|
|
7
|
+
__all__ = ["Target"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Target(StringEnum):
|
|
11
|
+
"""
|
|
12
|
+
What a translator needs an operand resolved into.
|
|
13
|
+
|
|
14
|
+
Only ``FieldExpression`` reads this: a literal is its own value everywhere.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
#: The field's value, read out of the context being resolved against.
|
|
18
|
+
VALUE = "value"
|
|
19
|
+
|
|
20
|
+
#: The field's name, for a query that names fields as strings.
|
|
21
|
+
NAME = "name"
|