PySerials 0.0.0.dev3__tar.gz → 0.0.0.dev5__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 (28) hide show
  1. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/PKG-INFO +5 -1
  2. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/pyproject.toml +5 -1
  3. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/PySerials.egg-info/PKG-INFO +5 -1
  4. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/PySerials.egg-info/SOURCES.txt +2 -0
  5. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/PySerials.egg-info/requires.txt +4 -0
  6. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/pyserials/__init__.py +2 -1
  7. pyserials-0.0.0.dev5/src/pyserials/compare.py +29 -0
  8. pyserials-0.0.0.dev5/src/pyserials/exception/_base.py +58 -0
  9. pyserials-0.0.0.dev5/src/pyserials/exception/read.py +211 -0
  10. pyserials-0.0.0.dev5/src/pyserials/exception/update.py +139 -0
  11. pyserials-0.0.0.dev5/src/pyserials/exception/validate.py +243 -0
  12. pyserials-0.0.0.dev5/src/pyserials/nested_dict.py +124 -0
  13. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/pyserials/read.py +53 -16
  14. pyserials-0.0.0.dev5/src/pyserials/update.py +243 -0
  15. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/pyserials/validate.py +22 -11
  16. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/pyserials/write.py +27 -11
  17. pyserials-0.0.0.dev3/src/pyserials/exception/_base.py +0 -10
  18. pyserials-0.0.0.dev3/src/pyserials/exception/read.py +0 -119
  19. pyserials-0.0.0.dev3/src/pyserials/exception/update.py +0 -131
  20. pyserials-0.0.0.dev3/src/pyserials/exception/validate.py +0 -73
  21. pyserials-0.0.0.dev3/src/pyserials/update.py +0 -157
  22. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/README.md +0 -0
  23. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/setup.cfg +0 -0
  24. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/PySerials.egg-info/dependency_links.txt +0 -0
  25. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/PySerials.egg-info/not-zip-safe +0 -0
  26. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/PySerials.egg-info/top_level.txt +0 -0
  27. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/pyserials/exception/__init__.py +0 -0
  28. {pyserials-0.0.0.dev3 → pyserials-0.0.0.dev5}/src/pyserials/format.py +0 -0
@@ -1,8 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev3
3
+ Version: 0.0.0.dev5
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: jsonschema<5,>=4.21.0
6
6
  Requires-Dist: ruamel.yaml<0.18,>=0.17.32
7
7
  Requires-Dist: ruamel.yaml.string<1,>=0.1.1
8
8
  Requires-Dist: tomlkit<0.12,>=0.11.8
9
+ Requires-Dist: markitup
10
+ Requires-Dist: ansi-sgr
11
+ Requires-Dist: ExceptionMan
12
+ Requires-Dist: jsonpath-ng<2,>=1.6.1
@@ -17,12 +17,16 @@ namespaces = true
17
17
  # ----------------------------------------- Project Metadata -------------------------------------
18
18
  #
19
19
  [project]
20
- version = "0.0.0.dev3"
20
+ version = "0.0.0.dev5"
21
21
  name = "PySerials"
22
22
  dependencies = [
23
23
  "jsonschema >= 4.21.0, < 5",
24
24
  "ruamel.yaml >= 0.17.32, < 0.18", # https://yaml.readthedocs.io/en/stable/
25
25
  "ruamel.yaml.string >= 0.1.1, < 1",
26
26
  "tomlkit >= 0.11.8, < 0.12", # https://tomlkit.readthedocs.io/en/stable/,
27
+ "markitup",
28
+ "ansi-sgr",
29
+ "ExceptionMan",
30
+ "jsonpath-ng >= 1.6.1, < 2",
27
31
  ]
28
32
  requires-python = ">=3.10"
@@ -1,8 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev3
3
+ Version: 0.0.0.dev5
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: jsonschema<5,>=4.21.0
6
6
  Requires-Dist: ruamel.yaml<0.18,>=0.17.32
7
7
  Requires-Dist: ruamel.yaml.string<1,>=0.1.1
8
8
  Requires-Dist: tomlkit<0.12,>=0.11.8
9
+ Requires-Dist: markitup
10
+ Requires-Dist: ansi-sgr
11
+ Requires-Dist: ExceptionMan
12
+ Requires-Dist: jsonpath-ng<2,>=1.6.1
@@ -7,7 +7,9 @@ src/PySerials.egg-info/not-zip-safe
7
7
  src/PySerials.egg-info/requires.txt
8
8
  src/PySerials.egg-info/top_level.txt
