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