j-perm 0.1.3.1__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.
- j_perm/__init__.py +15 -7
- j_perm/casters/__init__.py +4 -0
- j_perm/casters/_bool.py +9 -0
- j_perm/casters/_float.py +8 -0
- j_perm/casters/_int.py +8 -0
- j_perm/casters/_str.py +8 -0
- j_perm/constructs/__init__.py +2 -0
- j_perm/constructs/eval.py +16 -0
- j_perm/constructs/ref.py +21 -0
- j_perm/engine.py +139 -63
- j_perm/funcs/__init__.py +1 -0
- j_perm/funcs/subtract.py +11 -0
- j_perm/op_handler.py +57 -0
- j_perm/ops/__init__.py +1 -0
- j_perm/ops/{assert.py → _assert.py} +4 -4
- j_perm/ops/_exec.py +8 -9
- j_perm/ops/_if.py +8 -9
- j_perm/ops/copy.py +5 -5
- j_perm/ops/copy_d.py +5 -5
- j_perm/ops/delete.py +4 -4
- j_perm/ops/distinct.py +5 -5
- j_perm/ops/foreach.py +6 -7
- j_perm/ops/replace_root.py +5 -7
- j_perm/ops/set.py +6 -7
- j_perm/ops/update.py +7 -8
- j_perm/schema/__init__.py +109 -109
- j_perm/special_resolver.py +115 -0
- j_perm/subst.py +300 -0
- j_perm-0.2.0.dist-info/METADATA +818 -0
- j_perm-0.2.0.dist-info/RECORD +34 -0
- {j_perm-0.1.3.1.dist-info → j_perm-0.2.0.dist-info}/WHEEL +1 -1
- j_perm/jmes_ext.py +0 -17
- j_perm/registry.py +0 -30
- j_perm/utils/special.py +0 -41
- j_perm/utils/subst.py +0 -133
- j_perm/utils/tuples.py +0 -17
- j_perm-0.1.3.1.dist-info/METADATA +0 -766
- j_perm-0.1.3.1.dist-info/RECORD +0 -26
- {j_perm-0.1.3.1.dist-info → j_perm-0.2.0.dist-info}/top_level.txt +0 -0
j_perm/__init__.py
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# Import built-in operations so that they register on import.
|
|
2
|
+
from . import casters as _builtin_casters # noqa: F401
|
|
3
|
+
from . import constructs as _builtin_constructs # noqa: F401
|
|
4
|
+
from . import funcs as _builtin_funcs # noqa: F401
|
|
2
5
|
from . import ops as _builtin_ops # noqa: F401
|
|
3
|
-
from .engine import apply_actions,
|
|
4
|
-
from .
|
|
5
|
-
from .
|
|
6
|
+
from .engine import apply_actions, ActionEngine
|
|
7
|
+
from .subst import JpFuncRegistry, CasterRegistry, TemplateSubstitutor
|
|
8
|
+
from .op_handler import OpRegistry, Handlers
|
|
9
|
+
from .special_resolver import SpecialRegistry, SpecialResolver
|
|
6
10
|
|
|
7
11
|
__all__ = [
|
|
8
|
-
"Handler",
|
|
9
|
-
"register_op",
|
|
10
12
|
"apply_actions",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
+
"ActionEngine",
|
|
14
|
+
"JpFuncRegistry",
|
|
15
|
+
"CasterRegistry",
|
|
16
|
+
"TemplateSubstitutor",
|
|
17
|
+
"OpRegistry",
|
|
18
|
+
"Handlers",
|
|
19
|
+
"SpecialRegistry",
|
|
20
|
+
"SpecialResolver",
|
|
13
21
|
]
|
j_perm/casters/_bool.py
ADDED
j_perm/casters/_float.py
ADDED
j_perm/casters/_int.py
ADDED
j_perm/casters/_str.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Mapping, Any
|
|
4
|
+
|
|
5
|
+
from j_perm.special_resolver import SpecialRegistry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@SpecialRegistry.register("$eval")
|
|
9
|
+
def sp_eval(node: Mapping[str, Any], src: Mapping[str, Any], engine: "ActionEngine") -> Any:
|
|
10
|
+
out = engine.apply_actions(node["$eval"], dest={}, source=src)
|
|
11
|
+
|
|
12
|
+
if "$select" in node:
|
|
13
|
+
sel = resolver.slice(node["$select"], out) # type: ignore[arg-type]
|
|
14
|
+
return sel
|
|
15
|
+
|
|
16
|
+
return out
|
j_perm/constructs/ref.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Mapping, Any
|
|
5
|
+
|
|
6
|
+
from j_perm.special_resolver import _MISSING, SpecialRegistry
|
|
7
|
+
from j_perm.utils.pointers import maybe_slice
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@SpecialRegistry.register("$ref")
|
|
11
|
+
def of_ref(node: Mapping[str, Any], src: Mapping[str, Any], engine: "ActionEngine") -> Any:
|
|
12
|
+
# Expand templates inside "$ref" using configured substitutor
|
|
13
|
+
ptr = engine.substitutor.substitute(node["$ref"], src)
|
|
14
|
+
|
|
15
|
+
dflt = node.get("$default", _MISSING)
|
|
16
|
+
try:
|
|
17
|
+
return copy.deepcopy(maybe_slice(ptr, src))
|
|
18
|
+
except Exception:
|
|
19
|
+
if dflt is not _MISSING:
|
|
20
|
+
return copy.deepcopy(dflt)
|
|
21
|
+
raise
|
j_perm/engine.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
|
-
from
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, List, Mapping, TypeAlias, MutableMapping, Union
|
|
5
6
|
|
|
6
|
-
from .
|
|
7
|
-
from .
|
|
7
|
+
from .op_handler import Handlers
|
|
8
|
+
from .special_resolver import SpecialResolver
|
|
9
|
+
from .subst import TemplateSubstitutor
|
|
8
10
|
|
|
11
|
+
JsonLikeMapping: TypeAlias = MutableMapping[str, Any]
|
|
12
|
+
JsonLikeSource: TypeAlias = Union[Mapping[str, Any], List[Any]]
|
|
13
|
+
JsonLikeDest: TypeAlias = Union[MutableMapping[str, Any], List[Any]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Small utilities
|
|
18
|
+
# =============================================================================
|
|
9
19
|
|
|
10
20
|
def _is_pointer_string(v: Any) -> bool:
|
|
11
21
|
return isinstance(v, str) and v.startswith("/")
|
|
@@ -15,72 +25,138 @@ def _to_list(x: Any) -> List[Any]:
|
|
|
15
25
|
return x if isinstance(x, list) else [x]
|
|
16
26
|
|
|
17
27
|
|
|
18
|
-
def
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
def tuples_to_lists(obj: Any) -> Any:
|
|
29
|
+
"""Recursively convert all tuples into lists so JMESPath indexers work reliably."""
|
|
30
|
+
if isinstance(obj, tuple):
|
|
31
|
+
return [tuples_to_lists(x) for x in obj]
|
|
32
|
+
|
|
33
|
+
if isinstance(obj, list):
|
|
34
|
+
return [tuples_to_lists(x) for x in obj]
|
|
35
|
+
|
|
36
|
+
if isinstance(obj, Mapping):
|
|
37
|
+
return {k: tuples_to_lists(v) for k, v in obj.items()}
|
|
38
|
+
|
|
39
|
+
return obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(slots=True)
|
|
43
|
+
class ActionEngine:
|
|
44
|
+
"""
|
|
45
|
+
Executes a DSL script against dest with a given source context.
|
|
46
|
+
|
|
47
|
+
Configuration:
|
|
48
|
+
- handlers: a Handlers object (has get_handler)
|
|
49
|
+
- special: a SpecialResolver (resolves $ref/$eval/... inside the actions spec, if you want)
|
|
50
|
+
- substitutor: a TemplateSubstitutor (optional: expand ${...} in action specs)
|
|
51
|
+
- resolve_special_in_actions: enable resolving special constructs inside the actions spec
|
|
52
|
+
- substitute_templates_in_actions: enable template expansion inside the actions spec
|
|
53
|
+
|
|
54
|
+
Notes on order:
|
|
55
|
+
1) Optionally substitute templates in 'actions' using 'source' as context
|
|
56
|
+
2) Optionally resolve special constructs in 'actions' using 'source' as context
|
|
57
|
+
3) Normalize to flat step list
|
|
58
|
+
4) Execute handlers
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
handlers: Handlers = field(default_factory=Handlers)
|
|
62
|
+
special: SpecialResolver = field(default_factory=SpecialResolver)
|
|
63
|
+
substitutor: TemplateSubstitutor = field(default_factory=TemplateSubstitutor)
|
|
64
|
+
|
|
65
|
+
resolve_special_in_actions: bool = True
|
|
66
|
+
substitute_templates_in_actions: bool = True
|
|
67
|
+
|
|
68
|
+
def apply_actions(
|
|
69
|
+
self,
|
|
70
|
+
actions: Any,
|
|
71
|
+
*,
|
|
72
|
+
dest: JsonLikeDest,
|
|
73
|
+
source: JsonLikeSource,
|
|
74
|
+
) -> Any:
|
|
75
|
+
"""External API: apply a DSL script."""
|
|
76
|
+
# Work on copies to keep the function referentially safe.
|
|
77
|
+
result = copy.deepcopy(dest)
|
|
78
|
+
|
|
79
|
+
# Normalize actions into a flat list of step dicts.
|
|
80
|
+
steps = self.normalize_actions(actions)
|
|
81
|
+
|
|
82
|
+
# Convert tuples in source to lists.
|
|
83
|
+
source_norm = tuples_to_lists(source)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
for step in steps:
|
|
87
|
+
op = step.get("op")
|
|
88
|
+
if not op:
|
|
89
|
+
raise ValueError(f"Invalid step without 'op': {step!r}")
|
|
90
|
+
|
|
91
|
+
handler = self.handlers.get_handler(op)
|
|
92
|
+
result = handler(step, result, source_norm, self)
|
|
93
|
+
except ValueError:
|
|
94
|
+
raise
|
|
95
|
+
except Exception:
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
return copy.deepcopy(result)
|
|
99
|
+
|
|
100
|
+
# -------------------------------------------------------------------------
|
|
101
|
+
# Normalization (ported from your original code)
|
|
102
|
+
# -------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def normalize_actions(self, spec: Any) -> List[dict]:
|
|
105
|
+
"""Normalize DSL script into a flat list of step dicts."""
|
|
106
|
+
if isinstance(spec, list):
|
|
107
|
+
out: List[dict] = []
|
|
108
|
+
for item in spec:
|
|
109
|
+
if isinstance(item, Mapping) and "op" not in item:
|
|
110
|
+
out.extend(self._expand_shorthand(item))
|
|
111
|
+
else:
|
|
112
|
+
out.append(item)
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
if isinstance(spec, Mapping):
|
|
116
|
+
return self._expand_shorthand(spec)
|
|
117
|
+
|
|
118
|
+
raise TypeError("spec must be dict or list")
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _expand_shorthand(obj: Mapping[str, Any]) -> List[dict]:
|
|
122
|
+
"""Expand shorthand mapping form into explicit op steps."""
|
|
123
|
+
steps: List[dict] = []
|
|
124
|
+
|
|
125
|
+
for key, val in obj.items():
|
|
126
|
+
if key == "~delete":
|
|
33
127
|
for p in _to_list(val):
|
|
34
|
-
steps.append({"op": "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
out: List[dict] = []
|
|
52
|
-
for item in spec:
|
|
53
|
-
if isinstance(item, Mapping) and "op" not in item:
|
|
54
|
-
out.extend(_expand_shorthand(item))
|
|
128
|
+
steps.append({"op": "delete", "path": p})
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if key == "~assert":
|
|
132
|
+
if isinstance(val, Mapping):
|
|
133
|
+
for p, eq in val.items():
|
|
134
|
+
steps.append({"op": "assert", "path": p, "equals": eq})
|
|
135
|
+
else:
|
|
136
|
+
for p in _to_list(val):
|
|
137
|
+
steps.append({"op": "assert", "path": p})
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
append = isinstance(key, str) and key.endswith("[]")
|
|
141
|
+
dst = f"{key[:-2]}/-" if append else key
|
|
142
|
+
|
|
143
|
+
if _is_pointer_string(val):
|
|
144
|
+
steps.append({"op": "copy", "from": val, "path": dst, "ignore_missing": True})
|
|
55
145
|
else:
|
|
56
|
-
|
|
57
|
-
|
|
146
|
+
steps.append({"op": "set", "path": dst, "value": val})
|
|
147
|
+
|
|
148
|
+
return steps
|
|
58
149
|
|
|
59
|
-
if isinstance(spec, Mapping):
|
|
60
|
-
return _expand_shorthand(spec)
|
|
61
150
|
|
|
62
|
-
|
|
151
|
+
# Create a default engine instance for convenience
|
|
152
|
+
default_engine = ActionEngine()
|
|
63
153
|
|
|
64
154
|
|
|
65
155
|
def apply_actions(
|
|
66
156
|
actions: Any,
|
|
67
157
|
*,
|
|
68
|
-
dest:
|
|
69
|
-
source:
|
|
70
|
-
) ->
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
result = copy.deepcopy(dest)
|
|
74
|
-
|
|
75
|
-
source = tuples_to_lists(source)
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
for step in steps:
|
|
79
|
-
handler = get_handler(step["op"])
|
|
80
|
-
result = handler(step, result, source) # type: ignore[arg-type]
|
|
81
|
-
except ValueError:
|
|
82
|
-
raise
|
|
83
|
-
except Exception:
|
|
84
|
-
raise
|
|
85
|
-
|
|
86
|
-
return copy.deepcopy(result)
|
|
158
|
+
dest: JsonLikeDest,
|
|
159
|
+
source: JsonLikeSource,
|
|
160
|
+
) -> Any:
|
|
161
|
+
"""Convenience function: apply a DSL script using the default engine."""
|
|
162
|
+
return default_engine.apply_actions(actions, dest=dest, source=source)
|
j_perm/funcs/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .subtract import subtract
|
j_perm/funcs/subtract.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from jmespath import functions as _jp_funcs
|
|
4
|
+
|
|
5
|
+
from j_perm.subst import JpFuncRegistry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@JpFuncRegistry.register("subtract")
|
|
9
|
+
@_jp_funcs.signature({"types": ["number"]}, {"types": ["number"]})
|
|
10
|
+
def subtract(self, a: float, b: float) -> float:
|
|
11
|
+
return a - b
|
j_perm/op_handler.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Mapping, Any, Callable
|
|
3
|
+
|
|
4
|
+
OpHandler = Callable[[dict, Any, Any, "ActionEngine"], Any]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OpRegistry:
|
|
8
|
+
_ops: dict[str, OpHandler] = {}
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def register(cls, name: str) -> Callable[[OpHandler], OpHandler]:
|
|
12
|
+
def deco(fn: OpHandler) -> OpHandler:
|
|
13
|
+
if name in cls._ops:
|
|
14
|
+
raise ValueError(f"op already registered: {name!r}")
|
|
15
|
+
cls._ops[name] = fn
|
|
16
|
+
return fn
|
|
17
|
+
|
|
18
|
+
return deco
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def get(cls, name: str) -> OpHandler:
|
|
22
|
+
try:
|
|
23
|
+
return cls._ops[name]
|
|
24
|
+
except KeyError:
|
|
25
|
+
raise ValueError(f"Unknown op {name!r}") from None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def all(cls) -> dict[str, OpHandler]:
|
|
29
|
+
return dict(cls._ops)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class Handlers:
|
|
34
|
+
"""
|
|
35
|
+
If ops is None -> use all registered.
|
|
36
|
+
If ops is list[str] -> use only those names.
|
|
37
|
+
If ops is Mapping[str, handler] -> use that explicit mapping (no registry).
|
|
38
|
+
"""
|
|
39
|
+
ops: list[str] | Mapping[str, OpHandler] | None = None
|
|
40
|
+
on_conflict: str = "error" # "error" | "override"
|
|
41
|
+
_ops: dict[str, OpHandler] = field(init=False, repr=False)
|
|
42
|
+
|
|
43
|
+
def get_handler(self, name: str) -> OpHandler:
|
|
44
|
+
return self._ops[name]
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
if self.ops is None:
|
|
48
|
+
ops_map = OpRegistry.all()
|
|
49
|
+
elif isinstance(self.ops, Mapping):
|
|
50
|
+
ops_map = dict(self.ops)
|
|
51
|
+
else:
|
|
52
|
+
all_ops = OpRegistry.all()
|
|
53
|
+
ops_map = {}
|
|
54
|
+
for n in self.ops:
|
|
55
|
+
ops_map[n] = all_ops[n]
|
|
56
|
+
|
|
57
|
+
self._ops = ops_map
|
j_perm/ops/__init__.py
CHANGED
|
@@ -2,19 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import MutableMapping, Any, Mapping
|
|
4
4
|
|
|
5
|
-
from ..
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
6
6
|
from ..utils.pointers import jptr_get
|
|
7
|
-
from ..utils.subst import substitute
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
@
|
|
9
|
+
@OpRegistry.register("assert")
|
|
11
10
|
def op_assert(
|
|
12
11
|
step: dict,
|
|
13
12
|
dest: MutableMapping[str, Any],
|
|
14
13
|
src: Mapping[str, Any],
|
|
14
|
+
engine: "ActionEngine",
|
|
15
15
|
) -> MutableMapping[str, Any]:
|
|
16
16
|
"""Assert node existence and/or value at JSON Pointer path in dest."""
|
|
17
|
-
path = substitute(step["path"], src)
|
|
17
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
20
|
current = jptr_get(dest, path)
|
j_perm/ops/_exec.py
CHANGED
|
@@ -2,18 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import MutableMapping, Any, Mapping
|
|
4
4
|
|
|
5
|
-
from ..
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
6
6
|
from ..engine import apply_actions
|
|
7
7
|
from ..utils.pointers import maybe_slice
|
|
8
|
-
from ..utils.special import resolve_special
|
|
9
|
-
from ..utils.subst import substitute
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
@
|
|
10
|
+
@OpRegistry.register("exec")
|
|
13
11
|
def op_exec(
|
|
14
12
|
step: dict,
|
|
15
13
|
dest: MutableMapping[str, Any],
|
|
16
14
|
src: Mapping[str, Any],
|
|
15
|
+
engine: "ActionEngine",
|
|
17
16
|
) -> MutableMapping[str, Any]:
|
|
18
17
|
"""Execute nested actions stored either in source or inline in the op."""
|
|
19
18
|
has_from = "from" in step
|
|
@@ -26,20 +25,20 @@ def op_exec(
|
|
|
26
25
|
raise ValueError("exec operation requires either 'from' or 'actions' parameter")
|
|
27
26
|
|
|
28
27
|
if has_from:
|
|
29
|
-
actions_ptr = substitute(step["from"], src)
|
|
28
|
+
actions_ptr = engine.substitutor.substitute(step["from"], src)
|
|
30
29
|
try:
|
|
31
30
|
actions = maybe_slice(actions_ptr, src)
|
|
32
31
|
except Exception:
|
|
33
32
|
if "default" in step:
|
|
34
|
-
actions =
|
|
33
|
+
actions = engine.special.resolve(step["default"], src, engine)
|
|
35
34
|
if isinstance(actions, (str, list, dict)):
|
|
36
|
-
actions = substitute(actions, src)
|
|
35
|
+
actions = engine.substitutor.substitute(actions, src)
|
|
37
36
|
else:
|
|
38
37
|
raise ValueError(f"Cannot find actions at {actions_ptr}")
|
|
39
38
|
else:
|
|
40
|
-
actions =
|
|
39
|
+
actions = engine.special.resolve(step["actions"], src, engine)
|
|
41
40
|
if isinstance(actions, (str, list, dict)):
|
|
42
|
-
actions = substitute(actions, src)
|
|
41
|
+
actions = engine.substitutor.substitute(actions, src)
|
|
43
42
|
|
|
44
43
|
merge = bool(step.get("merge", False))
|
|
45
44
|
|
j_perm/ops/_if.py
CHANGED
|
@@ -3,22 +3,21 @@ from __future__ import annotations
|
|
|
3
3
|
import copy
|
|
4
4
|
from typing import MutableMapping, Any, Mapping
|
|
5
5
|
|
|
6
|
-
from ..
|
|
7
|
-
from ..engine import normalize_actions, apply_actions
|
|
6
|
+
from ..op_handler import OpRegistry
|
|
8
7
|
from ..utils.pointers import maybe_slice
|
|
9
|
-
from ..utils.subst import substitute
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
@
|
|
10
|
+
@OpRegistry.register("if")
|
|
13
11
|
def op_if(
|
|
14
12
|
step: dict,
|
|
15
13
|
dest: MutableMapping[str, Any],
|
|
16
14
|
src: Mapping[str, Any] | list,
|
|
15
|
+
engine: "ActionEngine",
|
|
17
16
|
) -> MutableMapping[str, Any]:
|
|
18
17
|
"""Conditionally execute nested actions based on a path or expression."""
|
|
19
18
|
if "path" in step:
|
|
20
19
|
try:
|
|
21
|
-
ptr = substitute(step["path"], src)
|
|
20
|
+
ptr = engine.substitutor.substitute(step["path"], src)
|
|
22
21
|
current = maybe_slice(ptr, dest)
|
|
23
22
|
missing = False
|
|
24
23
|
except Exception:
|
|
@@ -26,14 +25,14 @@ def op_if(
|
|
|
26
25
|
missing = True
|
|
27
26
|
|
|
28
27
|
if "equals" in step:
|
|
29
|
-
expected = substitute(step["equals"], src)
|
|
28
|
+
expected = engine.substitutor.substitute(step["equals"], src)
|
|
30
29
|
cond_val = current == expected and not missing
|
|
31
30
|
elif step.get("exists"):
|
|
32
31
|
cond_val = not missing
|
|
33
32
|
else:
|
|
34
33
|
cond_val = bool(current) and not missing
|
|
35
34
|
else:
|
|
36
|
-
raw_cond = substitute(step.get("cond"), src)
|
|
35
|
+
raw_cond = engine.substitutor.substitute(step.get("cond"), src)
|
|
37
36
|
cond_val = bool(raw_cond)
|
|
38
37
|
|
|
39
38
|
branch_key = "then" if cond_val else "else"
|
|
@@ -43,11 +42,11 @@ def op_if(
|
|
|
43
42
|
if not actions:
|
|
44
43
|
return dest
|
|
45
44
|
|
|
46
|
-
actions_norm = normalize_actions(actions)
|
|
45
|
+
actions_norm = engine.normalize_actions(actions)
|
|
47
46
|
snapshot = copy.deepcopy(dest)
|
|
48
47
|
|
|
49
48
|
try:
|
|
50
|
-
return apply_actions(actions_norm, dest=dest, source=src)
|
|
49
|
+
return engine.apply_actions(actions_norm, dest=dest, source=src)
|
|
51
50
|
except Exception:
|
|
52
51
|
dest.clear()
|
|
53
52
|
dest.update(snapshot)
|
j_perm/ops/copy.py
CHANGED
|
@@ -4,23 +4,23 @@ import copy
|
|
|
4
4
|
from typing import MutableMapping, Any, Mapping
|
|
5
5
|
|
|
6
6
|
from .set import op_set
|
|
7
|
-
from ..
|
|
7
|
+
from ..op_handler import OpRegistry
|
|
8
8
|
from ..utils.pointers import maybe_slice
|
|
9
|
-
from ..utils.subst import substitute
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@
|
|
11
|
+
@OpRegistry.register("copy")
|
|
13
12
|
def op_copy(
|
|
14
13
|
step: dict,
|
|
15
14
|
dest: MutableMapping[str, Any],
|
|
16
15
|
src: Mapping[str, Any],
|
|
16
|
+
engine: "ActionEngine",
|
|
17
17
|
) -> MutableMapping[str, Any]:
|
|
18
18
|
"""Copy value from source pointer into dest path."""
|
|
19
|
-
path = substitute(step["path"], src)
|
|
19
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
20
20
|
create = bool(step.get("create", True))
|
|
21
21
|
extend_list = bool(step.get("extend", True))
|
|
22
22
|
|
|
23
|
-
ptr = substitute(step["from"], src)
|
|
23
|
+
ptr = engine.substitutor.substitute(step["from"], src)
|
|
24
24
|
ignore = bool(step.get("ignore_missing", False))
|
|
25
25
|
|
|
26
26
|
try:
|
j_perm/ops/copy_d.py
CHANGED
|
@@ -4,22 +4,22 @@ import copy
|
|
|
4
4
|
from typing import MutableMapping, Any, Mapping
|
|
5
5
|
|
|
6
6
|
from .set import op_set
|
|
7
|
-
from ..
|
|
7
|
+
from ..op_handler import OpRegistry
|
|
8
8
|
from ..utils.pointers import maybe_slice
|
|
9
|
-
from ..utils.subst import substitute
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@
|
|
11
|
+
@OpRegistry.register("copyD")
|
|
13
12
|
def op_copy_d(
|
|
14
13
|
step: dict,
|
|
15
14
|
dest: MutableMapping[str, Any],
|
|
16
15
|
src: Mapping[str, Any],
|
|
16
|
+
engine: "ActionEngine",
|
|
17
17
|
) -> MutableMapping[str, Any]:
|
|
18
18
|
"""Copy value from dest (self) into another dest path."""
|
|
19
|
-
path = substitute(step["path"], src)
|
|
19
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
20
20
|
create = bool(step.get("create", True))
|
|
21
21
|
|
|
22
|
-
ptr = substitute(step["from"], dest)
|
|
22
|
+
ptr = engine.substitutor.substitute(step["from"], dest)
|
|
23
23
|
ignore = bool(step.get("ignore_missing", False))
|
|
24
24
|
|
|
25
25
|
try:
|
j_perm/ops/delete.py
CHANGED
|
@@ -2,19 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import MutableMapping, Any, Mapping
|
|
4
4
|
|
|
5
|
-
from ..
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
6
6
|
from ..utils.pointers import jptr_ensure_parent
|
|
7
|
-
from ..utils.subst import substitute
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
@
|
|
9
|
+
@OpRegistry.register("delete")
|
|
11
10
|
def op_delete(
|
|
12
11
|
step: dict,
|
|
13
12
|
dest: MutableMapping[str, Any],
|
|
14
13
|
src: Mapping[str, Any],
|
|
14
|
+
engine: "ActionEngine",
|
|
15
15
|
) -> MutableMapping[str, Any]:
|
|
16
16
|
"""Delete node at the given JSON Pointer path in dest."""
|
|
17
|
-
path = substitute(step["path"], src)
|
|
17
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
18
18
|
ignore = bool(step.get("ignore_missing", True))
|
|
19
19
|
|
|
20
20
|
try:
|
j_perm/ops/distinct.py
CHANGED
|
@@ -2,26 +2,26 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import MutableMapping, Any, Mapping
|
|
4
4
|
|
|
5
|
-
from ..
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
6
6
|
from ..utils.pointers import jptr_get
|
|
7
|
-
from ..utils.subst import substitute
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
@
|
|
9
|
+
@OpRegistry.register("distinct")
|
|
11
10
|
def op_distinct(
|
|
12
11
|
step: dict,
|
|
13
12
|
dest: MutableMapping[str, Any],
|
|
14
13
|
src: Mapping[str, Any],
|
|
14
|
+
engine: "ActionEngine",
|
|
15
15
|
) -> MutableMapping[str, Any]:
|
|
16
16
|
"""Remove duplicates from a list at the given path, preserving order."""
|
|
17
|
-
path = substitute(step["path"], src)
|
|
17
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
18
18
|
lst = jptr_get(dest, path)
|
|
19
19
|
|
|
20
20
|
if not isinstance(lst, list):
|
|
21
21
|
raise TypeError(f"{path} is not a list (distinct)")
|
|
22
22
|
|
|
23
23
|
key = step.get("key", None)
|
|
24
|
-
key_path = substitute(key, src)
|
|
24
|
+
key_path = engine.substitutor.substitute(key, src)
|
|
25
25
|
|
|
26
26
|
seen = set()
|
|
27
27
|
unique = []
|