PySerials 0.0.0.dev31__tar.gz → 0.0.0.dev33__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.dev31 → pyserials-0.0.0.dev33}/PKG-INFO +3 -3
  2. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/pyproject.toml +3 -3
  3. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/PySerials.egg-info/PKG-INFO +3 -3
  4. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/PySerials.egg-info/requires.txt +2 -2
  5. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/nested_dict.py +19 -47
  6. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/update.py +188 -142
  7. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/README.md +0 -0
  8. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/setup.cfg +0 -0
  9. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/PySerials.egg-info/SOURCES.txt +0 -0
  10. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/PySerials.egg-info/dependency_links.txt +0 -0
  11. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/PySerials.egg-info/not-zip-safe +0 -0
  12. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/PySerials.egg-info/top_level.txt +0 -0
  13. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/__init__.py +0 -0
  14. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/compare.py +0 -0
  15. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/exception/__init__.py +0 -0
  16. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/exception/_base.py +0 -0
  17. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/exception/read.py +0 -0
  18. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/exception/update.py +0 -0
  19. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/exception/validate.py +0 -0
  20. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/format.py +0 -0
  21. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/read.py +0 -0
  22. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/src/pyserials/validate.py +0 -0
  23. {pyserials-0.0.0.dev31 → pyserials-0.0.0.dev33}/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.dev31
3
+ Version: 0.0.0.dev33
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.dev28
12
- Requires-Dist: ExceptionMan==0.0.0.dev28
11
+ Requires-Dist: MDit==0.0.0.dev30
12
+ Requires-Dist: ExceptionMan==0.0.0.dev30
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.dev31"
20
+ version = "0.0.0.dev33"
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.dev28",
30
- "ExceptionMan == 0.0.0.dev28",
29
+ "MDit == 0.0.0.dev30",
30
+ "ExceptionMan == 0.0.0.dev30",
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.dev31
3
+ Version: 0.0.0.dev33
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.dev28
12
- Requires-Dist: ExceptionMan==0.0.0.dev28
11
+ Requires-Dist: MDit==0.0.0.dev30
12
+ Requires-Dist: ExceptionMan==0.0.0.dev30
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.dev28
8
- ExceptionMan==0.0.0.dev28
7
+ MDit==0.0.0.dev30
8
+ ExceptionMan==0.0.0.dev30
9
9
  ProtocolMan==0.0.0.dev2
@@ -24,14 +24,17 @@ class NestedDict:
24
24
  end_list: str = "]]$",
25
25
  end_unpack: str = "}}*",
26
26
  end_code: str = "}}#",
27
- recursive: bool = True,
28
27
  raise_no_match: bool = True,
29
28
  leave_no_match: bool = False,
30
29
  no_match_value: Any = None,
30
+ code_context: dict[str, Any] | None = None,
31
+ code_context_partial: dict[str, Callable | tuple[Callable, str]] | None = None,
32
+ code_context_call: dict[str, Callable[[Callable], Any]] | None = None,
31
33
  stringer: Callable[[str], str] = str,
32
34
  unpack_string_joiner: str = ", ",
33
35
  relative_template_keys: list[str] | None = None,
34
36
  implicit_root: bool = True,
37
+ getter_function_name: str = "get",
35
38
  ):
36
39
  self._data = data or {}
37
40
  self._templater = _ps.update.TemplateFiller(
@@ -46,27 +49,21 @@ class NestedDict:
46
49
  end_list=end_list,
47
50
  end_unpack=end_unpack,
48
51
  end_code=end_code,
52
+ raise_no_match=raise_no_match,
53
+ leave_no_match=leave_no_match,
54
+ no_match_value=no_match_value,
55
+ code_context=code_context,
56
+ code_context_partial=code_context_partial,
57
+ code_context_call=code_context_call,
58
+ stringer=stringer,
59
+ unpack_string_joiner=unpack_string_joiner,
60
+ relative_template_keys=relative_template_keys,
61
+ implicit_root=implicit_root,
62
+ getter_function_name=getter_function_name,
49
63
  )
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
58
64
  return
