python-jsonlogic 0.0.1__tar.gz → 0.1.0__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.
- python_jsonlogic-0.1.0/PKG-INFO +141 -0
- python_jsonlogic-0.1.0/README.rst +96 -0
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/pyproject.toml +9 -3
- python_jsonlogic-0.1.0/src/jsonlogic/__init__.py +3 -0
- python_jsonlogic-0.1.0/src/jsonlogic/_compat.py +12 -0
- python_jsonlogic-0.1.0/src/jsonlogic/core.py +132 -0
- python_jsonlogic-0.1.0/src/jsonlogic/evaluation/__init__.py +5 -0
- python_jsonlogic-0.1.0/src/jsonlogic/evaluation/evaluation_context.py +74 -0
- python_jsonlogic-0.1.0/src/jsonlogic/evaluation/evaluation_settings.py +104 -0
- python_jsonlogic-0.1.0/src/jsonlogic/evaluation/utils.py +62 -0
- python_jsonlogic-0.1.0/src/jsonlogic/json_schema/__init__.py +184 -0
- python_jsonlogic-0.1.0/src/jsonlogic/json_schema/types.py +370 -0
- python_jsonlogic-0.1.0/src/jsonlogic/operators/__init__.py +53 -0
- python_jsonlogic-0.1.0/src/jsonlogic/operators/operators.py +359 -0
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/src/jsonlogic/registry.py +55 -11
- python_jsonlogic-0.1.0/src/jsonlogic/resolving.py +201 -0
- python_jsonlogic-0.1.0/src/jsonlogic/typechecking/__init__.py +15 -0
- python_jsonlogic-0.1.0/src/jsonlogic/typechecking/diagnostics.py +37 -0
- python_jsonlogic-0.1.0/src/jsonlogic/typechecking/typecheck_context.py +69 -0
- python_jsonlogic-0.1.0/src/jsonlogic/typechecking/typecheck_settings.py +212 -0
- python_jsonlogic-0.1.0/src/jsonlogic/typechecking/utils.py +49 -0
- python_jsonlogic-0.1.0/src/jsonlogic/typing.py +50 -0
- python_jsonlogic-0.1.0/src/jsonlogic/utils.py +47 -0
- python_jsonlogic-0.1.0/src/python_jsonlogic.egg-info/PKG-INFO +141 -0
- python_jsonlogic-0.1.0/src/python_jsonlogic.egg-info/SOURCES.txt +32 -0
- python_jsonlogic-0.1.0/src/python_jsonlogic.egg-info/requires.txt +3 -0
- python_jsonlogic-0.1.0/tests/test_json_logic_expression.py +9 -0
- python_jsonlogic-0.1.0/tests/test_registry.py +107 -0
- python_jsonlogic-0.1.0/tests/test_resolving.py +140 -0
- python-jsonlogic-0.0.1/PKG-INFO +0 -66
- python-jsonlogic-0.0.1/README.rst +0 -21
- python-jsonlogic-0.0.1/src/jsonlogic/__init__.py +0 -0
- python-jsonlogic-0.0.1/src/jsonlogic/_compat.py +0 -8
- python-jsonlogic-0.0.1/src/jsonlogic/core.py +0 -73
- python-jsonlogic-0.0.1/src/jsonlogic/json_schema/__init__.py +0 -77
- python-jsonlogic-0.0.1/src/jsonlogic/json_schema/resolvers.py +0 -51
- python-jsonlogic-0.0.1/src/jsonlogic/json_schema/types.py +0 -130
- python-jsonlogic-0.0.1/src/jsonlogic/operators/__init__.py +0 -24
- python-jsonlogic-0.0.1/src/jsonlogic/operators/base.py +0 -0
- python-jsonlogic-0.0.1/src/jsonlogic/operators/operators.py +0 -159
- python-jsonlogic-0.0.1/src/jsonlogic/operators/typechecking.py +0 -16
- python-jsonlogic-0.0.1/src/jsonlogic/typing.py +0 -17
- python-jsonlogic-0.0.1/src/jsonlogic/utils.py +0 -14
- python-jsonlogic-0.0.1/src/python_jsonlogic.egg-info/PKG-INFO +0 -66
- python-jsonlogic-0.0.1/src/python_jsonlogic.egg-info/SOURCES.txt +0 -22
- python-jsonlogic-0.0.1/src/python_jsonlogic.egg-info/requires.txt +0 -3
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/LICENSE +0 -0
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/setup.cfg +0 -0
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/src/jsonlogic/py.typed +0 -0
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/src/python_jsonlogic.egg-info/dependency_links.txt +0 -0
- {python-jsonlogic-0.0.1 → python_jsonlogic-0.1.0}/src/python_jsonlogic.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-jsonlogic
|
|
3
|
+
Version: 0.1.0
|
|
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.10.0; python_version < "3.13"
|
|
45
|
+
|
|
46
|
+
================
|
|
47
|
+
python-jsonlogic
|
|
48
|
+
================
|
|
49
|
+
|
|
50
|
+
|Pythons| |PyPI| |Ruff|
|
|
51
|
+
|
|
52
|
+
.. |Pythons| image:: https://img.shields.io/pypi/pyversions/python-jsonlogic.svg
|
|
53
|
+
:alt: Supported Python versions
|
|
54
|
+
:target: https://pypi.org/project/python-jsonlogic/
|
|
55
|
+
|
|
56
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonlogic.svg
|
|
57
|
+
:alt: PyPI - Version
|
|
58
|
+
:target: https://pypi.org/project/python-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://github.com/astral-sh/ruff
|
|
63
|
+
|
|
64
|
+
``python-jsonlogic`` is an extensible and sane implementation of `JsonLogic`_, making use of the `JSON Schema`_ specification.
|
|
65
|
+
|
|
66
|
+
.. _`JSON Schema`: https://json-schema.org/
|
|
67
|
+
|
|
68
|
+
Motivation
|
|
69
|
+
----------
|
|
70
|
+
|
|
71
|
+
While the `JsonLogic`_ format can be great to serialize logic, it lacks a formal specification
|
|
72
|
+
and some aspects are unclear/unspecified:
|
|
73
|
+
|
|
74
|
+
* `operators <https://jsonlogic.com/operations.html>`_ arguments can take any value. For instance,
|
|
75
|
+
`comparison operators <https://jsonlogic.com/operations.html#---and->`_ are said to work with "numeric" values,
|
|
76
|
+
however the `JavaScript playground <https://jsonlogic.com/play.html>`_ doesn't validate inputs. It is
|
|
77
|
+
also convenient to allow such comparison operators for date and datetime objects as well.
|
|
78
|
+
* Operators `accessing data <https://jsonlogic.com/operations.html#accessing-data>`_ use a dot-like notation,
|
|
79
|
+
which is ambiguous when dealing with keys such as ``my.key``.
|
|
80
|
+
* Operators such as `map <https://jsonlogic.com/operations.html#map-reduce-and-filter>`_ provides their own data scope,
|
|
81
|
+
making it impossible to access higher-level data inside the operator expression.
|
|
82
|
+
|
|
83
|
+
For these reasons, ``python-jsonlogic`` provides a way to typecheck your JSON Logic expressions at "compile" time,
|
|
84
|
+
before applying input data to them.
|
|
85
|
+
|
|
86
|
+
.. _`JsonLogic`: https://jsonlogic.com/
|
|
87
|
+
|
|
88
|
+
Installation
|
|
89
|
+
------------
|
|
90
|
+
|
|
91
|
+
From PyPI:
|
|
92
|
+
|
|
93
|
+
.. code:: bash
|
|
94
|
+
|
|
95
|
+
pip install python-jsonlogic
|
|
96
|
+
|
|
97
|
+
The library can be imported from the ``jsonlogic`` module.
|
|
98
|
+
|
|
99
|
+
Usage
|
|
100
|
+
-----
|
|
101
|
+
|
|
102
|
+
.. code-block:: python
|
|
103
|
+
|
|
104
|
+
# 1. Create or use an already existing operator registry:
|
|
105
|
+
from jsonlogic.operators import operator_registry
|
|
106
|
+
|
|
107
|
+
# 2. Parse the JSON Logic expression:
|
|
108
|
+
from jsonlogic import JSONLogicExpression
|
|
109
|
+
|
|
110
|
+
expr = JSONLogicExpression.from_json({"map": [
|
|
111
|
+
[1, 2],
|
|
112
|
+
{"*": [{"var": ""}, {"var": "/my_int@1"}]},
|
|
113
|
+
]})
|
|
114
|
+
|
|
115
|
+
# 3. Create an operator tree:
|
|
116
|
+
root_op = expr.as_operator_tree(operator_registry)
|
|
117
|
+
|
|
118
|
+
# 4. Typecheck the expression:
|
|
119
|
+
from jsonlogic.typechecking import typecheck
|
|
120
|
+
|
|
121
|
+
typ, diagnostics = typecheck(
|
|
122
|
+
root_op,
|
|
123
|
+
data_schema={
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"my_int": {"type": "integer"}
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
print(typ)
|
|
131
|
+
#> ArrayType(IntegerType())
|
|
132
|
+
|
|
133
|
+
# 5. Evaluate with data:
|
|
134
|
+
from jsonlogic.evaluation import evaluate
|
|
135
|
+
value = evaluate(
|
|
136
|
+
root_op,
|
|
137
|
+
data={"my_int": 2},
|
|
138
|
+
data_schema=None,
|
|
139
|
+
)
|
|
140
|
+
print(value)
|
|
141
|
+
#> [2, 4]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
================
|
|
2
|
+
python-jsonlogic
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
|Pythons| |PyPI| |Ruff|
|
|
6
|
+
|
|
7
|
+
.. |Pythons| image:: https://img.shields.io/pypi/pyversions/python-jsonlogic.svg
|
|
8
|
+
:alt: Supported Python versions
|
|
9
|
+
:target: https://pypi.org/project/python-jsonlogic/
|
|
10
|
+
|
|
11
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonlogic.svg
|
|
12
|
+
:alt: PyPI - Version
|
|
13
|
+
:target: https://pypi.org/project/python-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://github.com/astral-sh/ruff
|
|
18
|
+
|
|
19
|
+
``python-jsonlogic`` is an extensible and sane implementation of `JsonLogic`_, making use of the `JSON Schema`_ specification.
|
|
20
|
+
|
|
21
|
+
.. _`JSON Schema`: https://json-schema.org/
|
|
22
|
+
|
|
23
|
+
Motivation
|
|
24
|
+
----------
|
|
25
|
+
|
|
26
|
+
While the `JsonLogic`_ format can be great to serialize logic, it lacks a formal specification
|
|
27
|
+
and some aspects are unclear/unspecified:
|
|
28
|
+
|
|
29
|
+
* `operators <https://jsonlogic.com/operations.html>`_ arguments can take any value. For instance,
|
|
30
|
+
`comparison operators <https://jsonlogic.com/operations.html#---and->`_ are said to work with "numeric" values,
|
|
31
|
+
however the `JavaScript playground <https://jsonlogic.com/play.html>`_ doesn't validate inputs. It is
|
|
32
|
+
also convenient to allow such comparison operators for date and datetime objects as well.
|
|
33
|
+
* Operators `accessing data <https://jsonlogic.com/operations.html#accessing-data>`_ use a dot-like notation,
|
|
34
|
+
which is ambiguous when dealing with keys such as ``my.key``.
|
|
35
|
+
* Operators such as `map <https://jsonlogic.com/operations.html#map-reduce-and-filter>`_ provides their own data scope,
|
|
36
|
+
making it impossible to access higher-level data inside the operator expression.
|
|
37
|
+
|
|
38
|
+
For these reasons, ``python-jsonlogic`` provides a way to typecheck your JSON Logic expressions at "compile" time,
|
|
39
|
+
before applying input data to them.
|
|
40
|
+
|
|
41
|
+
.. _`JsonLogic`: https://jsonlogic.com/
|
|
42
|
+
|
|
43
|
+
Installation
|
|
44
|
+
------------
|
|
45
|
+
|
|
46
|
+
From PyPI:
|
|
47
|
+
|
|
48
|
+
.. code:: bash
|
|
49
|
+
|
|
50
|
+
pip install python-jsonlogic
|
|
51
|
+
|
|
52
|
+
The library can be imported from the ``jsonlogic`` module.
|
|
53
|
+
|
|
54
|
+
Usage
|
|
55
|
+
-----
|
|
56
|
+
|
|
57
|
+
.. code-block:: python
|
|
58
|
+
|
|
59
|
+
# 1. Create or use an already existing operator registry:
|
|
60
|
+
from jsonlogic.operators import operator_registry
|
|
61
|
+
|
|
62
|
+
# 2. Parse the JSON Logic expression:
|
|
63
|
+
from jsonlogic import JSONLogicExpression
|
|
64
|
+
|
|
65
|
+
expr = JSONLogicExpression.from_json({"map": [
|
|
66
|
+
[1, 2],
|
|
67
|
+
{"*": [{"var": ""}, {"var": "/my_int@1"}]},
|
|
68
|
+
]})
|
|
69
|
+
|
|
70
|
+
# 3. Create an operator tree:
|
|
71
|
+
root_op = expr.as_operator_tree(operator_registry)
|
|
72
|
+
|
|
73
|
+
# 4. Typecheck the expression:
|
|
74
|
+
from jsonlogic.typechecking import typecheck
|
|
75
|
+
|
|
76
|
+
typ, diagnostics = typecheck(
|
|
77
|
+
root_op,
|
|
78
|
+
data_schema={
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"my_int": {"type": "integer"}
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
print(typ)
|
|
86
|
+
#> ArrayType(IntegerType())
|
|
87
|
+
|
|
88
|
+
# 5. Evaluate with data:
|
|
89
|
+
from jsonlogic.evaluation import evaluate
|
|
90
|
+
value = evaluate(
|
|
91
|
+
root_op,
|
|
92
|
+
data={"my_int": 2},
|
|
93
|
+
data_schema=None,
|
|
94
|
+
)
|
|
95
|
+
print(value)
|
|
96
|
+
#> [2, 4]
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-jsonlogic"
|
|
7
7
|
description = "An extensible and sane implementation of JsonLogic"
|
|
8
|
-
version = "0.0
|
|
8
|
+
version = "0.1.0"
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
authors = [
|
|
11
11
|
{name = "Victorien", email = "contact@vctrn.dev"}
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
license = {file = "LICENSE"}
|
|
29
29
|
requires-python = ">=3.8"
|
|
30
30
|
dependencies = [
|
|
31
|
-
"typing-extensions>=4.
|
|
31
|
+
"typing-extensions>=4.10.0; python_version < '3.13'",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
[tool.setuptools]
|
|
@@ -46,6 +46,7 @@ src = ["src"]
|
|
|
46
46
|
target-version = "py38"
|
|
47
47
|
|
|
48
48
|
[tool.ruff.lint]
|
|
49
|
+
typing-modules = ["_compat"]
|
|
49
50
|
preview = true
|
|
50
51
|
explicit-preview-rules = true
|
|
51
52
|
select = [
|
|
@@ -61,14 +62,19 @@ select = [
|
|
|
61
62
|
"B", # flake8-bugbear
|
|
62
63
|
"C4", # flake8-comprehensions
|
|
63
64
|
"T10", # flake8-debugger
|
|
65
|
+
"FA", # flake8-future-annotations
|
|
64
66
|
"PIE", # flake8-pie
|
|
65
67
|
"T20", # flake8-print
|
|
66
68
|
"RSE", # flake8-raise
|
|
67
69
|
"PTH", # flake8-use-pathlib
|
|
68
70
|
]
|
|
69
71
|
ignore = [
|
|
70
|
-
"PLR2004"
|
|
72
|
+
"PLR2004",
|
|
73
|
+
"PLR0911",
|
|
71
74
|
]
|
|
72
75
|
|
|
73
76
|
[tool.ruff.lint.isort]
|
|
74
77
|
known-first-party = ["jsonlogic"]
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
pythonpath = "src"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info >= (3, 13):
|
|
4
|
+
from types import NoneType
|
|
5
|
+
from typing import Self, TypeAlias, TypeIs, TypeVarTuple, Unpack
|
|
6
|
+
else:
|
|
7
|
+
from typing_extensions import Self, TypeAlias, TypeIs, TypeVarTuple, Unpack
|
|
8
|
+
|
|
9
|
+
NoneType = type(None)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ("NoneType", "Self", "TypeAlias", "TypeIs", "TypeVarTuple", "Unpack")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Base structures of the library. The two classes defined, :class:`Operator` and :class:`JSONLogicExpression`,
|
|
2
|
+
can be extended to provide extra functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from ._compat import Self, TypeAlias
|
|
12
|
+
from .json_schema.types import AnyType, JSONSchemaType
|
|
13
|
+
from .typing import JSON, JSONLogicPrimitive, JSONObject, OperatorArgument
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
# This is a hack to make Pylance think `TypeAlias` comes from `typing`
|
|
17
|
+
from typing import TypeAlias
|
|
18
|
+
|
|
19
|
+
from .evaluation import EvaluationContext
|
|
20
|
+
from .registry import OperatorRegistry
|
|
21
|
+
from .typechecking import TypecheckContext
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Operator(ABC):
|
|
26
|
+
"""The base class for all operators."""
|
|
27
|
+
|
|
28
|
+
operator: str = field(repr=False)
|
|
29
|
+
"""The string representation of the operator."""
|
|
30
|
+
|
|
31
|
+
# metadata: Any | None = None
|
|
32
|
+
# """Extra metadata for this operator.
|
|
33
|
+
|
|
34
|
+
# For any exception encountered, this will be included.
|
|
35
|
+
# """
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
|
|
40
|
+
"""Return an instance of the operator from the list of provided arguments.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
operator: The ID of the operator, as provided by the :class:`~jsonlogic.registry.OperatorRegistry`.
|
|
44
|
+
arguments: The list of the arguments for this operator. Subclasses are responsible
|
|
45
|
+
for checking the correct number of arguments and optionally the types.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def evaluate(self, context: EvaluationContext) -> Any:
|
|
50
|
+
"""Evaluate the operator with the provided data."""
|
|
51
|
+
|
|
52
|
+
def typecheck(self, context: TypecheckContext) -> JSONSchemaType:
|
|
53
|
+
"""Typecheck the operator (and all children) given the data schema."""
|
|
54
|
+
|
|
55
|
+
return AnyType()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class JSONLogicSyntaxError(Exception):
|
|
59
|
+
"""A syntax error when building an operator tree from a :class:`JSONLogicExpression`."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, message: str, /) -> None:
|
|
62
|
+
self.message = message
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
ExprArgument: TypeAlias = "JSONLogicPrimitive | JSONLogicExpression | list[ExprArgument]"
|
|
66
|
+
|
|
67
|
+
NormalizedExpression: TypeAlias = "dict[str, list[ExprArgument]]"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class JSONLogicExpression:
|
|
72
|
+
"""A parsed and normalized JSON Logic expression.
|
|
73
|
+
|
|
74
|
+
The underlying structure of an expression is a single item dictionary,
|
|
75
|
+
mapping the operator key to a list of arguments.
|
|
76
|
+
|
|
77
|
+
All JSON Logic expressions should be instantiated using the :meth:`from_json` constructor::
|
|
78
|
+
|
|
79
|
+
expr = JSONLogicExpression.from_json({"op": ...})
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
expression: NormalizedExpression
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def _parse_impl(cls, json: JSON) -> ExprArgument:
|
|
86
|
+
if isinstance(json, dict):
|
|
87
|
+
return cls.from_json(json)
|
|
88
|
+
if isinstance(json, list):
|
|
89
|
+
return [cls._parse_impl(s) for s in json]
|
|
90
|
+
return json
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_json(cls, json: JSONObject) -> Self:
|
|
94
|
+
"""Build a JSON Logic expression from JSON data.
|
|
95
|
+
|
|
96
|
+
Operator arguments are recursively normalized to a :class:`list`::
|
|
97
|
+
|
|
98
|
+
expr = JSONLogicExpression.from_json({"var": "varname"})
|
|
99
|
+
assert expr.expression == {"var": ["varname"]}
|
|
100
|
+
"""
|
|
101
|
+
if not isinstance(json, dict):
|
|
102
|
+
raise ValueError("The root node of the expression must be a dict")
|
|
103
|
+
|
|
104
|
+
operator, op_args = next(iter(json.items()))
|
|
105
|
+
if not isinstance(op_args, list):
|
|
106
|
+
op_args = [op_args]
|
|
107
|
+
|
|
108
|
+
return cls({operator: [cls._parse_impl(arg) for arg in op_args]})
|
|
109
|
+
|
|
110
|
+
def _as_op_impl(self, op_arg: ExprArgument, operator_registry: OperatorRegistry) -> OperatorArgument:
|
|
111
|
+
if isinstance(op_arg, JSONLogicExpression):
|
|
112
|
+
return op_arg.as_operator_tree(operator_registry)
|
|
113
|
+
if isinstance(op_arg, list):
|
|
114
|
+
return [self._as_op_impl(sub_arg, operator_registry) for sub_arg in op_arg]
|
|
115
|
+
return op_arg
|
|
116
|
+
|
|
117
|
+
def as_operator_tree(self, operator_registry: OperatorRegistry) -> Operator:
|
|
118
|
+
"""Return a recursive tree of operators, using the provided registry as a reference.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
operator_registry: The registry to use to resolve operator IDs.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
An :class:`Operator` instance.
|
|
125
|
+
"""
|
|
126
|
+
if not isinstance(self.expression, dict):
|
|
127
|
+
return self.expression
|
|
128
|
+
|
|
129
|
+
op_id, op_args = next(iter(self.expression.items()))
|
|
130
|
+
OperatorCls = operator_registry.get(op_id)
|
|
131
|
+
|
|
132
|
+
return OperatorCls.from_expression(op_id, [self._as_op_impl(op_arg, operator_registry) for op_arg in op_args])
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, overload
|
|
4
|
+
|
|
5
|
+
from jsonlogic.json_schema import cast_from_schema
|
|
6
|
+
from jsonlogic.resolving import resolve_data, resolve_json_schema
|
|
7
|
+
from jsonlogic.typing import JSON
|
|
8
|
+
from jsonlogic.utils import DataStack
|
|
9
|
+
|
|
10
|
+
from .evaluation_settings import EvaluationSettings, EvaluationSettingsDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EvaluationContext:
|
|
14
|
+
"""A context object used when evaluating operators.
|
|
15
|
+
|
|
16
|
+
When evaluating an :class:`~jsonlogic.core.Operator`, an instance of this
|
|
17
|
+
class should be used.
|
|
18
|
+
|
|
19
|
+
.. code-block:: pycon
|
|
20
|
+
|
|
21
|
+
>>> expr = JSONLogicExpression.from_json({"var": "/a_date"})
|
|
22
|
+
>>> root_op = expr.as_operator_tree(operator_registry)
|
|
23
|
+
>>> context = EvaluationContext(
|
|
24
|
+
... data={"a_date": "1970-01-01"},
|
|
25
|
+
... data_schema={
|
|
26
|
+
... "type": "object",
|
|
27
|
+
... "properties": {
|
|
28
|
+
... "a_date": {"type": "string", "format": "date"},
|
|
29
|
+
... },
|
|
30
|
+
... },
|
|
31
|
+
... )
|
|
32
|
+
>>> root_op.evaluate(context)
|
|
33
|
+
datetime.date(1970, 1, 1)
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
root_data: The root data available during evaluation.
|
|
37
|
+
data_schema: The matching JSON Schema describing the root data. This should be the same JSON Schema
|
|
38
|
+
used during typechecking (see :paramref:`~jsonlogic.typechecking.TypecheckContext.root_data_schema`).
|
|
39
|
+
settings: Settings to be used when evaluating an :class:`~jsonlogic.core.Operator`.
|
|
40
|
+
See :class:`EvaluationSettings` for the available settings and default values.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, root_data: JSON, data_schema: dict[str, Any] | None = None, settings: EvaluationSettingsDict | None = None
|
|
45
|
+
) -> None:
|
|
46
|
+
self.data_stack = DataStack((root_data, data_schema))
|
|
47
|
+
self.settings = EvaluationSettings.from_dict(settings) if settings is not None else EvaluationSettings()
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def resolve_variable(self, reference: str, *, bare: Literal[True]) -> JSON: ...
|
|
51
|
+
|
|
52
|
+
@overload
|
|
53
|
+
def resolve_variable(self, reference: str, *, bare: Literal[False] = ...) -> Any: ...
|
|
54
|
+
|
|
55
|
+
def resolve_variable(self, reference: str, *, bare: bool = False) -> JSON | Any:
|
|
56
|
+
"""Resolve a variable given the string reference pointing to it.
|
|
57
|
+
|
|
58
|
+
The format of the reference should match the reference parser defined
|
|
59
|
+
in the :class:`EvaluationSettings`.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
reference: The string reference of the variable.
|
|
63
|
+
bare: Whether the resolved value should be casted to a specific Python
|
|
64
|
+
type according to the matching JSON Schema. Note that this will only
|
|
65
|
+
be possible if a :paramref:`~EvaluationContext.data_schema` was provided.
|
|
66
|
+
"""
|
|
67
|
+
parsed_reference, scope = self.settings.reference_parser(reference)
|
|
68
|
+
root_data, root_schema = self.data_stack.get(scope)
|
|
69
|
+
bare_value = resolve_data(parsed_reference, root_data)
|
|
70
|
+
if bare or root_schema is None:
|
|
71
|
+
return bare_value
|
|
72
|
+
|
|
73
|
+
schema = resolve_json_schema(parsed_reference, root_schema)
|
|
74
|
+
return cast_from_schema(bare_value, schema, self.settings.variable_casts)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from typing import Any, Callable, TypedDict
|
|
6
|
+
|
|
7
|
+
from jsonlogic._compat import Self
|
|
8
|
+
from jsonlogic.resolving import PointerReferenceParser, ReferenceParser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _d_variable_casts() -> dict[str, Callable[[str], Any]]:
|
|
12
|
+
return {
|
|
13
|
+
"date": date.fromisoformat,
|
|
14
|
+
"date-time": datetime.fromisoformat,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class EvaluationSettings:
|
|
20
|
+
"""Settings used when evaluating an :class:`~jsonlogic.core.Operator`."""
|
|
21
|
+
|
|
22
|
+
reference_parser: ReferenceParser = field(default_factory=PointerReferenceParser)
|
|
23
|
+
"""A reference parser instance to use when resolving variables.
|
|
24
|
+
|
|
25
|
+
Default: :class:`~jsonlogic.resolving.PointerReferenceParser`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
variable_casts: dict[str, Callable[[str], Any]] = field(default_factory=_d_variable_casts)
|
|
29
|
+
"""A mapping between `JSON Schema formats`_ and their corresponding conversion callable.
|
|
30
|
+
|
|
31
|
+
When an operator reads variables from the provided data (such as the ``"var"`` operator),
|
|
32
|
+
such variables of type :class:`str` may be converted to a specific Python type if
|
|
33
|
+
the corresponding JSON Schema of the data was provided during evaluation.
|
|
34
|
+
|
|
35
|
+
This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.variable_casts`
|
|
36
|
+
configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
|
|
37
|
+
|
|
38
|
+
Default: :python:`{"date": date.fromisoformat, "date-time": datetime.fromisoformat}`.
|
|
39
|
+
|
|
40
|
+
.. _JSON Schema formats: https://json-schema.org/understanding-json-schema/reference/string#built-in-formats
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
literal_casts: list[Callable[[str], Any]] = field(default_factory=list)
|
|
44
|
+
"""A list of conversion callables to try when encountering a literal string value during evaluation.
|
|
45
|
+
|
|
46
|
+
When a literal string value is encountered in a JSON Logic expression, it might be
|
|
47
|
+
beneficial to convert it to a specific Python type.
|
|
48
|
+
|
|
49
|
+
This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.literal_casts`
|
|
50
|
+
configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
|
|
51
|
+
|
|
52
|
+
Default: :python:`[]` (no cast).
|
|
53
|
+
|
|
54
|
+
.. warning::
|
|
55
|
+
|
|
56
|
+
The order in which the conversion callables are defined matters. Each
|
|
57
|
+
callable will be applied one after the other until no exception is raised.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, dct: EvaluationSettingsDict, /) -> Self:
|
|
62
|
+
return cls(**dct)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EvaluationSettingsDict(TypedDict, total=False):
|
|
66
|
+
"""Settings used when evaluating an :class:`~jsonlogic.core.Operator`."""
|
|
67
|
+
|
|
68
|
+
reference_parser: ReferenceParser
|
|
69
|
+
"""A reference parser instance to use when resolving variables.
|
|
70
|
+
|
|
71
|
+
Default: :class:`~jsonlogic.resolving.PointerReferenceParser`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
variable_casts: dict[str, Callable[[str], Any]]
|
|
75
|
+
"""A mapping between `JSON Schema formats`_ and their corresponding conversion callable.
|
|
76
|
+
|
|
77
|
+
When an operator reads variables from the provided data (such as the ``"var"`` operator),
|
|
78
|
+
such variables of type :class:`str` may be converted to a specific Python type if
|
|
79
|
+
the corresponding JSON Schema of the data was provided during evaluation.
|
|
80
|
+
|
|
81
|
+
This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.variable_casts`
|
|
82
|
+
configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
|
|
83
|
+
|
|
84
|
+
Default: :python:`{"date": date.fromisoformat, "date-time": datetime.fromisoformat}`.
|
|
85
|
+
|
|
86
|
+
.. _JSON Schema formats: https://json-schema.org/understanding-json-schema/reference/string#built-in-formats
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
literal_casts: list[Callable[[str], Any]]
|
|
90
|
+
"""A list of conversion callables to try when encountering a literal string value during evaluation.
|
|
91
|
+
|
|
92
|
+
When a literal string value is encountered in a JSON Logic expression, it might be
|
|
93
|
+
beneficial to convert it to a specific Python type.
|
|
94
|
+
|
|
95
|
+
This setting is analogous to the :attr:`~jsonlogic.typechecking.TypecheckSettings.literal_casts`
|
|
96
|
+
configuration of the :class:`~jsonlogic.typechecking.TypecheckSettings` class.
|
|
97
|
+
|
|
98
|
+
Default: :python:`[]` (no cast).
|
|
99
|
+
|
|
100
|
+
.. warning::
|
|
101
|
+
|
|
102
|
+
The order in which the conversion callables are defined matters. Each
|
|
103
|
+
callable will be applied one after the other until no exception is raised.
|
|
104
|
+
"""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from jsonlogic.core import Operator
|
|
6
|
+
from jsonlogic.typing import JSON, JSONLogicPrimitive, OperatorArgument
|
|
7
|
+
|
|
8
|
+
from .evaluation_context import EvaluationContext
|
|
9
|
+
from .evaluation_settings import EvaluationSettingsDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def evaluate(
|
|
13
|
+
operator: Operator, data: JSON, data_schema: dict[str, Any] | None, settings: EvaluationSettingsDict | None = None
|
|
14
|
+
) -> Any:
|
|
15
|
+
"""Helper function to evaluate an :class:`~jsonlogic.core.Operator`.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
operator: The operator to evaluate.
|
|
19
|
+
data: The root data available during evaluation.
|
|
20
|
+
data_schema: The matching JSON Schema describing the root data. This should be the same JSON Schema
|
|
21
|
+
used during typechecking (see :paramref:`~jsonlogic.typechecking.TypecheckContext.root_data_schema`).
|
|
22
|
+
settings: Settings to be used when evaluating an :class:`~jsonlogic.core.Operator`.
|
|
23
|
+
See :class:`EvaluationSettings` for the available settings and default values.
|
|
24
|
+
Returns:
|
|
25
|
+
The evaluated value.
|
|
26
|
+
"""
|
|
27
|
+
context = EvaluationContext(data, data_schema, settings)
|
|
28
|
+
return operator.evaluate(context)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Function analogous to :func:`jsonlogic.json_schema.from_value`
|
|
32
|
+
def _cast_value(value: JSONLogicPrimitive, literal_casts: list[Callable[[str], Any]]) -> Any:
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
for func in literal_casts:
|
|
35
|
+
try:
|
|
36
|
+
casted_value = func(value)
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
else:
|
|
40
|
+
return casted_value
|
|
41
|
+
|
|
42
|
+
if not isinstance(value, list):
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
return [_cast_value(subval, literal_casts) for subval in value]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_value(obj: OperatorArgument, context: EvaluationContext) -> Any:
|
|
49
|
+
"""Get the value of an operator argument.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
obj: the object to evaluate. If this is an :class:`~jsonlogic.core.Operator`,
|
|
53
|
+
it is evaluated and the value is returned. Otherwise, it must be a
|
|
54
|
+
:data:`~jsonlogic.typing.JSONLogicPrimitive`, and the type is inferred from
|
|
55
|
+
the actual value according to the :attr:`~TypecheckSettings.literal_casts` setting.
|
|
56
|
+
context: The typecheck context.
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(obj, Operator):
|
|
59
|
+
return obj.evaluate(context)
|
|
60
|
+
if isinstance(obj, list):
|
|
61
|
+
return [get_value(sub_obj, context) for sub_obj in obj]
|
|
62
|
+
return _cast_value(obj, context.settings.literal_casts)
|