PySerials 0.0.0.dev30__tar.gz → 0.0.0.dev31__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.
Files changed (23) hide show
  1. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/PKG-INFO +3 -3
  2. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/pyproject.toml +3 -3
  3. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/PySerials.egg-info/PKG-INFO +3 -3
  4. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/PySerials.egg-info/requires.txt +2 -2
  5. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/nested_dict.py +66 -21
  6. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/update.py +213 -85
  7. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/README.md +0 -0
  8. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/setup.cfg +0 -0
  9. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/PySerials.egg-info/SOURCES.txt +0 -0
  10. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/PySerials.egg-info/dependency_links.txt +0 -0
  11. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/PySerials.egg-info/not-zip-safe +0 -0
  12. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/PySerials.egg-info/top_level.txt +0 -0
  13. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/__init__.py +0 -0
  14. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/compare.py +0 -0
  15. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/exception/__init__.py +0 -0
  16. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/exception/_base.py +0 -0
  17. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/exception/read.py +0 -0
  18. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/exception/update.py +0 -0
  19. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/exception/validate.py +0 -0
  20. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/format.py +0 -0
  21. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/read.py +0 -0
  22. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/validate.py +0 -0
  23. {pyserials-0.0.0.dev30 → pyserials-0.0.0.dev31}/src/pyserials/write.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev30
3
+ Version: 0.0.0.dev31
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,>=0.17.32
9
9
  Requires-Dist: ruamel.yaml.string<1,>=0.1.1
10
10
  Requires-Dist: tomlkit<0.12,>=0.11.8
11
- Requires-Dist: MDit==0.0.0.dev27
12
- Requires-Dist: ExceptionMan==0.0.0.dev27
11
+ Requires-Dist: MDit==0.0.0.dev28
12
+ Requires-Dist: ExceptionMan==0.0.0.dev28
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.dev30"
20
+ version = "0.0.0.dev31"
21
21
  name = "PySerials"