59
65
 
60
- def fill(
61
- self,
62
- path: str = "",
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,
69
- ):
66
+ def fill(self, path: str = ""):
70
67
  if not path:
71
68
  value = self._data
72
69
  else:
@@ -76,12 +73,6 @@ class NestedDict:
76
73
  filled_value = self.fill_data(
77
74
  data=value,
78
75
  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,
85
76
  )
86
77
  if not path:
87
78
  self._data = filled_value
@@ -89,30 +80,11 @@ class NestedDict:
89
80
  self.__setitem__(path, filled_value)
90
81
  return filled_value
91
82
 
92
- def fill_data(
93
- self,
94
- data,
95
- current_path: str = "",
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,
102
- ):
83
+ def fill_data(self, data, current_path: str = ""):
103
84
  return self._templater.fill(
104
- templated_data=data,
105
- source_data=self._data,
85
+ data=self._data,
86
+ template=data,
106
87
  current_path=current_path,
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,
113
- relative_template_keys=self._relative_template_keys,
114
- implicit_root=self._implicit_root,
115
- level=level,
116
88
  )
117
89
 
118
90
  def __call__(self):
@@ -3,6 +3,7 @@ from __future__ import annotations as _annotations
3
3
  import re
4
4
  from typing import TYPE_CHECKING as _TYPE_CHECKING
5
5
  import re as _re
6
+ from functools import partial as _partial
6
7
 
7
8
  import jsonpath_ng as _jsonpath
8
9
  from jsonpath_ng import exceptions as _jsonpath_exceptions
@@ -116,6 +117,17 @@ class TemplateFiller:
116
117
  end_list: str = "]]$",
117
118
  end_unpack: str = "}}*",
118
119
  end_code: str = "}}#",
120
+ raise_no_match: bool = True,
121
+ leave_no_match: bool = False,
122
+ no_match_value: Any = None,
123
+ code_context: dict[str, Any] | None = None,
124
+ code_context_partial: dict[str, Callable | tuple[Callable, str]] | None = None,
125
+ code_context_call: dict[str, Callable[[Callable], Any]] | None = None,
126
+ stringer: Callable[[str], str] = str,
127
+ unpack_string_joiner: str = ", ",
128
+ relative_template_keys: list[str] | None = None,
129
+ implicit_root: bool = True,
130
+ getter_function_name: str = "get",
119
131
  ):
120
132
  self._marker_start_value = marker_start_value
121
133
  self._marker_end_value = marker_end_value
@@ -125,93 +137,64 @@ class TemplateFiller:
125
137
  self._pattern_list = _RegexPattern(start=start_list, end=end_list)
126
138
  self._pattern_unpack = _RegexPattern(start=start_unpack, end=end_unpack)
127
139
  self._pattern_code = _RegexPattern(start=start_code, end=end_code)
128
- self._add_prefix = True
140
+
141
+ self._raise_no_match = raise_no_match
142
+ self._leave_no_match = leave_no_match
143
+ self._no_match_value = no_match_value
144
+ self._code_context = code_context or {}
145
+ self._code_context_partial = code_context_partial or {}
146
+ self._code_context_call = code_context_call or {}
147
+ self._stringer = stringer
148
+ self._unpack_string_joiner = unpack_string_joiner
149
+ self._add_prefix = implicit_root
150
+ self._template_keys = relative_template_keys or []
151
+ self._getter_function_name = getter_function_name
129
152
 
130
153
  self._pattern_value: dict[int, _RegexPattern] = {}
131
154
  self._data = None
132
- self._source = None
133
- self._recursive = None
134
- self._path = None
135
- self._raise_no_match = None
136
- self._template_keys = None
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 = ", "
142
155
  return
