PySerials 0.0.0.dev61__tar.gz → 0.0.0.dev63__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.
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/PKG-INFO +3 -3
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/pyproject.toml +3 -3
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/PySerials.egg-info/PKG-INFO +3 -3
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/PySerials.egg-info/requires.txt +2 -2
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/exception/update.py +2 -2
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/nested_dict.py +2 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/update.py +271 -57
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/write.py +23 -5
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/setup.cfg +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/PySerials.egg-info/SOURCES.txt +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/PySerials.egg-info/dependency_links.txt +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/PySerials.egg-info/not-zip-safe +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/PySerials.egg-info/top_level.txt +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/__init__.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/compare.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/exception/__init__.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/exception/_base.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/exception/read.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/exception/validate.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/format.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/property_dict.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/read.py +0 -0
- {pyserials-0.0.0.dev61 → pyserials-0.0.0.dev63}/src/pyserials/validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: PySerials
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev63
|
|
4
4
|
Requires-Python: >=3.10
|
|
5
5
|
Requires-Dist: jsonschema<5,>=4.21.0
|
|
6
6
|
Requires-Dist: referencing>=0.35.1
|
|
@@ -8,6 +8,6 @@ Requires-Dist: jsonpath-ng<2,>=1.6.1
|
|
|
8
8
|
Requires-Dist: ruamel.yaml>=0.18
|
|
9
9
|
Requires-Dist: ruamel.yaml.string<1,>=0.1.1
|
|
10
10
|
Requires-Dist: tomlkit<0.14,>=0.11.8
|
|
11
|
-
Requires-Dist: MDit==0.0.0.
|
|
12
|
-
Requires-Dist: ExceptionMan==0.0.0.
|
|
11
|
+
Requires-Dist: MDit==0.0.0.dev60
|
|
12
|
+
Requires-Dist: ExceptionMan==0.0.0.dev60
|
|
13
13
|
Requires-Dist: ProtocolMan==0.0.0.dev2
|
|
@@ -17,7 +17,7 @@ namespaces = true
|
|
|
17
17
|
# ----------------------------------------- Project Metadata -------------------------------------
|
|
18
18
|
#
|
|
19
19
|
[project]
|
|
20
|
-
version = "0.0.0.
|
|
20
|
+
version = "0.0.0.dev63"
|
|
21
21
|
name = "PySerials"
|
|
22
22
|
dependencies = [
|
|
23
23
|
"jsonschema >= 4.21.0, < 5",
|
|
@@ -26,8 +26,8 @@ dependencies = [
|
|
|
26
26
|
"ruamel.yaml >= 0.18", # https://yaml.readthedocs.io/en/stable/
|
|
27
27
|
"ruamel.yaml.string >= 0.1.1, < 1",
|
|
28
28
|
"tomlkit >= 0.11.8, < 0.14", # https://tomlkit.readthedocs.io/en/stable/,
|
|
29
|
-
"MDit == 0.0.0.
|
|
30
|
-
"ExceptionMan == 0.0.0.
|
|
29
|
+
"MDit == 0.0.0.dev60",
|
|
30
|
+
"ExceptionMan == 0.0.0.dev60",
|
|
31
31
|
"ProtocolMan == 0.0.0.dev2",
|
|
32
32
|
]
|
|
33
33
|
requires-python = ">=3.10"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: PySerials
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev63
|
|
4
4
|
Requires-Python: >=3.10
|
|
5
5
|
Requires-Dist: jsonschema<5,>=4.21.0
|
|
6
6
|
Requires-Dist: referencing>=0.35.1
|
|
@@ -8,6 +8,6 @@ Requires-Dist: jsonpath-ng<2,>=1.6.1
|
|
|
8
8
|
Requires-Dist: ruamel.yaml>=0.18
|
|
9
9
|
Requires-Dist: ruamel.yaml.string<1,>=0.1.1
|
|
10
10
|
Requires-Dist: tomlkit<0.14,>=0.11.8
|
|
11
|
-
Requires-Dist: MDit==0.0.0.
|
|
12
|
-
Requires-Dist: ExceptionMan==0.0.0.
|
|
11
|
+
Requires-Dist: MDit==0.0.0.dev60
|
|
12
|
+
Requires-Dist: ExceptionMan==0.0.0.dev60
|
|
13
13
|
Requires-Dist: ProtocolMan==0.0.0.dev2
|
|
@@ -49,8 +49,8 @@ class PySerialsUpdateException(_base.PySerialsException):
|
|
|
49
49
|
return
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
class
|
|
53
|
-
"""Base class for all exceptions raised by `pyserials.update.
|
|
52
|
+
class PySerialsUpdateRecursiveDataError(PySerialsUpdateException):
|
|
53
|
+
"""Base class for all exceptions raised by `pyserials.update.recursive_update`.
|
|
54
54
|
|
|
55
55
|
Attributes
|
|
56
56
|
----------
|
|
@@ -36,6 +36,7 @@ class NestedDict:
|
|
|
36
36
|
relative_key_key: str | None = None,
|
|
37
37
|
implicit_root: bool = True,
|
|
38
38
|
getter_function_name: str = "get",
|
|
39
|
+
skip_key_func: Callable[[list[str]], bool] | None = None,
|
|
39
40
|
):
|
|
40
41
|
self._data = data or {}
|
|
41
42
|
self._templater = _ps.update.TemplateFiller(
|
|
@@ -62,6 +63,7 @@ class NestedDict:
|
|
|
62
63
|
relative_key_key=relative_key_key,
|
|
63
64
|
implicit_root=implicit_root,
|
|
64
65
|
getter_function_name=getter_function_name,
|
|
66
|
+
skip_key_func=skip_key_func,
|
|
65
67
|
)
|
|
66
68
|
return
|
|
67
69
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
3
|
from typing import TYPE_CHECKING as _TYPE_CHECKING
|
|
5
4
|
import re as _re
|
|
6
5
|
from functools import partial as _partial
|
|
@@ -11,69 +10,261 @@ from jsonpath_ng import exceptions as _jsonpath_exceptions
|
|
|
11
10
|
import pyserials.exception as _exception
|
|
12
11
|
|
|
13
12
|
if _TYPE_CHECKING:
|
|
14
|
-
from typing import Literal, Sequence, Any, Callable
|
|
13
|
+
from typing import Literal, Sequence, Any, Callable, Iterable
|
|
14
|
+
UPDATE_OPTIONS = Literal["skip", "write", "raise"] | Callable[
|
|
15
|
+
[tuple[Any] | tuple[Any, Any]], None | tuple[Any, str]
|
|
16
|
+
]
|
|
17
|
+
FUNC_ITEMS = Callable[[Any], Iterable[tuple[Any, Any]]]
|
|
18
|
+
FUNC_CONTAINS = Callable[[Any, Any], bool]
|
|
19
|
+
FUNC_GET = Callable[[Any, Any], Any]
|
|
20
|
+
FUNC_SET = Callable[[Any, Any, Any], None]
|
|
21
|
+
FUNC_CONSTRUCT = Callable[[], Any]
|
|
22
|
+
RECURSIVE_DTYPE_FUNCS = tuple[FUNC_ITEMS, FUNC_CONTAINS, FUNC_GET, FUNC_SET, FUNC_CONSTRUCT]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def recursive_update(
|
|
26
|
+
source: Any,
|
|
27
|
+
addon: Any,
|
|
28
|
+
recursive_types: dict[type | tuple[type, ...], RECURSIVE_DTYPE_FUNCS] | None = None,
|
|
29
|
+
types: dict[type | tuple[type, ...], UPDATE_OPTIONS | tuple[UPDATE_OPTIONS, UPDATE_OPTIONS]] | None = None,
|
|
30
|
+
paths: dict[str, UPDATE_OPTIONS] | None = None,
|
|
31
|
+
constructor: Callable[[], Any] | None = None,
|
|
32
|
+
undefined_new: UPDATE_OPTIONS = "write",
|
|
33
|
+
undefined_existing: UPDATE_OPTIONS = "skip",
|
|
34
|
+
type_mismatch: UPDATE_OPTIONS = "raise",
|
|
35
|
+
) -> dict[str, list[str]]:
|
|
36
|
+
"""Recursively update a complex data structure using another data structure.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
source
|
|
41
|
+
Data structure to update in place.
|
|
42
|
+
The type of this object must be
|
|
43
|
+
defined in the `recursive_types` argument.
|
|
44
|
+
addon
|
|
45
|
+
Data structure containing additional data to update `source` with.
|
|
46
|
+
recursive_types
|
|
47
|
+
Definition of recursive data types.
|
|
48
|
+
Each key is a type (or tuple of types) used
|
|
49
|
+
to identify data types with the `isinstance` function.
|
|
50
|
+
Each value is a tuple of three functions:
|
|
51
|
+
1. Function to extract items from the data. It must accept an instance
|
|
52
|
+
of the type and return an iterable of key-value pairs.
|
|
53
|
+
2. Function to check if a key is in the data. It must accept an instance
|
|
54
|
+
of the type and a key, and return a boolean.
|
|
55
|
+
3. Function to get a value from the data. It must accept an instance
|
|
56
|
+
of the type and a key, and return the value.
|
|
57
|
+
4. Function to set a key-value pair in the data. It must accept an instance
|
|
58
|
+
of the type, a key, and a value, respectively.
|
|
59
|
+
5. Function to construct a new instance of the type. It must accept no arguments.
|
|
60
|
+
|
|
61
|
+
By default, `dict` is defined as follows:
|
|
62
|
+
```python
|
|
63
|
+
recursive_types = {
|
|
64
|
+
dict: (
|
|
65
|
+
lambda dic: dic.items(),
|
|
66
|
+
lambda dic, key: key in dic,
|
|
67
|
+
lambda dic, key: dic[key],
|
|
68
|
+
lambda dic, key, value: dic.update({key: value}),
|
|
69
|
+
lambda: dict(),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
This default argument will be updated/overwritten with any custom types provided.
|
|
74
|
+
For example, to add support for a custom class `MyClass`, you can use:
|
|
75
|
+
```python
|
|
76
|
+
recursive_types = {
|
|
77
|
+
MyClass: (
|
|
78
|
+
lambda obj: vars(object).items(),
|
|
79
|
+
lambda obj, key: hasattr(obj, key),
|
|
80
|
+
lambda obj, key: getattr(obj, key),
|
|
81
|
+
lambda obj, key, value: setattr(obj, key, value),
|
|
82
|
+
lambda: MyClass(),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
types
|
|
87
|
+
Update behavior for specific data types.
|
|
88
|
+
Each key is a type (or tuple of types) used
|
|
89
|
+
to identify data types with the `isinstance` function.
|
|
90
|
+
Each value is can be single update option for all cases,
|
|
91
|
+
or a tuple of two update options for when the corresponding
|
|
92
|
+
key/attribute does not exist in the source data, and when it does, respectively.
|
|
93
|
+
Each update option can be either a keyword specifying a predefined behavior,
|
|
94
|
+
or a function for custom behavior. The available keywords are:
|
|
95
|
+
- "skip": Ignore the key/attribute; do not change anything.
|
|
96
|
+
- "write": Write the key/attribute in source data with the value from the addon, overwriting if it exists.
|
|
97
|
+
|
|
98
|
+
A custom function must accept a single argument, a tuple of either one or two values.
|
|
99
|
+
If it is two values, the first value is the current value in the source data,
|
|
100
|
+
and the second value is the value from the addon.
|
|
101
|
+
If it is one value, the value is the value from the addon,
|
|
102
|
+
meaning the key/attribute does not exist in the source data.
|
|
103
|
+
The function must either return `None` (for when nothing must be changes in the source)
|
|
104
|
+
or a tuple of two values:
|
|
105
|
+
1. The new value to write in the source data.
|
|
106
|
+
2. A string specifying the change type.
|
|
107
|
+
|
|
108
|
+
By default, the following behavior is defined for basic types:
|
|
109
|
+
```python
|
|
110
|
+
types = {list: ("write", lambda data: (data[0] + data[1], "append"))}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The default behavior for any data type not specified in this argument
|
|
114
|
+
is determined by the `undefined_new` and `undefined_existing` arguments.
|
|
115
|
+
paths
|
|
116
|
+
Update behavior for specific keys using JSONPath expressions.
|
|
117
|
+
This is the same as the `types` argument, but targeting specific keys
|
|
118
|
+
instead of data types.
|
|
119
|
+
Everything is the same as in the `types` argument, except that the keys
|
|
120
|
+
are JSONPath expressions as strings.
|
|
121
|
+
constructor
|
|
122
|
+
Custom constructor for creating new instances of the source data type.
|
|
123
|
+
This is used when the addon data contains a recursive key/attribute
|
|
124
|
+
that is not present in the source data. If not provided,
|
|
125
|
+
the type of the addon value will be used to create a new instance.
|
|
126
|
+
undefined_new
|
|
127
|
+
Behavior for when a non-recursive data with no defined behavior
|
|
128
|
+
in the `types` argument is found in the addon data
|
|
129
|
+
but not in the source data.
|
|
130
|
+
undefined_existing
|
|
131
|
+
Behavior for when a non-recursive data with no defined behavior
|
|
132
|
+
in the `types` argument is found in the addon data
|
|
133
|
+
and in the source data.
|
|
134
|
+
type_mismatch
|
|
135
|
+
Behavior for when a key/attribute in the source data
|
|
136
|
+
is not a recursive type, but the corresponding key/attribute
|
|
137
|
+
in the addon data is a recursive type.
|
|
138
|
+
"""
|
|
139
|
+
def get_funcs(data: Any) -> RECURSIVE_DTYPE_FUNCS | None:
|
|
140
|
+
for typ, funcs in recursive_types.items():
|
|
141
|
+
if isinstance(data, typ):
|
|
142
|
+
return funcs
|
|
143
|
+
return None
|
|
15
144
|
|
|
145
|
+
def recursive(
|
|
146
|
+
src: Any,
|
|
147
|
+
add: Any,
|
|
148
|
+
src_funcs: RECURSIVE_DTYPE_FUNCS,
|
|
149
|
+
add_funcs: RECURSIVE_DTYPE_FUNCS,
|
|
150
|
+
path: str,
|
|
151
|
+
):
|
|
16
152
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
153
|
+
def apply(behavior: UPDATE_OPTIONS | tuple[UPDATE_OPTIONS, UPDATE_OPTIONS]):
|
|
154
|
+
action = behavior[int(key_exists_in_src)] if isinstance(behavior, tuple) else behavior
|
|
155
|
+
if action == "raise":
|
|
156
|
+
raise_error(typ="duplicate")
|
|
157
|
+
elif action == "skip":
|
|
158
|
+
change_type = "skip"
|
|
159
|
+
elif action == "write":
|
|
160
|
+
change_type = "write"
|
|
161
|
+
fn_src_set(src, key, value)
|
|
162
|
+
elif not isinstance(action, str):
|
|
163
|
+
out = action((source_value, value) if key_exists_in_src else (value,))
|
|
164
|
+
if out:
|
|
165
|
+
new_value, change_type = out
|
|
166
|
+
fn_src_set(src, key, new_value)
|
|
167
|
+
else:
|
|
168
|
+
change_type = "skip"
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Invalid update behavior '{action}' for key '{key}' at path '{path}'.")
|
|
171
|
+
log[fullpath] = (type(source_value) if key_exists_in_src else None, type(value), change_type)
|
|
172
|
+
return
|
|
27
173
|
|
|
28
174
|
def raise_error(typ: Literal["duplicate", "type_mismatch"]):
|
|
29
|
-
raise _exception.update.
|
|
175
|
+
raise _exception.update.PySerialsUpdateRecursiveDataError(
|
|
30
176
|
problem_type=typ,
|
|
31
177
|
path=fullpath,
|
|
32
|
-
data=
|
|
33
|
-
data_full=
|
|
178
|
+
data=src[key],
|
|
179
|
+
data_full=src,
|
|
34
180
|
data_addon=value,
|
|
35
181
|
data_addon_full=addon,
|
|
36
182
|
)
|
|
37
183
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
appended = False
|
|
55
|
-
for elem in value:
|
|
56
|
-
if elem not in source[key]:
|
|
57
|
-
source[key].append(elem)
|
|
58
|
-
appended = True
|
|
59
|
-
if appended:
|
|
60
|
-
log["list_appended"].append(fullpath)
|
|
61
|
-
elif raise_duplicates:
|
|
62
|
-
raise_error(typ="duplicate")
|
|
63
|
-
else:
|
|
64
|
-
log["skipped"].append(fullpath)
|
|
184
|
+
_, fn_src_contains, fn_src_get, fn_src_set, _ = src_funcs
|
|
185
|
+
fn_add_items, _, _, _, fn_add_construct = add_funcs
|
|
186
|
+
|
|
187
|
+
for key, value in fn_add_items(add):
|
|
188
|
+
fullpath = f"{path}.'{key}'"
|
|
189
|
+
try:
|
|
190
|
+
full_jpath = _jsonpath.parse(fullpath) # quote to avoid JSONPath syntax errors
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(fullpath)
|
|
193
|
+
raise e
|
|
194
|
+
key_exists_in_src = fn_src_contains(src, key)
|
|
195
|
+
source_value = fn_src_get(src, key) if key_exists_in_src else None
|
|
196
|
+
for jpath_str, matches in jsonpath_match.items():
|
|
197
|
+
if full_jpath in matches:
|
|
198
|
+
apply(paths[jpath_str])
|
|
199
|
+
break
|
|
65
200
|
else:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
201
|
+
for typ, action in type_to_arg.items():
|
|
202
|
+
if isinstance(value, typ):
|
|
203
|
+
apply(action)
|
|
204
|
+
break
|
|
70
205
|
else:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
206
|
+
funcs_value = get_funcs(value)
|
|
207
|
+
if funcs_value:
|
|
208
|
+
# Value is a recursive type
|
|
209
|
+
if key_exists_in_src:
|
|
210
|
+
funcs_src_value = get_funcs(source_value)
|
|
211
|
+
if not funcs_src_value:
|
|
212
|
+
# Source value is not a recursive type
|
|
213
|
+
apply(type_mismatch)
|
|
214
|
+
else:
|
|
215
|
+
recursive(
|
|
216
|
+
src=fn_src_get(src, key),
|
|
217
|
+
add=value,
|
|
218
|
+
path=fullpath,
|
|
219
|
+
src_funcs=src_funcs,
|
|
220
|
+
add_funcs=funcs_value,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
# Source value does not exist; create a new instance
|
|
224
|
+
new_instance = constructor() if constructor else fn_add_construct()
|
|
225
|
+
fn_src_set(src, key, new_instance)
|
|
226
|
+
funcs_src_value = get_funcs(new_instance)
|
|
227
|
+
recursive(
|
|
228
|
+
src=new_instance,
|
|
229
|
+
add=value,
|
|
230
|
+
path=fullpath,
|
|
231
|
+
src_funcs=funcs_src_value,
|
|
232
|
+
add_funcs=funcs_value,
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
# addon value is of a non-recursive type that does not have any defined behavior;
|
|
236
|
+
# Apply the default behavior for of ("write", "skip") for the key.
|
|
237
|
+
apply(undefined_existing if key_exists_in_src else undefined_new)
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
type_to_arg = {list: ("write", lambda data: (data[0] + data[1], "append"))} | (types or {})
|
|
241
|
+
recursive_types = {
|
|
242
|
+
dict: (
|
|
243
|
+
lambda dic: dic.items(),
|
|
244
|
+
lambda dic, key: key in dic,
|
|
245
|
+
lambda dic, key: dic[key],
|
|
246
|
+
lambda dic, key, value: dic.update({key: value}),
|
|
247
|
+
lambda: dict(),
|
|
248
|
+
)
|
|
249
|
+
} | (recursive_types or {})
|
|
250
|
+
jsonpath_match = {
|
|
251
|
+
jpath_str: [match.full_path for match in _jsonpath.parse(jpath_str).find(addon)]
|
|
252
|
+
for jpath_str in (paths or {}).keys()
|
|
253
|
+
}
|
|
254
|
+
log = {}
|
|
255
|
+
funcs_src = get_funcs(source)
|
|
256
|
+
funcs_add = get_funcs(addon)
|
|
257
|
+
for funcs, param_name, data in ((funcs_src, "source", source), (funcs_add, "addon", addon)):
|
|
258
|
+
if not funcs:
|
|
259
|
+
raise ValueError(f"Data type '{type(data)}' of '{param_name}' is not provided in 'recursive_types'.")
|
|
260
|
+
recursive(
|
|
261
|
+
src=source,
|
|
262
|
+
add=addon,
|
|
263
|
+
path="$",
|
|
264
|
+
src_funcs=funcs_src,
|
|
265
|
+
add_funcs=funcs_add,
|
|
75
266
|
)
|
|
76
|
-
return
|
|
267
|
+
return log
|
|
77
268
|
|
|
78
269
|
|
|
79
270
|
def data_from_jsonschema(data: dict | list, schema: dict) -> None:
|
|
@@ -129,6 +320,7 @@ class TemplateFiller:
|
|
|
129
320
|
relative_key_key: str | None = None,
|
|
130
321
|
implicit_root: bool = True,
|
|
131
322
|
getter_function_name: str = "get",
|
|
323
|
+
skip_key_func: Callable[[list[str]], bool] | None = None,
|
|
132
324
|
):
|
|
133
325
|
self._marker_start_value = marker_start_value
|
|
134
326
|
self._marker_end_value = marker_end_value
|
|
@@ -151,6 +343,7 @@ class TemplateFiller:
|
|
|
151
343
|
self._template_keys = relative_template_keys or []
|
|
152
344
|
self._relative_key_key = relative_key_key
|
|
153
345
|
self._getter_function_name = getter_function_name
|
|
346
|
+
self._skip_func = skip_key_func
|
|
154
347
|
|
|
155
348
|
self._pattern_value: dict[int, _RegexPattern] = {}
|
|
156
349
|
self._data = None
|
|
@@ -190,7 +383,11 @@ class TemplateFiller:
|
|
|
190
383
|
code_lines = ["def __inline_code__():"]
|
|
191
384
|
code_lines.extend([f" {line}" for line in code_str.strip("\n").splitlines()])
|
|
192
385
|
code_str_full = "\n".join(code_lines)
|
|
193
|
-
global_context = self._code_context.copy() | {
|
|
386
|
+
global_context = self._code_context.copy() | {
|
|
387
|
+
self._getter_function_name: getter_function,
|
|
388
|
+
"__current_path__": current_path,
|
|
389
|
+
"__relative_path_anchor__": relative_path_anchor
|
|
390
|
+
}
|
|
194
391
|
for name, partial_func_data in self._code_context_partial.items():
|
|
195
392
|
if isinstance(partial_func_data, tuple):
|
|
196
393
|
func, arg_name = partial_func_data
|
|
@@ -250,6 +447,7 @@ class TemplateFiller:
|
|
|
250
447
|
return output, True
|
|
251
448
|
return output
|
|
252
449
|
path_expr = self._concat_json_paths(root_path_expr, path_expr)
|
|
450
|
+
# print("IN GET ADD VAL", path_expr)
|
|
253
451
|
cached_result = self._visited_paths.get(path_expr)
|
|
254
452
|
if cached_result:
|
|
255
453
|
value, matched = cached_result
|
|
@@ -266,7 +464,7 @@ class TemplateFiller:
|
|
|
266
464
|
return self._no_match_value
|
|
267
465
|
|
|
268
466
|
def get_value(jsonpath, return_all_matches: bool, from_code: bool) -> tuple[Any, bool]:
|
|
269
|
-
matches =
|
|
467
|
+
matches = recursive_match(jsonpath)
|
|
270
468
|
if not matches:
|
|
271
469
|
if from_code:
|
|
272
470
|
return None, False
|
|
@@ -294,14 +492,14 @@ class TemplateFiller:
|
|
|
294
492
|
current_chain=current_chain + (jsonpath,),
|
|
295
493
|
), True
|
|
296
494
|
|
|
297
|
-
def
|
|
495
|
+
def recursive_match(expr) -> list:
|
|
298
496
|
matches = expr.find(self._data)
|
|
299
497
|
if matches:
|
|
300
498
|
return matches
|
|
301
499
|
if isinstance(expr.left, _jsonpath.Root):
|
|
302
500
|
return []
|
|
303
501
|
whole_matches = []
|
|
304
|
-
left_matches =
|
|
502
|
+
left_matches = recursive_match(expr.left)
|
|
305
503
|
for left_match in left_matches:
|
|
306
504
|
left_match_filled = self._recursive_subst(
|
|
307
505
|
templ=left_match.value,
|
|
@@ -357,6 +555,11 @@ class TemplateFiller:
|
|
|
357
555
|
template_end=self._marker_end_value,
|
|
358
556
|
) from exception
|
|
359
557
|
|
|
558
|
+
# print("IN MAIN", self._extract_fields(current_path))
|
|
559
|
+
|
|
560
|
+
if self._skip_func and self._skip_func(self._extract_fields(current_path)):
|
|
561
|
+
return templ
|
|
562
|
+
|
|
360
563
|
if current_path in self._visited_paths:
|
|
361
564
|
return self._visited_paths[current_path][0]
|
|
362
565
|
|
|
@@ -432,6 +635,7 @@ class TemplateFiller:
|
|
|
432
635
|
|
|
433
636
|
if isinstance(templ, dict):
|
|
434
637
|
new_dict = {}
|
|
638
|
+
addons = []
|
|
435
639
|
for key, val in templ.items():
|
|
436
640
|
key_filled = self._recursive_subst(
|
|
437
641
|
templ=key,
|
|
@@ -442,7 +646,7 @@ class TemplateFiller:
|
|
|
442
646
|
is_key=True,
|
|
443
647
|
)
|
|
444
648
|
if isinstance(key, str) and self._pattern_unpack.fullmatch(key):
|
|
445
|
-
|
|
649
|
+
addons.append((key_filled, val))
|
|
446
650
|
continue
|
|
447
651
|
if key_filled in self._template_keys:
|
|
448
652
|
new_dict[key_filled] = val
|
|
@@ -455,6 +659,15 @@ class TemplateFiller:
|
|
|
455
659
|
level=0,
|
|
456
660
|
current_chain=current_chain + (new_path,),
|
|
457
661
|
)
|
|
662
|
+
for addon_dict, addon_settings in sorted(
|
|
663
|
+
addons, key=lambda addon: addon[1].get("priority", 0) if addon[1] else 0
|
|
664
|
+
):
|
|
665
|
+
addon_settings = {k: v for k, v in (addon_settings or {}).items() if k not in ("priority",)}
|
|
666
|
+
recursive_update(
|
|
667
|
+
source=new_dict,
|
|
668
|
+
addon=addon_dict,
|
|
669
|
+
**addon_settings,
|
|
670
|
+
)
|
|
458
671
|
if not is_relative_template:
|
|
459
672
|
self._visited_paths[current_path] = (new_dict, True)
|
|
460
673
|
return new_dict
|
|
@@ -534,7 +747,7 @@ class _RegexPattern:
|
|
|
534
747
|
def __init__(self, start: str, end: str):
|
|
535
748
|
start_esc = _re.escape(start)
|
|
536
749
|
end_esc = _re.escape(end)
|
|
537
|
-
self.pattern = _re.compile(rf"{start_esc}(.*?)(?={end_esc}){end_esc}",
|
|
750
|
+
self.pattern = _re.compile(rf"{start_esc}(.*?)(?={end_esc}){end_esc}", _re.DOTALL)
|
|
538
751
|
return
|
|
539
752
|
|
|
540
753
|
def fullmatch(self, string: str) -> _re.Match | None:
|
|
@@ -547,3 +760,4 @@ class _RegexPattern:
|
|
|
547
760
|
|
|
548
761
|
def sub(self, repl, string: str) -> str:
|
|
549
762
|
return self.pattern.sub(repl, string)
|
|
763
|
+
|
|
@@ -15,13 +15,27 @@ def to_string(
|
|
|
15
15
|
data_type: _Literal["json", "yaml", "toml"],
|
|
16
16
|
sort_keys: bool = False,
|
|
17
17
|
indent: int | None = None,
|
|
18
|
+
default: Callable[[Any], Any] | None = None,
|
|
18
19
|
end_of_file_newline: bool = True,
|
|
20
|
+
indent_mapping: int = 2,
|
|
21
|
+
indent_sequence: int = 4,
|
|
22
|
+
indent_sequence_offset: int = 2,
|
|
23
|
+
multiline_string_to_block: bool = True,
|
|
24
|
+
remove_top_level_indent: bool = True
|
|
19
25
|
):
|
|
20
26
|
if data_type == "json":
|
|
21
|
-
return to_json_string(data, sort_keys=sort_keys, indent=indent)
|
|
27
|
+
return to_json_string(data, sort_keys=sort_keys, indent=indent, default=default, end_of_file_newline=end_of_file_newline)
|
|
22
28
|
if data_type == "yaml":
|
|
23
|
-
return to_yaml_string(
|
|
24
|
-
|
|
29
|
+
return to_yaml_string(
|
|
30
|
+
data,
|
|
31
|
+
end_of_file_newline=end_of_file_newline,
|
|
32
|
+
indent_mapping=indent_mapping,
|
|
33
|
+
indent_sequence=indent_sequence,
|
|
34
|
+
indent_sequence_offset=indent_sequence_offset,
|
|
35
|
+
multiline_string_to_block=multiline_string_to_block,
|
|
36
|
+
remove_top_level_indent=remove_top_level_indent
|
|
37
|
+
)
|
|
38
|
+
return to_toml_string(data, sort_keys=sort_keys, end_of_file_newline=end_of_file_newline)
|
|
25
39
|
|
|
26
40
|
|
|
27
41
|
def to_yaml_string(
|
|
@@ -49,8 +63,10 @@ def to_yaml_string(
|
|
|
49
63
|
def to_toml_string(
|
|
50
64
|
data: dict | list | str | int | float | bool | _yaml.CommentedMap | _yaml.CommentedSeq,
|
|
51
65
|
sort_keys: bool = False,
|
|
66
|
+
end_of_file_newline: bool = True,
|
|
52
67
|
) -> str:
|
|
53
|
-
|
|
68
|
+
string = _tomlkit.dumps(data, sort_keys=sort_keys)
|
|
69
|
+
return f"{string.rstrip("\n")}\n" if end_of_file_newline else string
|
|
54
70
|
|
|
55
71
|
|
|
56
72
|
def to_json_string(
|
|
@@ -58,8 +74,10 @@ def to_json_string(
|
|
|
58
74
|
sort_keys: bool = False,
|
|
59
75
|
indent: int | None = None,
|
|
60
76
|
default: Callable[[Any], Any] | None = None,
|
|
77
|
+
end_of_file_newline: bool = True,
|
|
61
78
|
) -> str:
|
|
62
|
-
|
|
79
|
+
string = _json.dumps(data, indent=indent, sort_keys=sort_keys, default=default)
|
|
80
|
+
return f"{string.rstrip("\n")}\n" if end_of_file_newline else string
|
|
63
81
|
|
|
64
82
|
|
|
65
83
|
def to_yaml_file(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|