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.
Files changed (109) hide show
  1. corekit/api/__init__.py +18 -3
  2. corekit/api/application.py +275 -0
  3. corekit/api/lifespan.py +233 -0
  4. corekit/api/middleware.py +93 -0
  5. corekit/api/routers.py +109 -1
  6. corekit/concurrency/__init__.py +2 -2
  7. corekit/concurrency/decorators.py +32 -5
  8. corekit/concurrency/thread_local.py +2 -2
  9. corekit/concurrency/worker.py +74 -65
  10. corekit/config/loader.py +42 -5
  11. corekit/config/settings.py +11 -1
  12. corekit/connections/__init__.py +7 -1
  13. corekit/connections/connectable.py +45 -4
  14. corekit/connections/redis/connection.py +53 -10
  15. corekit/connections/sql/__init__.py +33 -4
  16. corekit/connections/sql/connection.py +56 -3
  17. corekit/connections/sql/fields/__init__.py +2 -2
  18. corekit/connections/sql/fields/jsonb.py +13 -6
  19. corekit/connections/sql/migration/__init__.py +9 -5
  20. corekit/connections/sql/migration/base.py +3 -3
  21. corekit/connections/sql/migration/operations.py +135 -44
  22. corekit/connections/sql/migration/registry.py +2 -2
  23. corekit/connections/sql/operations/__init__.py +24 -0
  24. corekit/connections/sql/operations/base.py +111 -0
  25. corekit/connections/sql/operations/statements.py +170 -0
  26. corekit/connections/sql/query.py +4 -62
  27. corekit/connections/sql/table.py +33 -29
  28. corekit/crypto/__init__.py +3 -1
  29. corekit/crypto/constants.py +2 -2
  30. corekit/crypto/hasher.py +9 -4
  31. corekit/data/__init__.py +8 -0
  32. corekit/data/dataset.py +8 -2
  33. corekit/data/expressions/__init__.py +10 -2
  34. corekit/data/expressions/comparison.py +142 -123
  35. corekit/data/expressions/expression.py +71 -98
  36. corekit/data/expressions/operator.py +39 -0
  37. corekit/data/expressions/target.py +21 -0
  38. corekit/data/record.py +147 -147
  39. corekit/data/stats.py +162 -157
  40. corekit/decorators/__init__.py +2 -2
  41. corekit/decorators/exception_handling.py +38 -9
  42. corekit/docker/watchdog.py +50 -31
  43. corekit/etl/__init__.py +2 -1
  44. corekit/etl/connection.py +46 -44
  45. corekit/etl/extract/extractor.py +6 -13
  46. corekit/etl/orchestrator.py +19 -2
  47. corekit/etl/schemas.py +2 -2
  48. corekit/etl/transform/transformer.py +4 -1
  49. corekit/events/publisher.py +1 -1
  50. corekit/events/reader.py +26 -21
  51. corekit/events/sse.py +4 -1
  52. corekit/events/websocket.py +27 -13
  53. corekit/exceptions/__init__.py +33 -0
  54. corekit/exceptions/base.py +139 -10
  55. corekit/exceptions/enum.py +17 -0
  56. corekit/exceptions/types.py +6 -6
  57. corekit/files/__init__.py +2 -4
  58. corekit/files/base.py +15 -2
  59. corekit/files/enum.py +0 -5
  60. corekit/files/json.py +16 -2
  61. corekit/http/__init__.py +51 -0
  62. corekit/http/api.py +24 -0
  63. corekit/http/client.py +100 -73
  64. corekit/http/exceptions.py +140 -0
  65. corekit/http/response.py +50 -1
  66. corekit/http/status.py +89 -0
  67. corekit/jobs/__init__.py +26 -0
  68. corekit/jobs/registry.py +87 -0
  69. corekit/jobs/runner.py +80 -0
  70. corekit/jobs/task.py +173 -0
  71. corekit/log_monitor/models.py +8 -2
  72. corekit/log_monitor/service.py +77 -38
  73. corekit/notifications/base.py +18 -10
  74. corekit/observability/__init__.py +12 -3
  75. corekit/observability/benchmarkable.py +23 -5
  76. corekit/observability/loggable.py +21 -0
  77. corekit/observability/request_context.py +188 -0
  78. corekit/observability/timing/timer.py +4 -2
  79. corekit/registry/__init__.py +12 -7
  80. corekit/registry/ordered.py +86 -0
  81. corekit/registry/registry.py +55 -14
  82. corekit/schemas/__init__.py +10 -0
  83. corekit/schemas/enum.py +70 -49
  84. corekit/schemas/models/arbitrary.py +11 -11
  85. corekit/schemas/pydantic/fields.py +35 -35
  86. corekit/schemas/types.py +45 -40
  87. corekit/serialization/__init__.py +24 -0
  88. corekit/serialization/pickle_file.py +61 -0
  89. corekit/serialization/serializable.py +22 -2
  90. corekit/serialization/serializer.py +10 -3
  91. corekit/utils/__init__.py +59 -5
  92. corekit/utils/coercion.py +118 -0
  93. corekit/utils/collections.py +124 -0
  94. corekit/utils/ids.py +61 -5
  95. corekit/utils/payload.py +112 -0
  96. corekit/utils/raise_exc.py +8 -8
  97. corekit/utils/text.py +56 -0
  98. corekit/utils/time.py +74 -21
  99. corekit/utils/validators.py +15 -15
  100. corekit/utils/void.py +8 -8
  101. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/METADATA +103 -97
  102. python_corekit-0.3.0.dist-info/RECORD +145 -0
  103. corekit/constants.py +0 -45
  104. corekit/exceptions/http/exceptions.py +0 -37
  105. corekit/files/pickle.py +0 -12
  106. python_corekit-0.1.1.dist-info/RECORD +0 -125
  107. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/WHEEL +0 -0
  108. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/licenses/LICENSE +0 -0
  109. {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 `symbol` are public so external code --
11
- e.g. a future translator to SQL/Mongo/Elasticsearch -- can walk the
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
- symbol: str
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 _resolve_value(value: Any, *args: Any, **kwargs: Any) -> Any:
44
+ def resolve(value: Any, *args: Any, **kwargs: Any) -> Any:
30
45
  """
31
- Resolves a value to a concrete value. This method utilizes a neat trick
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.get(*args, **kwargs)
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
- Returns the right value of the comparison.
54
+ Whatever is on the left of this comparison, resolved.
45
55
  """
46
- return self._resolve_value(self.right, *args, **kwargs)
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
- class Equals(Comparison):
50
- symbol = "=="
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
- def __call__(self, *args: Any, **kwargs: Any) -> bool:
53
- return self.get_left(*args, **kwargs) == self.get_right(*args, **kwargs)
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 to_sql(self, *args: Any, **kwargs: Any) -> str:
56
- return f"{self.get_left(*args, **kwargs)} = {self.get_right(*args, **kwargs)}"
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
- def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
62
- return {"term": {self.get_left(*args, **kwargs): self.get_right(*args, **kwargs)}}
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
- class NotEquals(Comparison):
66
- symbol = "!="
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
- def to_sql(self, *args: Any, **kwargs: Any) -> str:
72
- return f"{self.get_left(*args, **kwargs)} != {self.get_right(*args, **kwargs)}"
91
+ class NotEquals(Comparison):
92
+ operator = Operator.NOT_EQUALS
73
93
 
74
- def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
75
- return {self.get_left(*args, **kwargs): {"$ne": self.get_right(*args, **kwargs)}}
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 to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
78
- return {"term": {self.get_left(*args, **kwargs): {"$ne": self.get_right(*args, **kwargs)}}}
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
- symbol = "<"
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 to_sql(self, *args: Any, **kwargs: Any) -> str:
88
- return f"{self.get_left(*args, **kwargs)} < {self.get_right(*args, **kwargs)}"
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 to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
91
- return {self.get_left(*args, **kwargs): {"$lt": self.get_right(*args, **kwargs)}}
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
- symbol = "<="
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 to_sql(self, *args: Any, **kwargs: Any) -> str:
104
- return f"{self.get_left(*args, **kwargs)} <= {self.get_right(*args, **kwargs)}"
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 to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
107
- return {self.get_left(*args, **kwargs): {"$lte": self.get_right(*args, **kwargs)}}
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
- symbol = ">"
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 to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
123
- return {self.get_left(*args, **kwargs): {"$gt": self.get_right(*args, **kwargs)}}
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 to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
126
- return {"range": {self.get_left(*args, **kwargs): {"gt": self.get_right(*args, **kwargs)}}}
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
- symbol = ">="
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 to_sql(self, *args: Any, **kwargs: Any) -> str:
136
- return f"{self.get_left(*args, **kwargs)} >= {self.get_right(*args, **kwargs)}"
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
- symbol = "is in"
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 to_sql(self, *args: Any, **kwargs: Any) -> str:
152
- # TODO: this might not actually be valid SQL
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 to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
156
- return {self.get_left(*args, **kwargs): {"$in": self.get_right(*args, **kwargs)}}
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 to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
159
- return {"terms": {self.get_left(*args, **kwargs): self.get_right(*args, **kwargs)}}
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
- symbol = "contains"
164
-
165
- def __call__(self, *args: Any, **kwargs: Any) -> bool:
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
- def to_sql(self, *args: Any, **kwargs: Any) -> str:
169
- return ""
174
+ operator = Operator.CONTAINS
170
175
 
171
- def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
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 to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
175
- return
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. These could be used in the future to emit structured queries
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 get(self, *args: Any, **kwargs: Any) -> Any:
218
- return self.value
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
- def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
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
- FieldExpression instances will evaluate against anything that supports __getitem__ by field name.
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, record: Any, *args, **kwargs) -> bool:
237
- return bool(record[self.value])
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 get(self, record: Any, *args: Any, **kwargs: Any) -> Any:
243
- return record[self.value]
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
- def to_mongo(self, record: Any, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
249
- return record[self.value]
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
- def to_elasticsearch(self, record: Any, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
252
- return record[self.value]
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 to_sql(self, *args: Any, **kwargs: Any) -> str:
22
- raise NotImplementedError
23
-
24
- def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
25
- raise NotImplementedError
26
-
27
- def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
28
- raise NotImplementedError
29
-
30
-
31
- class And(Expression):
32
- def __init__(self, left: Expression, right: Expression) -> None:
33
- self.left = left
34
- self.right = right
35
-
36
- def __call__(self, *args: Any, **kwargs: Any) -> bool:
37
- return bool(self.left(*args, **kwargs)) and bool(self.right(*args, **kwargs))
38
-
39
- def __repr__(self) -> str:
40
- return f"({self.left!r} & {self.right!r})"
41
-
42
- def to_sql(self, *args: Any, **kwargs: Any) -> str:
43
- return f"({self.left.to_sql(*args, **kwargs)} AND {self.right.to_sql(*args, **kwargs)})"
44
-
45
- def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
46
- return {"$and": [self.left.to_mongo(), self.right.to_mongo()]}
47
-
48
- def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
49
- return {
50
- "bool": {
51
- "must": [self.left.to_elasticsearch(*args, **kwargs), self.right.to_elasticsearch(*args, **kwargs)]
52
- }
53
- }
54
-
55
-
56
- class Or(Expression):
57
- def __init__(self, left: Expression, right: Expression) -> None:
58
- self.left = left
59
- self.right = right
60
-
61
- def __call__(self, *args: Any, **kwargs: Any) -> bool:
62
- return bool(self.left(*args, **kwargs)) or bool(self.right(*args, **kwargs))
63
-
64
- def __repr__(self) -> str:
65
- return f"({self.left!r} | {self.right!r})"
66
-
67
- def to_sql(self, *args: Any, **kwargs: Any) -> str:
68
- return f"({self.left.to_sql(*args, **kwargs)} OR {self.right.to_sql(*args, **kwargs)})"
69
-
70
- def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
71
- return {"$or": [self.left.to_mongo(*args, **kwargs), self.right.to_mongo(*args, **kwargs)]}
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"