22
22
  dependencies = [
23
23
  "jsonschema >= 4.21.0, < 5",
@@ -26,8 +26,8 @@ dependencies = [
26
26
  "ruamel.yaml >= 0.17.32, < 0.18", # https://yaml.readthedocs.io/en/stable/
27
27
  "ruamel.yaml.string >= 0.1.1, < 1",
28
28
  "tomlkit >= 0.11.8, < 0.12", # https://tomlkit.readthedocs.io/en/stable/,
29
- "MDit == 0.0.0.dev27",
30
- "ExceptionMan == 0.0.0.dev27",
29
+ "MDit == 0.0.0.dev28",
30
+ "ExceptionMan == 0.0.0.dev28",
31
31
  "ProtocolMan == 0.0.0.dev2",
32
32
  ]
33
33
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev30
3
+ Version: 0.0.0.dev31
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,>=0.17.32
9
9
  Requires-Dist: ruamel.yaml.string<1,>=0.1.1
10
10
  Requires-Dist: tomlkit<0.12,>=0.11.8
11
- Requires-Dist: MDit==0.0.0.dev27
12
- Requires-Dist: ExceptionMan==0.0.0.dev27
11
+ Requires-Dist: MDit==0.0.0.dev28
12
+ Requires-Dist: ExceptionMan==0.0.0.dev28
13
13
  Requires-Dist: ProtocolMan==0.0.0.dev2
@@ -4,6 +4,6 @@ jsonpath-ng<2,>=1.6.1
4
4
  ruamel.yaml<0.18,>=0.17.32
5
5
  ruamel.yaml.string<1,>=0.1.1
6
6
  tomlkit<0.12,>=0.11.8
7
- MDit==0.0.0.dev27
8
- ExceptionMan==0.0.0.dev27
7
+ MDit==0.0.0.dev28
8
+ ExceptionMan==0.0.0.dev28
9
9
  ProtocolMan==0.0.0.dev2
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING as _TYPE_CHECKING
5
5
  import pyserials as _ps
6
6
 
7
7
  if _TYPE_CHECKING:
8
- from typing import Callable
8
+ from typing import Callable, Any
9
9
 
10
10
 
11
11
  class NestedDict:
@@ -13,31 +13,59 @@ class NestedDict:
13
13
  def __init__(
14
14
  self,
15
15
  data: dict | None = None,
16
- template_marker_start: str = "${{",
17
- template_marker_end: str = "}}",
18
- template_marker_unpack_start: str = "*{{",
19
- template_marker_unpack_end: str = "}}",
20
- template_implicit_root: bool = True,
21
- template_stringer: Callable[[str], str] = str,
16
+ marker_start_value: str = "$",
17
+ marker_end_value: str = "$",
18
+ repeater_start_value: str = "{",
19
+ repeater_end_value: str = "}",
20
+ repeater_count_value: int = 2,
21
+ start_list: str = "$[[",
22
+ start_unpack: str = "*{{",
23
+ start_code: str = "#{{",
24
+ end_list: str = "]]$",
25
+ end_unpack: str = "}}*",
26
+ end_code: str = "}}#",
27
+ recursive: bool = True,
28
+ raise_no_match: bool = True,
29
+ leave_no_match: bool = False,
30
+ no_match_value: Any = None,
31
+ stringer: Callable[[str], str] = str,
32
+ unpack_string_joiner: str = ", ",
22
33
  relative_template_keys: list[str] | None = None,
34
+ implicit_root: bool = True,
23
35
  ):
24
36
  self._data = data or {}
25
37
  self._templater = _ps.update.TemplateFiller(
26
- marker_start=template_marker_start,
27
- marker_end=template_marker_end,
28
- marker_unpack_start=template_marker_unpack_start,
29
- marker_unpack_end=template_marker_unpack_end,
30
- implicit_root=template_implicit_root,
31
- stringer=template_stringer,
38
+ marker_start_value=marker_start_value,
39
+ marker_end_value=marker_end_value,
40
+ repeater_start_value=repeater_start_value,
41
+ repeater_end_value=repeater_end_value,
42
+ repeater_count_value=repeater_count_value,
43
+ start_list=start_list,
44
+ start_unpack=start_unpack,
45
+ start_code=start_code,
46
+ end_list=end_list,
47
+ end_unpack=end_unpack,
48
+ end_code=end_code,
32
49
  )
33
- self._relative_template_keys = relative_template_keys
50
+ self._recursive = recursive
51
+ self._raise_no_match = raise_no_match
52
+ self._leave_no_match = leave_no_match
53
+ self._no_match_value = no_match_value
54
+ self._stringer = stringer
55
+ self._unpack_string_joiner = unpack_string_joiner
56
+ self._relative_template_keys = relative_template_keys or []
57
+ self._implicit_root = implicit_root
34
58
  return
35
59
 
36
60
  def fill(
37
61
  self,
38
62
  path: str = "",
39
- always_list: bool = False,
40
- recursive: bool = True,
63
+ recursive: bool | None = None,
64
+ raise_no_match: bool | None = None,
65
+ leave_no_match: bool | None = None,
66
+ stringer: Callable[[str], str] | None = None,
67
+ unpack_string_joiner: str | None = None,
68
+ level: int = 0,
41
69
  ):
42
70
  if not path:
43
71
  value = self._data
@@ -46,7 +74,14 @@ class NestedDict:
46
74
  if not value:
47
75
  return
48
76
  filled_value = self.fill_data(
49
- data=value, current_path=path, always_list=always_list, recursive=recursive,
77
+ data=value,
78
+ current_path=path,
79
+ recursive=recursive,
80
+ raise_no_match=raise_no_match,
81
+ leave_no_match=leave_no_match,
82
+ stringer=stringer,
83
+ unpack_string_joiner=unpack_string_joiner,
84
+ level=level,
50
85
  )
51
86
  if not path:
52
87
  self._data = filled_value
@@ -58,16 +93,26 @@ class NestedDict:
58
93
  self,
59
94
  data,
60
95
  current_path: str = "",
61
- always_list: bool = False,
62
- recursive: bool = True,
96
+ recursive: bool | None = None,
97
+ raise_no_match: bool | None = None,
98
+ leave_no_match: bool | None = None,
99
+ stringer: Callable[[str], str] | None = None,
100
+ unpack_string_joiner: str | None = None,
101
+ level: int = 0,
63
102
  ):
