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.
- lupaxa_data_converter-0.1.0/.gitignore +24 -0
- lupaxa_data_converter-0.1.0/LICENCE +21 -0
- lupaxa_data_converter-0.1.0/PKG-INFO +145 -0
- lupaxa_data_converter-0.1.0/README-PYPI.md +80 -0
- lupaxa_data_converter-0.1.0/README.md +51 -0
- lupaxa_data_converter-0.1.0/docs/README.md +23 -0
- lupaxa_data_converter-0.1.0/pyproject.toml +104 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/__init__.py +14 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/converter.py +123 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/exceptions.py +10 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/json5_codec.py +27 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/json_codec.py +26 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/toml_codec.py +33 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/version.py +10 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/xml_codec.py +103 -0
- lupaxa_data_converter-0.1.0/src/lupaxa/data_converter/yaml_codec.py +33 -0
|
@@ -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,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
|