tallipoika 2024.1.3__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.
- tallipoika-2024.1.3/LICENSE +21 -0
- tallipoika-2024.1.3/MANIFEST.in +2 -0
- tallipoika-2024.1.3/PKG-INFO +87 -0
- tallipoika-2024.1.3/README.md +54 -0
- tallipoika-2024.1.3/pyproject.toml +116 -0
- tallipoika-2024.1.3/setup.cfg +4 -0
- tallipoika-2024.1.3/tallipoika/__init__.py +78 -0
- tallipoika-2024.1.3/tallipoika/api.py +473 -0
- tallipoika-2024.1.3/tallipoika/py2es6.py +82 -0
- tallipoika-2024.1.3/tallipoika.egg-info/PKG-INFO +87 -0
- tallipoika-2024.1.3/tallipoika.egg-info/SOURCES.txt +15 -0
- tallipoika-2024.1.3/tallipoika.egg-info/dependency_links.txt +1 -0
- tallipoika-2024.1.3/tallipoika.egg-info/entry_points.txt +2 -0
- tallipoika-2024.1.3/tallipoika.egg-info/requires.txt +11 -0
- tallipoika-2024.1.3/tallipoika.egg-info/top_level.txt +1 -0
- tallipoika-2024.1.3/test/test_api.py +26 -0
- tallipoika-2024.1.3/test/test_py2es6.py +48 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stefan Hagen
|
|
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,87 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tallipoika
|
|
3
|
+
Version: 2024.1.3
|
|
4
|
+
Summary: Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation.
|
|
5
|
+
Author-email: Stefan Hagen <stefan@hagen.link>
|
|
6
|
+
Maintainer-email: Stefan Hagen <stefan@hagen.link>
|
|
7
|
+
Project-URL: Homepage, https://git.sr.ht/~sthagen/tallipoika
|
|
8
|
+
Project-URL: Bug-Tracker, https://todo.sr.ht/~sthagen/tallipoika
|
|
9
|
+
Project-URL: Documentation, https://codes.dilettant.life/docs/tallipoika
|
|
10
|
+
Project-URL: Source-Code, https://git.sr.ht/~sthagen/tallipoika
|
|
11
|
+
Project-URL: Test-Coverage, https://codes.dilettant.life/coverage/tallipoika
|
|
12
|
+
Keywords: developer-tools,devops
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: typer
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black; extra == "dev"
|
|
26
|
+
Requires-Dist: coverage; extra == "dev"
|
|
27
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-flake8; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# Tallipoika
|
|
35
|
+
|
|
36
|
+
Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation.
|
|
37
|
+
|
|
38
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/LICENSE)
|
|
39
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/COUNTRY-OF-ORIGIN)
|
|
40
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/EXPORT-CONTROL-CLASSIFICATION-NUMBER)
|
|
41
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/docs/third-party/README.md)
|
|
42
|
+
|
|
43
|
+
[](https://pypi.python.org/pypi/tallipoika/)
|
|
44
|
+
[](https://pepy.tech/project/tallipoika)
|
|
45
|
+
[](https://pypi.python.org/pypi/tallipoika/)
|
|
46
|
+
[](https://git.sr.ht/~sthagen/tallipoika/log)
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
User and developer [documentation of tallipoika](https://codes.dilettant.life/docs/tallipoika).
|
|
51
|
+
|
|
52
|
+
## Bug Tracker
|
|
53
|
+
|
|
54
|
+
Any feature requests or bug reports shall go to the [todos of tallipoika](https://todo.sr.ht/~sthagen/tallipoika).
|
|
55
|
+
|
|
56
|
+
## Primary Source repository
|
|
57
|
+
|
|
58
|
+
The main source of `tallipoika` is on a mountain in central Switzerland.
|
|
59
|
+
We use distributed version control (git).
|
|
60
|
+
There is no central hub.
|
|
61
|
+
Every clone can become a new source for the benefit of all.
|
|
62
|
+
The preferred public clone of `tallipoika` is:
|
|
63
|
+
|
|
64
|
+
* [at sourcehut](https://git.sr.ht/~sthagen/tallipoika) - a collection of tools useful for software development.
|
|
65
|
+
|
|
66
|
+
## Contributions
|
|
67
|
+
|
|
68
|
+
Please do not submit "pull requests" (I found no way to disable that "feature" on GitHub).
|
|
69
|
+
If you like to share small changes under the repositories license please kindly do so by sending a patchset.
|
|
70
|
+
You can either send such a patchset per email using [git send-email](https://git-send-email.io) or
|
|
71
|
+
if you are a sourcehut user by selecting "Prepare a patchset" on the summary page of your fork at [sourcehut](https://git.sr.ht/).
|
|
72
|
+
|
|
73
|
+
## Support
|
|
74
|
+
|
|
75
|
+
Please kindly submit issues at https://todo.sr.ht/~sthagen/tallipoika or write plain text email to ~sthagen/tallipoika@lists.sr.ht to submit patches and request support. Thanks.
|
|
76
|
+
|
|
77
|
+
## Thanks also to
|
|
78
|
+
|
|
79
|
+
This implementation is based on the [original implementation of the JSON Canonicalization Scheme (JCS)](https://github.com/cyberphone/json-canonicalization) gently provided under the APACHE-2.0 license by Anders Rundgren.
|
|
80
|
+
The initial implementation of `tallipoika` is kept similar to show the identical functioning of both implementation.
|
|
81
|
+
Also, the input and output JSON tests are provided within this repository as a service for validators.
|
|
82
|
+
|
|
83
|
+
# Status
|
|
84
|
+
|
|
85
|
+
Experimental.
|
|
86
|
+
|
|
87
|
+
**Note**: The default branch is `default`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Tallipoika
|
|
2
|
+
|
|
3
|
+
Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation.
|
|
4
|
+
|
|
5
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/LICENSE)
|
|
6
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/COUNTRY-OF-ORIGIN)
|
|
7
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/EXPORT-CONTROL-CLASSIFICATION-NUMBER)
|
|
8
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/docs/third-party/README.md)
|
|
9
|
+
|
|
10
|
+
[](https://pypi.python.org/pypi/tallipoika/)
|
|
11
|
+
[](https://pepy.tech/project/tallipoika)
|
|
12
|
+
[](https://pypi.python.org/pypi/tallipoika/)
|
|
13
|
+
[](https://git.sr.ht/~sthagen/tallipoika/log)
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
|
|
17
|
+
User and developer [documentation of tallipoika](https://codes.dilettant.life/docs/tallipoika).
|
|
18
|
+
|
|
19
|
+
## Bug Tracker
|
|
20
|
+
|
|
21
|
+
Any feature requests or bug reports shall go to the [todos of tallipoika](https://todo.sr.ht/~sthagen/tallipoika).
|
|
22
|
+
|
|
23
|
+
## Primary Source repository
|
|
24
|
+
|
|
25
|
+
The main source of `tallipoika` is on a mountain in central Switzerland.
|
|
26
|
+
We use distributed version control (git).
|
|
27
|
+
There is no central hub.
|
|
28
|
+
Every clone can become a new source for the benefit of all.
|
|
29
|
+
The preferred public clone of `tallipoika` is:
|
|
30
|
+
|
|
31
|
+
* [at sourcehut](https://git.sr.ht/~sthagen/tallipoika) - a collection of tools useful for software development.
|
|
32
|
+
|
|
33
|
+
## Contributions
|
|
34
|
+
|
|
35
|
+
Please do not submit "pull requests" (I found no way to disable that "feature" on GitHub).
|
|
36
|
+
If you like to share small changes under the repositories license please kindly do so by sending a patchset.
|
|
37
|
+
You can either send such a patchset per email using [git send-email](https://git-send-email.io) or
|
|
38
|
+
if you are a sourcehut user by selecting "Prepare a patchset" on the summary page of your fork at [sourcehut](https://git.sr.ht/).
|
|
39
|
+
|
|
40
|
+
## Support
|
|
41
|
+
|
|
42
|
+
Please kindly submit issues at https://todo.sr.ht/~sthagen/tallipoika or write plain text email to ~sthagen/tallipoika@lists.sr.ht to submit patches and request support. Thanks.
|
|
43
|
+
|
|
44
|
+
## Thanks also to
|
|
45
|
+
|
|
46
|
+
This implementation is based on the [original implementation of the JSON Canonicalization Scheme (JCS)](https://github.com/cyberphone/json-canonicalization) gently provided under the APACHE-2.0 license by Anders Rundgren.
|
|
47
|
+
The initial implementation of `tallipoika` is kept similar to show the identical functioning of both implementation.
|
|
48
|
+
Also, the input and output JSON tests are provided within this repository as a service for validators.
|
|
49
|
+
|
|
50
|
+
# Status
|
|
51
|
+
|
|
52
|
+
Experimental.
|
|
53
|
+
|
|
54
|
+
**Note**: The default branch is `default`.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tallipoika"
|
|
7
|
+
version = "2024.1.3"
|
|
8
|
+
description = "Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Stefan Hagen", email = "stefan@hagen.link" }]
|
|
11
|
+
maintainers = [{ name = "Stefan Hagen", email = "stefan@hagen.link" }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
]
|
|
21
|
+
keywords = ["developer-tools", "devops"]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer",
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">=3.9"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["black", "coverage", "hypothesis", "mypy", "pytest", "pytest-cov", "pytest-flake8", "ruff"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://git.sr.ht/~sthagen/tallipoika"
|
|
32
|
+
Bug-Tracker = "https://todo.sr.ht/~sthagen/tallipoika"
|
|
33
|
+
Documentation = "https://codes.dilettant.life/docs/tallipoika"
|
|
34
|
+
Source-Code = "https://git.sr.ht/~sthagen/tallipoika"
|
|
35
|
+
Test-Coverage = "https://codes.dilettant.life/coverage/tallipoika"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
tallipoika = "tallipoika.cli:app"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["tallipoika"]
|
|
42
|
+
exclude = ["test*"]
|
|
43
|
+
|
|
44
|
+
[tool.black]
|
|
45
|
+
line-length = 120
|
|
46
|
+
skip-string-normalization = true
|
|
47
|
+
target-version = ["py39", "py310", "py311", "py312"]
|
|
48
|
+
|
|
49
|
+
[tool.coverage.run]
|
|
50
|
+
branch = true
|
|
51
|
+
|
|
52
|
+
[tool.coverage.report]
|
|
53
|
+
precision = 2
|
|
54
|
+
exclude_lines = [
|
|
55
|
+
"pragma: no cover",
|
|
56
|
+
"raise NotImplementedError",
|
|
57
|
+
"if TYPE_CHECKING:",
|
|
58
|
+
"@overload",
|
|
59
|
+
]
|
|
60
|
+
omit = [
|
|
61
|
+
"*/__main__.py",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 120
|
|
66
|
+
select = ["E", "F", "Q"]
|
|
67
|
+
ignore = []
|
|
68
|
+
exclude = [
|
|
69
|
+
".bzr",
|
|
70
|
+
".direnv",
|
|
71
|
+
".eggs",
|
|
72
|
+
".git",
|
|
73
|
+
".hg",
|
|
74
|
+
".mypy_cache",
|
|
75
|
+
".nox",
|
|
76
|
+
".pants.d",
|
|
77
|
+
".ruff_cache",
|
|
78
|
+
".svn",
|
|
79
|
+
".tox",
|
|
80
|
+
".venv",
|
|
81
|
+
"__pypackages__",
|
|
82
|
+
"_build",
|
|
83
|
+
"buck-out",
|
|
84
|
+
"build",
|
|
85
|
+
"dist",
|
|
86
|
+
"node_modules",
|
|
87
|
+
"venv",
|
|
88
|
+
]
|
|
89
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
90
|
+
target-version = "py310"
|
|
91
|
+
unfixable = ["F401"]
|
|
92
|
+
|
|
93
|
+
[tool.ruff.flake8-quotes]
|
|
94
|
+
docstring-quotes = "double"
|
|
95
|
+
inline-quotes = "single"
|
|
96
|
+
|
|
97
|
+
[tool.ruff.per-file-ignores]
|
|
98
|
+
"__init__.py" = ["E402"]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.flake8-import-conventions.aliases]
|
|
101
|
+
altair = "alt"
|
|
102
|
+
"matplotlib.pyplot" = "plt"
|
|
103
|
+
numpy = "np"
|
|
104
|
+
pandas = "pd"
|
|
105
|
+
seaborn = "sns"
|
|
106
|
+
|
|
107
|
+
[tool.ruff.mccabe]
|
|
108
|
+
max-complexity = 42 # default is 10
|
|
109
|
+
|
|
110
|
+
[tool.mypy]
|
|
111
|
+
strict = true
|
|
112
|
+
implicit_reexport = true
|
|
113
|
+
|
|
114
|
+
[tool.pytest]
|
|
115
|
+
testpaths = "test"
|
|
116
|
+
filterwarnings = "error"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation."""
|
|
2
|
+
import datetime as dti
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import List, no_type_check
|
|
7
|
+
|
|
8
|
+
# [[[fill git_describe()]]]
|
|
9
|
+
__version__ = '2024.1.3+parent.2e105da5'
|
|
10
|
+
# [[[end]]] (checksum: 74ad8c59fd85ec37e5170a11f7e6a7a4)
|
|
11
|
+
__version_info__ = tuple(
|
|
12
|
+
e if '-' not in e else e.split('-')[0] for part in __version__.split('+') for e in part.split('.') if e != 'parent'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
APP_ALIAS = str(pathlib.Path(__file__).parent.name)
|
|
16
|
+
APP_ENV = APP_ALIAS.upper()
|
|
17
|
+
APP_NAME = locals()['__doc__']
|
|
18
|
+
DEBUG = bool(os.getenv(f'{APP_ENV}_DEBUG', ''))
|
|
19
|
+
VERBOSE = bool(os.getenv(f'{APP_ENV}_VERBOSE', ''))
|
|
20
|
+
QUIET = False
|
|
21
|
+
STRICT = bool(os.getenv(f'{APP_ENV}_STRICT', ''))
|
|
22
|
+
ENCODING = 'utf-8'
|
|
23
|
+
ENCODING_ERRORS_POLICY = 'ignore'
|
|
24
|
+
DEFAULT_CONFIG_NAME = f'.{APP_ALIAS}.json'
|
|
25
|
+
DEFAULT_LF_ONLY = 'YES'
|
|
26
|
+
DEFAULT_STRUCTURE_NAME = 'structure.yml'
|
|
27
|
+
DEFAULT_STRUCTURES_NAME = 'structures.yml'
|
|
28
|
+
log = logging.getLogger() # Module level logger is sufficient
|
|
29
|
+
LOG_FOLDER = pathlib.Path('logs')
|
|
30
|
+
LOG_FILE = f'{APP_ALIAS}.log'
|
|
31
|
+
LOG_PATH = pathlib.Path(LOG_FOLDER, LOG_FILE) if LOG_FOLDER.is_dir() else pathlib.Path(LOG_FILE)
|
|
32
|
+
LOG_LEVEL = logging.INFO
|
|
33
|
+
VERSION = __version__
|
|
34
|
+
VERSION_DOTTED_TRIPLE = '.'.join(__version_info__[:3])
|
|
35
|
+
TS_FORMAT_LOG = '%Y-%m-%dT%H:%M:%S'
|
|
36
|
+
TS_FORMAT_PAYLOADS = '%Y-%m-%d %H:%M:%S.%f UTC'
|
|
37
|
+
TS_FORMAT_GENERATOR = '%Y-%m-%d %H:%M:%S.%f +00:00'
|
|
38
|
+
|
|
39
|
+
__all__: List[str] = [
|
|
40
|
+
'APP_ALIAS',
|
|
41
|
+
'APP_ENV',
|
|
42
|
+
'APP_NAME',
|
|
43
|
+
'DEBUG',
|
|
44
|
+
'DEFAULT_CONFIG_NAME',
|
|
45
|
+
'DEFAULT_STRUCTURE_NAME',
|
|
46
|
+
'DEFAULT_STRUCTURES_NAME',
|
|
47
|
+
'ENCODING',
|
|
48
|
+
'TS_FORMAT_GENERATOR',
|
|
49
|
+
'VERSION',
|
|
50
|
+
'VERSION_DOTTED_TRIPLE',
|
|
51
|
+
'log',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@no_type_check
|
|
56
|
+
def formatTime_RFC3339(self, record, datefmt=None): # noqa
|
|
57
|
+
"""HACK A DID ACK we could inject .astimezone() to localize ..."""
|
|
58
|
+
return dti.datetime.fromtimestamp(record.created, dti.timezone.utc).isoformat() # pragma: no cover
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@no_type_check
|
|
62
|
+
def init_logger(name=None, level=None):
|
|
63
|
+
"""Initialize module level logger"""
|
|
64
|
+
global log # pylint: disable=global-statement
|
|
65
|
+
|
|
66
|
+
log_format = {
|
|
67
|
+
'format': '%(asctime)s %(levelname)s [%(name)s]: %(message)s',
|
|
68
|
+
'datefmt': TS_FORMAT_LOG,
|
|
69
|
+
# 'filename': LOG_PATH,
|
|
70
|
+
'level': LOG_LEVEL if level is None else level,
|
|
71
|
+
}
|
|
72
|
+
logging.Formatter.formatTime = formatTime_RFC3339
|
|
73
|
+
logging.basicConfig(**log_format)
|
|
74
|
+
log = logging.getLogger(APP_ENV if name is None else name)
|
|
75
|
+
log.propagate = True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
init_logger(name=APP_ENV, level=logging.DEBUG if DEBUG else None)
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""JSON Canonicalization Scheme (JCS) serializer."""
|
|
2
|
+
import re
|
|
3
|
+
from typing import no_type_check
|
|
4
|
+
|
|
5
|
+
import tallipoika.py2es6 as py2es6
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from _json import encode_basestring_ascii as c_encode_basestring_ascii
|
|
9
|
+
except ImportError:
|
|
10
|
+
c_encode_basestring_ascii = None # type: ignore
|
|
11
|
+
try:
|
|
12
|
+
from _json import encode_basestring as c_encode_basestring # type: ignore
|
|
13
|
+
except ImportError:
|
|
14
|
+
c_encode_basestring = None
|
|
15
|
+
try:
|
|
16
|
+
from _json import make_encoder as c_make_encoder
|
|
17
|
+
except ImportError:
|
|
18
|
+
c_make_encoder = None # type: ignore
|
|
19
|
+
|
|
20
|
+
COLON = ':'
|
|
21
|
+
COMMA = ','
|
|
22
|
+
SPACE = ' '
|
|
23
|
+
OPEN_SB = '['
|
|
24
|
+
CLOSE_SB = ']'
|
|
25
|
+
EMPTY_ARRAY_REP = f'{OPEN_SB}{CLOSE_SB}'
|
|
26
|
+
OPEN_CB = '{'
|
|
27
|
+
CLOSE_CB = '}'
|
|
28
|
+
EMPTY_OBJECT_REP = f'{OPEN_CB}{CLOSE_CB}'
|
|
29
|
+
NL = '\n'
|
|
30
|
+
|
|
31
|
+
JSON_FALSE_REP = 'false'
|
|
32
|
+
JSON_NULL_REP = 'null'
|
|
33
|
+
JSON_TRUE_REP = 'true'
|
|
34
|
+
|
|
35
|
+
ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
|
|
36
|
+
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
|
|
37
|
+
HAS_UTF8 = re.compile(b'[\x80-\xff]')
|
|
38
|
+
ESCAPE_DCT = {
|
|
39
|
+
'\\': '\\\\',
|
|
40
|
+
'"': '\\"',
|
|
41
|
+
'\b': '\\b',
|
|
42
|
+
'\f': '\\f',
|
|
43
|
+
'\n': '\\n',
|
|
44
|
+
'\r': '\\r',
|
|
45
|
+
'\t': '\\t',
|
|
46
|
+
}
|
|
47
|
+
for i in range(0x20):
|
|
48
|
+
ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
|
|
49
|
+
|
|
50
|
+
INFINITY = float('inf')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@no_type_check
|
|
54
|
+
def py_encode_basestring(s):
|
|
55
|
+
"""Return a JSON representation of a Python string."""
|
|
56
|
+
|
|
57
|
+
def replace(match):
|
|
58
|
+
return ESCAPE_DCT[match.group(0)]
|
|
59
|
+
|
|
60
|
+
return f'"{ESCAPE.sub(replace, s)}"'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
encode_basestring = c_encode_basestring or py_encode_basestring
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@no_type_check
|
|
67
|
+
def py_encode_basestring_ascii(s):
|
|
68
|
+
"""Return an ASCII-only JSON representation of a Python string."""
|
|
69
|
+
|
|
70
|
+
def replace(match):
|
|
71
|
+
s = match.group(0)
|
|
72
|
+
try:
|
|
73
|
+
return ESCAPE_DCT[s]
|
|
74
|
+
except KeyError:
|
|
75
|
+
n = ord(s)
|
|
76
|
+
if n < 0x10000:
|
|
77
|
+
return '\\u{0:04x}'.format(n)
|
|
78
|
+
else:
|
|
79
|
+
# surrogate pair
|
|
80
|
+
n -= 0x10000
|
|
81
|
+
s1 = 0xD800 | ((n >> 10) & 0x3FF)
|
|
82
|
+
s2 = 0xDC00 | (n & 0x3FF)
|
|
83
|
+
return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
|
|
84
|
+
|
|
85
|
+
return f'"{ESCAPE_ASCII.sub(replace, s)}"'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii # type: ignore
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@no_type_check
|
|
92
|
+
class JSONEncoder(object):
|
|
93
|
+
"""JSON encoder for typical Python data structures.
|
|
94
|
+
|
|
95
|
+
Encodes the following nine Python types as the following 7 JSON types:
|
|
96
|
+
|
|
97
|
+
- Python `dict` -> JSON object
|
|
98
|
+
- Python `list` and `tuple` -> JSON array
|
|
99
|
+
- Python `str` -> JSON string
|
|
100
|
+
- Python `float` and `int` -> JSON number
|
|
101
|
+
- Python `True` ->JSON true
|
|
102
|
+
- Python `False` -> JSON false
|
|
103
|
+
- Python `None` -> JSON null
|
|
104
|
+
|
|
105
|
+
To extend recognition to other objects, subclass and implement a `.default()` method that returns a serializable
|
|
106
|
+
object for `o` if possible, otherwise it should call the superclass implementation (to raise `TypeError`).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
item_separator = f'{COMMA}{SPACE}'
|
|
110
|
+
key_separator = f'{COLON}{SPACE}'
|
|
111
|
+
|
|
112
|
+
@no_type_check
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
skipkeys=False,
|
|
117
|
+
ensure_ascii=False,
|
|
118
|
+
check_circular=True,
|
|
119
|
+
allow_nan=True,
|
|
120
|
+
sort_keys=True,
|
|
121
|
+
indent=None,
|
|
122
|
+
separators=(COMMA, COLON),
|
|
123
|
+
default=None,
|
|
124
|
+
):
|
|
125
|
+
"""Constructor for JSONEncoder, with sensible defaults.
|
|
126
|
+
|
|
127
|
+
If skipkeys is false, then it is a TypeError to attempt encoding of keys that are not str, int, float or None.
|
|
128
|
+
If skipkeys is True, such items are simply skipped.
|
|
129
|
+
|
|
130
|
+
If ensure_ascii is true, the output is guaranteed to be str objects with all incoming non-ASCII characters
|
|
131
|
+
escaped. If ensure_ascii is false, the output can contain non-ASCII characters.
|
|
132
|
+
|
|
133
|
+
If check_circular is true, then lists, dicts, and custom encoded objects will be checked for circular references
|
|
134
|
+
during encoding to prevent an infinite recursion (which would cause an OverflowError). Otherwise, no such check
|
|
135
|
+
takes place.
|
|
136
|
+
|
|
137
|
+
If allow_nan is true, then NaN, Infinity, and -Infinity will be encoded as such. This behavior is not JSON
|
|
138
|
+
specification compliant, but is consistent with most JavaScript based encoders and decoders. Otherwise, it will
|
|
139
|
+
be a ValueError to encode such floats.
|
|
140
|
+
|
|
141
|
+
If sort_keys is true, then the output of dictionaries will be sorted by key; this is useful for regression tests
|
|
142
|
+
to ensure that JSON serializations can be compared on a day-to-day basis.
|
|
143
|
+
|
|
144
|
+
If indent is a non-negative integer, then JSON array elements and object members will be pretty-printed with
|
|
145
|
+
that indent level. An indent level of 0 will only insert newlines. None is the most compact representation.
|
|
146
|
+
|
|
147
|
+
If specified, separators should be an (item_separator, key_separator) tuple. The default is
|
|
148
|
+
(', ', ': ') if *indent* is ``None`` and(',', ': ') otherwise. To get the most compact JSON representation,
|
|
149
|
+
you should specify (',', ':') to eliminate whitespace.
|
|
150
|
+
|
|
151
|
+
If specified, default is a function that gets called for objects that can't otherwise be serialized. It should
|
|
152
|
+
return a JSON encodable version of the object or raise a ``TypeError``.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
self.skipkeys = skipkeys
|
|
157
|
+
self.ensure_ascii = ensure_ascii
|
|
158
|
+
self.check_circular = check_circular
|
|
159
|
+
self.allow_nan = allow_nan
|
|
160
|
+
self.sort_keys = sort_keys
|
|
161
|
+
self.indent = indent
|
|
162
|
+
if separators is not None:
|
|
163
|
+
self.item_separator, self.key_separator = separators
|
|
164
|
+
elif indent is not None:
|
|
165
|
+
self.item_separator = COMMA
|
|
166
|
+
if default is not None:
|
|
167
|
+
self.default = default
|
|
168
|
+
|
|
169
|
+
@no_type_check
|
|
170
|
+
def default(self, o):
|
|
171
|
+
"""Implement this method in a subclass such that it returns a serializable object for o.
|
|
172
|
+
|
|
173
|
+
For example, to support arbitrary iterators, you could implement default like this::
|
|
174
|
+
|
|
175
|
+
def default(self, o):
|
|
176
|
+
try:
|
|
177
|
+
iterable = iter(o)
|
|
178
|
+
except TypeError:
|
|
179
|
+
pass
|
|
180
|
+
else:
|
|
181
|
+
return list(iterable)
|
|
182
|
+
return JSONEncoder.default(self, o) # Let the base class default method raise the TypeError
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
raise TypeError(f"Object of type '{o.__class__.__name__}' is not JSON serializable")
|
|
186
|
+
|
|
187
|
+
@no_type_check
|
|
188
|
+
def encode(self, o):
|
|
189
|
+
"""Return a JSON string representation of a Python data structure.
|
|
190
|
+
|
|
191
|
+
>>> from json.encoder import JSONEncoder
|
|
192
|
+
>>> JSONEncoder().encode({"foo": ["bar", "baz"]})
|
|
193
|
+
'{"foo": ["bar", "baz"]}'
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
# This is for extremely simple cases and benchmarks.
|
|
197
|
+
if isinstance(o, str):
|
|
198
|
+
if self.ensure_ascii:
|
|
199
|
+
return encode_basestring_ascii(o)
|
|
200
|
+
else:
|
|
201
|
+
return encode_basestring(o)
|
|
202
|
+
# This doesn't pass the iterator directly to ''.join() because the exceptions aren't as detailed.
|
|
203
|
+
# The list call should be roughly equivalent to the PySequence_Fast that ''.join() would do.
|
|
204
|
+
chunks = self.iterencode(o, _one_shot=False)
|
|
205
|
+
if not isinstance(chunks, (list, tuple)):
|
|
206
|
+
chunks = list(chunks)
|
|
207
|
+
return ''.join(chunks)
|
|
208
|
+
|
|
209
|
+
@no_type_check
|
|
210
|
+
def iterencode(self, o, _one_shot=False):
|
|
211
|
+
"""Encode the given object and yield each string representation as available.
|
|
212
|
+
|
|
213
|
+
For example::
|
|
214
|
+
|
|
215
|
+
for chunk in JSONEncoder().iterencode(bigobject):
|
|
216
|
+
mysocket.write(chunk)
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
if self.check_circular:
|
|
220
|
+
markers = {}
|
|
221
|
+
else:
|
|
222
|
+
markers = None
|
|
223
|
+
if self.ensure_ascii:
|
|
224
|
+
_encoder = encode_basestring_ascii
|
|
225
|
+
else:
|
|
226
|
+
_encoder = encode_basestring
|
|
227
|
+
|
|
228
|
+
def floatstr(o, allow_nan=self.allow_nan, _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
|
|
229
|
+
"""Check for specials.
|
|
230
|
+
|
|
231
|
+
Note that this type of test is processor and platform-specific, so tests shall not rely on the internals.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if o != o:
|
|
235
|
+
text = 'NaN'
|
|
236
|
+
elif o == _inf:
|
|
237
|
+
text = 'Infinity'
|
|
238
|
+
elif o == _neginf:
|
|
239
|
+
text = '-Infinity'
|
|
240
|
+
else:
|
|
241
|
+
return _repr(o)
|
|
242
|
+
|
|
243
|
+
if not allow_nan:
|
|
244
|
+
raise ValueError(f'out of range float value {repr(o)} is not JSON compliant')
|
|
245
|
+
|
|
246
|
+
return text
|
|
247
|
+
|
|
248
|
+
if _one_shot and c_make_encoder is not None and self.indent is None:
|
|
249
|
+
_iterencode = c_make_encoder(
|
|
250
|
+
markers,
|
|
251
|
+
self.default,
|
|
252
|
+
_encoder,
|
|
253
|
+
self.indent,
|
|
254
|
+
self.key_separator,
|
|
255
|
+
self.item_separator,
|
|
256
|
+
self.sort_keys,
|
|
257
|
+
self.skipkeys,
|
|
258
|
+
self.allow_nan,
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
_iterencode = _make_iterencode(
|
|
262
|
+
markers,
|
|
263
|
+
self.default,
|
|
264
|
+
_encoder,
|
|
265
|
+
self.indent,
|
|
266
|
+
floatstr,
|
|
267
|
+
self.key_separator,
|
|
268
|
+
self.item_separator,
|
|
269
|
+
self.sort_keys,
|
|
270
|
+
self.skipkeys,
|
|
271
|
+
_one_shot,
|
|
272
|
+
)
|
|
273
|
+
return _iterencode(o, 0)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@no_type_check
|
|
277
|
+
def _make_iterencode(
|
|
278
|
+
markers,
|
|
279
|
+
_default,
|
|
280
|
+
_encoder,
|
|
281
|
+
_indent,
|
|
282
|
+
_floatstr,
|
|
283
|
+
_key_separator,
|
|
284
|
+
_item_separator,
|
|
285
|
+
_sort_keys,
|
|
286
|
+
_skipkeys,
|
|
287
|
+
_one_shot,
|
|
288
|
+
# HACK: hand-optimized bytecode; turn globals into locals
|
|
289
|
+
ValueError=ValueError,
|
|
290
|
+
dict=dict,
|
|
291
|
+
float=float,
|
|
292
|
+
id=id,
|
|
293
|
+
int=int,
|
|
294
|
+
isinstance=isinstance,
|
|
295
|
+
list=list,
|
|
296
|
+
str=str,
|
|
297
|
+
tuple=tuple,
|
|
298
|
+
_intstr=int.__str__,
|
|
299
|
+
):
|
|
300
|
+
if _indent is not None and not isinstance(_indent, str):
|
|
301
|
+
_indent = SPACE * _indent
|
|
302
|
+
|
|
303
|
+
@no_type_check
|
|
304
|
+
def _iterencode_list(lst, _current_indent_level):
|
|
305
|
+
if not lst:
|
|
306
|
+
yield EMPTY_ARRAY_REP
|
|
307
|
+
return
|
|
308
|
+
if markers is not None:
|
|
309
|
+
markerid = id(lst)
|
|
310
|
+
if markerid in markers:
|
|
311
|
+
raise ValueError('circular reference detected for sequence')
|
|
312
|
+
markers[markerid] = lst
|
|
313
|
+
buf = OPEN_SB
|
|
314
|
+
if _indent is not None:
|
|
315
|
+
_current_indent_level += 1
|
|
316
|
+
newline_indent = NL + _indent * _current_indent_level
|
|
317
|
+
separator = _item_separator + newline_indent
|
|
318
|
+
buf += newline_indent
|
|
319
|
+
else:
|
|
320
|
+
newline_indent = None
|
|
321
|
+
separator = _item_separator
|
|
322
|
+
first = True
|
|
323
|
+
for value in lst:
|
|
324
|
+
if first:
|
|
325
|
+
first = False
|
|
326
|
+
else:
|
|
327
|
+
buf = separator
|
|
328
|
+
if isinstance(value, str):
|
|
329
|
+
yield buf + _encoder(value)
|
|
330
|
+
elif value is None:
|
|
331
|
+
yield buf + JSON_NULL_REP
|
|
332
|
+
elif value is True:
|
|
333
|
+
yield buf + JSON_TRUE_REP
|
|
334
|
+
elif value is False:
|
|
335
|
+
yield buf + JSON_FALSE_REP
|
|
336
|
+
elif isinstance(value, (float, int)):
|
|
337
|
+
# Subclasses of float and int may override __str__, but should still serialize as numbers in JSON.
|
|
338
|
+
# One example within the standard library is IntEnum.
|
|
339
|
+
yield buf + py2es6.serialize(value)
|
|
340
|
+
else:
|
|
341
|
+
yield buf
|
|
342
|
+
if isinstance(value, (list, tuple)):
|
|
343
|
+
chunks = _iterencode_list(value, _current_indent_level)
|
|
344
|
+
elif isinstance(value, dict):
|
|
345
|
+
chunks = _iterencode_dict(value, _current_indent_level)
|
|
346
|
+
else:
|
|
347
|
+
chunks = _iterencode(value, _current_indent_level)
|
|
348
|
+
yield from chunks
|
|
349
|
+
if newline_indent is not None:
|
|
350
|
+
_current_indent_level -= 1
|
|
351
|
+
yield NL + _indent * _current_indent_level
|
|
352
|
+
yield CLOSE_SB
|
|
353
|
+
if markers is not None:
|
|
354
|
+
del markers[markerid]
|
|
355
|
+
|
|
356
|
+
@no_type_check
|
|
357
|
+
def _iterencode_dict(dct, _current_indent_level):
|
|
358
|
+
if not dct:
|
|
359
|
+
yield EMPTY_OBJECT_REP
|
|
360
|
+
return
|
|
361
|
+
if markers is not None:
|
|
362
|
+
markerid = id(dct)
|
|
363
|
+
if markerid in markers:
|
|
364
|
+
raise ValueError('circular reference detected for dict')
|
|
365
|
+
markers[markerid] = dct
|
|
366
|
+
yield OPEN_CB
|
|
367
|
+
if _indent is not None:
|
|
368
|
+
_current_indent_level += 1
|
|
369
|
+
newline_indent = '\n' + _indent * _current_indent_level
|
|
370
|
+
item_separator = _item_separator + newline_indent
|
|
371
|
+
yield newline_indent
|
|
372
|
+
else:
|
|
373
|
+
newline_indent = None
|
|
374
|
+
item_separator = _item_separator
|
|
375
|
+
first = True
|
|
376
|
+
if _sort_keys:
|
|
377
|
+
items = sorted(dct.items(), key=lambda kv: kv[0].encode('utf-16_be'))
|
|
378
|
+
else:
|
|
379
|
+
items = dct.items()
|
|
380
|
+
for key, value in items:
|
|
381
|
+
if isinstance(key, str):
|
|
382
|
+
pass
|
|
383
|
+
# JavaScript is weakly typed for these, so it makes sense to
|
|
384
|
+
# also allow them. Many encoders seem to do something like this.
|
|
385
|
+
elif isinstance(key, (float, int)):
|
|
386
|
+
# see comment for float/int in _make_iterencode
|
|
387
|
+
key = py2es6.serialize(key)
|
|
388
|
+
elif key is True:
|
|
389
|
+
key = 'true'
|
|
390
|
+
elif key is False:
|
|
391
|
+
key = 'false'
|
|
392
|
+
elif key is None:
|
|
393
|
+
key = 'null'
|
|
394
|
+
elif _skipkeys:
|
|
395
|
+
continue
|
|
396
|
+
else:
|
|
397
|
+
raise TypeError(f'key {repr(key)} is not a string')
|
|
398
|
+
if first:
|
|
399
|
+
first = False
|
|
400
|
+
else:
|
|
401
|
+
yield item_separator
|
|
402
|
+
yield _encoder(key)
|
|
403
|
+
yield _key_separator
|
|
404
|
+
if isinstance(value, str):
|
|
405
|
+
yield _encoder(value)
|
|
406
|
+
elif value is None:
|
|
407
|
+
yield JSON_NULL_REP
|
|
408
|
+
elif value is True:
|
|
409
|
+
yield JSON_TRUE_REP
|
|
410
|
+
elif value is False:
|
|
411
|
+
yield JSON_FALSE_REP
|
|
412
|
+
elif isinstance(value, (float, int)):
|
|
413
|
+
# see comment for int/float in _make_iterencode
|
|
414
|
+
yield py2es6.serialize(value)
|
|
415
|
+
else:
|
|
416
|
+
if isinstance(value, (list, tuple)):
|
|
417
|
+
chunks = _iterencode_list(value, _current_indent_level)
|
|
418
|
+
elif isinstance(value, dict):
|
|
419
|
+
chunks = _iterencode_dict(value, _current_indent_level)
|
|
420
|
+
else:
|
|
421
|
+
chunks = _iterencode(value, _current_indent_level)
|
|
422
|
+
yield from chunks
|
|
423
|
+
if newline_indent is not None:
|
|
424
|
+
_current_indent_level -= 1
|
|
425
|
+
yield NL + _indent * _current_indent_level
|
|
426
|
+
yield CLOSE_CB
|
|
427
|
+
if markers is not None:
|
|
428
|
+
del markers[markerid]
|
|
429
|
+
|
|
430
|
+
@no_type_check
|
|
431
|
+
def _iterencode(o, _current_indent_level):
|
|
432
|
+
if isinstance(o, str):
|
|
433
|
+
yield _encoder(o)
|
|
434
|
+
elif o is None:
|
|
435
|
+
yield JSON_NULL_REP
|
|
436
|
+
elif o is True:
|
|
437
|
+
yield JSON_TRUE_REP
|
|
438
|
+
elif o is False:
|
|
439
|
+
yield JSON_FALSE_REP
|
|
440
|
+
elif isinstance(o, (float, int)):
|
|
441
|
+
# see comment for float/int in _make_iterencode
|
|
442
|
+
yield py2es6.serialize(o)
|
|
443
|
+
elif isinstance(o, (list, tuple)):
|
|
444
|
+
yield from _iterencode_list(o, _current_indent_level)
|
|
445
|
+
elif isinstance(o, dict):
|
|
446
|
+
yield from _iterencode_dict(o, _current_indent_level)
|
|
447
|
+
else:
|
|
448
|
+
if markers is not None:
|
|
449
|
+
markerid = id(o)
|
|
450
|
+
if markerid in markers:
|
|
451
|
+
raise ValueError('circular reference detected')
|
|
452
|
+
markers[markerid] = o
|
|
453
|
+
o = _default(o)
|
|
454
|
+
yield from _iterencode(o, _current_indent_level)
|
|
455
|
+
if markers is not None:
|
|
456
|
+
del markers[markerid]
|
|
457
|
+
|
|
458
|
+
return _iterencode
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@no_type_check
|
|
462
|
+
def ensure_encoding(text: str, utf8: bool) -> str:
|
|
463
|
+
return text.encode() if utf8 else text
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@no_type_check
|
|
467
|
+
def canonicalize(obj, utf8=True):
|
|
468
|
+
return ensure_encoding(JSONEncoder(sort_keys=True).encode(obj), utf8=utf8)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@no_type_check
|
|
472
|
+
def serialize(obj, utf8=True):
|
|
473
|
+
return ensure_encoding(JSONEncoder(sort_keys=False).encode(obj), utf8=utf8)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Serialize a Python numerical value as an ECMAScript v6 or later string."""
|
|
2
|
+
import math
|
|
3
|
+
from typing import Union, no_type_check
|
|
4
|
+
|
|
5
|
+
DASH = '-'
|
|
6
|
+
DECIMAL_POINT = '.'
|
|
7
|
+
DIGIT_ZERO = '0'
|
|
8
|
+
MIN_INTEGER_DIGITS = 0
|
|
9
|
+
MAX_INTEGER_DIGITS = 21
|
|
10
|
+
REPR_SWITCH_ABOVE = -7
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@no_type_check
|
|
14
|
+
def serialize(number: Union[float, int]) -> str:
|
|
15
|
+
"""Serialize a Python builtin number as an ECMAScript v6 or later string.
|
|
16
|
+
|
|
17
|
+
Implementation Note(s):
|
|
18
|
+
|
|
19
|
+
- (builtin) numbers are for now int and float but may be extended to other types
|
|
20
|
+
that can be mapped to JSON numbers
|
|
21
|
+
- separate into the components:
|
|
22
|
+
+ sign
|
|
23
|
+
+ integral part
|
|
24
|
+
+ decimal point
|
|
25
|
+
+ fractional part
|
|
26
|
+
+ exponent part
|
|
27
|
+
- return the concatenation of the components (some components may be empty)
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
if not math.isfinite(number):
|
|
31
|
+
raise ValueError(f'invalid number ({number})')
|
|
32
|
+
|
|
33
|
+
as_float = float(number)
|
|
34
|
+
|
|
35
|
+
if as_float == 0:
|
|
36
|
+
return DIGIT_ZERO
|
|
37
|
+
|
|
38
|
+
as_text = str(as_float)
|
|
39
|
+
|
|
40
|
+
sign = DASH if as_text[0] == DASH else ''
|
|
41
|
+
magnitude = as_text[1:] if sign else as_text
|
|
42
|
+
|
|
43
|
+
mantissa = magnitude
|
|
44
|
+
exponent = ''
|
|
45
|
+
exponent_number = 0
|
|
46
|
+
if (fndx := magnitude.find('e')) > 0:
|
|
47
|
+
mantissa, exponent = magnitude[0:fndx], magnitude[fndx:]
|
|
48
|
+
if exponent[2:3] == '0': # Suppress leading zero on exponents
|
|
49
|
+
exponent = exponent[:2] + exponent[3:]
|
|
50
|
+
exponent_number = int(exponent[1:])
|
|
51
|
+
|
|
52
|
+
integral = mantissa
|
|
53
|
+
decimal_point = ''
|
|
54
|
+
fractional = ''
|
|
55
|
+
if (fndx := mantissa.find(DECIMAL_POINT)) > 0:
|
|
56
|
+
decimal_point = DECIMAL_POINT
|
|
57
|
+
integral, fractional = mantissa[:fndx], mantissa[fndx + 1 :]
|
|
58
|
+
|
|
59
|
+
if fractional == DIGIT_ZERO: # x.0 -> x
|
|
60
|
+
decimal_point = ''
|
|
61
|
+
fractional = ''
|
|
62
|
+
|
|
63
|
+
if MIN_INTEGER_DIGITS < exponent_number < MAX_INTEGER_DIGITS:
|
|
64
|
+
integral += fractional
|
|
65
|
+
fractional = ''
|
|
66
|
+
decimal_point = ''
|
|
67
|
+
exponent = ''
|
|
68
|
+
n = exponent_number - len(integral)
|
|
69
|
+
while n >= 0:
|
|
70
|
+
n -= 1
|
|
71
|
+
integral += DIGIT_ZERO
|
|
72
|
+
elif REPR_SWITCH_ABOVE < exponent_number < 0:
|
|
73
|
+
fractional = integral + fractional
|
|
74
|
+
integral = DIGIT_ZERO
|
|
75
|
+
decimal_point = DECIMAL_POINT
|
|
76
|
+
exponent = ''
|
|
77
|
+
n = exponent_number
|
|
78
|
+
while n < -1:
|
|
79
|
+
n += 1
|
|
80
|
+
fractional = DIGIT_ZERO + fractional
|
|
81
|
+
|
|
82
|
+
return f'{sign}{integral}{decimal_point}{fractional}{exponent}'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tallipoika
|
|
3
|
+
Version: 2024.1.3
|
|
4
|
+
Summary: Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation.
|
|
5
|
+
Author-email: Stefan Hagen <stefan@hagen.link>
|
|
6
|
+
Maintainer-email: Stefan Hagen <stefan@hagen.link>
|
|
7
|
+
Project-URL: Homepage, https://git.sr.ht/~sthagen/tallipoika
|
|
8
|
+
Project-URL: Bug-Tracker, https://todo.sr.ht/~sthagen/tallipoika
|
|
9
|
+
Project-URL: Documentation, https://codes.dilettant.life/docs/tallipoika
|
|
10
|
+
Project-URL: Source-Code, https://git.sr.ht/~sthagen/tallipoika
|
|
11
|
+
Project-URL: Test-Coverage, https://codes.dilettant.life/coverage/tallipoika
|
|
12
|
+
Keywords: developer-tools,devops
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: typer
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black; extra == "dev"
|
|
26
|
+
Requires-Dist: coverage; extra == "dev"
|
|
27
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-flake8; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# Tallipoika
|
|
35
|
+
|
|
36
|
+
Stableson (Finnish: tallipoika) - a JSON Canonicalization Scheme (JCS) implementation.
|
|
37
|
+
|
|
38
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/LICENSE)
|
|
39
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/COUNTRY-OF-ORIGIN)
|
|
40
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/EXPORT-CONTROL-CLASSIFICATION-NUMBER)
|
|
41
|
+
[](https://git.sr.ht/~sthagen/tallipoika/tree/default/item/docs/third-party/README.md)
|
|
42
|
+
|
|
43
|
+
[](https://pypi.python.org/pypi/tallipoika/)
|
|
44
|
+
[](https://pepy.tech/project/tallipoika)
|
|
45
|
+
[](https://pypi.python.org/pypi/tallipoika/)
|
|
46
|
+
[](https://git.sr.ht/~sthagen/tallipoika/log)
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
User and developer [documentation of tallipoika](https://codes.dilettant.life/docs/tallipoika).
|
|
51
|
+
|
|
52
|
+
## Bug Tracker
|
|
53
|
+
|
|
54
|
+
Any feature requests or bug reports shall go to the [todos of tallipoika](https://todo.sr.ht/~sthagen/tallipoika).
|
|
55
|
+
|
|
56
|
+
## Primary Source repository
|
|
57
|
+
|
|
58
|
+
The main source of `tallipoika` is on a mountain in central Switzerland.
|
|
59
|
+
We use distributed version control (git).
|
|
60
|
+
There is no central hub.
|
|
61
|
+
Every clone can become a new source for the benefit of all.
|
|
62
|
+
The preferred public clone of `tallipoika` is:
|
|
63
|
+
|
|
64
|
+
* [at sourcehut](https://git.sr.ht/~sthagen/tallipoika) - a collection of tools useful for software development.
|
|
65
|
+
|
|
66
|
+
## Contributions
|
|
67
|
+
|
|
68
|
+
Please do not submit "pull requests" (I found no way to disable that "feature" on GitHub).
|
|
69
|
+
If you like to share small changes under the repositories license please kindly do so by sending a patchset.
|
|
70
|
+
You can either send such a patchset per email using [git send-email](https://git-send-email.io) or
|
|
71
|
+
if you are a sourcehut user by selecting "Prepare a patchset" on the summary page of your fork at [sourcehut](https://git.sr.ht/).
|
|
72
|
+
|
|
73
|
+
## Support
|
|
74
|
+
|
|
75
|
+
Please kindly submit issues at https://todo.sr.ht/~sthagen/tallipoika or write plain text email to ~sthagen/tallipoika@lists.sr.ht to submit patches and request support. Thanks.
|
|
76
|
+
|
|
77
|
+
## Thanks also to
|
|
78
|
+
|
|
79
|
+
This implementation is based on the [original implementation of the JSON Canonicalization Scheme (JCS)](https://github.com/cyberphone/json-canonicalization) gently provided under the APACHE-2.0 license by Anders Rundgren.
|
|
80
|
+
The initial implementation of `tallipoika` is kept similar to show the identical functioning of both implementation.
|
|
81
|
+
Also, the input and output JSON tests are provided within this repository as a service for validators.
|
|
82
|
+
|
|
83
|
+
# Status
|
|
84
|
+
|
|
85
|
+
Experimental.
|
|
86
|
+
|
|
87
|
+
**Note**: The default branch is `default`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
tallipoika/__init__.py
|
|
6
|
+
tallipoika/api.py
|
|
7
|
+
tallipoika/py2es6.py
|
|
8
|
+
tallipoika.egg-info/PKG-INFO
|
|
9
|
+
tallipoika.egg-info/SOURCES.txt
|
|
10
|
+
tallipoika.egg-info/dependency_links.txt
|
|
11
|
+
tallipoika.egg-info/entry_points.txt
|
|
12
|
+
tallipoika.egg-info/requires.txt
|
|
13
|
+
tallipoika.egg-info/top_level.txt
|
|
14
|
+
test/test_api.py
|
|
15
|
+
test/test_py2es6.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tallipoika
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
from tallipoika.api import canonicalize
|
|
5
|
+
|
|
6
|
+
ENCODING = 'utf-8'
|
|
7
|
+
NL = '\n'
|
|
8
|
+
test_data_root: pathlib.Path = pathlib.Path('test') / 'fixtures'
|
|
9
|
+
input_data_root: pathlib.Path = test_data_root / 'reference_upstream_input'
|
|
10
|
+
output_data_root: pathlib.Path = test_data_root / 'reference_upstream_output'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_upstream():
|
|
14
|
+
for case in sorted(input_data_root.glob('*.json')):
|
|
15
|
+
file_name = case.name
|
|
16
|
+
with open(output_data_root / file_name, 'r', encoding=ENCODING) as sink:
|
|
17
|
+
expected = sink.read().encode()
|
|
18
|
+
|
|
19
|
+
with open(input_data_root / file_name, 'r', encoding=ENCODING) as source:
|
|
20
|
+
loaded = source.read()
|
|
21
|
+
|
|
22
|
+
actual = canonicalize(json.loads(loaded))
|
|
23
|
+
recycled = canonicalize(json.loads(actual.decode(ENCODING, 'strict')))
|
|
24
|
+
|
|
25
|
+
assert actual == expected
|
|
26
|
+
assert actual == recycled
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import binascii
|
|
2
|
+
import pathlib
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
import tallipoika.py2es6 as py2es6
|
|
6
|
+
|
|
7
|
+
COMMA = ','
|
|
8
|
+
INVALID_NUMBER = 'null'
|
|
9
|
+
UPSTREAM_REF_TEST_DATA = pathlib.Path('test') / 'fixtures' / 'es6testfile100m.txt'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify(ieee_hex, expected) -> None:
|
|
13
|
+
"""Test program using a 100 million value file formatted as follows:
|
|
14
|
+
value in ieee hex representation (1-16 digits) + ',' + correct ES6 format + '\n'
|
|
15
|
+
"""
|
|
16
|
+
while len(ieee_hex) < 16:
|
|
17
|
+
ieee_hex = '0' + ieee_hex
|
|
18
|
+
value = struct.unpack('>d', binascii.a2b_hex(ieee_hex))[0]
|
|
19
|
+
try:
|
|
20
|
+
serialized = py2es6.serialize(value)
|
|
21
|
+
except ValueError:
|
|
22
|
+
if expected == INVALID_NUMBER:
|
|
23
|
+
return
|
|
24
|
+
if serialized == expected and value == float(serialized) and repr(value) == str(value):
|
|
25
|
+
return
|
|
26
|
+
raise ValueError('IEEE: ' + ieee_hex + '\nPython: ' + serialized + '\nExpected: ' + expected)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_corners():
|
|
30
|
+
assert verify('4340000000000001', '9007199254740994') is None
|
|
31
|
+
assert verify('4340000000000002', '9007199254740996') is None
|
|
32
|
+
assert verify('444b1ae4d6e2ef50', '1e+21') is None
|
|
33
|
+
assert verify('3eb0c6f7a0b5ed8d', '0.000001') is None
|
|
34
|
+
assert verify('3eb0c6f7a0b5ed8c', '9.999999999999997e-7') is None
|
|
35
|
+
assert verify('8000000000000000', '0') is None
|
|
36
|
+
assert verify('7fffffffffffffff', INVALID_NUMBER) is None
|
|
37
|
+
assert verify('7ff0000000000000', INVALID_NUMBER) is None
|
|
38
|
+
assert verify('fff0000000000000', INVALID_NUMBER) is None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_reference():
|
|
42
|
+
with open(UPSTREAM_REF_TEST_DATA, 'r') as line_source:
|
|
43
|
+
for count, line in enumerate(line_source):
|
|
44
|
+
if line:
|
|
45
|
+
i = line.find(COMMA)
|
|
46
|
+
if i <= 0 or i >= len(line) - 1:
|
|
47
|
+
raise ValueError(f'bad line ({line}) at {count}')
|
|
48
|
+
assert verify(line[:i], line[i + 1 : len(line) - 1]) is None
|