64
103
  return self._templater.fill(
65
104
  templated_data=data,
66
105
  source_data=self._data,
67
106
  current_path=current_path,
68
- always_list=always_list,
69
- recursive=recursive,
107
+ recursive=recursive if recursive is not None else self._recursive,
108
+ raise_no_match=raise_no_match if raise_no_match is not None else self._raise_no_match,
109
+ leave_no_match=leave_no_match if leave_no_match is not None else self._leave_no_match,
110
+ no_match_value=self._no_match_value,
111
+ stringer=stringer if stringer is not None else self._stringer,
112
+ unpack_string_joiner=unpack_string_joiner if unpack_string_joiner is not None else self._unpack_string_joiner,
70
113
  relative_template_keys=self._relative_template_keys,
114
+ implicit_root=self._implicit_root,
115
+ level=level,
71
116
  )
72
117
 
73
118
  def __call__(self):
@@ -1,4 +1,6 @@
1
1
  from __future__ import annotations as _annotations
2
+
3
+ import re
2
4
  from typing import TYPE_CHECKING as _TYPE_CHECKING
3
5
  import re as _re
4
6
 
@@ -8,7 +10,7 @@ from jsonpath_ng import exceptions as _jsonpath_exceptions
8
10
  import pyserials.exception as _exception
9
11
 
10
12
  if _TYPE_CHECKING:
11
- from typing import Literal, Callable, Sequence
13
+ from typing import Literal, Sequence, Any, Callable
12
14
 
13
15
 
