dynamic-expressions 0.1.1__tar.gz
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.
- dynamic_expressions-0.1.1/PKG-INFO +11 -0
- dynamic_expressions-0.1.1/README.md +0 -0
- dynamic_expressions-0.1.1/dynamic_expressions/__init__.py +0 -0
- dynamic_expressions-0.1.1/dynamic_expressions/cache/__init__.py +5 -0
- dynamic_expressions-0.1.1/dynamic_expressions/cache/base.py +81 -0
- dynamic_expressions-0.1.1/dynamic_expressions/cache/redis.py +39 -0
- dynamic_expressions-0.1.1/dynamic_expressions/dispatcher.py +64 -0
- dynamic_expressions-0.1.1/dynamic_expressions/extensions.py +16 -0
- dynamic_expressions-0.1.1/dynamic_expressions/nodes.py +36 -0
- dynamic_expressions-0.1.1/dynamic_expressions/py.typed +0 -0
- dynamic_expressions-0.1.1/dynamic_expressions/serialization/__init__.py +5 -0
- dynamic_expressions-0.1.1/dynamic_expressions/serialization/_serialization.py +6 -0
- dynamic_expressions-0.1.1/dynamic_expressions/serialization/msgspec.py +24 -0
- dynamic_expressions-0.1.1/dynamic_expressions/serialization/pydantic.py +104 -0
- dynamic_expressions-0.1.1/dynamic_expressions/types.py +29 -0
- dynamic_expressions-0.1.1/dynamic_expressions/visitors.py +105 -0
- dynamic_expressions-0.1.1/dynamic_expressions.egg-info/PKG-INFO +11 -0
- dynamic_expressions-0.1.1/dynamic_expressions.egg-info/SOURCES.txt +21 -0
- dynamic_expressions-0.1.1/dynamic_expressions.egg-info/dependency_links.txt +1 -0
- dynamic_expressions-0.1.1/dynamic_expressions.egg-info/requires.txt +9 -0
- dynamic_expressions-0.1.1/dynamic_expressions.egg-info/top_level.txt +1 -0
- dynamic_expressions-0.1.1/pyproject.toml +144 -0
- dynamic_expressions-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: dynamic-expressions
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Requires-Python: >=3.12
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
Provides-Extra: cache-redis
|
|
7
|
+
Requires-Dist: redis>=5.2.1; extra == "cache-redis"
|
|
8
|
+
Provides-Extra: serialization-pydantic
|
|
9
|
+
Requires-Dist: pydantic>=2.10.6; extra == "serialization-pydantic"
|
|
10
|
+
Provides-Extra: serialization-msgspec
|
|
11
|
+
Requires-Dist: msgspec>=0.19.0; extra == "serialization-msgspec"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import contextlib
|
|
3
|
+
import dataclasses
|
|
4
|
+
from collections.abc import AsyncIterator, Callable, MutableMapping, Sequence
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dynamic_expressions.extensions import OnVisitExtension
|
|
9
|
+
from dynamic_expressions.nodes import Node
|
|
10
|
+
from dynamic_expressions.serialization import Serializer
|
|
11
|
+
from dynamic_expressions.types import EmptyContext, ExecutionContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass(slots=True, kw_only=True)
|
|
15
|
+
class CachePolicy[Context: EmptyContext]:
|
|
16
|
+
types: tuple[type[Node], ...]
|
|
17
|
+
key: Callable[[Node, Context], str]
|
|
18
|
+
ttl: timedelta
|
|
19
|
+
serializer: Serializer[Any] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CacheExtension[
|
|
23
|
+
Context: EmptyContext,
|
|
24
|
+
](OnVisitExtension[Context]):
|
|
25
|
+
policies: Sequence[CachePolicy[Context]]
|
|
26
|
+
default_serializer: Serializer[Any]
|
|
27
|
+
_policy_cache: MutableMapping[type[Node], CachePolicy[Context] | None]
|
|
28
|
+
|
|
29
|
+
@contextlib.asynccontextmanager
|
|
30
|
+
async def on_visit(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
node: Node,
|
|
34
|
+
provided_context: Context,
|
|
35
|
+
execution_context: ExecutionContext,
|
|
36
|
+
) -> AsyncIterator[None]:
|
|
37
|
+
policy = self._get_policy(node)
|
|
38
|
+
|
|
39
|
+
if policy is None or node in execution_context.cache:
|
|
40
|
+
yield
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
key = policy.key(node, provided_context)
|
|
44
|
+
serializer = policy.serializer or self.default_serializer
|
|
45
|
+
cached_value = await self.get_cache(key)
|
|
46
|
+
if cached_value is not None:
|
|
47
|
+
execution_context.cache[node] = serializer.deserialize(cached_value)
|
|
48
|
+
|
|
49
|
+
yield
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
node in execution_context.cache
|
|
53
|
+
and cached_value != execution_context.cache[node]
|
|
54
|
+
):
|
|
55
|
+
await self.set_cache(
|
|
56
|
+
key=key,
|
|
57
|
+
value=serializer.serialize(execution_context.cache[node]),
|
|
58
|
+
policy=policy,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _get_policy(self, node: Node) -> CachePolicy[Context] | None:
|
|
62
|
+
node_cls = type(node)
|
|
63
|
+
if node_cls in self._policy_cache:
|
|
64
|
+
return self._policy_cache[node_cls]
|
|
65
|
+
|
|
66
|
+
self._policy_cache[node_cls] = next(
|
|
67
|
+
(policy for policy in self.policies if isinstance(node, policy.types)),
|
|
68
|
+
None,
|
|
69
|
+
)
|
|
70
|
+
return self._policy_cache[node_cls]
|
|
71
|
+
|
|
72
|
+
@abc.abstractmethod
|
|
73
|
+
async def get_cache(self, key: str) -> Any | None: ... # noqa: ANN401
|
|
74
|
+
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
async def set_cache(
|
|
77
|
+
self,
|
|
78
|
+
key: str,
|
|
79
|
+
value: Any, # noqa: ANN401
|
|
80
|
+
policy: CachePolicy[Context],
|
|
81
|
+
) -> None: ...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from redis.asyncio import Redis
|
|
5
|
+
|
|
6
|
+
from dynamic_expressions.cache.base import CacheExtension, CachePolicy
|
|
7
|
+
from dynamic_expressions.serialization import Serializer
|
|
8
|
+
from dynamic_expressions.types import EmptyContext
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
RedisClient = Redis[bytes]
|
|
12
|
+
else:
|
|
13
|
+
RedisClient = Redis
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RedisCacheExtension[
|
|
17
|
+
Context: EmptyContext,
|
|
18
|
+
](CacheExtension[Context]):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
client: RedisClient,
|
|
22
|
+
policies: Sequence[CachePolicy[Context]],
|
|
23
|
+
default_serializer: Serializer[Any],
|
|
24
|
+
) -> None:
|
|
25
|
+
self.policies = policies
|
|
26
|
+
self.default_serializer = default_serializer
|
|
27
|
+
self._client = client
|
|
28
|
+
self._policy_cache = {}
|
|
29
|
+
|
|
30
|
+
async def get_cache(self, key: str) -> bytes | None:
|
|
31
|
+
return await self._client.get(name=key)
|
|
32
|
+
|
|
33
|
+
async def set_cache(
|
|
34
|
+
self,
|
|
35
|
+
key: str,
|
|
36
|
+
value: bytes,
|
|
37
|
+
policy: CachePolicy[Context],
|
|
38
|
+
) -> None:
|
|
39
|
+
await self._client.set(name=key, value=value, ex=policy.ttl)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections.abc import Mapping, Sequence
|
|
3
|
+
from contextlib import AsyncExitStack
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from dynamic_expressions.extensions import (
|
|
7
|
+
OnVisitExtension,
|
|
8
|
+
)
|
|
9
|
+
from dynamic_expressions.nodes import Node
|
|
10
|
+
from dynamic_expressions.types import EmptyContext, ExecutionContext
|
|
11
|
+
from dynamic_expressions.visitors import Visitor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VisitorDispatcher[Context: EmptyContext]:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
visitors: Mapping[type[Node], Visitor[Any, Context]],
|
|
18
|
+
extensions: Sequence[OnVisitExtension[Context]] = (),
|
|
19
|
+
) -> None:
|
|
20
|
+
self._visitors = visitors
|
|
21
|
+
self._on_visit_exts = extensions
|
|
22
|
+
|
|
23
|
+
async def visit(
|
|
24
|
+
self,
|
|
25
|
+
node: Node,
|
|
26
|
+
context: Context,
|
|
27
|
+
) -> Any: # noqa: ANN401
|
|
28
|
+
execution_context = ExecutionContext()
|
|
29
|
+
return await self._visit(
|
|
30
|
+
node=node,
|
|
31
|
+
context=context,
|
|
32
|
+
execution_context=execution_context,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def _visit(
|
|
36
|
+
self,
|
|
37
|
+
node: Node,
|
|
38
|
+
context: Context,
|
|
39
|
+
execution_context: ExecutionContext,
|
|
40
|
+
) -> Any: # noqa: ANN401
|
|
41
|
+
async with AsyncExitStack() as stack:
|
|
42
|
+
for ext in self._on_visit_exts:
|
|
43
|
+
await stack.enter_async_context(
|
|
44
|
+
ext.on_visit(
|
|
45
|
+
node=node,
|
|
46
|
+
provided_context=context,
|
|
47
|
+
execution_context=execution_context,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if node in execution_context.cache:
|
|
52
|
+
return execution_context.cache[node]
|
|
53
|
+
|
|
54
|
+
visitor = self._visitors[type(node)]
|
|
55
|
+
result = await visitor.visit(
|
|
56
|
+
node=node,
|
|
57
|
+
context=context,
|
|
58
|
+
dispatch=functools.partial(
|
|
59
|
+
self._visit,
|
|
60
|
+
execution_context=execution_context,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
execution_context.cache[node] = result
|
|
64
|
+
return result
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from contextlib import AbstractAsyncContextManager
|
|
2
|
+
from typing import Protocol, runtime_checkable
|
|
3
|
+
|
|
4
|
+
from dynamic_expressions.nodes import Node
|
|
5
|
+
from dynamic_expressions.types import EmptyContext, ExecutionContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class OnVisitExtension[Context: EmptyContext](Protocol):
|
|
10
|
+
def on_visit(
|
|
11
|
+
self,
|
|
12
|
+
*,
|
|
13
|
+
node: Node,
|
|
14
|
+
provided_context: Context,
|
|
15
|
+
execution_context: ExecutionContext,
|
|
16
|
+
) -> AbstractAsyncContextManager[None]: ...
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from dynamic_expressions.types import BinaryExpressionOperator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True, frozen=True, kw_only=True, unsafe_hash=True)
|
|
11
|
+
class Node: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True, frozen=True, kw_only=True, unsafe_hash=True)
|
|
15
|
+
class AnyOfNode(Node):
|
|
16
|
+
expressions: tuple[Node, ...]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True, frozen=True, kw_only=True, unsafe_hash=True)
|
|
20
|
+
class AllOfNode(Node):
|
|
21
|
+
expressions: tuple[Node, ...]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True, frozen=True, kw_only=True, unsafe_hash=True)
|
|
25
|
+
class BinaryExpressionNode(Node):
|
|
26
|
+
operator: BinaryExpressionOperator
|
|
27
|
+
left: Node
|
|
28
|
+
right: Node
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
32
|
+
class LiteralNode(Node):
|
|
33
|
+
value: Any
|
|
34
|
+
|
|
35
|
+
def __hash__(self) -> int:
|
|
36
|
+
return hash((self.value, type(self.value)))
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from ._serialization import Serializer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MsgSpecSerializer[T](Serializer[T]):
|
|
9
|
+
def __init__(self, instance_of: type[T]) -> None:
|
|
10
|
+
self.instance_of = instance_of
|
|
11
|
+
|
|
12
|
+
def serialize(self, value: T) -> bytes:
|
|
13
|
+
return msgspec.json.encode(value)
|
|
14
|
+
|
|
15
|
+
def deserialize(self, value: bytes) -> T:
|
|
16
|
+
return msgspec.json.decode(value, type=self.instance_of)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MsgSpecScalarSerializer(Serializer[Any]):
|
|
20
|
+
def serialize(self, value: Any) -> bytes: # noqa: ANN401
|
|
21
|
+
return msgspec.json.encode(value)
|
|
22
|
+
|
|
23
|
+
def deserialize(self, value: bytes) -> Any: # noqa: ANN401
|
|
24
|
+
return msgspec.json.decode(value)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Any, Literal, Union, overload
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
|
6
|
+
|
|
7
|
+
from dynamic_expressions.nodes import (
|
|
8
|
+
AllOfNode,
|
|
9
|
+
AnyOfNode,
|
|
10
|
+
BinaryExpressionNode,
|
|
11
|
+
LiteralNode,
|
|
12
|
+
Node,
|
|
13
|
+
)
|
|
14
|
+
from dynamic_expressions.types import BinaryExpressionOperator
|
|
15
|
+
|
|
16
|
+
from ._serialization import Serializer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NodeSchema(BaseModel):
|
|
20
|
+
model_config = ConfigDict(strict=True)
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def to_node(self) -> Node: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AnyOfNodeSchema[T: NodeSchema](NodeSchema):
|
|
27
|
+
type: Literal["any-of"]
|
|
28
|
+
expressions: tuple[T, ...]
|
|
29
|
+
|
|
30
|
+
def to_node(self) -> AnyOfNode:
|
|
31
|
+
return AnyOfNode(expressions=tuple(expr.to_node() for expr in self.expressions))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AllOfNodeSchema[T: NodeSchema](NodeSchema):
|
|
35
|
+
type: Literal["all-of"]
|
|
36
|
+
expressions: tuple[T, ...]
|
|
37
|
+
|
|
38
|
+
def to_node(self) -> AllOfNode:
|
|
39
|
+
return AllOfNode(expressions=tuple(expr.to_node() for expr in self.expressions))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BinaryExpressionNodeSchema[T: NodeSchema](NodeSchema):
|
|
43
|
+
type: Literal["binary"]
|
|
44
|
+
operator: BinaryExpressionOperator
|
|
45
|
+
left: T
|
|
46
|
+
right: T
|
|
47
|
+
|
|
48
|
+
def to_node(self) -> BinaryExpressionNode:
|
|
49
|
+
return BinaryExpressionNode(
|
|
50
|
+
operator=self.operator,
|
|
51
|
+
left=self.left.to_node(),
|
|
52
|
+
right=self.right.to_node(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LiteralNodeSchema[T](NodeSchema):
|
|
57
|
+
type: Literal["literal"]
|
|
58
|
+
value: int | str | bool
|
|
59
|
+
|
|
60
|
+
def to_node(self) -> LiteralNode:
|
|
61
|
+
return LiteralNode(value=self.value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
BUILTIN_SCHEMAS: Sequence[type[NodeSchema]] = [
|
|
65
|
+
AnyOfNodeSchema,
|
|
66
|
+
AllOfNodeSchema,
|
|
67
|
+
BinaryExpressionNodeSchema,
|
|
68
|
+
LiteralNodeSchema,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PydanticExpressionParser:
|
|
73
|
+
def __init__(self, types: Sequence[type[NodeSchema]]) -> None:
|
|
74
|
+
self._types = list(types)
|
|
75
|
+
self._needs_rebuild = True
|
|
76
|
+
self._type_adapter: TypeAdapter[NodeSchema] | None = None
|
|
77
|
+
|
|
78
|
+
def add_type(self, cls: type[NodeSchema]) -> None:
|
|
79
|
+
self._types.append(cls)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def type_adapter(self) -> TypeAdapter[NodeSchema]:
|
|
83
|
+
if self._needs_rebuild or self._type_adapter is None:
|
|
84
|
+
union = Union[tuple(model["union"] for model in self._types)] # type: ignore[valid-type,index] # noqa: UP007
|
|
85
|
+
self._type_adapter = TypeAdapter(union)
|
|
86
|
+
|
|
87
|
+
return self._type_adapter
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PydanticSerializer[T](Serializer[T]):
|
|
91
|
+
@overload
|
|
92
|
+
def __init__(self, instance_of: type[T]) -> None: ...
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def __init__(self, instance_of: Any) -> None: ... # noqa: ANN401
|
|
96
|
+
|
|
97
|
+
def __init__(self, instance_of: Any) -> None:
|
|
98
|
+
self._type_adapter = TypeAdapter[T](instance_of)
|
|
99
|
+
|
|
100
|
+
def serialize(self, value: T) -> bytes:
|
|
101
|
+
return self._type_adapter.dump_json(value, by_alias=True)
|
|
102
|
+
|
|
103
|
+
def deserialize(self, value: bytes) -> T:
|
|
104
|
+
return self._type_adapter.validate_json(value, strict=True)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Literal, Protocol
|
|
3
|
+
|
|
4
|
+
from dynamic_expressions.nodes import Node
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EmptyContext(Protocol): ...
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ExecutionContext:
|
|
12
|
+
cache: dict[Node, Any] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
BinaryExpressionOperator = Literal[
|
|
16
|
+
"=",
|
|
17
|
+
">",
|
|
18
|
+
">=",
|
|
19
|
+
"<",
|
|
20
|
+
"<=",
|
|
21
|
+
"!=",
|
|
22
|
+
"in",
|
|
23
|
+
"+",
|
|
24
|
+
"-",
|
|
25
|
+
"*",
|
|
26
|
+
"/",
|
|
27
|
+
"//",
|
|
28
|
+
"^",
|
|
29
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import operator
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import Any, ClassVar, Protocol
|
|
5
|
+
|
|
6
|
+
from dynamic_expressions.nodes import (
|
|
7
|
+
AllOfNode,
|
|
8
|
+
AnyOfNode,
|
|
9
|
+
BinaryExpressionNode,
|
|
10
|
+
LiteralNode,
|
|
11
|
+
Node,
|
|
12
|
+
)
|
|
13
|
+
from dynamic_expressions.types import BinaryExpressionOperator, EmptyContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Dispatch[TContext: EmptyContext](Protocol):
|
|
17
|
+
async def __call__(self, node: Node, context: TContext) -> Any: ... # noqa: ANN401
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Visitor[TNode: Node, TContext: EmptyContext]:
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
async def visit(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
node: TNode,
|
|
26
|
+
dispatch: Dispatch[TContext],
|
|
27
|
+
context: TContext,
|
|
28
|
+
) -> Any: ... # noqa: ANN401
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AnyOfVisitor(Visitor[AnyOfNode, EmptyContext]):
|
|
32
|
+
async def visit(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
node: AnyOfNode,
|
|
36
|
+
dispatch: Dispatch[EmptyContext],
|
|
37
|
+
context: object,
|
|
38
|
+
) -> bool:
|
|
39
|
+
for expr in node.expressions:
|
|
40
|
+
value = await dispatch(expr, context)
|
|
41
|
+
if value:
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AllOfVisitor(Visitor[AllOfNode, EmptyContext]):
|
|
47
|
+
async def visit(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
node: AllOfNode,
|
|
51
|
+
dispatch: Dispatch[EmptyContext],
|
|
52
|
+
context: EmptyContext,
|
|
53
|
+
) -> bool:
|
|
54
|
+
for expr in node.expressions:
|
|
55
|
+
value = await dispatch(expr, context)
|
|
56
|
+
if not value:
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BinaryExpressionVisitor(Visitor[BinaryExpressionNode, EmptyContext]):
|
|
62
|
+
operator_mapping: ClassVar[
|
|
63
|
+
Mapping[BinaryExpressionOperator, Callable[[Any, Any], bool]]
|
|
64
|
+
] = {
|
|
65
|
+
"=": operator.eq,
|
|
66
|
+
">": operator.gt,
|
|
67
|
+
">=": operator.ge,
|
|
68
|
+
"<": operator.lt,
|
|
69
|
+
"<=": operator.le,
|
|
70
|
+
"!=": operator.ne,
|
|
71
|
+
"in": operator.contains,
|
|
72
|
+
"+": operator.add,
|
|
73
|
+
"-": operator.sub,
|
|
74
|
+
"*": operator.mul,
|
|
75
|
+
"/": operator.truediv,
|
|
76
|
+
"//": operator.floordiv,
|
|
77
|
+
"^": operator.pow,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async def visit(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
node: BinaryExpressionNode,
|
|
84
|
+
dispatch: Dispatch[EmptyContext],
|
|
85
|
+
context: EmptyContext,
|
|
86
|
+
) -> bool:
|
|
87
|
+
left = await dispatch(node.left, context)
|
|
88
|
+
right = await dispatch(node.right, context)
|
|
89
|
+
|
|
90
|
+
operator_callable = self.operator_mapping.get(node.operator)
|
|
91
|
+
if operator_callable is None:
|
|
92
|
+
msg = f"Unknown operator '{node.operator}'"
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
return operator_callable(left, right)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LiteralVisitor(Visitor[LiteralNode, EmptyContext]):
|
|
98
|
+
async def visit(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
node: LiteralNode,
|
|
102
|
+
dispatch: Dispatch[EmptyContext], # noqa: ARG002
|
|
103
|
+
context: EmptyContext, # noqa: ARG002
|
|
104
|
+
) -> Any: # noqa: ANN401
|
|
105
|
+
return node.value
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: dynamic-expressions
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Requires-Python: >=3.12
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
Provides-Extra: cache-redis
|
|
7
|
+
Requires-Dist: redis>=5.2.1; extra == "cache-redis"
|
|
8
|
+
Provides-Extra: serialization-pydantic
|
|
9
|
+
Requires-Dist: pydantic>=2.10.6; extra == "serialization-pydantic"
|
|
10
|
+
Provides-Extra: serialization-msgspec
|
|
11
|
+
Requires-Dist: msgspec>=0.19.0; extra == "serialization-msgspec"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
dynamic_expressions/__init__.py
|
|
4
|
+
dynamic_expressions/dispatcher.py
|
|
5
|
+
dynamic_expressions/extensions.py
|
|
6
|
+
dynamic_expressions/nodes.py
|
|
7
|
+
dynamic_expressions/py.typed
|
|
8
|
+
dynamic_expressions/types.py
|
|
9
|
+
dynamic_expressions/visitors.py
|
|
10
|
+
dynamic_expressions.egg-info/PKG-INFO
|
|
11
|
+
dynamic_expressions.egg-info/SOURCES.txt
|
|
12
|
+
dynamic_expressions.egg-info/dependency_links.txt
|
|
13
|
+
dynamic_expressions.egg-info/requires.txt
|
|
14
|
+
dynamic_expressions.egg-info/top_level.txt
|
|
15
|
+
dynamic_expressions/cache/__init__.py
|
|
16
|
+
dynamic_expressions/cache/base.py
|
|
17
|
+
dynamic_expressions/cache/redis.py
|
|
18
|
+
dynamic_expressions/serialization/__init__.py
|
|
19
|
+
dynamic_expressions/serialization/_serialization.py
|
|
20
|
+
dynamic_expressions/serialization/msgspec.py
|
|
21
|
+
dynamic_expressions/serialization/pydantic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dynamic_expressions
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dynamic-expressions"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = ""
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[project.optional-dependencies]
|
|
10
|
+
cache-redis = [
|
|
11
|
+
"redis>=5.2.1",
|
|
12
|
+
]
|
|
13
|
+
serialization-pydantic = [
|
|
14
|
+
"pydantic>=2.10.6",
|
|
15
|
+
]
|
|
16
|
+
serialization-msgspec = [
|
|
17
|
+
"msgspec>=0.19.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"anyio>=4.8.0",
|
|
23
|
+
"commitizen",
|
|
24
|
+
"coverage>=7.6.10",
|
|
25
|
+
"deptry>=0.23.0",
|
|
26
|
+
"mypy>=1.14.1",
|
|
27
|
+
"pytest>=8.3.4",
|
|
28
|
+
"redis>=5.2.1",
|
|
29
|
+
"ruff>=0.9.4",
|
|
30
|
+
"testcontainers>=4.9.1",
|
|
31
|
+
"types-redis>=4.6.0.20241004",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
pythonpath = "."
|
|
37
|
+
markers = ["redis"]
|
|
38
|
+
|
|
39
|
+
[tool.coverage.run]
|
|
40
|
+
source = ["dynamic_expressions"]
|
|
41
|
+
omit = []
|
|
42
|
+
command_line = "-m pytest -v"
|
|
43
|
+
concurrency = []
|
|
44
|
+
branch = true
|
|
45
|
+
|
|
46
|
+
[tool.coverage.report]
|
|
47
|
+
exclude_lines = [
|
|
48
|
+
"class .*\\(.*\\bProtocol\\b.*\\):",
|
|
49
|
+
"@(?:typing\\.)?overload",
|
|
50
|
+
"if TYPE_CHECKING:",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.deptry]
|
|
54
|
+
[tool.deptry.per_rule_ignores]
|
|
55
|
+
DEP002 = []
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
plugins = [
|
|
59
|
+
"pydantic.mypy",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
strict = true
|
|
63
|
+
follow_imports = "normal"
|
|
64
|
+
ignore_missing_imports = false
|
|
65
|
+
|
|
66
|
+
allow_redefinition = false
|
|
67
|
+
disallow_any_explicit = false
|
|
68
|
+
ignore_errors = false
|
|
69
|
+
local_partial_types = true
|
|
70
|
+
no_implicit_optional = true
|
|
71
|
+
strict_optional = true
|
|
72
|
+
warn_no_return = true
|
|
73
|
+
warn_return_any = true
|
|
74
|
+
warn_unreachable = true
|
|
75
|
+
|
|
76
|
+
pretty = true
|
|
77
|
+
show_column_numbers = true
|
|
78
|
+
show_error_codes = true
|
|
79
|
+
|
|
80
|
+
[tool.pydantic-mypy]
|
|
81
|
+
init_forbid_extra = true
|
|
82
|
+
init_typed = true
|
|
83
|
+
|
|
84
|
+
[tool.ruff]
|
|
85
|
+
src = ["src", "tests"]
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
fixable = [
|
|
88
|
+
"F",
|
|
89
|
+
"E",
|
|
90
|
+
"W",
|
|
91
|
+
"I",
|
|
92
|
+
"COM",
|
|
93
|
+
"UP",
|
|
94
|
+
"RUF",
|
|
95
|
+
]
|
|
96
|
+
unfixable = [
|
|
97
|
+
"F841", # Variable is assigned to but never used
|
|
98
|
+
]
|
|
99
|
+
select = ["ALL"]
|
|
100
|
+
ignore = [
|
|
101
|
+
"E501", # Line Length
|
|
102
|
+
"D10", # Disable mandatory docstrings
|
|
103
|
+
"D203", # one-blank-line-before-class
|
|
104
|
+
"D212", # multi-line-summary-first-line
|
|
105
|
+
"PD", # pandas-vet
|
|
106
|
+
"EXE",
|
|
107
|
+
"COM812", # ruff format conflict
|
|
108
|
+
"ISC001", # ruff format conflict
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[tool.ruff.lint.per-file-ignores]
|
|
112
|
+
"tests/*" = ["S101"]
|
|
113
|
+
|
|
114
|
+
[tool.ruff.lint.flake8-builtins]
|
|
115
|
+
builtins-allowed-modules = ["types"]
|
|
116
|
+
|
|
117
|
+
[tool.ruff.lint.flake8-pytest-style]
|
|
118
|
+
fixture-parentheses = false
|
|
119
|
+
mark-parentheses = false
|
|
120
|
+
|
|
121
|
+
[tool.ruff.lint.mccabe]
|
|
122
|
+
max-complexity = 6
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
125
|
+
extend-immutable-calls = []
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.pep8-naming]
|
|
128
|
+
classmethod-decorators = ["classmethod"]
|
|
129
|
+
staticmethod-decorators = ["staticmethod"]
|
|
130
|
+
|
|
131
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
132
|
+
ban-relative-imports = "parents"
|
|
133
|
+
|
|
134
|
+
[tool.commitizen]
|
|
135
|
+
name = "cz_conventional_commits"
|
|
136
|
+
version = "0.1.1"
|
|
137
|
+
tag_format = "$version"
|
|
138
|
+
version_scheme = "pep440"
|
|
139
|
+
version_provider = "pep621"
|
|
140
|
+
major_version_zero = true
|
|
141
|
+
update_changelog_on_bump = true
|
|
142
|
+
version_files = [
|
|
143
|
+
"pyproject.toml:version",
|
|
144
|
+
]
|