9
9
  src/pyserials/__init__.py
10
+ src/pyserials/compare.py
10
11
  src/pyserials/format.py
12
+ src/pyserials/nested_dict.py
11
13
  src/pyserials/read.py
12
14
  src/pyserials/update.py
13
15
  src/pyserials/validate.py
@@ -2,3 +2,7 @@ jsonschema<5,>=4.21.0
2
2
  ruamel.yaml<0.18,>=0.17.32
3
3
  ruamel.yaml.string<1,>=0.1.1
4
4
  tomlkit<0.12,>=0.11.8
5
+ markitup
6
+ ansi-sgr
7
+ ExceptionMan
8
+ jsonpath-ng<2,>=1.6.1
@@ -1,3 +1,4 @@
1
1
  """PySerials"""
2
2
 
3
- from pyserials import exception, update, validate, read, write, format
3
+ from pyserials import exception, update, validate, read, write, format, compare
4
+ from pyserials.nested_dict import NestedDict
@@ -0,0 +1,29 @@
1
+ def items(source, target, path: str = "$"):
2
+ def recursive_compare(src, trg, curr_path):
3
+ if type(src) is not type(trg):
4
+ comp["modified"].append(curr_path)
5
+ return
6
+ if isinstance(src, dict):
7
+ for key in src:
8
+ if key not in trg:
9
+ comp["added"].append(f"{curr_path}.{key}")
10
+ continue
11
+ recursive_compare(src[key], trg[key], f"{curr_path}.{key}")
12
+ for key in trg:
13
+ if key not in src:
14
+ comp["removed"].append(f"{curr_path}.{key}")
15
+ return
16
+ if isinstance(src, (list, tuple)):
17
+ len_src = len(src)
18
+ len_trg = len(trg)
19
+ min_len = min(len_src, len_trg)
20
+ for i in range(min_len):
21
+ recursive_compare(src[i], trg[i], f"{curr_path}[{i}]")
22
+ for i in range(min_len, max(len_src, len_trg)):
23
+ comp["added" if len_src > len_trg else "removed"].append(f"{curr_path}[{i}]")
24
+ return
25
+ comp["unchanged" if src == trg else "modified"].append(curr_path)
26
+ return
27
+ comp = {"added": [], "removed": [], "modified": [], "unchanged": []}
28
+ recursive_compare(source, target, path)
29
+ return {key: sorted(comp[key]) for key in comp}
@@ -0,0 +1,58 @@
1
+ """PySerials base Exception class."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Literal as _Literal
5
+ import ansi_sgr as _sgr
6
+ from markitup import html as _html, doc as _doc
7
+ from exceptionman import ReporterException as _ReporterException
8
+
9
+
10
+ _ANSI_HEADING_STYLE = {
11
+ 1: _sgr.style(text_styles="bold", background_color="red"),
12
+ 2: _sgr.style(text_styles="bold", background_color="yellow"),
13
+ 3: _sgr.style(text_styles="bold", background_color="green"),
14
+ 4: _sgr.style(text_styles="bold", background_color="blue"),
15
+ 5: _sgr.style(text_styles="bold", background_color="magenta"),
16
+ 6: _sgr.style(text_styles="bold", background_color="cyan"),
17
+ }
18
+
19
+
20
+ class PySerialsException(_ReporterException):
21
+ """Base class for all exceptions raised by PySerials."""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ description: str | None = None,
27
+ message_html: str | _html.Element | None = None,
28
+ description_html: str | _html.Element | None = None,
29
+ report_heading: str = "PySerials Error Report",
30
+ ):
31
+ super().__init__(
32
+ message=message,
33
+ description=description,
34
+ message_html=message_html,
35
+ description_html=description_html,
36
+ report_heading=report_heading,
37
+ )
38
+ return
39
+
40
+
41
+ def ansi_heading(section: list[int], title: str) -> str:
42
+ """Get an ANSI SGR formatted heading."""
43
+ sec = ".".join(str(n) for n in section)
44
+ sec_level = min(len(section), 6)
45
+ heading = f" {sec}. {title} "
46
+ return _sgr.format(heading, control_sequence=_ANSI_HEADING_STYLE[sec_level])
47
+
48
+
49
+ def ansi_bold(text: str) -> str:
50
+ return _sgr.format(text, control_sequence=_sgr.style(text_styles="bold"))
51
+
52
+
53
+ def format_code(code: str) -> tuple[str, str]:
54
+ console = _sgr.format(
55
+ code, control_sequence=_sgr.style(text_color=(220, 220, 220), background_color=(20, 20, 20))
56
+ )
57
+ html = str(_html.elem.code(code))
58
+ return console, html
@@ -0,0 +1,211 @@
1
+ """Exceptions raised by `pyserials.read` module."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Literal as _Literal
5
+ from pathlib import Path as _Path
6
+
7
+ import ruamel.yaml as _yaml
8
+ import json as _json
9
+
10
+ from markitup import html as _html, md as _md
11
+ from tomlkit.exceptions import TOMLKitError as _TOMLKitError
12
+
13
+ from pyserials.exception._base import PySerialsException as _PySerialsException
14
+
15
+
16
+ class PySerialsReadException(_PySerialsException):
17
+ """Base class for all exceptions raised by `pyserials.read` module.
18
+
19
+ Attributes
20
+ ----------
21
+ source_type : {"file", "string"}
22
+ Type of source from which data was read.
23
+ data_type : {"json", "yaml", "toml"} or None
24
+ Type of input data, if known.
25
+ filepath : pathlib.Path or None
26
+ Path to the input datafile, if data was read from a file.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ source_type: _Literal["file", "string"],
32
+ description: str,
33
+ description_html: str | _html.Element | None = None,
34
+ data_type: _Literal["json", "yaml", "toml"] | None = None,
35
+ filepath: _Path | None = None,
36
+ ):
37
+ if source_type == "string":
38
+ source = source_html = "string"
39
+ else:
40
+ source = f"file at '{filepath}'"
41
+ source_html = f"file at <code>{filepath}</code>"
42
+ data_ = f"{data_type.upper()} data" if data_type else "data"
43
+ message_template = f"Failed to read {data_} from input {{source}}."
44
+ super().__init__(
45
+ message=message_template.format(source=source),
46
+ message_html=message_template.format(source=source_html),
47
+ description=description,
48
+ description_html=description_html,
49
+ report_heading="PySerials Read Error Report",
50
+ )
51
+ self.source_type: _Literal["file", "string"] = source_type
52
+ self.data_type: _Literal["json", "yaml", "toml"] | None = data_type
53
+ self.filepath: _Path | None = filepath
54
+ return
55
+
56
+
57
+ class PySerialsEmptyStringError(PySerialsReadException):
58
+ """Exception raised when a string to be read is empty."""
59
+
60
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"]):
61
+ description = f"The string is empty."
62
+ super().__init__(description=description, source_type="string", data_type=data_type)
63
+ return
64
+
65
+
66
+ class PySerialsInvalidFileExtensionError(PySerialsReadException):
67
+ """Exception raised when a file to be read has an unrecognized extension."""
68
+
69
+ def __init__(self, filepath: _Path):
70
+ description = (
71
+ f"The file extension must be one of 'json', 'yaml', 'yml', or '.toml', "
72
+ f"but got '{filepath.suffix.removeprefix('.')}'. "
73
+ "Please provide the extension explicitly, or rename the file to have a valid extension."
74
+ )
75
+ super().__init__(description=description, source_type="file", filepath=filepath)
76
+ return
77
+
78
+
79
+ class PySerialsMissingFileError(PySerialsReadException):
80
+ """Exception raised when a file to be read does not exist."""
81
+
82
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
83
+ description = f"The file does not exist."
84
+ super().__init__(description=description, source_type="file", data_type=data_type, filepath=filepath)
85
+ return
86
+
87
+
88
+ class PySerialsEmptyFileError(PySerialsReadException):
89
+ """Exception raised when a file to be read is empty."""
90
+
91
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
92
+ description = f"The file is empty."
93
+ super().__init__(description=description, source_type="file", data_type=data_type, filepath=filepath)
94
+ return
95
+
96
+
97
+ class PySerialsInvalidDataError(PySerialsReadException):
98
+ """Exception raised when the data is invalid.
99
+
100
+ Attributes
101
+ ----------
102
+ data : str
103
+ The input data that was supposed to be read.
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ source_type: _Literal["file", "string"],
109
+ data_type: _Literal["json", "yaml", "toml"],
110
+ data: str,
111
+ cause: Exception,
112
+ filepath: _Path | None = None,
113
+ ):
114
+ self.data = data
115
+ self.cause = cause
116
+ self.problem: str = str(cause)
117
+ self.problem_line: int | None = None
118
+ self.problem_column: int | None = None
119
+ self.problem_data_type: str | None = None
120
+ self.context: str | None = None
121
+ self.context_line: int | None = None
122
+ self.context_column: int | None = None
123
+ self.context_data_type: str | None = None
124
+
125
+ if isinstance(cause, _yaml.YAMLError):
126
+ self.problem_line = cause.problem_mark.line + 1
127
+ self.problem_column = cause.problem_mark.column + 1
128
+ self.problem_data_type = cause.problem_mark.name
129
+ self.problem = cause.problem.strip()
130
+ if cause.context:
131
+ self.context = cause.context.strip()
132
+ self.context_line = cause.context_mark.line + 1
133
+ self.context_column = cause.context_mark.column + 1
134
+ self.context_data_type = cause.context_mark.name
135
+ elif isinstance(cause, _json.JSONDecodeError):
136
+ self.problem = cause.msg
137
+ self.problem_line = cause.lineno
138
+ self.problem_column = cause.colno
139
+ elif isinstance(cause, _TOMLKitError):
140
+ self.problem_line = cause.line
141
+ self.problem_column = cause.col
142
+ self.problem = cause.args[0].removesuffix(f" at line {self.problem_line} col {self.problem_column}")
143
+ self.problem = self.problem.strip().capitalize().removesuffix(".")
144
+ description = "The data is not valid"
145
+ if self.problem_line:
146
+ description += f" at line {self.problem_line}, column {self.problem_column}"
147
+ description += f": {self.problem}."
148
+ super().__init__(description=description, source_type=source_type, data_type=data_type, filepath=filepath)
149
+ return
150
+
151
+ def _report_content(self, mode: _Literal["full", "short"], md: bool) -> _html.elem.Ul:
152
+
153
+ def make_tabel(problem, line, column, data_type):
154
+ rows = [
155
+ [title, value] for title, value in [
156
+ ["Description", problem],
157
+ ["Line Number", line],
158
+ ["Column Number", column],
159
+ ["Data Type", data_type],
160
+ ] if value is not None
161
+ ]
162
+ return _html.elem.table_from_rows(rows_body=rows)
163
+
164
+ content_list = [
165
+ _html.elem.details(
166
+ [
167
+ _html.elem.summary("❌ Problem"),
168
+ make_tabel(self.problem, self.problem_line, self.problem_column, self.problem_data_type)
169
+ ]
170
+ ),
171
+ ]
172
+ if self.context:
173
+ content_list.append(
174
+ _html.elem.details(
175
+ [
176
+ _html.elem.summary("🔍 Context"),
177
+ make_tabel(self.context, self.context_line, self.context_column, self.context_data_type)
178
+ ]
179
+ ),
180
+ )
181
+ data_lines = self.data.splitlines()
182
+ if self.problem_line:
183
+ line_idx = self.problem_line - 1
184
+ problem_line = data_lines[line_idx]
185
+ if md:
186
+ problem_line = f"- {problem_line}"
187
+ else:
188
+ if self.problem_column:
189
+ col_idx = self.problem_column - 1
190
+ prob_col = problem_line[col_idx]
191
+ prob_col_highlight = _html.elem.span(prob_col, {"class": "highlight-char"})
192
+ problem_line = f"{problem_line[:col_idx]}{prob_col_highlight}{problem_line[col_idx+1:]}"
193
+ problem_line = str(_html.elem.span(problem_line, {"class": "highlight-line"}))
194
+ data_lines[line_idx] = problem_line
195
+ if mode == "short":
196
+ if self.problem_line is not None:
197
+ problem_line_idx = self.problem_line - 1
198
+ start_line = max(0, problem_line_idx)
199
+ end_line = min(len(data_lines), problem_line_idx + 1)
200
+ if self.context_line is not None:
201
+ context_line_idx = self.context_line - 1
202
+ start_line = max(0, context_line_idx, problem_line_idx)
203
+ end_line = min(len(data_lines), context_line_idx + 1, problem_line_idx + 1)
204
+ data_lines = data_lines[start_line:end_line]
205
+ data = "\n".join(data_lines)
206
+ if md:
207
+ code_block = _md.elem.code_fence(data, info="diff")
208
+ else:
209
+ code_block = _html.elem.pre(_html.elem.code(data, {"class": f"language-{self.data_type}"}))
210
+ content_list.append(_html.elem.details([_html.elem.summary("📄 Data"), code_block]))
211
+ return _html.elem.ul([_html.elem.li(content) for content in content_list])
@@ -0,0 +1,139 @@
1
+ """Exceptions raised by `pyserials.update` module."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Any as _Any, Literal as _Literal
5
+
6
+ from markitup.html import elem as _html
7
+
8
+ from pyserials.exception import _base
9
+
10
+
11
+ class PySerialsUpdateException(_base.PySerialsException):
12
+ """Base class for all exceptions raised by `pyserials.update` module.
13
+
14
+ Attributes
15
+ ----------
16
+ path : str
17
+ JSONPath to where the update failed.
18
+ data : dict | list | str | int | float | bool
19
+ Data that failed to update.
20
+ data_full : dict | list | str | int | float | bool
21
+ Full data input.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ path: str,
27
+ data: dict | list | str | int | float | bool,
28
+ data_full: dict | list | str | int | float | bool,
29
+ description: str,
30
+ description_html: str | _html.Element | None = None,
31
+ ):
32
+ message_template = "Failed to update data at {path}."
33
+ path_console, path_html = _base.format_code(path)
34
+ super().__init__(
35
+ message=message_template.format(path=path_console),
36
+ message_html=message_template.format(path=path_html),
37
+ description=description,
38
+ description_html=description_html,
39
+ report_heading="PySerials Update Error Report",
40
+ )
41
+ self.path = path
42
+ self.data = data
43
+ self.data_full = data_full
44
+ return
45
+
46
+
47
+ class PySerialsUpdateDictFromAddonError(PySerialsUpdateException):
48
+ """Base class for all exceptions raised by `pyserials.update.dict_from_addon`.
49
+
50
+ Attributes
51
+ ----------
52
+ data_addon : Any
53
+ Value of the failed data in the addon dictionary.
54
+ data_addon_full : dictionary
55
+ Full addon input.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ problem_type: _Literal["duplicate", "type_mismatch"],
61
+ path: str,
62
+ data: _Any,
63
+ data_full: dict,
64
+ data_addon: _Any,
65
+ data_addon_full: dict,
66
+ ):
67
+ self.type_data = type(data)
68
+ self.type_data_addon = type(data_addon)
69
+ type_data_console, type_data_html = _base.format_code(self.type_data.__name__)
70
+ type_data_addon_console, type_data_addon_html = _base.format_code(self.type_data_addon.__name__)
71
+ path_console, path_html = _base.format_code(path)
72
+ kwargs_console, kwargs_html = (
73
+ {"path": path, "type_data": type_data, "type_data_addon": type_data_addon}
74
+ for path, type_data, type_data_addon in zip(
75
+ (path_console, path_html),
76
+ (type_data_console, type_data_html),
77
+ (type_data_addon_console, type_data_addon_html),
78
+ )
79
+ )
80
+ description_template = (
81
+ "There was a duplicate in the addon dictionary; "
82
+ "the value of type {type_data_addon} already exists in the source data."
83
+ ) if problem_type == "duplicate" else (
84
+ "There was a type mismatch between the source and addon dictionary values; "
85
+ "the value is of type {type_data} in the source data, "
86
+ "but of type {type_data_addon} in the addon data."
87
+ )
88
+ super().__init__(
89
+ description=description_template.format(**kwargs_console),
90
+ description_html=description_template.format(**kwargs_html),
91
+ path=path,
92
+ data=data,
93
+ data_full=data_full,
94
+ )
95
+ self.problem_type: _Literal["duplicate", "type_mismatch"] = problem_type
96
+ self.data_addon = data_addon
97
+ self.data_addon_full = data_addon_full
98
+ return
99
+
100
+
101
+ class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
102
+ """Exception raised when updating templated data fails.
103
+
104
+ Attributes
105
+ ----------
106
+ path_invalid : str
107
+ JSONPath that caused the update to fail.
108
+ data_source : dict
109
+ Source data that was used to update the template.
110
+ template_start : str
111
+ The start marker of the template.
112
+ template_end : str
113
+ The end marker of the template.
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ description_template: str,
119
+ path_invalid: str,
120
+ path: str,
121
+ data: str,
122
+ data_full: dict | list | str | int | float | bool,
123
+ data_source: dict,
124
+ template_start: str,
125
+ template_end: str,
126
+ ):
127
+ path_invalid_console, path_invalid_html = _base.format_code(path_invalid.replace("'", ""))
128
+ super().__init__(
129
+ description=description_template.format(path_invalid=path_invalid_console),
130
+ description_html=description_template.format(path_invalid=path_invalid_html),
131
+ path=path.replace("'", ""),
132
+ data=data,
133
+ data_full=data_full,
134
+ )
135
+ self.path_invalid = path_invalid
136
+ self.data_source = data_source
137
+ self.template_start = template_start
138
+ self.template_end = template_end
139
+ return