14
16
  def dict_from_addon(
@@ -103,25 +105,29 @@ class TemplateFiller:
103
105
 
104
106
  def __init__(
105
107
  self,
106
- marker_start: str = "${{",
107
- marker_end: str = "}}",
108
- marker_unpack_start: str = "*{{",
109
- marker_unpack_end: str = "}}",
110
- implicit_root: bool = True,
111
- stringer: Callable[[str], str] = str,
108
+ marker_start_value: str = "$",
109
+ marker_end_value: str = "$",
110
+ repeater_start_value: str = "{",
111
+ repeater_end_value: str = "}",
112
+ repeater_count_value: int = 2,
113
+ start_list: str = "$[[",
114
+ start_unpack: str = "*{{",
115
+ start_code: str = "#{{",
116
+ end_list: str = "]]$",
117
+ end_unpack: str = "}}*",
118
+ end_code: str = "}}#",
112
119
  ):
113
- def make_regex(start, end):
114
- start_esc = _re.escape(start)
115
- end_esc = _re.escape(end)
116
- regex_sub = rf"{start_esc}([^{end_esc}]+){end_esc}"
117
- return _re.compile(regex_sub)
118
-
119
- self._marker_start = marker_start
120
- self._marker_end = marker_end
121
- self._pattern_template = make_regex(marker_start, marker_end)
122
- self._pattern_template_unpack = make_regex(marker_unpack_start, marker_unpack_end)
123
- self._add_prefix = implicit_root
124
- self._stringer = stringer
120
+ self._marker_start_value = marker_start_value
121
+ self._marker_end_value = marker_end_value
122
+ self._repeater_start_value = repeater_start_value
123
+ self._repeater_end_value = repeater_end_value
124
+ self._repeater_count_value = repeater_count_value
125
+ self._pattern_list = _RegexPattern(start=start_list, end=end_list)
126
+ self._pattern_unpack = _RegexPattern(start=start_unpack, end=end_unpack)
127
+ self._pattern_code = _RegexPattern(start=start_code, end=end_code)
128
+ self._add_prefix = True
129
+
130
+ self._pattern_value: dict[int, _RegexPattern] = {}
125
131
  self._data = None
126
132
  self._source = None
127
133
  self._recursive = None
@@ -129,22 +135,48 @@ class TemplateFiller:
129
135
  self._raise_no_match = None
130
136
  self._template_keys = None
131
137
  self._ignore_templates = True
138
+ self._leave_no_match = False
139
+ self._no_match_value = None
140
+ self._stringer = str
141
+ self._unpack_string_joiner = ", "
132
142
  return
133
143
 
144
+ def _get_value_regex_pattern(self, level: int = 0) -> _RegexPattern:
145
+ level_patterns = self._pattern_value.setdefault(level, {})
146
+ if level in level_patterns:
147
+ return level_patterns[level]
148
+ count = self._repeater_count_value + level
149
+ pattern = _RegexPattern(
150
+ start=f"{self._marker_start_value}{self._repeater_start_value * count} ",
151
+ end=f" {self._repeater_end_value * count}{self._marker_end_value}",
152
+ )
153
+ level_patterns[level] = pattern
154
+ return pattern
155
+
134
156
  def fill(
135
157
  self,
136
158
  templated_data: dict | list | str,
137
159
  source_data: dict | list,
138
160
  current_path: str = "",
139
- always_list: bool = True,
140
161
  recursive: bool = True,
141
162
  raise_no_match: bool = True,
163
+ leave_no_match: bool = False,
164
+ no_match_value: Any = None,
165
+ stringer: Callable[[str], str] = str,
166
+ unpack_string_joiner: str = ", ",
142
167
  relative_template_keys: list[str] | None = None,
168
+ implicit_root: bool = True,
169
+ level: int = 0,
143
170
  ):
144
171
  self._data = templated_data
145
172
  self._source = source_data
146
173
  self._recursive = recursive
147
174
  self._raise_no_match = raise_no_match
175
+ self._leave_no_match = leave_no_match
176
+ self._no_match_value = no_match_value
177
+ self._stringer = stringer
178
+ self._unpack_string_joiner = unpack_string_joiner
179
+ self._add_prefix = implicit_root
148
180
  self._template_keys = relative_template_keys or []
149
181
  path = (f"$.{current_path}" if self._add_prefix else current_path) if current_path else "$"
150
182
  if not relative_template_keys:
@@ -152,15 +184,15 @@ class TemplateFiller:
152
184
  return self._recursive_subst(
153
185
  templ=self._data,
154
186
  current_path=path,
155
- always_list=always_list,
156
187
  relative_path_anchor=path,
188
+ level=level,
157
189
  )
158
190
  self._ignore_templates = True
159
191
  first_pass = self._recursive_subst(
160
192
  templ=self._data,
161
193
  current_path=path,
162
- always_list=always_list,
163
194
  relative_path_anchor=path,
195
+ level=level,
164
196
  )
165
197
  if self._data is self._source:
166
198
  self._source = first_pass
@@ -169,53 +201,28 @@ class TemplateFiller:
169
201
  return self._recursive_subst(
170
202
  templ=self._data,
171
203
  current_path=path,
172
- always_list=always_list,
173
204
  relative_path_anchor=path,
205
+ level=level,
174
206
  )
175
207
 
176
- def _recursive_subst(self, templ, current_path: str, always_list: bool, relative_path_anchor: str):
177
-
178
- def raise_error(
179
- path_invalid: str,
180
- description_template: str,
181
- ):
182
- raise _exception.update.PySerialsUpdateTemplatedDataError(
183
- description_template=description_template,
184
- path_invalid=path_invalid,
185
- path=current_path,
186
- data=templ,
187
- data_full=self._data,
188
- data_source=self._source,
189
- template_start=self._marker_start,
190
- template_end=self._marker_end,
191
- )
192
-
193
- def _rec_match(expr):
208
+ def _recursive_subst(self, templ, current_path: str, relative_path_anchor: str, level: int):
194
209
 
195
- def raise_error_path_invalid():
210
+ def get_code_value(code_str: str):
211
+ code_lines = ["def __inline_code__():"]
212
+ code_lines.extend([f" {line}" for line in code_str.strip("\n").splitlines()])
213
+ code_str_full = "\n".join(code_lines)
214
+ global_context = {}
215
+ local_context = {}
216
+ try:
217
+ exec(code_str_full, global_context, local_context)
218
+ return local_context["__inline_code__"]()
219
+ except Exception as e:
196
220
  raise_error(
197
- path_invalid=str(expr),
198
- description_template="Path {path_invalid} is missing in the source data.",
221
+ description_template=f"Code at {{path_invalid}} raised an exception: {e}\n{code_str_full}",
222
+ path_invalid=current_path,
199
223
  )
200
224
 
201
- matches = expr.find(self._source)
202
- if matches:
203
- return matches
204
- if isinstance(expr.left, _jsonpath.Root):
205
- raise_error_path_invalid()
206
- whole_matches = []
207
- left_matches = _rec_match(expr.left)
208
- for left_match in left_matches:
209
- left_match_filled = self._recursive_subst(
210
- left_match.value, current_path=str(expr.left), always_list=False, relative_path_anchor=str(expr.left)
211
- ) if isinstance(left_match.value, str) else left_match.value
212
- right_matches = expr.right.find(left_match_filled)
213
- whole_matches.extend(right_matches)
214
- if not whole_matches:
215
- raise_error_path_invalid()
216
- return whole_matches
217
-
218
- def get_address_value(re_match):
225
+ def get_address_value(re_match, return_all_matches: bool = False):
219
226
  path, num_periods = self._remove_leading_periods(re_match.group(1).strip())
220
227
  if num_periods == 0:
221
228
  path = f"$.{path}" if self._add_prefix else path
@@ -244,20 +251,26 @@ class TemplateFiller:
244
251
  )