143
156
 
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
-
156
157
  def fill(
157
158
  self,
158
- templated_data: dict | list | str,
159
- source_data: dict | list,
159
+ data: dict | list,
160
+ template: dict | list | str | None = None,
160
161
  current_path: str = "",
161
- recursive: bool = True,
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 = ", ",
167
- relative_template_keys: list[str] | None = None,
168
- implicit_root: bool = True,
169
- level: int = 0,
170
162
  ):
171
- self._data = templated_data
172
- self._source = source_data
173
- self._recursive = recursive
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
180
- self._template_keys = relative_template_keys or []
163
+ self._data = data
181
164
  path = (f"$.{current_path}" if self._add_prefix else current_path) if current_path else "$"
182
- if not relative_template_keys:
183
- self._ignore_templates = False
184
- return self._recursive_subst(
185
- templ=self._data,
186
- current_path=path,
187
- relative_path_anchor=path,
188
- level=level,
189
- )
190
- self._ignore_templates = True
191
- first_pass = self._recursive_subst(
192
- templ=self._data,
193
- current_path=path,
194
- relative_path_anchor=path,
195
- level=level,
196
- )
197
- if self._data is self._source:
198
- self._source = first_pass
199
- self._data = first_pass
200
- self._ignore_templates = False
201
165
  return self._recursive_subst(
202
- templ=self._data,
166
+ templ=template or data,
203
167
  current_path=path,
204
168
  relative_path_anchor=path,
205
- level=level,
169
+ level=0,
170
+ current_chain=[path],
206
171
  )
207
172
 
208
- def _recursive_subst(self, templ, current_path: str, relative_path_anchor: str, level: int):
173
+ def _recursive_subst(self, templ, current_path: str, relative_path_anchor: str, level: int, current_chain: list[str]):
174
+
175
+ def get_code_value(match: _re.Match | str):
209
176
 
210
- def get_code_value(code_str: str):
177
+ def getter_function(path: str, default: Any = None, search: bool = False):
178
+ value, matched = get_address_value(path, return_all_matches=search, from_code=True)
179
+ if matched:
180
+ return value
181
+ if search:
182
+ return []
183
+ return default
184
+
185
+ code_str = match if isinstance(match, str) else match.group(1)
211
186
  code_lines = ["def __inline_code__():"]
212
187
  code_lines.extend([f" {line}" for line in code_str.strip("\n").splitlines()])
213
188
  code_str_full = "\n".join(code_lines)
214
- global_context = {}
189
+ global_context = self._code_context.copy() | {self._getter_function_name: getter_function}
190
+ for name, partial_func_data in self._code_context_partial.items():
191
+ if isinstance(partial_func_data, tuple):
192
+ func, arg_name = partial_func_data
193
+ global_context[name] = _partial(func, **{arg_name: getter_function})
194
+ else:
195
+ global_context[name] = _partial(partial_func_data, getter_function)
196
+ for name, call_func in self._code_context_call.items():
197
+ global_context[name] = call_func(getter_function)
215
198
  local_context = {}
216
199
  try:
217
200
  exec(code_str_full, global_context, local_context)
@@ -222,8 +205,9 @@ class TemplateFiller:
222
205
  path_invalid=current_path,
223
206
  )
224
207
 
225
- def get_address_value(re_match, return_all_matches: bool = False):
226
- path, num_periods = self._remove_leading_periods(re_match.group(1).strip())
208
+ def get_address_value(match: _re.Match | str, return_all_matches: bool = False, from_code: bool = False):
209
+ raw_path = match if isinstance(match, str) else str(match.group(1))
210
+ path, num_periods = self._remove_leading_periods(raw_path.strip())
227
211
  if num_periods == 0:
228
212
  path = f"$.{path}" if self._add_prefix else path
229
213
  try:
@@ -233,13 +217,14 @@ class TemplateFiller:
233
217
  path_invalid=path,
234
218
  description_template="JSONPath expression {path_invalid} is invalid.",
235
219
  )
