lupaxa-data-converter 0.1.0__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.
@@ -0,0 +1,24 @@
1
+ # OS
2
+ .DS_Store
3
+
4
+ # Cursor
5
+ .cursor/
6
+ .superpowers/
7
+ .worktrees/
8
+ cursor-docs
9
+
10
+ # Make
11
+ .makefiles/
12
+
13
+ # MkDocs
14
+ site/
15
+
16
+ # Python
17
+ .venv/
18
+ __pycache__/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .pytest_cache/
22
+ dist/
23
+ build/
24
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) The Lupaxa Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: lupaxa-data-converter
3
+ Version: 0.1.0
4
+ Summary: Python library that converts mappings between JSON, JSON5, XML, YAML, and TOML.
5
+ Project-URL: Homepage, https://data-converter.thelupaxaproject.org/
6
+ Project-URL: Documentation, https://data-converter.thelupaxaproject.org/
7
+ Project-URL: Repository, https://github.com/lupaxa-developers-toolbox/data-converter
8
+ Project-URL: Issues, https://github.com/lupaxa-developers-toolbox/data-converter/issues
9
+ Author: The Lupaxa Project
10
+ License: MIT License
11
+
12
+ Copyright (c) The Lupaxa Project
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENCE
32
+ Keywords: data-conversion,json,json5,toml,xml,yaml
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: 3.14
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Topic :: Text Processing :: Markup
45
+ Classifier: Topic :: Utilities
46
+ Requires-Python: >=3.10
47
+ Requires-Dist: defusedxml>=0.7
48
+ Requires-Dist: json5>=0.12
49
+ Requires-Dist: pyyaml>=6.0
50
+ Requires-Dist: tomli-w>=1.1
51
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
52
+ Provides-Extra: dev
53
+ Requires-Dist: bump-my-version>=1.2.0; extra == 'dev'
54
+ Requires-Dist: hatch>=1.12.0; extra == 'dev'
55
+ Requires-Dist: mkdocs-material==9.7.7; extra == 'dev'
56
+ Requires-Dist: mkdocs==1.6.1; extra == 'dev'
57
+ Requires-Dist: mypy>=1.12.0; extra == 'dev'
58
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
59
+ Requires-Dist: pytest>=8.0; extra == 'dev'
60
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
61
+ Provides-Extra: test
62
+ Requires-Dist: pytest-cov>=5.0; extra == 'test'
63
+ Requires-Dist: pytest>=8.0; extra == 'test'
64
+ Description-Content-Type: text/markdown
65
+
66
+ <!-- markdownlint-disable -->
67
+ <p align="center">
68
+ <a href="https://github.com/lupaxa-developers-toolbox">
69
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/developers-toolbox/readme-logo.png" alt="Project Logo" width="256"/><br/>
70
+ </a>
71
+ </p>
72
+ <h3 align="center">
73
+ The Lupaxa Developers Toolbox<br />
74
+ Part of The Lupaxa Project
75
+ </h3>
76
+
77
+ <br />
78
+
79
+ # lupaxa-data-converter
80
+
81
+ Python library that loads JSON, JSON5, XML, YAML, or TOML into a
82
+ dictionary, then writes any of those formats.
83
+
84
+ Built for scripts and tools used by The Lupaxa Project.
85
+
86
+ The PyPI name is `lupaxa-data-converter`. The import path is
87
+ `lupaxa.data_converter`. This is a library only — there is no console
88
+ script and no `python -m` entry point.
89
+
90
+ ## Features
91
+
92
+ - Load JSON, JSON5, XML, YAML, TOML, or a dictionary into one `DataConverter`
93
+ - Dump the loaded mapping to any of those formats
94
+ - Two passes through a standard dict — no pairwise converters
95
+ - Fully typed, linted, formatted, and tested
96
+
97
+ ## Installation
98
+
99
+ ### From PyPI
100
+
101
+ ```bash
102
+ pip install lupaxa-data-converter
103
+ ```
104
+
105
+ ### From source (development mode)
106
+
107
+ ```bash
108
+ pip install -e ".[dev]"
109
+ ```
110
+
111
+ Requires Python 3.10+. Runtime dependencies: `PyYAML`, `defusedxml`,
112
+ `json5`, and `tomli-w` (`tomli` on Python 3.10).
113
+ `lupaxa` is a namespace package — there is no `lupaxa/__init__.py`.
114
+
115
+ ## Usage
116
+
117
+ ```python
118
+ from lupaxa.data_converter import DataConverter
119
+
120
+ payload = {"name": "Lupaxa", "formats": ["json", "json5", "xml", "yaml", "toml"]}
121
+ converter = DataConverter(payload, data_type="dict")
122
+
123
+ print(converter.to_json())
124
+ print(converter.to_json5())
125
+ print(converter.to_xml())
126
+ print(converter.to_yaml())
127
+ print(converter.to_toml())
128
+ ```
129
+
130
+ ## Development
131
+
132
+ Clone the repository and install with Make:
133
+
134
+ ```bash
135
+ make init # first-time makefile-skills checkout
136
+ make python-install-dev # editable install with [dev]
137
+ make python-check # lint, type-check, and test
138
+ make mkdocs-serve # local docs site
139
+ ```
140
+
141
+ Documentation: <https://data-converter.thelupaxaproject.org/>.
142
+
143
+ <a href="https://github.com/the-lupaxa-project">
144
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
145
+ </a>
@@ -0,0 +1,80 @@
1
+ <!-- markdownlint-disable -->
2
+ <p align="center">
3
+ <a href="https://github.com/lupaxa-developers-toolbox">
4
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/developers-toolbox/readme-logo.png" alt="Project Logo" width="256"/><br/>
5
+ </a>
6
+ </p>
7
+ <h3 align="center">
8
+ The Lupaxa Developers Toolbox<br />
9
+ Part of The Lupaxa Project
10
+ </h3>
11
+
12
+ <br />
13
+
14
+ # lupaxa-data-converter
15
+
16
+ Python library that loads JSON, JSON5, XML, YAML, or TOML into a
17
+ dictionary, then writes any of those formats.
18
+
19
+ Built for scripts and tools used by The Lupaxa Project.
20
+
21
+ The PyPI name is `lupaxa-data-converter`. The import path is
22
+ `lupaxa.data_converter`. This is a library only — there is no console
23
+ script and no `python -m` entry point.
24
+
25
+ ## Features
26
+
27
+ - Load JSON, JSON5, XML, YAML, TOML, or a dictionary into one `DataConverter`
28
+ - Dump the loaded mapping to any of those formats
29
+ - Two passes through a standard dict — no pairwise converters
30
+ - Fully typed, linted, formatted, and tested
31
+
32
+ ## Installation
33
+
34
+ ### From PyPI
35
+
36
+ ```bash
37
+ pip install lupaxa-data-converter
38
+ ```
39
+
40
+ ### From source (development mode)
41
+
42
+ ```bash
43
+ pip install -e ".[dev]"
44
+ ```
45
+
46
+ Requires Python 3.10+. Runtime dependencies: `PyYAML`, `defusedxml`,
47
+ `json5`, and `tomli-w` (`tomli` on Python 3.10).
48
+ `lupaxa` is a namespace package — there is no `lupaxa/__init__.py`.
49
+
50
+ ## Usage
51
+
52
+ ```python
53
+ from lupaxa.data_converter import DataConverter
54
+
55
+ payload = {"name": "Lupaxa", "formats": ["json", "json5", "xml", "yaml", "toml"]}
56
+ converter = DataConverter(payload, data_type="dict")
57
+
58
+ print(converter.to_json())
59
+ print(converter.to_json5())
60
+ print(converter.to_xml())
61
+ print(converter.to_yaml())
62
+ print(converter.to_toml())
63
+ ```
64
+
65
+ ## Development
66
+
67
+ Clone the repository and install with Make:
68
+
69
+ ```bash
70
+ make init # first-time makefile-skills checkout
71
+ make python-install-dev # editable install with [dev]
72
+ make python-check # lint, type-check, and test
73
+ make mkdocs-serve # local docs site
74
+ ```
75
+
76
+ Documentation: <https://data-converter.thelupaxaproject.org/>.
77
+
78
+ <a href="https://github.com/the-lupaxa-project">
79
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
80
+ </a>
@@ -0,0 +1,51 @@
1
+ <p align="center">
2
+ <a href="https://github.com/lupaxa-developers-toolbox">
3
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/developers-toolbox/readme-logo.png" alt="Developers Toolbox" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">data-converter</h1>
8
+
9
+ Python library that loads JSON, JSON5, XML, YAML, or TOML into a
10
+ dictionary, then writes any of those formats.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install lupaxa-data-converter
16
+ ```
17
+
18
+ Requires Python 3.10+.
19
+
20
+ ## Usage
21
+
22
+ ```python
23
+ from lupaxa.data_converter import DataConverter
24
+
25
+ payload = {"name": "Lupaxa", "formats": ["json", "json5", "xml", "yaml", "toml"]}
26
+ converter = DataConverter(payload, data_type="dict")
27
+
28
+ print(converter.to_json())
29
+ print(converter.to_json5())
30
+ print(converter.to_xml())
31
+ print(converter.to_yaml())
32
+ print(converter.to_toml())
33
+ ```
34
+
35
+ This is a library only. There is no console script and no
36
+ `python -m` entry point.
37
+
38
+ ## Documentation
39
+
40
+ Site pages live in `mkdocs/` and publish to
41
+ <https://data-converter.thelupaxaproject.org/>.
42
+
43
+ ```bash
44
+ make init
45
+ make python-install-dev
46
+ make mkdocs-serve
47
+ ```
48
+
49
+ <a href="https://github.com/the-lupaxa-project">
50
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
51
+ </a>
@@ -0,0 +1,23 @@
1
+ <p align="center">
2
+ <a href="https://github.com/lupaxa-developers-toolbox">
3
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/developers-toolbox/readme-logo.png" alt="Organisation Logo" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">Core Documentation</h1>
8
+
9
+ The following documents define the standards that apply across **The Lupaxa Project**:
10
+
11
+ | Document | Purpose |
12
+ | :------------------------------------ | :---------------------------------------------------------------- |
13
+ | [Code of Conduct](CODE_OF_CONDUCT.md) | Expected behaviour and reporting procedures. |
14
+ | [How to Contribute](CONTRIBUTING.md) | Development workflow, coding standards, and contribution process. |
15
+ | [Governance](GOVERNANCE.md) | Project governance, roles, and decision-making. |
16
+ | [Maintainers](MAINTAINERS.md) | Maintainer responsibilities and expectations. |
17
+ | [Projects Overview](PROJECTS.md) | Overview of organisations, repositories, and project structure. |
18
+ | [Security Policy](SECURITY.md) | Responsible disclosure and secure development practices. |
19
+ | [Support Guide](SUPPORT.md) | How to obtain help and support. |
20
+
21
+ <a href="https://github.com/the-lupaxa-project">
22
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
23
+ </a>
@@ -0,0 +1,104 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.18"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lupaxa-data-converter"
7
+ dynamic = ["version"]
8
+ description = "Python library that converts mappings between JSON, JSON5, XML, YAML, and TOML."
9
+ readme = "README-PYPI.md"
10
+ license = { file = "LICENCE" }
11
+ requires-python = ">=3.10"
12
+
13
+ authors = [
14
+ { name = "The Lupaxa Project" }
15
+ ]
16
+
17
+ keywords = ["json", "json5", "xml", "yaml", "toml", "data-conversion"]
18
+
19
+ dependencies = [
20
+ "PyYAML>=6.0",
21
+ "defusedxml>=0.7",
22
+ "json5>=0.12",
23
+ "tomli>=2.0; python_version < '3.11'",
24
+ "tomli-w>=1.1",
25
+ ]
26
+
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Operating System :: OS Independent",
32
+ "Programming Language :: Python :: 3",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Programming Language :: Python :: 3.14",
38
+ "Topic :: Software Development :: Libraries :: Python Modules",
39
+ "Topic :: Text Processing :: Markup",
40
+ "Topic :: Utilities",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://data-converter.thelupaxaproject.org/"
45
+ Documentation = "https://data-converter.thelupaxaproject.org/"
46
+ Repository = "https://github.com/lupaxa-developers-toolbox/data-converter"
47
+ Issues = "https://github.com/lupaxa-developers-toolbox/data-converter/issues"
48
+
49
+ [project.optional-dependencies]
50
+ dev = [
51
+ "pytest>=8.0",
52
+ "pytest-cov>=5.0",
53
+ "ruff>=0.6.0",
54
+ "mypy>=1.12.0",
55
+ "hatch>=1.12.0",
56
+ "bump-my-version>=1.2.0",
57
+ "mkdocs==1.6.1",
58
+ "mkdocs-material==9.7.7",
59
+ ]
60
+ test = [
61
+ "pytest>=8.0",
62
+ "pytest-cov>=5.0",
63
+ ]
64
+
65
+ [tool.hatch.version]
66
+ path = "src/lupaxa/data_converter/version.py"
67
+
68
+ [tool.hatch.build.targets.sdist]
69
+ include = [
70
+ "src/**",
71
+ "README.md",
72
+ "README-PYPI.md",
73
+ "LICENCE",
74
+ ]
75
+
76
+ [tool.hatch.build.targets.wheel]
77
+ packages = ["src/lupaxa"]
78
+
79
+ [tool.ruff]
80
+ line-length = 100
81
+ target-version = "py310"
82
+
83
+ [tool.ruff.lint]
84
+ select = ["E", "F", "I", "B", "UP", "SIM"]
85
+
86
+ [tool.ruff.lint.per-file-ignores]
87
+ "tests/**/*.py" = ["S101"]
88
+
89
+ [tool.ruff.format]
90
+ quote-style = "double"
91
+
92
+ [tool.mypy]
93
+ python_version = "3.10"
94
+ files = ["src", "tests"]
95
+ mypy_path = "src"
96
+ explicit_package_bases = true
97
+ namespace_packages = true
98
+ warn_unused_configs = true
99
+ ignore_missing_imports = true
100
+
101
+ [tool.pytest.ini_options]
102
+ addopts = "-ra -q --disable-warnings"
103
+ testpaths = ["tests"]
104
+ pythonpath = ["src", "."]
@@ -0,0 +1,14 @@
1
+ """lupaxa.data_converter — convert mappings between JSON, JSON5, XML, YAML, and TOML."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .converter import DataConverter
6
+ from .exceptions import DataConverterError
7
+ from .version import __version__, get_version
8
+
9
+ __all__ = [
10
+ "DataConverter",
11
+ "DataConverterError",
12
+ "__version__",
13
+ "get_version",
14
+ ]
@@ -0,0 +1,123 @@
1
+ """Public converter facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any, Literal
7
+
8
+ from . import json5_codec, json_codec, toml_codec, xml_codec, yaml_codec
9
+ from .exceptions import DataConverterError
10
+
11
+ DataType = Literal["json", "json5", "xml", "yaml", "toml", "dict"]
12
+ _SUPPORTED_TYPES = frozenset({"json", "json5", "xml", "yaml", "toml", "dict"})
13
+ _TEXT_LOADERS = {
14
+ "json": json_codec.loads,
15
+ "json5": json5_codec.loads,
16
+ "yaml": yaml_codec.loads,
17
+ "toml": toml_codec.loads,
18
+ }
19
+
20
+
21
+ class DataConverter:
22
+ """Load any supported format to a dict, then dump to any supported format."""
23
+
24
+ def __init__(self, data: str | Mapping[str, Any], data_type: str = "json") -> None:
25
+ self._data_type = _normalize_type(data_type)
26
+ if self._data_type == "xml":
27
+ self._root_tag, self._data = xml_codec.loads(_as_text(data, "XML"))
28
+ else:
29
+ self._root_tag = "root"
30
+ self._data = _load(data, self._data_type)
31
+
32
+ @property
33
+ def data(self) -> dict[str, Any]:
34
+ """The loaded mapping."""
35
+ return self._data
36
+
37
+ @property
38
+ def data_type(self) -> str:
39
+ """Normalized source type."""
40
+ return self._data_type
41
+
42
+ @property
43
+ def root_tag(self) -> str:
44
+ """XML root tag used when serializing to XML."""
45
+ return self._root_tag
46
+
47
+ def to_json(self) -> str:
48
+ return json_codec.dumps(self._data)
49
+
50
+ def to_yaml(self) -> str:
51
+ return yaml_codec.dumps(self._data)
52
+
53
+ def to_xml(self, root_tag: str | None = None) -> str:
54
+ return xml_codec.dumps(self._data, root_tag=root_tag or self._root_tag)
55
+
56
+ def to_toml(self) -> str:
57
+ return toml_codec.dumps(self._data)
58
+
59
+ def to_json5(self) -> str:
60
+ return json5_codec.dumps(self._data)
61
+
62
+ @staticmethod
63
+ def dict_to_xml(data: Mapping[str, Any], root_tag: str = "root") -> str:
64
+ _require_mapping(data, "dictionary")
65
+ return xml_codec.dumps(data, root_tag=root_tag)
66
+
67
+ @staticmethod
68
+ def json_to_xml(json_data: Mapping[str, Any], root_tag: str = "root") -> str:
69
+ _require_mapping(json_data, "JSON object")
70
+ return xml_codec.dumps(json_data, root_tag=root_tag)
71
+
72
+ @staticmethod
73
+ def xml_to_json(xml_data: str) -> str:
74
+ _root_tag, payload = xml_codec.loads(_as_text(xml_data, "XML"))
75
+ return json_codec.dumps(payload)
76
+
77
+ @staticmethod
78
+ def yaml_to_dict(yaml_data: str) -> dict[str, Any]:
79
+ return yaml_codec.loads(_as_text(yaml_data, "YAML"))
80
+
81
+ @staticmethod
82
+ def toml_to_dict(toml_data: str) -> dict[str, Any]:
83
+ return toml_codec.loads(_as_text(toml_data, "TOML"))
84
+
85
+ @staticmethod
86
+ def json5_to_dict(json5_data: str) -> dict[str, Any]:
87
+ return json5_codec.loads(_as_text(json5_data, "JSON5"))
88
+
89
+
90
+ def _normalize_type(data_type: str) -> DataType:
91
+ normalized = data_type.strip().lower()
92
+ if normalized not in _SUPPORTED_TYPES:
93
+ raise DataConverterError(
94
+ f"Unsupported data type: {data_type!r}. Use json, json5, xml, yaml, toml, or dict."
95
+ )
96
+ return normalized # type: ignore[return-value]
97
+
98
+
99
+ def _load(data: str | Mapping[str, Any], data_type: DataType) -> dict[str, Any]:
100
+ if data_type == "dict":
101
+ if not isinstance(data, Mapping):
102
+ raise DataConverterError("A dictionary mapping is required.")
103
+ return dict(data)
104
+ loader = _TEXT_LOADERS.get(data_type)
105
+ if loader is None:
106
+ raise DataConverterError(f"Unsupported data type: {data_type!r}.")
107
+ return loader(_as_text(data, data_type.upper()))
108
+
109
+
110
+ def _as_text(data: object, label: str) -> str:
111
+ if isinstance(data, bytes):
112
+ try:
113
+ return data.decode("utf-8")
114
+ except UnicodeDecodeError as exc:
115
+ raise DataConverterError(f"Invalid {label} string.") from exc
116
+ if not isinstance(data, str):
117
+ raise DataConverterError(f"{label} string required.")
118
+ return data
119
+
120
+
121
+ def _require_mapping(data: object, label: str) -> None:
122
+ if not isinstance(data, Mapping):
123
+ raise DataConverterError(f"A {label} mapping is required.")
@@ -0,0 +1,10 @@
1
+ """Errors raised by ``lupaxa.data_converter``."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class DataConverterError(Exception):
7
+ """Raised when a conversion cannot be completed."""
8
+
9
+ def __init__(self, message: str = "Data conversion error.") -> None:
10
+ super().__init__(message)
@@ -0,0 +1,27 @@
1
+ """JSON5 load and dump helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ import json5
9
+
10
+ from .exceptions import DataConverterError
11
+
12
+
13
+ def dumps(data: Mapping[str, Any]) -> str:
14
+ try:
15
+ return json5.dumps(dict(data), ensure_ascii=False)
16
+ except (TypeError, ValueError) as exc:
17
+ raise DataConverterError("Cannot encode value as JSON5.") from exc
18
+
19
+
20
+ def loads(text: str) -> dict[str, Any]:
21
+ try:
22
+ parsed = json5.loads(text)
23
+ except (ValueError, TypeError) as exc:
24
+ raise DataConverterError("Invalid JSON5.") from exc
25
+ if not isinstance(parsed, dict):
26
+ raise DataConverterError("JSON5 document must be a mapping.")
27
+ return parsed
@@ -0,0 +1,26 @@
1
+ """JSON load and dump helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ from .exceptions import DataConverterError
10
+
11
+
12
+ def dumps(data: Mapping[str, Any]) -> str:
13
+ try:
14
+ return json.dumps(data, ensure_ascii=False)
15
+ except (TypeError, ValueError) as exc:
16
+ raise DataConverterError("Cannot encode value as JSON.") from exc
17
+
18
+
19
+ def loads(text: str) -> dict[str, Any]:
20
+ try:
21
+ parsed = json.loads(text)
22
+ except json.JSONDecodeError as exc:
23
+ raise DataConverterError(f"Invalid JSON: {exc.msg}.") from exc
24
+ if not isinstance(parsed, dict):
25
+ raise DataConverterError("JSON document must be a mapping.")
26
+ return parsed
@@ -0,0 +1,33 @@
1
+ """TOML load and dump helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ import tomli_w
10
+
11
+ from .exceptions import DataConverterError
12
+
13
+ if sys.version_info >= (3, 11):
14
+ import tomllib
15
+ else:
16
+ import tomli as tomllib
17
+
18
+
19
+ def dumps(data: Mapping[str, Any]) -> str:
20
+ try:
21
+ return tomli_w.dumps(dict(data))
22
+ except (TypeError, ValueError) as exc:
23
+ raise DataConverterError("Cannot encode value as TOML.") from exc
24
+
25
+
26
+ def loads(text: str) -> dict[str, Any]:
27
+ try:
28
+ parsed = tomllib.loads(text)
29
+ except tomllib.TOMLDecodeError as exc:
30
+ raise DataConverterError("Invalid TOML.") from exc
31
+ if not isinstance(parsed, dict):
32
+ raise DataConverterError("TOML document must be a mapping.")
33
+ return parsed
@@ -0,0 +1,10 @@
1
+ """Version information for the ``lupaxa.data_converter`` package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+
8
+ def get_version() -> str:
9
+ """Return the current package version string."""
10
+ return __version__
@@ -0,0 +1,103 @@
1
+ """XML load and dump helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import xml.etree.ElementTree as ET
7
+ from collections.abc import Mapping
8
+ from typing import Any
9
+
10
+ from defusedxml.ElementTree import ParseError, fromstring
11
+ from defusedxml.minidom import parseString
12
+
13
+ from .exceptions import DataConverterError
14
+
15
+ _XML_NAME = re.compile(r"^[_A-Za-z][-._A-Za-z0-9]*$")
16
+
17
+
18
+ def dumps(data: Mapping[str, Any], root_tag: str = "root") -> str:
19
+ root = ET.Element(_require_name(root_tag))
20
+ _append_mapping(data, root)
21
+ try:
22
+ raw = ET.tostring(root, encoding="unicode")
23
+ return parseString(raw).toprettyxml()
24
+ except DataConverterError:
25
+ raise
26
+ except Exception as exc:
27
+ raise DataConverterError("Cannot encode value as XML.") from exc
28
+
29
+
30
+ def loads(text: str) -> tuple[str, dict[str, Any]]:
31
+ try:
32
+ root = fromstring(text)
33
+ except ParseError as exc:
34
+ raise DataConverterError("Invalid XML.") from exc
35
+ except Exception as exc:
36
+ raise DataConverterError("Invalid XML.") from exc
37
+ return root.tag, _element_payload(root)
38
+
39
+
40
+ def _require_name(name: str) -> str:
41
+ if not name or not _XML_NAME.match(name):
42
+ raise DataConverterError(f"Invalid XML name: {name!r}.")
43
+ return name
44
+
45
+
46
+ def _append_mapping(data: Mapping[str, Any], parent: ET.Element) -> None:
47
+ for key, value in data.items():
48
+ if key == "#text":
49
+ parent.text = "" if value is None else str(value)
50
+ continue
51
+ if key.startswith("@"):
52
+ parent.set(_require_name(key[1:]), "" if value is None else str(value))
53
+ continue
54
+ tag = _require_name(key)
55
+ if isinstance(value, list):
56
+ for item in value:
57
+ child = ET.SubElement(parent, tag)
58
+ _append_value(item, child)
59
+ continue
60
+ child = ET.SubElement(parent, tag)
61
+ _append_value(value, child)
62
+
63
+
64
+ def _append_value(value: Any, parent: ET.Element) -> None:
65
+ if isinstance(value, Mapping):
66
+ _append_mapping(value, parent)
67
+ return
68
+ if isinstance(value, list):
69
+ for item in value:
70
+ child = ET.SubElement(parent, "item")
71
+ _append_value(item, child)
72
+ return
73
+ if value is None:
74
+ return
75
+ parent.text = str(value)
76
+
77
+
78
+ def _element_payload(element: ET.Element) -> dict[str, Any]:
79
+ payload: dict[str, Any] = {}
80
+ for name, value in element.attrib.items():
81
+ payload[f"@{name}"] = value
82
+
83
+ for child in list(element):
84
+ child_value = _child_value(child)
85
+ if child.tag in payload:
86
+ existing = payload[child.tag]
87
+ if not isinstance(existing, list):
88
+ payload[child.tag] = [existing]
89
+ payload[child.tag].append(child_value)
90
+ else:
91
+ payload[child.tag] = child_value
92
+
93
+ text = element.text.strip() if element.text and element.text.strip() else None
94
+ if text:
95
+ payload["#text"] = text
96
+ return payload
97
+
98
+
99
+ def _child_value(element: ET.Element) -> Any:
100
+ payload = _element_payload(element)
101
+ if not element.attrib and not list(element):
102
+ return payload.get("#text", "")
103
+ return payload
@@ -0,0 +1,33 @@
1
+ """YAML load and dump helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from yaml import YAMLError
10
+
11
+ from .exceptions import DataConverterError
12
+
13
+
14
+ def dumps(data: Mapping[str, Any]) -> str:
15
+ try:
16
+ return yaml.safe_dump(
17
+ dict(data),
18
+ default_flow_style=False,
19
+ allow_unicode=True,
20
+ sort_keys=False,
21
+ )
22
+ except YAMLError as exc:
23
+ raise DataConverterError("Cannot encode value as YAML.") from exc
24
+
25
+
26
+ def loads(text: str) -> dict[str, Any]:
27
+ try:
28
+ parsed = yaml.safe_load(text)
29
+ except YAMLError as exc:
30
+ raise DataConverterError("Invalid YAML.") from exc
31
+ if not isinstance(parsed, dict):
32
+ raise DataConverterError("YAML document must be a mapping.")
33
+ return parsed