245
252
  root_path_expr = root_path_expr.left
246
253
  path_expr = root_path_expr.child(path_expr)
247
- return get_value(path_expr)
254
+ value, matched = get_value(path_expr, return_all_matches)
255
+ if matched:
256
+ return value
257
+ if self._leave_no_match:
258
+ return re_match.group()
259
+ return self._no_match_value
248
260
 
249
- def get_value(jsonpath):
261
+ def get_value(jsonpath, return_all_matches: bool) -> tuple[Any, bool]:
250
262
  matches = _rec_match(jsonpath)
263
+ if not matches:
264
+ if not return_all_matches and self._raise_no_match:
265
+ raise_error(
266
+ path_invalid=str(jsonpath),
267
+ description_template="JSONPath expression {path_invalid} did not match any data.",
268
+ )
269
+ return None, False
251
270
  values = [m.value for m in matches]
252
- if not values and self._raise_no_match:
253
- raise_error(
254
- path_invalid=str(jsonpath),
255
- description_template="JSONPath expression {path_invalid} did not match any data.",
256
- )
257
- single = len(values) == 1 and not always_list
258
- output = values[0] if single else values
271
+ output = values if return_all_matches or len(values) > 1 else values[0]
259
272
  if not self._recursive:
260
- return output
273
+ return output, True
261
274
  if relative_path_anchor == current_path:
262
275
  path_fields = self._extract_fields(jsonpath)
263
276
  has_template_key = any(field in self._template_keys for field in path_fields)