236
- if self._ignore_templates:
237
- path_fields = self._extract_fields(path_expr)
238
- has_template_key = any(field in self._template_keys for field in path_fields)
239
- if has_template_key:
240
- return re_match.string
241
220
  if num_periods:
242
- root_path_expr = _jsonpath.parse(relative_path_anchor)
221
+ if relative_path_anchor != current_path:
222
+ path_fields = self._extract_fields(_jsonpath.parse(current_path))
223
+ has_template_key = any(field in self._template_keys for field in path_fields)
224
+ anchor_path = relative_path_anchor if has_template_key else current_path
225
+ else:
226
+ anchor_path = current_path
227
+ root_path_expr = _jsonpath.parse(anchor_path)
243
228
  for period in range(num_periods):
244
229
  if isinstance(root_path_expr, _jsonpath.Root):
245
230
  raise_error(
@@ -250,18 +235,24 @@ class TemplateFiller:
250
235
  ),
251
236
  )
252
237
  root_path_expr = root_path_expr.left
253
- path_expr = root_path_expr.child(path_expr)
254
- value, matched = get_value(path_expr, return_all_matches)
238
+ path_expr = self._concat_json_paths(root_path_expr, path_expr)
239
+ value, matched = get_value(path_expr, return_all_matches, from_code)
240
+ if from_code:
241
+ return value, matched
255
242
  if matched:
256
243
  return value
257
244
  if self._leave_no_match:
258
- return re_match.group()
245
+ return match.group()
259
246
  return self._no_match_value
260
247
 
261
- def get_value(jsonpath, return_all_matches: bool) -> tuple[Any, bool]:
248
+ def get_value(jsonpath, return_all_matches: bool, from_code: bool) -> tuple[Any, bool]:
262
249
  matches = _rec_match(jsonpath)
263
250
  if not matches:
264
- if not return_all_matches and self._raise_no_match:
251
+ if from_code:
252
+ return None, False
253
+ if return_all_matches:
254
+ return [], True
255
+ if self._raise_no_match:
265
256
  raise_error(
266
257
  path_invalid=str(jsonpath),
267
258
  description_template="JSONPath expression {path_invalid} did not match any data.",
@@ -269,8 +260,6 @@ class TemplateFiller:
269
260
  return None, False
270
261
  values = [m.value for m in matches]
271
262
  output = values if return_all_matches or len(values) > 1 else values[0]
272
- if not self._recursive:
273
- return output, True
274
263
  if relative_path_anchor == current_path:
275
264
  path_fields = self._extract_fields(jsonpath)
276
265
  has_template_key = any(field in self._template_keys for field in path_fields)
@@ -282,10 +271,11 @@ class TemplateFiller:
282
271
  current_path=str(jsonpath),
283
272
  relative_path_anchor=_rel_path_anchor,
284
273
  level=0,
274
+ current_chain=current_chain + [str(jsonpath)],
285
275
  ), True
286
276
 
287
277
  def _rec_match(expr) -> list:
288
- matches = expr.find(self._source)
278
+ matches = expr.find(self._data)
289
279
  if matches:
290
280
  return matches
291
281
  if isinstance(expr.left, _jsonpath.Root):
@@ -298,6 +288,7 @@ class TemplateFiller:
298
288
  current_path=str(expr.left),
299
289
  relative_path_anchor=str(expr.left),
300
290
  level=0,
291
+ current_chain=current_chain + [str(expr.left)],
301
292
  ) if isinstance(left_match.value, str) else left_match.value
302
293
  right_matches = expr.right.find(left_match_filled)
303
294
  whole_matches.extend(right_matches)
@@ -306,76 +297,102 @@ class TemplateFiller:
306
297
  def get_relative_path(new_path):
307
298
  return new_path if current_path == relative_path_anchor else relative_path_anchor
308
299
 
