python-jsonlogic 0.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Victorien
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-jsonlogic
3
+ Version: 0.0.1
4
+ Summary: An extensible and sane implementation of JsonLogic
5
+ Author-email: Victorien <contact@vctrn.dev>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Victorien
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Operating System :: OS Independent
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Programming Language :: Python :: 3.8
33
+ Classifier: Programming Language :: Python :: 3.9
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Typing :: Typed
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
41
+ Requires-Python: >=3.8
42
+ Description-Content-Type: text/x-rst
43
+ License-File: LICENSE
44
+ Requires-Dist: typing-extensions>=4.6.0; python_version < "3.12"
45
+
46
+ =========
47
+ jsonlogic
48
+ =========
49
+
50
+ |Pythons| |PyPI|
51
+
52
+ .. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonlogic.svg
53
+ :alt: Supported Python versions
54
+ :target: https://pypi.org/project/jsonlogic/
55
+
56
+ .. |PyPI| image:: https://img.shields.io/pypi/v/jsonlogic.svg
57
+ :alt: PyPI - Version
58
+ :target: https://pypi.org/project/jsonlogic/
59
+
60
+ .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
61
+ :alt: PyPI - Version
62
+ :target: https://pypi.org/project/jsonlogic/
63
+
64
+ ``jsonlogic`` is an extensible and sane implementation of `JsonLogic`_.
65
+
66
+ .. _`JsonLogic`: https://jsonlogic.com/
@@ -0,0 +1,21 @@
1
+ =========
2
+ jsonlogic
3
+ =========
4
+
5
+ |Pythons| |PyPI|
6
+
7
+ .. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonlogic.svg
8
+ :alt: Supported Python versions
9
+ :target: https://pypi.org/project/jsonlogic/
10
+
11
+ .. |PyPI| image:: https://img.shields.io/pypi/v/jsonlogic.svg
12
+ :alt: PyPI - Version
13
+ :target: https://pypi.org/project/jsonlogic/
14
+
15
+ .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
16
+ :alt: PyPI - Version
17
+ :target: https://pypi.org/project/jsonlogic/
18
+
19
+ ``jsonlogic`` is an extensible and sane implementation of `JsonLogic`_.
20
+
21
+ .. _`JsonLogic`: https://jsonlogic.com/
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-jsonlogic"
7
+ description = "An extensible and sane implementation of JsonLogic"
8
+ version = "0.0.1"
9
+ readme = "README.rst"
10
+ authors = [
11
+ {name = "Victorien", email = "contact@vctrn.dev"}
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Typing :: Typed",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Intended Audience :: Developers",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ ]
28
+ license = {file = "LICENSE"}
29
+ requires-python = ">=3.8"
30
+ dependencies = [
31
+ "typing-extensions>=4.6.0; python_version < '3.12'",
32
+ ]
33
+
34
+ [tool.setuptools]
35
+ package-dir = {"" = "src"}
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.setuptools.package-data]
41
+ "jsonlogic" = ["py.typed"]
42
+
43
+ [tool.ruff]
44
+ line-length = 120
45
+ src = ["src"]
46
+ target-version = "py38"
47
+
48
+ [tool.ruff.lint]
49
+ preview = true
50
+ explicit-preview-rules = true
51
+ select = [
52
+ "E", # pycodestyle (E)
53
+ "W", # pycodestyle (W)
54
+ "F", # Pyflakes
55
+ "UP", # pyupgrade
56
+ "I", # isort
57
+ "PL", # Pylint
58
+ "RUF", # Ruff
59
+ "RUF022", # Ruff-preview
60
+ "YTT", # flake8-2020
61
+ "B", # flake8-bugbear
62
+ "C4", # flake8-comprehensions
63
+ "T10", # flake8-debugger
64
+ "PIE", # flake8-pie
65
+ "T20", # flake8-print
66
+ "RSE", # flake8-raise
67
+ "PTH", # flake8-use-pathlib
68
+ ]
69
+ ignore = [
70
+ "PLR2004"
71
+ ]
72
+
73
+ [tool.ruff.lint.isort]
74
+ known-first-party = ["jsonlogic"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,8 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 12):
4
+ from typing import Self, TypeAlias, TypeAliasType
5
+ else:
6
+ from typing_extensions import Self, TypeAlias, TypeAliasType
7
+
8
+ __all__ = ("Self", "TypeAlias", "TypeAliasType")
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from ._compat import Self, TypeAlias
8
+ from .json_schema.types import AnyType, JSONSchemaType
9
+ from .typing import JSON, JSONLogicPrimitive, OperatorArgument
10
+
11
+ if TYPE_CHECKING:
12
+ # This is a hack to make pyright think `TypeAlias` comes from `typing`
13
+ from typing import TypeAlias
14
+
15
+ from .registry import OperatorRegistry
16
+
17
+
18
+ @dataclass
19
+ class Operator(ABC):
20
+ """The base class for all operators."""
21
+
22
+ operator: str = field(repr=False)
23
+ """The string representation of the operator."""
24
+
25
+ # metadata: Any | None = None
26
+ # """Extra metadata for this operator.
27
+
28
+ # For any exception encountered, this will be included.
29
+ # """
30
+
31
+ @classmethod
32
+ @abstractmethod
33
+ def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
34
+ """Return an instance of the operator from the list of provided arguments."""
35
+
36
+ @abstractmethod
37
+ def apply(self, data: JSON) -> Any:
38
+ pass
39
+
40
+ def typecheck(self, data_schema: dict[str, Any]) -> JSONSchemaType:
41
+ """Typecheck the operator (and all children) given the data schema."""
42
+
43
+ return AnyType()
44
+
45
+
46
+ NormalizedExpression: TypeAlias = "dict[str, list[JSONLogicExpression]]"
47
+
48
+
49
+ @dataclass
50
+ class JSONLogicExpression:
51
+ expression: JSONLogicPrimitive | NormalizedExpression
52
+
53
+ @classmethod
54
+ def from_json(cls, json: JSON) -> Self: # TODO disallow list?
55
+ if not isinstance(json, dict):
56
+ return cls(expression=json)
57
+
58
+ operator, op_args = next(iter(json.items()))
59
+ if not isinstance(op_args, list):
60
+ op_args = [op_args]
61
+
62
+ sub_expressions = [cls.from_json(op_arg) for op_arg in op_args]
63
+
64
+ return cls({operator: sub_expressions})
65
+
66
+ def as_operator_tree(self, operator_registry: OperatorRegistry) -> JSONLogicPrimitive | Operator:
67
+ if not isinstance(self.expression, dict):
68
+ return self.expression
69
+
70
+ op_id, op_args = next(iter(self.expression.items()))
71
+ OperatorCls = operator_registry.get(op_id)
72
+
73
+ return OperatorCls.from_expression(op_id, [op_arg.as_operator_tree(operator_registry) for op_arg in op_args])
@@ -0,0 +1,77 @@
1
+ from datetime import date, datetime
2
+ from types import NoneType
3
+ from typing import Any, Callable, cast
4
+
5
+ from jsonlogic.typing import JSONLogicPrimitive
6
+
7
+ from .types import (
8
+ AnyType,
9
+ BooleanType,
10
+ DatetimeType,
11
+ DateType,
12
+ IntegerType,
13
+ JSONSchemaType,
14
+ NullType,
15
+ NumberType,
16
+ StringType,
17
+ )
18
+
19
+ _VALUE_TYPE_MAP: dict[type[Any], JSONSchemaType] = {
20
+ bool: BooleanType(),
21
+ float: NumberType(),
22
+ int: IntegerType(),
23
+ NoneType: NullType(),
24
+ }
25
+
26
+ _VALUE_FORMAT_MAP: dict[Callable[[str], Any], JSONSchemaType] = {
27
+ datetime.fromisoformat: DatetimeType(),
28
+ date.fromisoformat: DateType(),
29
+ }
30
+
31
+
32
+ def from_value(value: JSONLogicPrimitive) -> JSONSchemaType:
33
+ if type(value) in _VALUE_TYPE_MAP:
34
+ return _VALUE_TYPE_MAP[type(value)]
35
+
36
+ if isinstance(value, str):
37
+ for func, js_type in _VALUE_FORMAT_MAP.items():
38
+ try:
39
+ func(value)
40
+ except Exception:
41
+ pass
42
+ else:
43
+ return js_type
44
+
45
+ return StringType()
46
+
47
+ return AnyType()
48
+
49
+
50
+ _TYPE_MAP: dict[str, JSONSchemaType] = {
51
+ "boolean": BooleanType(),
52
+ "number": NumberType(),
53
+ "integer": IntegerType(),
54
+ "null": NullType(),
55
+ }
56
+
57
+ _FORMAT_MAP: dict[str, JSONSchemaType] = {
58
+ "date-time": DatetimeType(),
59
+ "date": DateType(),
60
+ }
61
+
62
+
63
+ def from_json_schema(json_schema: dict[str, Any]) -> JSONSchemaType:
64
+ # TODO support for unions
65
+ js_type = cast(str | None, json_schema.get("type"))
66
+ if js_type in _TYPE_MAP:
67
+ return _TYPE_MAP[js_type]
68
+
69
+ if js_type == "string":
70
+ format = cast(str | None, json_schema.get("format"))
71
+
72
+ if format in _FORMAT_MAP:
73
+ return _FORMAT_MAP[format]
74
+
75
+ return StringType()
76
+
77
+ return AnyType()
@@ -0,0 +1,51 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Generic, TypeVar
3
+ from urllib.parse import unquote
4
+
5
+ PointerT = TypeVar("PointerT")
6
+
7
+
8
+ class Unresolvable(Exception, Generic[PointerT]):
9
+ """A pointer was unresolvable."""
10
+
11
+ # TODO `details` argument, providing more info?
12
+ def __init__(self, pointer: PointerT, /) -> None:
13
+ self.pointer = pointer
14
+
15
+
16
+ class JSONSchemaResolver(ABC, Generic[PointerT]):
17
+ """Resolves the (sub) JSON Schema at the given pointer.
18
+
19
+ The implementation of the "pointer" is managed by subclasses.
20
+ """
21
+
22
+ def __init__(self, json_schema: dict[str, Any]) -> None:
23
+ self.json_schema = json_schema
24
+
25
+ @abstractmethod
26
+ def resolve(self, pointer: PointerT) -> dict[str, Any]:
27
+ pass
28
+
29
+
30
+ class JSONSchemaPointerResolver(JSONSchemaResolver[str]):
31
+ """Resolves the (sub) JSON Schema at the given JSON Pointer, according to RFC 6901."""
32
+
33
+ def resolve(self, pointer: str) -> dict[str, Any]:
34
+ # Implementation inspired from the `referencing` library
35
+ schema = self.json_schema
36
+ for segment in unquote(pointer[1:]).split("/"):
37
+ schema_type = schema.get("type")
38
+ parsed_segment = segment.replace("~1", "/").replace("~0", "~")
39
+ if schema_type == "object":
40
+ try:
41
+ schema = schema["properties"][parsed_segment]
42
+ continue
43
+ except KeyError as e:
44
+ raise Unresolvable(pointer) from e
45
+ if schema_type == "array":
46
+ try:
47
+ int(parsed_segment)
48
+ except ValueError as e:
49
+ raise Unresolvable(pointer) from e
50
+ schema = schema["items"]
51
+ return schema
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import Literal, TypeAlias, TypeVar, overload
6
+
7
+ from jsonlogic._compat import Self
8
+
9
+ JSONSchemaPrimitiveType: TypeAlias = (
10
+ "AnyType | BooleanType | NumberType | StringType | DatetimeType | DateType | NullType"
11
+ )
12
+
13
+ JSONSchemaPrimitiveTypeT = TypeVar(
14
+ "JSONSchemaPrimitiveTypeT",
15
+ "AnyType",
16
+ "BooleanType",
17
+ "NumberType",
18
+ "IntegerType",
19
+ "StringType",
20
+ "DatetimeType",
21
+ "DateType",
22
+ "NullType",
23
+ )
24
+
25
+
26
+ class JSONSchemaType(ABC):
27
+ @abstractmethod
28
+ def comparable_with(self, other: JSONSchemaType) -> bool:
29
+ pass
30
+
31
+ @overload
32
+ def __or__(self, value: Self, /) -> Self: ... # type: ignore
33
+
34
+ @overload
35
+ def __or__(self, value: JSONSchemaType, /) -> UnionType: ...
36
+
37
+ def __or__(self, value, /): # type: ignore
38
+ return UnionType(self, value)
39
+
40
+
41
+ class UnionType(JSONSchemaType):
42
+ types: set[JSONSchemaPrimitiveType]
43
+
44
+ @overload
45
+ def __new__(cls, *types: JSONSchemaPrimitiveTypeT) -> JSONSchemaPrimitiveTypeT: ...
46
+
47
+ @overload
48
+ def __new__(cls, *types: JSONSchemaType) -> Self: ...
49
+
50
+ def __new__(cls, *types):
51
+ if all(isinstance(t, type(types[0])) for t in types):
52
+ return types[0]
53
+ self = super().__new__(cls)
54
+ self.types = set()
55
+ for typ in types:
56
+ if isinstance(typ, UnionType):
57
+ self.types.update(typ.types)
58
+ else:
59
+ self.types.add(typ)
60
+ return self
61
+
62
+ def __repr__(self) -> str:
63
+ return f"{self.__class__.__qualname__}({', '.join(str(t) for t in self.types)})"
64
+
65
+ def comparable_with(self, other: JSONSchemaType) -> bool:
66
+ for typ in self.types:
67
+ if isinstance(other, UnionType):
68
+ comparable = all(other_typ.comparable_with(typ) for other_typ in other.types)
69
+ else:
70
+ comparable = other.comparable_with(typ)
71
+ if not comparable:
72
+ return False
73
+ return True
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class AnyType(JSONSchemaType):
78
+ def comparable_with(self, other: JSONSchemaType) -> Literal[False]:
79
+ return False
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class BooleanType(JSONSchemaType):
84
+ def comparable_with(self, other: JSONSchemaType) -> Literal[False]:
85
+ return False
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class NumberType(JSONSchemaType):
90
+ def comparable_with(self, other: JSONSchemaType) -> bool:
91
+ if isinstance(other, UnionType):
92
+ return other.comparable_with(self)
93
+ return isinstance(other, (NumberType, IntegerType))
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class IntegerType(JSONSchemaType):
98
+ def comparable_with(self, other: JSONSchemaType) -> bool:
99
+ if isinstance(other, UnionType):
100
+ return other.comparable_with(self)
101
+ return isinstance(other, (NumberType, IntegerType))
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class StringType(JSONSchemaType):
106
+ def comparable_with(self, other: JSONSchemaType) -> Literal[False]:
107
+ return False
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class DatetimeType(JSONSchemaType):
112
+ def comparable_with(self, other: JSONSchemaType) -> bool:
113
+ if isinstance(other, UnionType):
114
+ return other.comparable_with(self)
115
+ # Probably doesn't make sense with date?
116
+ return isinstance(other, DatetimeType)
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class DateType(JSONSchemaType):
121
+ def comparable_with(self, other: JSONSchemaType) -> bool:
122
+ if isinstance(other, UnionType):
123
+ return other.comparable_with(self)
124
+ return isinstance(other, DateType)
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class NullType(JSONSchemaType):
129
+ def comparable_with(self, other: JSONSchemaType) -> Literal[False]:
130
+ return False
@@ -0,0 +1,24 @@
1
+ from jsonlogic.registry import OperatorRegistry
2
+
3
+ from .operators import (
4
+ EqualOperator,
5
+ GreaterThan,
6
+ GreaterThanOrEqual,
7
+ IfOperator,
8
+ LessThan,
9
+ LessThanOrEqual,
10
+ NotEqualOperator,
11
+ Var,
12
+ )
13
+
14
+ operator_registry = OperatorRegistry()
15
+ """The operator registry."""
16
+
17
+ operator_registry.register("var", Var)
18
+ operator_registry.register("==", EqualOperator)
19
+ operator_registry.register("!=", NotEqualOperator)
20
+ operator_registry.register("if", IfOperator)
21
+ operator_registry.register(">", GreaterThan)
22
+ operator_registry.register(">=", GreaterThanOrEqual)
23
+ operator_registry.register("<", LessThan)
24
+ operator_registry.register("<=", LessThanOrEqual)
File without changes
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import operator
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar
7
+
8
+ from jsonlogic._compat import Self
9
+ from jsonlogic.core import Operator
10
+ from jsonlogic.json_schema import from_json_schema, from_value
11
+ from jsonlogic.json_schema.resolvers import JSONSchemaPointerResolver, JSONSchemaResolver, Unresolvable
12
+ from jsonlogic.json_schema.types import AnyType, BooleanType, JSONSchemaType
13
+ from jsonlogic.typing import OperatorArgument
14
+ from jsonlogic.utils import UNSET, Unset
15
+
16
+ from .typechecking import TypecheckError, get_type
17
+
18
+ if TYPE_CHECKING:
19
+ from _typeshed import SupportsAllComparisons
20
+
21
+
22
+ @dataclass
23
+ class Var(Operator):
24
+ json_schema_resolver: ClassVar[type[JSONSchemaResolver]] = JSONSchemaPointerResolver
25
+
26
+ variable_path: str | Operator
27
+
28
+ default_value: OperatorArgument | Unset = UNSET
29
+
30
+ @classmethod
31
+ def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
32
+ assert len(arguments) in (1, 2)
33
+ assert isinstance(arguments[0], (str, Operator))
34
+ if len(arguments) == 1:
35
+ return cls(operator=operator, variable_path=arguments[0])
36
+ return cls(operator=operator, variable_path=arguments[0], default_value=arguments[1])
37
+
38
+ def typecheck(self, data_schema: dict[str, Any]) -> JSONSchemaType:
39
+ if isinstance(self.variable_path, Operator):
40
+ self.variable_path.typecheck(data_schema)
41
+ return AnyType()
42
+
43
+ resolver = self.json_schema_resolver(data_schema)
44
+
45
+ try:
46
+ js_type = resolver.resolve(self.variable_path)
47
+ return from_json_schema(js_type)
48
+ except Unresolvable as e:
49
+ if isinstance(self.default_value, Unset):
50
+ raise TypecheckError("Unresolvable reference", self) from e
51
+ if isinstance(self.default_value, Operator):
52
+ self.default_value.typecheck(data_schema)
53
+ return AnyType()
54
+ return from_value(self.default_value)
55
+
56
+
57
+ @dataclass
58
+ class EqualityOperator(Operator):
59
+ equality_func: ClassVar[Callable[[object, object], bool]]
60
+
61
+ left: OperatorArgument
62
+ right: OperatorArgument
63
+
64
+ @classmethod
65
+ def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
66
+ assert len(arguments) == 2
67
+ return cls(operator=operator, left=arguments[0], right=arguments[1])
68
+
69
+ def typecheck(self, data_schema: dict[str, Any]) -> JSONSchemaType:
70
+ if isinstance(self.left, Operator):
71
+ self.left.typecheck(data_schema)
72
+ if isinstance(self.right, Operator):
73
+ self.right.typecheck(data_schema)
74
+ return BooleanType()
75
+
76
+
77
+ @dataclass
78
+ class EqualOperator(EqualityOperator):
79
+ equality_func = operator.eq
80
+
81
+
82
+ @dataclass
83
+ class NotEqualOperator(EqualityOperator):
84
+ equality_func = operator.ne
85
+
86
+
87
+ @dataclass
88
+ class IfOperator(Operator):
89
+ if_elses: list[tuple[OperatorArgument, OperatorArgument]]
90
+ leading_else: OperatorArgument
91
+
92
+ @classmethod
93
+ def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
94
+ assert len(arguments) % 2 == 1
95
+ return cls(operator=operator, if_elses=list(zip(arguments[::2], arguments[1::2])), leading_else=arguments[-1])
96
+
97
+ def typecheck(self, data_schema: dict[str, Any]) -> JSONSchemaType:
98
+ for cond, _ in self.if_elses:
99
+ cond_type = get_type(cond, data_schema)
100
+ if not isinstance(cond_type, BooleanType):
101
+ raise TypecheckError("should be boolean", self)
102
+
103
+ return functools.reduce(operator.or_, (get_type(rv, data_schema) for _, rv in self.if_elses)) | get_type(
104
+ self.leading_else, data_schema
105
+ )
106
+
107
+
108
+ @dataclass
109
+ class ComparableOperator(Operator):
110
+ operator_func: ClassVar[Callable[[SupportsAllComparisons, SupportsAllComparisons], bool]]
111
+
112
+ left: OperatorArgument
113
+ right: OperatorArgument
114
+
115
+ @classmethod
116
+ def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
117
+ assert len(arguments) == 2
118
+ # TODO validate valid primitives (e.g. array probably not valid)
119
+ # Maybe not necessary, `typecheck` will handle it
120
+ return cls(operator=operator, left=arguments[0], right=arguments[1])
121
+
122
+ def typecheck(self, data_schema: dict[str, Any]) -> JSONSchemaType:
123
+ left_type = get_type(self.left, data_schema)
124
+ right_type = get_type(self.right, data_schema)
125
+
126
+ if not left_type.comparable_with(right_type):
127
+ raise TypecheckError("Cannot compare types", self)
128
+
129
+ return BooleanType()
130
+
131
+ def apply(self, data) -> bool:
132
+ left = self.left
133
+ if isinstance(left, Operator):
134
+ left = left.apply(data)
135
+ right = self.right
136
+ if isinstance(right, Operator):
137
+ right = right.apply(data)
138
+
139
+ return self.operator_func(left, right)
140
+
141
+
142
+ @dataclass
143
+ class GreaterThan(ComparableOperator):
144
+ operator_func = operator.gt
145
+
146
+
147
+ @dataclass
148
+ class GreaterThanOrEqual(ComparableOperator):
149
+ operator_func = operator.ge
150
+
151
+
152
+ @dataclass
153
+ class LessThan(ComparableOperator):
154
+ operator_func = operator.lt
155
+
156
+
157
+ @dataclass
158
+ class LessThanOrEqual(ComparableOperator):
159
+ operator_func = operator.le
@@ -0,0 +1,16 @@
1
+ from typing import Any
2
+
3
+ from jsonlogic.core import Operator
4
+ from jsonlogic.json_schema import from_value
5
+ from jsonlogic.json_schema.types import JSONSchemaType
6
+ from jsonlogic.typing import OperatorArgument
7
+
8
+
9
+ def get_type(obj: OperatorArgument, data_schema: dict[str, Any]) -> JSONSchemaType:
10
+ return obj.typecheck(data_schema) if isinstance(obj, Operator) else from_value(obj)
11
+
12
+
13
+ class TypecheckError(Exception):
14
+ def __init__(self, message: str, operator: Operator, /) -> None:
15
+ self.operator = operator
16
+ self.message = message
File without changes
@@ -0,0 +1,94 @@
1
+ from typing import Callable, overload
2
+
3
+ from ._compat import Self, TypeAlias
4
+ from .core import Operator
5
+
6
+
7
+ class AlreadyRegistered(Exception):
8
+ """The provided ID is already registered."""
9
+
10
+ def __init__(self, operator_id: str, /) -> None:
11
+ self.operator_id = operator_id
12
+
13
+
14
+ class UnkownOperator(Exception):
15
+ """The provided ID does not exist in the registry."""
16
+
17
+ def __init__(self, operator_id: str, /) -> None:
18
+ self.operator_id = operator_id
19
+
20
+
21
+ OperatorType: TypeAlias = type[Operator]
22
+
23
+
24
+ class OperatorRegistry:
25
+ def __init__(self) -> None:
26
+ self._registry: dict[str, OperatorType] = {}
27
+
28
+ @overload
29
+ def register(self, operator_id: str, *, force: bool = ...) -> Callable[[OperatorType], OperatorType]: ...
30
+
31
+ @overload
32
+ def register(self, operator_id: str, operator_type: OperatorType, *, force: bool = ...) -> OperatorType: ...
33
+
34
+ def register(
35
+ self, operator_id: str, operator_type: OperatorType | None = None, *, force: bool = False
36
+ ) -> Callable[[OperatorType], OperatorType] | OperatorType:
37
+ """Register an operator type under the provided ID.
38
+
39
+ Args:
40
+ operator_id: The ID to be used to register the operator.
41
+ operator_type: Type class of the operator. If not provided,
42
+ will return a callable to be applied on an operator class.
43
+ force: Whether to override any existing operator under the provided ID.
44
+
45
+ Raises:
46
+ AlreadyRegistered: If ``force`` wasn't set and the ID already exist.
47
+
48
+ Note:
49
+ The method is usable as a decorator:
50
+
51
+ .. code-block:: python
52
+ :emphasize-lines: 5,6
53
+
54
+ reg = OperatorRegistry()
55
+ reg.register("==", EqualOperator)
56
+
57
+ # Or:
58
+ @reg.register("==")
59
+ class EqualOperator(Operator): ...
60
+ """
61
+
62
+ if operator_type is None:
63
+
64
+ def dec(operator_type: OperatorType, /) -> OperatorType:
65
+ return self.register(operator_id, operator_type)
66
+
67
+ return dec
68
+
69
+ if operator_id in self._registry and not force:
70
+ raise AlreadyRegistered(operator_id)
71
+
72
+ self._registry[operator_id] = operator_type
73
+ return operator_type
74
+
75
+ def get(self, operator_id: str, /) -> OperatorType:
76
+ """Get the operator class corresponding to the ID.
77
+
78
+ Args:
79
+ operator_id: The registered ID of the operator.
80
+
81
+ Raises:
82
+ UnkownOperator: If the provided ID does not exist.
83
+ """
84
+ try:
85
+ return self._registry[operator_id]
86
+ except KeyError:
87
+ raise UnkownOperator(operator_id) # noqa: B904
88
+
89
+ def copy(self) -> Self:
90
+ """Create a new instance of the registry."""
91
+
92
+ new = self.__class__()
93
+ new._registry = self._registry.copy()
94
+ return new
@@ -0,0 +1,17 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ._compat import TypeAlias
4
+
5
+ if TYPE_CHECKING:
6
+ # This is a hack to make pyright think `TypeAlias` comes from `typing`
7
+ from .typing import TypeAlias
8
+
9
+ JSONPrimitive: TypeAlias = str | int | float | bool | None
10
+ JSONObject: TypeAlias = "dict[str, JSON]"
11
+ JSONArray: TypeAlias = "list[JSON]"
12
+ JSON: TypeAlias = "JSONPrimitive | JSONArray | JSONObject"
13
+
14
+ JSONLogicPrimitive: TypeAlias = JSONPrimitive | list[JSONPrimitive]
15
+
16
+ OperatorArgument: TypeAlias = "JSONLogicPrimitive | Operator"
17
+ """A valid operator argument, either a JSON Logic primitive or an operator."""
@@ -0,0 +1,14 @@
1
+ from typing import Literal
2
+
3
+
4
+ class Unset:
5
+ """A class representing an unset value."""
6
+
7
+ def __bool__(self) -> Literal[False]:
8
+ return False
9
+
10
+ def __repr__(self) -> str:
11
+ return f"{self.__class__.__qualname__}()"
12
+
13
+
14
+ UNSET: Unset = Unset()
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-jsonlogic
3
+ Version: 0.0.1
4
+ Summary: An extensible and sane implementation of JsonLogic
5
+ Author-email: Victorien <contact@vctrn.dev>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Victorien
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Operating System :: OS Independent
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Programming Language :: Python :: 3.8
33
+ Classifier: Programming Language :: Python :: 3.9
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Typing :: Typed
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
41
+ Requires-Python: >=3.8
42
+ Description-Content-Type: text/x-rst
43
+ License-File: LICENSE
44
+ Requires-Dist: typing-extensions>=4.6.0; python_version < "3.12"
45
+
46
+ =========
47
+ jsonlogic
48
+ =========
49
+
50
+ |Pythons| |PyPI|
51
+
52
+ .. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonlogic.svg
53
+ :alt: Supported Python versions
54
+ :target: https://pypi.org/project/jsonlogic/
55
+
56
+ .. |PyPI| image:: https://img.shields.io/pypi/v/jsonlogic.svg
57
+ :alt: PyPI - Version
58
+ :target: https://pypi.org/project/jsonlogic/
59
+
60
+ .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
61
+ :alt: PyPI - Version
62
+ :target: https://pypi.org/project/jsonlogic/
63
+
64
+ ``jsonlogic`` is an extensible and sane implementation of `JsonLogic`_.
65
+
66
+ .. _`JsonLogic`: https://jsonlogic.com/
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.rst
3
+ pyproject.toml
4
+ src/jsonlogic/__init__.py
5
+ src/jsonlogic/_compat.py
6
+ src/jsonlogic/core.py
7
+ src/jsonlogic/py.typed
8
+ src/jsonlogic/registry.py
9
+ src/jsonlogic/typing.py
10
+ src/jsonlogic/utils.py
11
+ src/jsonlogic/json_schema/__init__.py
12
+ src/jsonlogic/json_schema/resolvers.py
13
+ src/jsonlogic/json_schema/types.py
14
+ src/jsonlogic/operators/__init__.py
15
+ src/jsonlogic/operators/base.py
16
+ src/jsonlogic/operators/operators.py
17
+ src/jsonlogic/operators/typechecking.py
18
+ src/python_jsonlogic.egg-info/PKG-INFO
19
+ src/python_jsonlogic.egg-info/SOURCES.txt
20
+ src/python_jsonlogic.egg-info/dependency_links.txt
21
+ src/python_jsonlogic.egg-info/requires.txt
22
+ src/python_jsonlogic.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [:python_version < "3.12"]
3
+ typing-extensions>=4.6.0