@@ -267,45 +280,140 @@ class TemplateFiller:
267
280
  return self._recursive_subst(
268
281
  output,
269
282
  current_path=str(jsonpath),
270
- always_list=always_list,
271
- relative_path_anchor=_rel_path_anchor
272
- )
283
+ relative_path_anchor=_rel_path_anchor,
284
+ level=0,
285
+ ), True
286
+
287
+ def _rec_match(expr) -> list:
288
+ matches = expr.find(self._source)
289
+ if matches:
290
+ return matches
291
+ if isinstance(expr.left, _jsonpath.Root):
292
+ return []
293
+ whole_matches = []
294
+ left_matches = _rec_match(expr.left)
295
+ for left_match in left_matches:
296
+ left_match_filled = self._recursive_subst(
297
+ templ=left_match.value,
298
+ current_path=str(expr.left),
299
+ relative_path_anchor=str(expr.left),
300
+ level=0,
301
+ ) if isinstance(left_match.value, str) else left_match.value
302
+ right_matches = expr.right.find(left_match_filled)
303
+ whole_matches.extend(right_matches)
304
+ return whole_matches
273
305
 
274
306
  def get_relative_path(new_path):
275
307
  return new_path if current_path == relative_path_anchor else relative_path_anchor
276
308
 
309
+ def raise_error(
310
+ path_invalid: str,
311
+ description_template: str,
312
+ ):
313
+ raise _exception.update.PySerialsUpdateTemplatedDataError(
314
+ description_template=description_template,
315
+ path_invalid=path_invalid,
316
+ path=current_path,
317
+ data=templ,
318
+ data_full=self._data,
319
+ data_source=self._source,
320
+ template_start=self._marker_start_value,
321
+ template_end=self._marker_end_value,
322
+ )
323
+
324
+ def string_filler_unpack(match: _re.Match):
325
+ match_list = self._pattern_list.fullmatch(match.group(1).strip())
326
+ if match_list:
327
+ values = get_address_value(match_list, return_all_matches=True)
328
+ else:
329
+ match_code = self._pattern_code.fullmatch(match.group(1).strip())
330
+ if match_code:
331
+ values = get_code_value(match_code.group(1))
332
+ else:
333
+ values = get_address_value(match)
334
+ return self._unpack_string_joiner.join([self._stringer(val) for val in values])
335
+
277
336
  if isinstance(templ, str):