309
- def raise_error(
310
- path_invalid: str,
311
- description_template: str,
312
- ):
300
+ def fill_nested_values(match: _re.Match | str):
301
+ pattern_nested = self._get_value_regex_pattern(level=level + 1)
302
+ return pattern_nested.sub(
303
+ lambda x: self._recursive_subst(
304
+ templ=x.group(),
305
+ current_path=current_path,
306
+ relative_path_anchor=get_relative_path(current_path),
307
+ level=level + 1,
308
+ current_chain=current_chain,
309
+ ),
310
+ match if isinstance(match, str) else match.group(1),
311
+ )
312
+
313
+ def string_filler_unpack(match: _re.Match):
314
+ path = str(match.group(1)).strip()
315
+ match_list = self._pattern_list.fullmatch(path)
316
+ if match_list:
317
+ values = get_address_value(match_list, return_all_matches=True)
318
+ else:
319
+ match_code = self._pattern_code.fullmatch(path)
320
+ if match_code:
321
+ values = get_code_value(match_code)
322
+ else:
323
+ values = get_address_value(path)
324
+ return self._unpack_string_joiner.join([self._stringer(val) for val in values])
325
+
326
+ def raise_error(path_invalid: str, description_template: str):
313
327
  raise _exception.update.PySerialsUpdateTemplatedDataError(
314
328
  description_template=description_template,
315
329
  path_invalid=path_invalid,
316
330
  path=current_path,
317
331
  data=templ,
318
332
  data_full=self._data,
319
- data_source=self._source,
333
+ data_source=self._data,
320
334
  template_start=self._marker_start_value,
321
335
  template_end=self._marker_end_value,
322
336
  )
323
337
 
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])
338
+ # if not internal:
339
+ # self._path_history.append(current_path)
340
+ # loop = self._find_loop()
341
+ # if loop:
342
+ # loop_str = "\n".join([f"- {path.replace("'", "")}" for path in loop])
343
+ # raise _exception.update.PySerialsUpdateTemplatedDataError(
344
+ # description_template=f"Path {{path_invalid}} starts a loop: {loop_str}",
345
+ # path_invalid=loop[0],
346
+ # path=current_path,
347
+ # data=templ,
348
+ # data_full=self._data,
349
+ # data_source=self._source,
350
+ # template_start=self._marker_start_value,
351
+ # template_end=self._marker_end_value,
352
+ # )
335
353
 
336
354
  if isinstance(templ, str):
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
- ),
345
- templ
346
- )
355
+ # Handle value blocks
347
356
  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())
357
+ match_value = pattern_value.fullmatch(templ)
358
+ if match_value:
359
+ return get_address_value(fill_nested_values(match_value))
360
+ # Handle list blocks
361
+ match_list = self._pattern_list.fullmatch(templ)
362
+ if match_list:
363
+ return get_address_value(fill_nested_values(match_list), return_all_matches=True)
364
+ # Handle code blocks
365
+ match_code = self._pattern_code.fullmatch(templ)
366
+ if match_code:
367
+ return get_code_value(match_code)
368
+ # Handle unpack blocks
369
+ match_unpack = self._pattern_unpack.fullmatch(templ)
370
+ if match_unpack:
371
+ unpack_value = match_unpack.group(1)
372
+ submatch_code = self._pattern_code.fullmatch(unpack_value)
364
373
  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))),
374
+ return get_code_value(submatch_code)
375
+ unpack_value = fill_nested_values(unpack_value)
376
+ submatch_list = self._pattern_list.fullmatch(unpack_value)
377
+ if submatch_list:
378
+ return get_address_value(unpack_value, return_all_matches=True)
379
+ return get_address_value(unpack_value)
380
+ # Handle strings
381
+ code_blocks_filled = self._pattern_code.sub(
382
+ lambda x: self._stringer(get_code_value(x)),
383
+ templ
384
+ )
385
+ nested_values_filled = fill_nested_values(code_blocks_filled)
386
+ unpacked_filled = self._pattern_unpack.sub(string_filler_unpack, nested_values_filled)
387
+ lists_filled = self._pattern_list.sub(
388
+ lambda x: self._stringer(get_address_value(x)),
377
389
  unpacked_filled
378
390
  )
