python-corekit 0.1.0__py3-none-any.whl → 0.2.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 +237 -0
- corekit/api/lifespan.py +210 -0
- corekit/api/middleware.py +93 -0
- corekit/api/routers.py +109 -1
- corekit/concurrency/worker.py +65 -65
- corekit/config/settings.py +3 -3
- corekit/connections/sql/__init__.py +31 -3
- corekit/connections/sql/connection.py +19 -0
- corekit/connections/sql/migration/__init__.py +5 -5
- corekit/connections/sql/migration/base.py +3 -3
- corekit/connections/sql/migration/operations.py +66 -42
- corekit/connections/sql/migration/registry.py +2 -2
- corekit/connections/sql/operations/__init__.py +24 -0
- corekit/connections/sql/operations/base.py +102 -0
- corekit/connections/sql/operations/statements.py +150 -0
- corekit/connections/sql/query.py +4 -62
- corekit/connections/sql/table.py +30 -4
- corekit/constants.py +45 -45
- corekit/crypto/constants.py +4 -4
- corekit/data/__init__.py +8 -0
- corekit/data/expressions/__init__.py +10 -2
- corekit/data/expressions/comparison.py +184 -104
- corekit/data/expressions/expression.py +103 -98
- corekit/data/expressions/operator.py +54 -0
- corekit/data/expressions/target.py +21 -0
- corekit/data/record.py +147 -147
- corekit/data/stats.py +159 -157
- corekit/decorators/__init__.py +2 -2
- corekit/decorators/exception_handling.py +2 -1
- corekit/etl/connection.py +44 -44
- corekit/events/websocket.py +3 -2
- corekit/exceptions/__init__.py +18 -0
- corekit/http/__init__.py +13 -0
- corekit/jobs/__init__.py +26 -0
- corekit/jobs/registry.py +87 -0
- corekit/jobs/runner.py +69 -0
- corekit/jobs/task.py +152 -0
- corekit/observability/__init__.py +5 -3
- corekit/observability/request_context.py +135 -0
- corekit/registry/__init__.py +11 -6
- corekit/registry/ordered.py +86 -0
- corekit/schemas/__init__.py +10 -0
- corekit/schemas/enum.py +49 -49
- corekit/schemas/models/arbitrary.py +11 -11
- corekit/schemas/pydantic/fields.py +35 -35
- corekit/schemas/types.py +40 -40
- corekit/serialization/__init__.py +22 -0
- corekit/serialization/serializer.py +1 -1
- corekit/utils/__init__.py +59 -5
- corekit/utils/coercion.py +118 -0
- corekit/utils/collections.py +115 -0
- corekit/utils/ids.py +61 -5
- corekit/utils/payload.py +100 -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.0.dist-info → python_corekit-0.2.0.dist-info}/METADATA +105 -100
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/RECORD +64 -46
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/top_level.txt +0 -0
corekit/connections/sql/table.py
CHANGED
|
@@ -4,13 +4,24 @@ Base table type.
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, ClassVar
|
|
8
8
|
|
|
9
|
+
from pydantic import BaseModel
|
|
9
10
|
from sqlmodel import SQLModel
|
|
10
11
|
|
|
11
12
|
from corekit.registry import SmartRegistry
|
|
12
13
|
|
|
13
|
-
__all__ = ["NamedTable"]
|
|
14
|
+
__all__ = ["NamedTable", "TableExportItem"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TableExportItem(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
One exported row, tagged with the table it came from.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
table_name: str
|
|
23
|
+
data: dict[str, Any]
|
|
24
|
+
|
|
14
25
|
|
|
15
26
|
logger = logging.getLogger(__name__)
|
|
16
27
|
|
|
@@ -26,6 +37,10 @@ class NamedTable(SQLModel):
|
|
|
26
37
|
|
|
27
38
|
__registry__: SmartRegistry = SmartRegistry()
|
|
28
39
|
|
|
40
|
+
#: A human-readable name for the table. Declared as a ClassVar so pydantic
|
|
41
|
+
#: leaves a subclass's value as a plain string rather than wrapping it.
|
|
42
|
+
_table_display_name: ClassVar[str | None] = None
|
|
43
|
+
|
|
29
44
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
30
45
|
"""
|
|
31
46
|
Register every subclass under its class name.
|
|
@@ -57,9 +72,14 @@ class NamedTable(SQLModel):
|
|
|
57
72
|
@classmethod
|
|
58
73
|
def get_display_name(cls) -> str:
|
|
59
74
|
"""
|
|
60
|
-
A human-readable name for the table.
|
|
75
|
+
A human-readable name for the table, falling back to the class name.
|
|
61
76
|
"""
|
|
62
|
-
|
|
77
|
+
name = cls._table_display_name
|
|
78
|
+
if name is not None and not isinstance(name, str):
|
|
79
|
+
# A subclass declared outside this module's ClassVar annotation
|
|
80
|
+
# gets its value wrapped, and the wrapper's repr is not a label.
|
|
81
|
+
name = getattr(name, "default", None) or str(name)
|
|
82
|
+
return name or cls.__name__
|
|
63
83
|
|
|
64
84
|
@classmethod
|
|
65
85
|
def create_default(cls, identifier: str, **kwargs: Any) -> "NamedTable":
|
|
@@ -70,6 +90,12 @@ class NamedTable(SQLModel):
|
|
|
70
90
|
logger.info(f"Creating default {cls.__name__} with id {kwargs['id']!r}")
|
|
71
91
|
return cls(**kwargs)
|
|
72
92
|
|
|
93
|
+
def export(self) -> TableExportItem:
|
|
94
|
+
"""
|
|
95
|
+
This row as a portable record, tagged with its table.
|
|
96
|
+
"""
|
|
97
|
+
return TableExportItem(table_name=self.table_name, data=self.model_dump())
|
|
98
|
+
|
|
73
99
|
@staticmethod
|
|
74
100
|
def _serialize(value: Any) -> str:
|
|
75
101
|
return json.dumps(value)
|
corekit/constants.py
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# Minutes
|
|
2
|
-
ONE_MINUTE = 60
|
|
3
|
-
TWO_MINUTES = ONE_MINUTE * 2
|
|
4
|
-
THREE_MINUTES = ONE_MINUTE * 3
|
|
5
|
-
FIVE_MINUTES = ONE_MINUTE * 5
|
|
6
|
-
TEN_MINUTES = ONE_MINUTE * 10
|
|
7
|
-
FIFTEEN_MINUTES = ONE_MINUTE * 15
|
|
8
|
-
THIRTY_MINUTES = ONE_MINUTE * 30
|
|
9
|
-
FORTY_FIVE_MINUTES = ONE_MINUTE * 45
|
|
10
|
-
|
|
11
|
-
# Hours
|
|
12
|
-
ONE_HOUR = ONE_MINUTE * 60
|
|
13
|
-
TWO_HOURS = ONE_HOUR * 2
|
|
14
|
-
THREE_HOURS = ONE_HOUR * 3
|
|
15
|
-
FIVE_HOURS = ONE_HOUR * 5
|
|
16
|
-
SIX_HOURS = ONE_HOUR * 6
|
|
17
|
-
NINE_HOURS = ONE_HOUR * 9
|
|
18
|
-
TWELVE_HOURS = ONE_HOUR * 12
|
|
19
|
-
FIFTEEN_HOURS = ONE_HOUR * 15
|
|
20
|
-
EIGHTEEN_HOURS = ONE_HOUR * 18
|
|
21
|
-
|
|
22
|
-
# Days
|
|
23
|
-
ONE_DAY = ONE_HOUR * 24
|
|
24
|
-
TWO_DAYS = ONE_DAY * 2
|
|
25
|
-
THREE_DAYS = ONE_DAY * 3
|
|
26
|
-
FOUR_DAYS = ONE_DAY * 4
|
|
27
|
-
FIVE_DAYS = ONE_DAY * 5
|
|
28
|
-
SIX_DAYS = ONE_DAY * 6
|
|
29
|
-
TEN_DAYS = ONE_DAY * 10
|
|
30
|
-
FIFTEEN_DAYS = ONE_DAY * 15
|
|
31
|
-
THIRTY_DAYS = ONE_DAY * 30
|
|
32
|
-
FORTY_FIVE_DAYS = ONE_DAY * 45
|
|
33
|
-
SIXTY_FIVE_DAYS = ONE_DAY * 60
|
|
34
|
-
NINETY_FIVE_DAYS = ONE_DAY * 90
|
|
35
|
-
ONE_HUNDRED_TWENTY_DAYS = ONE_DAY * 120
|
|
36
|
-
ONE_HUNDRED_EIGHTY_DAYS = ONE_DAY * 180
|
|
37
|
-
|
|
38
|
-
# Weeks
|
|
39
|
-
ONE_WEEK = ONE_DAY * 7
|
|
40
|
-
TWO_WEEKS = ONE_WEEK * 2
|
|
41
|
-
THREE_WEEKS = ONE_WEEK * 3
|
|
42
|
-
FOUR_WEEKS = ONE_WEEK * 4
|
|
43
|
-
|
|
44
|
-
# Years
|
|
45
|
-
ONE_YEAR = ONE_DAY * 365
|
|
1
|
+
# Minutes
|
|
2
|
+
ONE_MINUTE = 60
|
|
3
|
+
TWO_MINUTES = ONE_MINUTE * 2
|
|
4
|
+
THREE_MINUTES = ONE_MINUTE * 3
|
|
5
|
+
FIVE_MINUTES = ONE_MINUTE * 5
|
|
6
|
+
TEN_MINUTES = ONE_MINUTE * 10
|
|
7
|
+
FIFTEEN_MINUTES = ONE_MINUTE * 15
|
|
8
|
+
THIRTY_MINUTES = ONE_MINUTE * 30
|
|
9
|
+
FORTY_FIVE_MINUTES = ONE_MINUTE * 45
|
|
10
|
+
|
|
11
|
+
# Hours
|
|
12
|
+
ONE_HOUR = ONE_MINUTE * 60
|
|
13
|
+
TWO_HOURS = ONE_HOUR * 2
|
|
14
|
+
THREE_HOURS = ONE_HOUR * 3
|
|
15
|
+
FIVE_HOURS = ONE_HOUR * 5
|
|
16
|
+
SIX_HOURS = ONE_HOUR * 6
|
|
17
|
+
NINE_HOURS = ONE_HOUR * 9
|
|
18
|
+
TWELVE_HOURS = ONE_HOUR * 12
|
|
19
|
+
FIFTEEN_HOURS = ONE_HOUR * 15
|
|
20
|
+
EIGHTEEN_HOURS = ONE_HOUR * 18
|
|
21
|
+
|
|
22
|
+
# Days
|
|
23
|
+
ONE_DAY = ONE_HOUR * 24
|
|
24
|
+
TWO_DAYS = ONE_DAY * 2
|
|
25
|
+
THREE_DAYS = ONE_DAY * 3
|
|
26
|
+
FOUR_DAYS = ONE_DAY * 4
|
|
27
|
+
FIVE_DAYS = ONE_DAY * 5
|
|
28
|
+
SIX_DAYS = ONE_DAY * 6
|
|
29
|
+
TEN_DAYS = ONE_DAY * 10
|
|
30
|
+
FIFTEEN_DAYS = ONE_DAY * 15
|
|
31
|
+
THIRTY_DAYS = ONE_DAY * 30
|
|
32
|
+
FORTY_FIVE_DAYS = ONE_DAY * 45
|
|
33
|
+
SIXTY_FIVE_DAYS = ONE_DAY * 60
|
|
34
|
+
NINETY_FIVE_DAYS = ONE_DAY * 90
|
|
35
|
+
ONE_HUNDRED_TWENTY_DAYS = ONE_DAY * 120
|
|
36
|
+
ONE_HUNDRED_EIGHTY_DAYS = ONE_DAY * 180
|
|
37
|
+
|
|
38
|
+
# Weeks
|
|
39
|
+
ONE_WEEK = ONE_DAY * 7
|
|
40
|
+
TWO_WEEKS = ONE_WEEK * 2
|
|
41
|
+
THREE_WEEKS = ONE_WEEK * 3
|
|
42
|
+
FOUR_WEEKS = ONE_WEEK * 4
|
|
43
|
+
|
|
44
|
+
# Years
|
|
45
|
+
ONE_YEAR = ONE_DAY * 365
|
corekit/crypto/constants.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
HASH_JOINER = ":"
|
|
2
2
|
EMPTY_HASH = ""
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# `corekit.
|
|
4
|
+
# There is deliberately no default salt. A salt shipped with a library is shared
|
|
5
|
+
# by every install, which defeats the purpose of salting. Supply one through
|
|
6
|
+
# configuration -- see `corekit.config.CorekitSettings.crypto_salt` and
|
|
7
|
+
# `corekit.crypto.hasher.Hasher`.
|
corekit/data/__init__.py
CHANGED
|
@@ -24,6 +24,7 @@ from corekit.data.expressions import (
|
|
|
24
24
|
And,
|
|
25
25
|
Comparison,
|
|
26
26
|
Contains,
|
|
27
|
+
Dialect,
|
|
27
28
|
Equals,
|
|
28
29
|
Expression,
|
|
29
30
|
FieldExpression,
|
|
@@ -34,7 +35,10 @@ from corekit.data.expressions import (
|
|
|
34
35
|
LessThanOrEquals,
|
|
35
36
|
Not,
|
|
36
37
|
NotEquals,
|
|
38
|
+
Operator,
|
|
37
39
|
Or,
|
|
40
|
+
Target,
|
|
41
|
+
UnknownFieldError,
|
|
38
42
|
ValueExpression,
|
|
39
43
|
)
|
|
40
44
|
from corekit.data.record import BaseRecord, is_valid_key, resolve_default
|
|
@@ -74,6 +78,10 @@ __all__ = [
|
|
|
74
78
|
"NotEquals",
|
|
75
79
|
"NumericFieldStats",
|
|
76
80
|
"Or",
|
|
81
|
+
"Dialect",
|
|
82
|
+
"Operator",
|
|
83
|
+
"Target",
|
|
84
|
+
"UnknownFieldError",
|
|
77
85
|
"ValueCounts",
|
|
78
86
|
"ValueExpression",
|
|
79
87
|
"is_valid_key",
|
|
@@ -8,8 +8,9 @@ values or against each other::
|
|
|
8
8
|
Field("first_name") == Field("nickname")
|
|
9
9
|
(Field("age") > 20) & ~Field("name").contains("test")
|
|
10
10
|
|
|
11
|
-
Every node can also emit a backend query
|
|
12
|
-
``
|
|
11
|
+
Every node can also emit a backend query: ``to_mongo``, ``to_elasticsearch`` or
|
|
12
|
+
``to_sqlalchemy``. ``Target`` names the context an operand is resolved for, so a
|
|
13
|
+
new backend needs one more translation method and no changes to the tree.
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from corekit.data.expressions.comparison import (
|
|
@@ -23,9 +24,12 @@ from corekit.data.expressions.comparison import (
|
|
|
23
24
|
LessThan,
|
|
24
25
|
LessThanOrEquals,
|
|
25
26
|
NotEquals,
|
|
27
|
+
UnknownFieldError,
|
|
26
28
|
ValueExpression,
|
|
27
29
|
)
|
|
28
30
|
from corekit.data.expressions.expression import And, Expression, Not, Or
|
|
31
|
+
from corekit.data.expressions.operator import Dialect, Operator
|
|
32
|
+
from corekit.data.expressions.target import Target
|
|
29
33
|
|
|
30
34
|
__all__ = [
|
|
31
35
|
"And",
|
|
@@ -41,6 +45,10 @@ __all__ = [
|
|
|
41
45
|
"LessThanOrEquals",
|
|
42
46
|
"Not",
|
|
43
47
|
"NotEquals",
|
|
48
|
+
"UnknownFieldError",
|
|
44
49
|
"Or",
|
|
50
|
+
"Dialect",
|
|
51
|
+
"Operator",
|
|
52
|
+
"Target",
|
|
45
53
|
"ValueExpression",
|
|
46
54
|
]
|
|
@@ -1,22 +1,32 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from typing import Any, Iterable
|
|
2
3
|
|
|
3
4
|
from corekit.data.expressions.expression import Expression
|
|
5
|
+
from corekit.data.expressions.operator import Operator
|
|
6
|
+
from corekit.data.expressions.target import Target
|
|
7
|
+
|
|
8
|
+
UNSET = object()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UnknownFieldError(AttributeError):
|
|
12
|
+
"""
|
|
13
|
+
Raised when an expression names a field the target does not have.
|
|
14
|
+
"""
|
|
4
15
|
|
|
5
16
|
|
|
6
17
|
class Comparison(Expression):
|
|
7
18
|
"""
|
|
8
19
|
Base for comparing two values (Equals, GreaterThan, IsIn, ...).
|
|
9
20
|
|
|
10
|
-
`left`, `right`, and `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`symbol` is a human-readable operator string; the class itself
|
|
21
|
+
`left`, `right`, and `operator` are public so external code -- e.g. a
|
|
22
|
+
translator to SQL/Mongo/Elasticsearch -- can walk the tree and read out
|
|
23
|
+
what each node represents without evaluating it. The class itself
|
|
14
24
|
(Equals, GreaterThan, etc.) is the canonical, type-safe identifier of
|
|
15
25
|
*which* operation a node represents, so a translator can dispatch on
|
|
16
26
|
isinstance(node, GreaterThan) rather than string-matching on symbol.
|
|
17
27
|
"""
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
operator: Operator
|
|
20
30
|
|
|
21
31
|
def __init__(self, left: Any, right: Any) -> None:
|
|
22
32
|
self.left = left
|
|
@@ -25,154 +35,205 @@ class Comparison(Expression):
|
|
|
25
35
|
def __repr__(self) -> str:
|
|
26
36
|
return f"({self.left!r} {self.symbol} {self.right!r})"
|
|
27
37
|
|
|
38
|
+
@property
|
|
39
|
+
def symbol(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
The human-readable operator, e.g. ``>=``.
|
|
42
|
+
"""
|
|
43
|
+
return self.operator.symbol
|
|
44
|
+
|
|
28
45
|
@staticmethod
|
|
29
|
-
def
|
|
46
|
+
def resolve(value: Any, *args: Any, **kwargs: Any) -> Any:
|
|
30
47
|
"""
|
|
31
|
-
|
|
32
|
-
to be able to resolve the final value of complex expressions without
|
|
33
|
-
having to evaluate the entire tree.
|
|
48
|
+
Resolve one operand, passing everything through to the expression.
|
|
34
49
|
"""
|
|
35
50
|
if isinstance(value, ValueExpression):
|
|
36
|
-
return value.
|
|
51
|
+
return value.resolve(*args, **kwargs)
|
|
37
52
|
return value
|
|
38
53
|
|
|
39
54
|
def get_left(self, *args: Any, **kwargs: Any) -> Any:
|
|
40
|
-
|
|
55
|
+
"""
|
|
56
|
+
Whatever is on the left of this comparison, resolved.
|
|
57
|
+
"""
|
|
58
|
+
return self.resolve(self.left, *args, **kwargs)
|
|
41
59
|
|
|
42
|
-
def get_right(self, *args, **kwargs) -> Any:
|
|
60
|
+
def get_right(self, *args: Any, **kwargs: Any) -> Any:
|
|
43
61
|
"""
|
|
44
|
-
|
|
62
|
+
Whatever is on the right of this comparison, resolved.
|
|
45
63
|
"""
|
|
46
|
-
return self.
|
|
64
|
+
return self.resolve(self.right, *args, **kwargs)
|
|
47
65
|
|
|
66
|
+
@property
|
|
67
|
+
def compares_fields(self) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Whether the right side names a field rather than holding a value.
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
A dialect that only compares a field to a value needs a different form
|
|
72
|
+
for this, so each renderer has to ask.
|
|
73
|
+
"""
|
|
74
|
+
return isinstance(self.right, FieldExpression)
|
|
75
|
+
|
|
76
|
+
def _reject_field_comparison(self) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Guard a dialect whose clauses only compare a field to a literal.
|
|
51
79
|
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
Rendering the right side would emit a field's *name* where a value was
|
|
81
|
+
meant, which is a silently wrong query rather than a failed one.
|
|
82
|
+
"""
|
|
83
|
+
if self.compares_fields:
|
|
84
|
+
raise NotImplementedError(f"Elasticsearch cannot express `{self.symbol}` between two fields")
|
|
54
85
|
|
|
55
|
-
def
|
|
56
|
-
|
|
86
|
+
def _reference(self, side: Any) -> str:
|
|
87
|
+
"""
|
|
88
|
+
One side as a Mongo field reference, e.g. ``$age``.
|
|
89
|
+
"""
|
|
90
|
+
return f"${self.resolve(side, target=Target.NAME)}"
|
|
57
91
|
|
|
58
92
|
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
59
|
-
|
|
93
|
+
if self.compares_fields:
|
|
94
|
+
return {"$expr": {self.operator.mongo: [self._reference(self.left), self._reference(self.right)]}}
|
|
95
|
+
return {self.get_left(target=Target.NAME): {self.operator.mongo: self.get_right()}}
|
|
60
96
|
|
|
61
97
|
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
62
|
-
|
|
98
|
+
self._reject_field_comparison()
|
|
99
|
+
# Elasticsearch range operators are the Mongo spelling without the `$`.
|
|
100
|
+
return {
|
|
101
|
+
self.operator.elasticsearch: {
|
|
102
|
+
self.get_left(target=Target.NAME): {self.operator.mongo.lstrip("$"): self.get_right()}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
63
105
|
|
|
64
106
|
|
|
65
|
-
class
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
69
|
-
return self.get_left(*args, **kwargs) != self.get_right(*args, **kwargs)
|
|
107
|
+
class Equals(Comparison):
|
|
108
|
+
operator = Operator.EQUALS
|
|
70
109
|
|
|
71
|
-
def
|
|
72
|
-
return
|
|
110
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
111
|
+
return self.get_left(context, *args, **kwargs) == self.get_right(context, *args, **kwargs)
|
|
73
112
|
|
|
74
113
|
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
75
|
-
|
|
114
|
+
if self.compares_fields:
|
|
115
|
+
return super().to_mongo(*args, **kwargs)
|
|
116
|
+
# Equality is the one operator Mongo spells as a bare value.
|
|
117
|
+
return {self.get_left(target=Target.NAME): self.get_right()}
|
|
76
118
|
|
|
77
119
|
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
78
|
-
|
|
120
|
+
self._reject_field_comparison()
|
|
121
|
+
return {self.operator.elasticsearch: {self.get_left(target=Target.NAME): self.get_right()}}
|
|
79
122
|
|
|
123
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
124
|
+
return self.get_left(context) == self.get_right(context)
|
|
80
125
|
|
|
81
|
-
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)
|
|
86
126
|
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
class NotEquals(Comparison):
|
|
128
|
+
operator = Operator.NOT_EQUALS
|
|
89
129
|
|
|
90
|
-
def
|
|
91
|
-
return
|
|
130
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
131
|
+
return self.get_left(context, *args, **kwargs) != self.get_right(context, *args, **kwargs)
|
|
92
132
|
|
|
93
133
|
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
94
|
-
|
|
134
|
+
self._reject_field_comparison()
|
|
135
|
+
return {
|
|
136
|
+
self.operator.elasticsearch: {self.get_left(target=Target.NAME): {self.operator.mongo: self.get_right()}}
|
|
137
|
+
}
|
|
95
138
|
|
|
139
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
140
|
+
return self.get_left(context) != self.get_right(context)
|
|
96
141
|
|
|
97
|
-
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)
|
|
102
142
|
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
class LessThan(Comparison):
|
|
144
|
+
operator = Operator.LESS_THAN
|
|
105
145
|
|
|
106
|
-
def
|
|
107
|
-
return
|
|
146
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
147
|
+
return self.get_left(context, *args, **kwargs) < self.get_right(context, *args, **kwargs)
|
|
108
148
|
|
|
109
|
-
def
|
|
110
|
-
return
|
|
149
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
150
|
+
return self.get_left(context) < self.get_right(context)
|
|
111
151
|
|
|
112
152
|
|
|
113
|
-
class
|
|
114
|
-
|
|
153
|
+
class LessThanOrEquals(Comparison):
|
|
154
|
+
operator = Operator.LESS_THAN_OR_EQUALS
|
|
115
155
|
|
|
116
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
117
|
-
return self.get_left(*args, **kwargs)
|
|
156
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
157
|
+
return self.get_left(context, *args, **kwargs) <= self.get_right(context, *args, **kwargs)
|
|
118
158
|
|
|
119
|
-
def
|
|
120
|
-
return
|
|
159
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
160
|
+
return self.get_left(context) <= self.get_right(context)
|
|
121
161
|
|
|
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)}}
|
|
124
162
|
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
class GreaterThan(Comparison):
|
|
164
|
+
operator = Operator.GREATER_THAN
|
|
127
165
|
|
|
166
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
167
|
+
return self.get_left(context, *args, **kwargs) > self.get_right(context, *args, **kwargs)
|
|
128
168
|
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
170
|
+
return self.get_left(context) > self.get_right(context)
|
|
131
171
|
|
|
132
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
133
|
-
return self.get_left(*args, **kwargs) >= self.get_right(*args, **kwargs)
|
|
134
172
|
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
class GreaterThanOrEquals(Comparison):
|
|
174
|
+
operator = Operator.GREATER_THAN_OR_EQUALS
|
|
137
175
|
|
|
138
|
-
def
|
|
139
|
-
return
|
|
176
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
177
|
+
return self.get_left(context, *args, **kwargs) >= self.get_right(context, *args, **kwargs)
|
|
140
178
|
|
|
141
|
-
def
|
|
142
|
-
return
|
|
179
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
180
|
+
return self.get_left(context) >= self.get_right(context)
|
|
143
181
|
|
|
144
182
|
|
|
145
183
|
class IsIn(Comparison):
|
|
146
|
-
|
|
184
|
+
operator = Operator.IS_IN
|
|
147
185
|
|
|
148
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
149
|
-
return self.get_left(*args, **kwargs) in self.get_right(*args, **kwargs)
|
|
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)}"
|
|
186
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
187
|
+
return self.get_left(context, *args, **kwargs) in self.get_right(context, *args, **kwargs)
|
|
154
188
|
|
|
155
189
|
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
156
|
-
|
|
190
|
+
if self.compares_fields:
|
|
191
|
+
return {"$expr": {self.operator.mongo: [self._reference(self.left), self._reference(self.right)]}}
|
|
192
|
+
# A list, not the stored frozenset: no driver can serialize a set.
|
|
193
|
+
return {self.get_left(target=Target.NAME): {self.operator.mongo: self._values()}}
|
|
157
194
|
|
|
158
195
|
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
159
|
-
|
|
196
|
+
self._reject_field_comparison()
|
|
197
|
+
return {self.operator.elasticsearch: {self.get_left(target=Target.NAME): self._values()}}
|
|
198
|
+
|
|
199
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
200
|
+
return self.get_left(context).in_(self._values(context))
|
|
201
|
+
|
|
202
|
+
def _values(self, context: Any = None) -> list[Any]:
|
|
203
|
+
"""
|
|
204
|
+
The candidate values as a sorted list, so rendering is deterministic.
|
|
205
|
+
"""
|
|
206
|
+
operand = self.get_right(context)
|
|
207
|
+
if isinstance(operand, (set, frozenset)):
|
|
208
|
+
return sorted(operand, key=repr)
|
|
209
|
+
return list(operand)
|
|
160
210
|
|
|
161
211
|
|
|
162
212
|
class Contains(Comparison):
|
|
163
|
-
|
|
213
|
+
"""
|
|
214
|
+
Substring or membership: the left side contains the right.
|
|
215
|
+
"""
|
|
164
216
|
|
|
165
|
-
|
|
166
|
-
return self.get_right(*args, **kwargs) in self.get_left(*args, **kwargs)
|
|
217
|
+
operator = Operator.CONTAINS
|
|
167
218
|
|
|
168
|
-
def
|
|
169
|
-
return
|
|
219
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
220
|
+
return self.get_right(context, *args, **kwargs) in self.get_left(context, *args, **kwargs)
|
|
170
221
|
|
|
171
222
|
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
172
|
-
|
|
223
|
+
if self.compares_fields:
|
|
224
|
+
# $regex is a query operator, not an aggregation one, so it cannot
|
|
225
|
+
# appear inside the $expr form that comparing two fields needs.
|
|
226
|
+
raise NotImplementedError("Mongo cannot express `contains` between two fields")
|
|
227
|
+
# $regex with the operand escaped, so a value containing regex
|
|
228
|
+
# metacharacters is matched literally rather than as a pattern.
|
|
229
|
+
return {self.get_left(target=Target.NAME): {self.operator.mongo: re.escape(str(self.get_right()))}}
|
|
173
230
|
|
|
174
231
|
def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
175
|
-
|
|
232
|
+
self._reject_field_comparison()
|
|
233
|
+
return {self.operator.elasticsearch: {self.get_left(target=Target.NAME): f"*{self.get_right()}*"}}
|
|
234
|
+
|
|
235
|
+
def to_sqlalchemy(self, context: Any) -> Any:
|
|
236
|
+
return self.get_left(context).contains(self.get_right(context))
|
|
176
237
|
|
|
177
238
|
|
|
178
239
|
class ValueExpression(Expression):
|
|
@@ -214,11 +275,14 @@ class ValueExpression(Expression):
|
|
|
214
275
|
def contains(self, substring: Any) -> "Comparison":
|
|
215
276
|
return Contains(self, substring)
|
|
216
277
|
|
|
217
|
-
def
|
|
218
|
-
|
|
278
|
+
def resolve(self, context: Any = None, target: Target = Target.VALUE, *args: Any, **kwargs: Any) -> Any:
|
|
279
|
+
"""
|
|
280
|
+
Resolve this literal, which is its own value for every target.
|
|
219
281
|
|
|
220
|
-
|
|
221
|
-
|
|
282
|
+
The signature matches ``FieldExpression.resolve`` so a caller can resolve
|
|
283
|
+
either side of a comparison without knowing which it holds.
|
|
284
|
+
"""
|
|
285
|
+
return self.value
|
|
222
286
|
|
|
223
287
|
def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
|
|
224
288
|
return self.value
|
|
@@ -230,23 +294,39 @@ class ValueExpression(Expression):
|
|
|
230
294
|
class FieldExpression(ValueExpression):
|
|
231
295
|
"""
|
|
232
296
|
A value expression representing a named field reference for building filter predicates.
|
|
233
|
-
|
|
297
|
+
|
|
298
|
+
FieldExpression resolves against anything that holds fields by name: a mapping,
|
|
299
|
+
a dataclass, a pydantic model, a plain object, or a SQLAlchemy model class.
|
|
234
300
|
"""
|
|
235
301
|
|
|
236
|
-
def __call__(self,
|
|
237
|
-
return bool(
|
|
302
|
+
def __call__(self, context: Any, *args: Any, **kwargs: Any) -> bool:
|
|
303
|
+
return bool(self.resolve(context))
|
|
238
304
|
|
|
239
305
|
def __repr__(self) -> str:
|
|
240
306
|
return f"Field({self.value!r})"
|
|
241
307
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def to_sql(self, record: Any, *args: Any, **kwargs: Any) -> str:
|
|
246
|
-
return record[self.value]
|
|
308
|
+
def resolve(self, context: Any = None, target: Target = Target.VALUE, *args: Any, **kwargs: Any) -> Any:
|
|
309
|
+
"""
|
|
310
|
+
Resolve this field against ``context``.
|
|
247
311
|
|
|
248
|
-
|
|
249
|
-
|
|
312
|
+
Args:
|
|
313
|
+
context: Whatever holds the field -- a mapping, an object, or a model class.
|
|
314
|
+
target: ``NAME`` for the field's name, ``VALUE`` for what ``context`` holds for it.
|
|
250
315
|
|
|
251
|
-
|
|
252
|
-
|
|
316
|
+
Raises:
|
|
317
|
+
UnknownFieldError: If ``context`` has no such field.
|
|
318
|
+
"""
|
|
319
|
+
if target is Target.NAME:
|
|
320
|
+
return self.value
|
|
321
|
+
|
|
322
|
+
getter = getattr(context, "__getitem__", None)
|
|
323
|
+
if getter is not None:
|
|
324
|
+
try:
|
|
325
|
+
return getter(self.value)
|
|
326
|
+
except (KeyError, IndexError, TypeError):
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
value = getattr(context, self.value, UNSET)
|
|
330
|
+
if value is UNSET:
|
|
331
|
+
raise UnknownFieldError(f"{context!r} has no field {self.value!r}")
|
|
332
|
+
return value
|