278
- match_whole_str = self._pattern_template.fullmatch(templ) or self._pattern_template_unpack.fullmatch(templ)
279
- if match_whole_str:
280
- return get_address_value(match_whole_str)
281
- return self._pattern_template.sub(
282
- lambda x: self._stringer(get_address_value(x)),
337
+ pattern_nested = self._get_value_regex_pattern(level=level + 1)
338
+ templ_nested_filled = pattern_nested.sub(
339
+ lambda x: self._recursive_subst(
340
+ templ=x.group(),
341
+ current_path=current_path,
342
+ relative_path_anchor=get_relative_path(current_path),
343
+ level=level+1,
344
+ ),
283
345
  templ
284
346
  )
347
+ pattern_value = self._get_value_regex_pattern(level=level)
348
+ whole_match_value = pattern_value.fullmatch(templ_nested_filled)
349
+ if whole_match_value:
350
+ return get_address_value(whole_match_value)
351
+ templ_values_filled = pattern_value.sub(
352
+ lambda x: str(get_address_value(x)),
353
+ templ_nested_filled
354
+ )
355
+ whole_match_list = self._pattern_list.fullmatch(templ_values_filled.strip())
356
+ if whole_match_list:
357
+ return get_address_value(whole_match_list, return_all_matches=True)
358
+ whole_match_unpack = self._pattern_unpack.fullmatch(templ_values_filled.strip())
359
+ if whole_match_unpack:
360
+ submatch_list = self._pattern_list.fullmatch(whole_match_unpack.group(1).strip())
361
+ if submatch_list:
362
+ return get_address_value(submatch_list, return_all_matches=True)
363
+ submatch_code = self._pattern_code.fullmatch(whole_match_unpack.group(1).strip())
364
+ if submatch_code:
365
+ return get_code_value(submatch_code.group(1))
366
+ return get_address_value(whole_match_unpack)
367
+ whole_match_code = self._pattern_code.fullmatch(templ_values_filled.strip())
368
+ if whole_match_code:
369
+ templ_list_filled = self._pattern_list.sub(
370
+ lambda x: str(get_address_value(x, return_all_matches=True)),
371
+ whole_match_code.group(1)
372
+ )
373
+ return get_code_value(templ_list_filled)
374
+ unpacked_filled = self._pattern_unpack.sub(string_filler_unpack, templ_values_filled)
375
+ return self._pattern_code.sub(
376
+ lambda x: self._stringer(get_code_value(x.group(1))),
377
+ unpacked_filled
378
+ )
379
+
285
380
  if isinstance(templ, list):
286
381
  out = []
287
382
  for idx, elem in enumerate(templ):
288
383
  new_path = f"{current_path}[{idx}]"
289
384
  elem_filled = self._recursive_subst(
290
- templ=elem, current_path=new_path, always_list=always_list, relative_path_anchor=get_relative_path(new_path),
385
+ templ=elem,
386
+ current_path=new_path,
387
+ relative_path_anchor=get_relative_path(new_path),
388
+ level=0,
291
389
  )
292
- if isinstance(elem, str) and self._pattern_template_unpack.fullmatch(elem):
390
+ if isinstance(elem, str) and self._pattern_unpack.fullmatch(elem.strip()):
293
391
  out.extend(elem_filled)
294
392
  else:
295
393
  out.append(elem_filled)
296
394
  return out
395
+
297
396
  if isinstance(templ, dict):
298
397
  new_dict = {}
299
398
  for key, val in templ.items():
300
399
  key_filled = self._recursive_subst(
301
- templ=key, current_path=current_path, always_list=False, relative_path_anchor=relative_path_anchor,
400
+ templ=key,
401
+ current_path=current_path,
402
+ relative_path_anchor=relative_path_anchor,
403
+ level=0,
302
404
  )
405
+ if isinstance(key, str) and self._pattern_unpack.fullmatch(key.strip()):
406
+ new_dict.update(key_filled)
407
+ continue
303
408
  if key_filled in self._template_keys:
304
409
  new_dict[key_filled] = val
305
410
  continue
306
411
  new_path = f"{current_path}.'{key_filled}'"
307
412
  new_dict[key_filled] = self._recursive_subst(
308
- templ=val, current_path=new_path, always_list=always_list, relative_path_anchor=get_relative_path(new_path),
413
+ templ=val,
414
+ current_path=new_path,
415
+ relative_path_anchor=get_relative_path(new_path),
416
+ level=0,
309
417
  )
310
418
  return new_dict
311
419
  return templ
@@ -334,4 +442,24 @@ class TemplateFiller:
334
442
  return
335
443
  fields = []
336
444
  _recursive_extract(jsonpath)
337
- return fields
445
+ return fields
446
+
447
+
448
+ class _RegexPattern:
449
+
450
+ def __init__(self, start: str, end: str):
451
+ start_esc = _re.escape(start)
452
+ end_esc = _re.escape(end)
453
+ self.pattern = _re.compile(rf"{start_esc}(.*?)(?={end_esc}){end_esc}", re.DOTALL)
454
+ return
455
+
456
+ def fullmatch(self, string: str) -> _re.Match | None:
457
+ # Use findall to count occurrences of segments in the text
458
+ matches = self.pattern.findall(string)
459
+ if len(matches) == 1:
460
+ # Verify the match spans the entire string
461
+ return self.pattern.fullmatch(string)
462
+ return None
463
+
464
+ def sub(self, repl, string: str) -> str:
465
+ return self.pattern.sub(repl, string)