391
+ templ_values_filled = pattern_value.sub(
392
+ lambda x: self._stringer(get_address_value(x)),
393
+ lists_filled
394
+ )
395
+ return templ_values_filled
379
396
 
380
397
  if isinstance(templ, list):
381
398
  out = []
@@ -386,8 +403,9 @@ class TemplateFiller:
386
403
  current_path=new_path,
387
404
  relative_path_anchor=get_relative_path(new_path),
388
405
  level=0,
406
+ current_chain=current_chain + [new_path],
389
407
  )
390
- if isinstance(elem, str) and self._pattern_unpack.fullmatch(elem.strip()):
408
+ if isinstance(elem, str) and self._pattern_unpack.fullmatch(elem):
391
409
  out.extend(elem_filled)
392
410
  else:
393
411
  out.append(elem_filled)
@@ -401,8 +419,9 @@ class TemplateFiller:
401
419
  current_path=current_path,
402
420
  relative_path_anchor=relative_path_anchor,
403
421
  level=0,
422
+ current_chain=current_chain,
404
423
  )
405
- if isinstance(key, str) and self._pattern_unpack.fullmatch(key.strip()):
424
+ if isinstance(key, str) and self._pattern_unpack.fullmatch(key):
406
425
  new_dict.update(key_filled)
407
426
  continue
408
427
  if key_filled in self._template_keys:
@@ -414,10 +433,33 @@ class TemplateFiller:
414
433
  current_path=new_path,
415
434
  relative_path_anchor=get_relative_path(new_path),
416
435
  level=0,
436
+ current_chain=current_chain + [new_path],
417
437
  )
418
438
  return new_dict
419
439
  return templ
420
440
 
441
+ # def _find_loop(self):
442
+ # for pattern_length in range(1, len(self._path_history) // 2 + 1):
443
+ # # Slice the end of the list into two consecutive patterns
444
+ # pattern = self._path_history[-pattern_length:]
445
+ # previous_pattern = self._path_history[-2 * pattern_length:-pattern_length]
446
+ # # Check if the two patterns are the same
447
+ # if pattern == previous_pattern:
448
+ # pattern.insert(0, pattern[-1])
449
+ # return pattern
450
+ # return
451
+
452
+ def _get_value_regex_pattern(self, level: int = 0) -> _RegexPattern:
453
+ if level in self._pattern_value:
454
+ return self._pattern_value[level]
455
+ count = self._repeater_count_value + level
456
+ pattern = _RegexPattern(
457
+ start=f"{self._marker_start_value}{self._repeater_start_value * count} ",
458
+ end=f" {self._repeater_end_value * count}{self._marker_end_value}",
459
+ )
460
+ self._pattern_value[level] = pattern
461
+ return pattern
462
+
421
463
  @staticmethod
422
464
  def _remove_leading_periods(s: str) -> (str, int):
423
465
  match = _re.match(r"^(\.*)(.*)", s)
@@ -444,6 +486,10 @@ class TemplateFiller:
444
486
  _recursive_extract(jsonpath)
445
487
  return fields
446
488
 
489
+ def _concat_json_paths(self, path1, path2):
490
+ if not isinstance(path2, _jsonpath.Child):
491
+ return _jsonpath.Child(path1, path2)
492
+ return _jsonpath.Child(self._concat_json_paths(path1, path2.left), path2.right)
447
493
 
448
494
  class _RegexPattern:
449
495
 
@@ -458,8 +504,8 @@ class _RegexPattern:
458
504
  matches = self.pattern.findall(string)
459
505
  if len(matches) == 1:
460
506
  # Verify the match spans the entire string
461
- return self.pattern.fullmatch(string)
507
+ return self.pattern.fullmatch(string.strip())
462
508
  return None
463
509
 
464
510
  def sub(self, repl, string: str) -> str:
465
- return self.pattern.sub(repl, string)
511
+ return self.pattern.sub(repl, string)