j-perm 0.1.3__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.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.dist-info/METADATA +0 -116
- j_perm-0.1.3.dist-info/RECORD +0 -26
- {j_perm-0.1.3.dist-info → j_perm-0.2.0.dist-info}/top_level.txt +0 -0
j_perm/ops/foreach.py
CHANGED
|
@@ -3,20 +3,19 @@ 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("foreach")
|
|
13
11
|
def op_foreach(
|
|
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
|
"""Iterate over array in source and execute nested actions for each element."""
|
|
19
|
-
arr_ptr = substitute(step["in"], src)
|
|
18
|
+
arr_ptr = engine.substitutor.substitute(step["in"], src)
|
|
20
19
|
|
|
21
20
|
default = copy.deepcopy(step.get("default", []))
|
|
22
21
|
skip_empty = bool(step.get("skip_empty", True))
|
|
@@ -33,7 +32,7 @@ def op_foreach(
|
|
|
33
32
|
arr = list(arr.items())
|
|
34
33
|
|
|
35
34
|
var = step.get("as", "item")
|
|
36
|
-
body = normalize_actions(step["do"])
|
|
35
|
+
body = engine.normalize_actions(step["do"])
|
|
37
36
|
snapshot = copy.deepcopy(dest)
|
|
38
37
|
|
|
39
38
|
try:
|
|
@@ -44,7 +43,7 @@ def op_foreach(
|
|
|
44
43
|
extended = {"_": src}
|
|
45
44
|
|
|
46
45
|
extended[var] = elem
|
|
47
|
-
dest = apply_actions(body, dest=dest, source=extended)
|
|
46
|
+
dest = engine.apply_actions(body, dest=dest, source=extended)
|
|
48
47
|
except Exception:
|
|
49
48
|
dest.clear()
|
|
50
49
|
if isinstance(dest, list):
|
j_perm/ops/replace_root.py
CHANGED
|
@@ -2,15 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
|
|
5
|
-
from ..
|
|
6
|
-
from ..utils.special import resolve_special
|
|
7
|
-
from ..utils.subst import substitute
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
8
6
|
|
|
9
7
|
|
|
10
|
-
@
|
|
11
|
-
def op_replace_root(step, dest, src):
|
|
8
|
+
@OpRegistry.register("replace_root")
|
|
9
|
+
def op_replace_root(step, dest, src, engine):
|
|
12
10
|
"""Replace the whole dest root value with the resolved special value."""
|
|
13
|
-
value =
|
|
11
|
+
value = engine.special.resolve(step["value"], src, engine)
|
|
14
12
|
if isinstance(value, (str, list, dict)):
|
|
15
|
-
value = substitute(value, src)
|
|
13
|
+
value = engine.substitutor.substitute(value, src)
|
|
16
14
|
return copy.deepcopy(value)
|
j_perm/ops/set.py
CHANGED
|
@@ -2,26 +2,25 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Mapping, MutableMapping
|
|
4
4
|
|
|
5
|
-
from ..
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
6
6
|
from ..utils.pointers import jptr_ensure_parent
|
|
7
|
-
from ..utils.special import resolve_special
|
|
8
|
-
from ..utils.subst import substitute
|
|
9
7
|
|
|
10
8
|
|
|
11
|
-
@
|
|
9
|
+
@OpRegistry.register("set")
|
|
12
10
|
def op_set(
|
|
13
11
|
step: dict,
|
|
14
12
|
dest: MutableMapping[str, Any],
|
|
15
13
|
src: Mapping[str, Any],
|
|
14
|
+
engine: "ActionEngine",
|
|
16
15
|
) -> MutableMapping[str, Any]:
|
|
17
16
|
"""Set or append a value at JSON Pointer path in dest."""
|
|
18
|
-
path = substitute(step["path"], src)
|
|
17
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
19
18
|
create = bool(step.get("create", True))
|
|
20
19
|
extend_list = bool(step.get("extend", True))
|
|
21
20
|
|
|
22
|
-
value =
|
|
21
|
+
value = engine.special.resolve(step["value"], src, engine)
|
|
23
22
|
if isinstance(value, (str, list, Mapping)):
|
|
24
|
-
value = substitute(value, src)
|
|
23
|
+
value = engine.substitutor.substitute(value, src)
|
|
25
24
|
|
|
26
25
|
parent, leaf = jptr_ensure_parent(dest, path, create=create)
|
|
27
26
|
|
j_perm/ops/update.py
CHANGED
|
@@ -3,25 +3,24 @@ from __future__ import annotations
|
|
|
3
3
|
import copy
|
|
4
4
|
from typing import MutableMapping, Any, Mapping
|
|
5
5
|
|
|
6
|
-
from ..
|
|
6
|
+
from ..op_handler import OpRegistry
|
|
7
7
|
from ..utils.pointers import maybe_slice, jptr_ensure_parent
|
|
8
|
-
from ..utils.special import resolve_special
|
|
9
|
-
from ..utils.subst import substitute
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
@
|
|
10
|
+
@OpRegistry.register("update")
|
|
13
11
|
def op_update(
|
|
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
|
"""Update a mapping at the given path using a mapping from source or inline value."""
|
|
19
|
-
path = substitute(step["path"], src)
|
|
18
|
+
path = engine.substitutor.substitute(step["path"], src)
|
|
20
19
|
create = bool(step.get("create", True))
|
|
21
20
|
deep = bool(step.get("deep", False))
|
|
22
21
|
|
|
23
22
|
if "from" in step:
|
|
24
|
-
ptr = substitute(step["from"], src)
|
|
23
|
+
ptr = engine.substitutor.substitute(step["from"], src)
|
|
25
24
|
try:
|
|
26
25
|
update_value = copy.deepcopy(maybe_slice(ptr, src))
|
|
27
26
|
except Exception:
|
|
@@ -30,9 +29,9 @@ def op_update(
|
|
|
30
29
|
else:
|
|
31
30
|
raise
|
|
32
31
|
elif "value" in step:
|
|
33
|
-
update_value =
|
|
32
|
+
update_value = engine.special.resolve(step["value"], src, engine)
|
|
34
33
|
if isinstance(update_value, (str, list, Mapping)):
|
|
35
|
-
update_value = substitute(update_value, src)
|
|
34
|
+
update_value = engine.substitutor.substitute(update_value, src)
|
|
36
35
|
else:
|
|
37
36
|
raise ValueError("update operation requires either 'from' or 'value' parameter")
|
|
38
37
|
|
j_perm/schema/__init__.py
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from typing import Any, Dict, List, Mapping
|
|
5
|
-
|
|
6
|
-
from ..engine import normalize_actions
|
|
7
|
-
|
|
8
|
-
_JSON_TYPE = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _split_pointer(p: str) -> List[str]:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _merge(dst: dict, patch: dict) -> None:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def build_schema(spec: Any) -> dict:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
# from __future__ import annotations
|
|
2
|
+
#
|
|
3
|
+
# import re
|
|
4
|
+
# from typing import Any, Dict, List, Mapping
|
|
5
|
+
#
|
|
6
|
+
# from ..engine import normalize_actions
|
|
7
|
+
#
|
|
8
|
+
# _JSON_TYPE = {
|
|
9
|
+
# str: "string",
|
|
10
|
+
# int: "integer",
|
|
11
|
+
# float: "number",
|
|
12
|
+
# bool: "boolean",
|
|
13
|
+
# list: "array",
|
|
14
|
+
# dict: "object",
|
|
15
|
+
# type(None): "null",
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
#
|
|
19
|
+
# def _split_pointer(p: str) -> List[str]:
|
|
20
|
+
# return [t.replace("~1", "/").replace("~0", "~") for t in p.lstrip("/").split("/")]
|
|
21
|
+
#
|
|
22
|
+
#
|
|
23
|
+
# def _merge(dst: dict, patch: dict) -> None:
|
|
24
|
+
# for key, val in patch.items():
|
|
25
|
+
# if isinstance(dst.get(key), dict) and isinstance(val, dict):
|
|
26
|
+
# _merge(dst[key], val)
|
|
27
|
+
# else:
|
|
28
|
+
# dst[key] = val
|
|
29
|
+
#
|
|
30
|
+
#
|
|
31
|
+
# def build_schema(spec: Any) -> dict:
|
|
32
|
+
# """Build a JSON Schema approximation from a DSL script.
|
|
33
|
+
#
|
|
34
|
+
# The algorithm scans all steps that write to paths, infers basic JSON types from
|
|
35
|
+
# literal values and respects replace_root / foreach / $eval nesting.
|
|
36
|
+
# """
|
|
37
|
+
# root: Dict[str, Any] = {
|
|
38
|
+
# "type": "object",
|
|
39
|
+
# "properties": {},
|
|
40
|
+
# "required": [],
|
|
41
|
+
# }
|
|
42
|
+
#
|
|
43
|
+
# steps = normalize_actions(spec)
|
|
44
|
+
#
|
|
45
|
+
# def _wild(pointer: str) -> str:
|
|
46
|
+
# """Replace any ${...} template segments in pointer with '*' to avoid collisions."""
|
|
47
|
+
# return re.sub(r"\$\{[^}]+}", "*", pointer)
|
|
48
|
+
#
|
|
49
|
+
# def _ensure(pointer: str, is_append: bool) -> Dict[str, Any]:
|
|
50
|
+
# """Ensure that the schema tree contains the path described by pointer."""
|
|
51
|
+
# cur = root
|
|
52
|
+
# for tok in _split_pointer(pointer):
|
|
53
|
+
# cur.setdefault("required", [])
|
|
54
|
+
# if tok not in cur["required"]:
|
|
55
|
+
# cur["required"].append(tok)
|
|
56
|
+
#
|
|
57
|
+
# cur = cur.setdefault("properties", {}).setdefault(
|
|
58
|
+
# tok,
|
|
59
|
+
# {"type": "object", "properties": {}},
|
|
60
|
+
# )
|
|
61
|
+
#
|
|
62
|
+
# if is_append:
|
|
63
|
+
# cur["type"] = "array"
|
|
64
|
+
#
|
|
65
|
+
# return cur
|
|
66
|
+
#
|
|
67
|
+
# def _scan_value(val: Any) -> None:
|
|
68
|
+
# if isinstance(val, Mapping):
|
|
69
|
+
# if "$eval" in val:
|
|
70
|
+
# nested = normalize_actions(val["$eval"])
|
|
71
|
+
# _scan(nested)
|
|
72
|
+
#
|
|
73
|
+
# for v in val.values():
|
|
74
|
+
# _scan_value(v)
|
|
75
|
+
#
|
|
76
|
+
# elif isinstance(val, (list, tuple)):
|
|
77
|
+
# for item in val:
|
|
78
|
+
# _scan_value(item)
|
|
79
|
+
#
|
|
80
|
+
# def _scan(items: List[dict]) -> None:
|
|
81
|
+
# for step in items:
|
|
82
|
+
# op = step.get("op")
|
|
83
|
+
#
|
|
84
|
+
# if op == "replace_root":
|
|
85
|
+
# val = step.get("value")
|
|
86
|
+
# if not (isinstance(val, Mapping) and "$ref" in val):
|
|
87
|
+
# root["type"] = _JSON_TYPE.get(type(val), "object")
|
|
88
|
+
# _scan_value(val)
|
|
89
|
+
# continue
|
|
90
|
+
#
|
|
91
|
+
# if "do" in step and isinstance(step["do"], list):
|
|
92
|
+
# _scan(step["do"])
|
|
93
|
+
#
|
|
94
|
+
# if "path" in step:
|
|
95
|
+
# path_raw = step["path"]
|
|
96
|
+
# path = _wild(path_raw)
|
|
97
|
+
#
|
|
98
|
+
# is_append = path.endswith("/-")
|
|
99
|
+
# leaf = _ensure(path[:-2] if is_append else path, is_append)
|
|
100
|
+
#
|
|
101
|
+
# if "value" in step and not isinstance(step["value"], Mapping):
|
|
102
|
+
# leaf["type"] = _JSON_TYPE.get(type(step["value"]), "object")
|
|
103
|
+
#
|
|
104
|
+
# _scan_value(step.get("value"))
|
|
105
|
+
# else:
|
|
106
|
+
# _scan_value(step.get("value"))
|
|
107
|
+
#
|
|
108
|
+
# _scan(steps)
|
|
109
|
+
# return root
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Mapping, MutableMapping
|
|
5
|
+
|
|
6
|
+
# =============================================================================
|
|
7
|
+
# Types
|
|
8
|
+
# =============================================================================
|
|
9
|
+
|
|
10
|
+
SpecialHandler = Callable[[Mapping[str, Any], Mapping[str, Any], "ActionEngine"], Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# Special registry (register constructs one-by-one)
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
class SpecialRegistry:
|
|
18
|
+
"""
|
|
19
|
+
Registry of special constructs keyed by the marker key in a dict,
|
|
20
|
+
e.g. {"$ref": ...}, {"$eval": ...}.
|
|
21
|
+
|
|
22
|
+
Registration is per construct KEY (no "set" wrappers).
|
|
23
|
+
|
|
24
|
+
IMPORTANT:
|
|
25
|
+
If you rely on 'None => all registered', you must ensure modules that call
|
|
26
|
+
SpecialRegistry.register(...) are imported somewhere at startup.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_handlers: MutableMapping[str, SpecialHandler] = {}
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def register(cls, key: str) -> Callable[[SpecialHandler], SpecialHandler]:
|
|
33
|
+
"""
|
|
34
|
+
Decorator to register a special handler under a dict key (e.g. "$ref").
|
|
35
|
+
|
|
36
|
+
By default we raise on duplicates to avoid accidental overrides.
|
|
37
|
+
If you want overrides, change the duplicate check logic.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def decorator(fn: SpecialHandler) -> SpecialHandler:
|
|
41
|
+
if key in cls._handlers:
|
|
42
|
+
raise ValueError(f"special key already registered: {key!r}")
|
|
43
|
+
cls._handlers[key] = fn
|
|
44
|
+
return fn
|
|
45
|
+
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get(cls, key: str) -> SpecialHandler:
|
|
50
|
+
"""Return a registered handler or raise KeyError."""
|
|
51
|
+
return cls._handlers[key]
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def all(cls) -> dict[str, SpecialHandler]:
|
|
55
|
+
"""Return a copy of all registered handlers."""
|
|
56
|
+
return dict(cls._handlers)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# SpecialResolver (configurable)
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
_MISSING = object()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True)
|
|
67
|
+
class SpecialResolver:
|
|
68
|
+
"""
|
|
69
|
+
Walk a value tree and resolve registered special constructs.
|
|
70
|
+
|
|
71
|
+
Configuration:
|
|
72
|
+
- substitutor: used for template expansion inside constructs (e.g. "$ref" value)
|
|
73
|
+
- specials:
|
|
74
|
+
* None -> use all registered keys/handlers from SpecialRegistry
|
|
75
|
+
* list[str] -> use only these keys from registry
|
|
76
|
+
* Mapping[str, SpecialHandler] -> explicit mapping, bypass registry
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
specials: list[str] | Mapping[str, SpecialHandler] | None = None
|
|
80
|
+
|
|
81
|
+
_specials: dict[str, SpecialHandler] = field(init=False)
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
# Build the active special-handler map.
|
|
85
|
+
if self.specials is None:
|
|
86
|
+
self._specials = SpecialRegistry.all()
|
|
87
|
+
elif isinstance(self.specials, Mapping):
|
|
88
|
+
self._specials = dict(self.specials)
|
|
89
|
+
else:
|
|
90
|
+
all_sp = SpecialRegistry.all()
|
|
91
|
+
self._specials = {k: all_sp[k] for k in self.specials}
|
|
92
|
+
|
|
93
|
+
def resolve(self, val: Any, src: Mapping[str, Any], engine: "ActionEngine") -> Any:
|
|
94
|
+
"""
|
|
95
|
+
Resolve special constructs inside an arbitrary value tree.
|
|
96
|
+
|
|
97
|
+
Semantics:
|
|
98
|
+
- If a dict contains a registered special key, that handler "takes over"
|
|
99
|
+
and the whole dict is replaced with the handler result.
|
|
100
|
+
- Otherwise we recurse into dict/list/tuple.
|
|
101
|
+
"""
|
|
102
|
+
if isinstance(val, dict):
|
|
103
|
+
for key, fn in self._specials.items():
|
|
104
|
+
if key in val:
|
|
105
|
+
return fn(val, src, engine)
|
|
106
|
+
|
|
107
|
+
return {k: self.resolve(v, src, engine) for k, v in val.items()}
|
|
108
|
+
|
|
109
|
+
if isinstance(val, list):
|
|
110
|
+
return [self.resolve(x, src, engine) for x in val]
|
|
111
|
+
|
|
112
|
+
if isinstance(val, tuple):
|
|
113
|
+
return tuple(self.resolve(x, src, engine) for x in val)
|
|
114
|
+
|
|
115
|
+
return val
|