Pyjoyment 0.1.0__py3-none-any.whl
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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/JSON/__init__.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.JSON - Minimalistic JSON
|
|
3
|
+
=============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.JSON import decode_json, encode_json
|
|
7
|
+
|
|
8
|
+
data = encode_json({'foo': [1, 2], 'bar': 'hello!', 'baz': True})
|
|
9
|
+
value = decode_json(data)
|
|
10
|
+
|
|
11
|
+
:mod:`Pyjo.JSON` is a minimalistic pure-Python implementation of :rfc:`8259`.
|
|
12
|
+
|
|
13
|
+
It supports normal Python data types like :class:`dict`, :class:`list`,
|
|
14
|
+
:class:`tuple`, :class:`str`, numbers, booleans and ``None``, and will try to
|
|
15
|
+
call the ``__json__`` method on other objects, or stringify them if their class
|
|
16
|
+
has its own ``__str__``. Anything else becomes ``null``. Strings and numbers are
|
|
17
|
+
told apart by their types. ::
|
|
18
|
+
|
|
19
|
+
[1, -2, 3] -> [1, -2, 3]
|
|
20
|
+
{"foo": "bar"} -> {'foo': 'bar'}
|
|
21
|
+
|
|
22
|
+
The keys of an object keep the order of the :class:`dict`, or are sorted with
|
|
23
|
+
``sort_keys``. ::
|
|
24
|
+
|
|
25
|
+
{'b': 1, 'a': 2} -> {"b":1,"a":2}
|
|
26
|
+
{'b': 1, 'a': 2} -> {"a":2,"b":1} # sort_keys=True
|
|
27
|
+
|
|
28
|
+
Literal names will be translated to and from the native Python values, which
|
|
29
|
+
the constants :data:`true` and :data:`false` hold too. ::
|
|
30
|
+
|
|
31
|
+
true -> True
|
|
32
|
+
false -> False
|
|
33
|
+
null -> None
|
|
34
|
+
|
|
35
|
+
The characters ``/`` and ``<`` will always be escaped to prevent XSS attacks,
|
|
36
|
+
so that JSON can be put into an element ``script`` of HTML as it is. ::
|
|
37
|
+
|
|
38
|
+
"</script>" -> "\u003C\/script>"
|
|
39
|
+
"<!--<script>" -> "\u003C!--\u003Cscript>"
|
|
40
|
+
|
|
41
|
+
For better performance the :mod:`json` module of the standard library will be
|
|
42
|
+
used, set up like the pure-Python implementation, with error messages of its
|
|
43
|
+
own. This can also be disabled with the ``PYJO_NO_JSON_STDLIB`` environment
|
|
44
|
+
variable.
|
|
45
|
+
|
|
46
|
+
See also
|
|
47
|
+
--------
|
|
48
|
+
|
|
49
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
50
|
+
|
|
51
|
+
Ported from `Mojo::JSON <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/JSON.pm>`_
|
|
52
|
+
of Mojolicious 9.49.
|
|
53
|
+
|
|
54
|
+
Functions
|
|
55
|
+
---------
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from Pyjo.Util import decode, encode
|
|
59
|
+
|
|
60
|
+
import json
|
|
61
|
+
import math
|
|
62
|
+
import os
|
|
63
|
+
import re
|
|
64
|
+
|
|
65
|
+
from typing import Any, NoReturn, cast, overload
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Renamed from Mojolicious: JSON_XS and MOJO_NO_JSON_XS, after the json module of
|
|
69
|
+
# the standard library, which takes the place of Cpanel::JSON::XS
|
|
70
|
+
JSON_STDLIB = not os.environ.get('PYJO_NO_JSON_STDLIB')
|
|
71
|
+
|
|
72
|
+
# Left out from Mojolicious: CORE_BOOLS, as Python always has booleans
|
|
73
|
+
|
|
74
|
+
# Maximum nesting level for decoding, to match the default of Cpanel::JSON::XS
|
|
75
|
+
MAX_DEPTH = 512
|
|
76
|
+
|
|
77
|
+
# Escaped special character map
|
|
78
|
+
_ESCAPE = {'"': '"', '\\': '\\', '/': '/', 'b': '\x08', 'f': '\x0c', 'n': '\x0a', 'r': '\x0d', 't': '\x09'}
|
|
79
|
+
_REVERSE = {_ESCAPE[c]: f'\\{c}' for c in _ESCAPE} | {
|
|
80
|
+
chr(c): f'\\u{c:04X}' for c in range(0x20) if chr(c) not in _ESCAPE.values()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Unlike Mojolicious: "<" is escaped too, where Mojo escapes only "/", as "<!--<script>" in a string of JSON in an
|
|
84
|
+
# element "script" of HTML makes "</script>" of the element end the escape of the parser of HTML, not the element
|
|
85
|
+
_REVERSE['<'] = '\\u003C'
|
|
86
|
+
_TRANSLATE = str.maketrans(_REVERSE)
|
|
87
|
+
|
|
88
|
+
_RE_ARRAY_END = re.compile(r'[\x20\x09\x0a\x0d]*\]')
|
|
89
|
+
_RE_COLON = re.compile(r'[\x20\x09\x0a\x0d]*:')
|
|
90
|
+
_RE_COMMA = re.compile(r'[\x20\x09\x0a\x0d]*,')
|
|
91
|
+
_RE_END = re.compile(r'[\x20\x09\x0a\x0d]*\Z')
|
|
92
|
+
_RE_ESCAPE = re.compile(r'\\(["\\/bfnrt])')
|
|
93
|
+
_RE_INVALID = re.compile(r'[\x00-\x1f\\]')
|
|
94
|
+
_RE_LOW_SURROGATE = re.compile(r'\\u([Dd][C-Fc-f]..)')
|
|
95
|
+
_RE_NUMBER = re.compile(r'-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?')
|
|
96
|
+
_RE_OBJECT_END = re.compile(r'[\x20\x09\x0a\x0d]*\}')
|
|
97
|
+
_RE_QUOTE = re.compile(r'[\x20\x09\x0a\x0d]*"')
|
|
98
|
+
_RE_STRING = re.compile(r'(?:[^\x00-\x1f\\"]+|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*')
|
|
99
|
+
_RE_UNESCAPE = re.compile(r'([^\\]*)\\(?:([^u])|u(.{4}))', re.S)
|
|
100
|
+
_RE_WS = re.compile(r'[\x20\x09\x0a\x0d]*')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decode_json(data: bytes | bytearray | str) -> Any:
|
|
104
|
+
"""::
|
|
105
|
+
|
|
106
|
+
value = decode_json(data)
|
|
107
|
+
|
|
108
|
+
Decode JSON to Python value and raise :exc:`ValueError` if decoding fails.
|
|
109
|
+
"""
|
|
110
|
+
# Not in Mojolicious: the json module of the standard library, which takes
|
|
111
|
+
# the place of Cpanel::JSON::XS
|
|
112
|
+
if JSON_STDLIB:
|
|
113
|
+
return _stdlib_decode(data)
|
|
114
|
+
|
|
115
|
+
return _decode(data)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def default(value: object) -> Any:
|
|
119
|
+
"""::
|
|
120
|
+
|
|
121
|
+
data = json.dumps(value, default=Pyjo.JSON.default)
|
|
122
|
+
|
|
123
|
+
Turn an object that is not a :class:`dict`, :class:`list`, :class:`tuple`,
|
|
124
|
+
:class:`str`, number, boolean or ``None`` into one that is, as
|
|
125
|
+
:mod:`Pyjo.JSON` does: with its ``__json__`` method, as a string if its class
|
|
126
|
+
has its own ``__str__``, or as ``None``. Bytes become a string of the
|
|
127
|
+
characters 0-255. This is the ``default`` function for the encoder of the
|
|
128
|
+
:mod:`json` module of the standard library. ::
|
|
129
|
+
|
|
130
|
+
# '{"url": "http://example.com"}'
|
|
131
|
+
json.dumps({'url': Pyjo.URL.new('http://example.com')}, default=Pyjo.JSON.default)
|
|
132
|
+
"""
|
|
133
|
+
# Not in Mojolicious: _encode_value does it for blessed references, and the
|
|
134
|
+
# json module of the standard library calls it for objects it does not know
|
|
135
|
+
method = getattr(value, '__json__', None)
|
|
136
|
+
if method is not None:
|
|
137
|
+
return method()
|
|
138
|
+
if isinstance(value, (bytes, bytearray)):
|
|
139
|
+
return value.decode('latin-1')
|
|
140
|
+
if type(value).__str__ is not object.__str__:
|
|
141
|
+
return str(value)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def encode_json(value: object, *, sort_keys: bool = False) -> bytes:
|
|
146
|
+
"""::
|
|
147
|
+
|
|
148
|
+
data = encode_json({'i': '♥ pyjoyment'})
|
|
149
|
+
data = encode_json({'i': '♥ pyjoyment'}, sort_keys=True)
|
|
150
|
+
|
|
151
|
+
Encode Python value to JSON, with the keys of an object in the order of
|
|
152
|
+
the :class:`dict`, or sorted with ``sort_keys``.
|
|
153
|
+
"""
|
|
154
|
+
# Unlike Mojolicious: the keys in the order of the dict, which Mojo sorts, as a hash of Perl has no order
|
|
155
|
+
return encode('UTF-8', to_json(value, sort_keys=sort_keys))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
false = False
|
|
159
|
+
"""::
|
|
160
|
+
|
|
161
|
+
from Pyjo.JSON import false
|
|
162
|
+
|
|
163
|
+
False value, the same as ``False``.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def from_json(chars: str) -> Any:
|
|
168
|
+
"""::
|
|
169
|
+
|
|
170
|
+
value = from_json(chars)
|
|
171
|
+
|
|
172
|
+
Decode JSON text that is not ``UTF-8`` encoded to Python value and raise
|
|
173
|
+
:exc:`ValueError` if decoding fails.
|
|
174
|
+
"""
|
|
175
|
+
# Not in Mojolicious: the json module of the standard library
|
|
176
|
+
if JSON_STDLIB:
|
|
177
|
+
return _stdlib_decode(chars, True)
|
|
178
|
+
|
|
179
|
+
return _decode(chars, True)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@overload
|
|
183
|
+
def j(value: list[Any] | tuple[Any, ...] | dict[Any, Any], *, sort_keys: bool = False) -> bytes: ...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@overload
|
|
187
|
+
def j(value: bytes | bytearray | str, *, sort_keys: bool = False) -> Any: ...
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def j(value: list[Any] | tuple[Any, ...] | dict[Any, Any] | bytes | bytearray | str, *, sort_keys: bool = False) -> Any:
|
|
191
|
+
"""::
|
|
192
|
+
|
|
193
|
+
data = j([1, 2, 3])
|
|
194
|
+
data = j({'i': '♥ pyjoyment'})
|
|
195
|
+
data = j({'i': '♥ pyjoyment'}, sort_keys=True)
|
|
196
|
+
value = j(data)
|
|
197
|
+
|
|
198
|
+
Encode Python data structure (which may only be a :class:`list`, a
|
|
199
|
+
:class:`tuple` or a :class:`dict`) or decode JSON, a ``None`` return value
|
|
200
|
+
indicates a bare ``null`` or that decoding failed. ``sort_keys`` sorts the
|
|
201
|
+
keys of the objects of the JSON that it encodes.
|
|
202
|
+
"""
|
|
203
|
+
if isinstance(value, (list, tuple, dict)):
|
|
204
|
+
return encode_json(value, sort_keys=sort_keys)
|
|
205
|
+
try:
|
|
206
|
+
return decode_json(value)
|
|
207
|
+
except ValueError:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def to_json(value: object, *, sort_keys: bool = False) -> str:
|
|
212
|
+
"""::
|
|
213
|
+
|
|
214
|
+
chars = to_json({'i': '♥ pyjoyment'})
|
|
215
|
+
chars = to_json({'i': '♥ pyjoyment'}, sort_keys=True)
|
|
216
|
+
|
|
217
|
+
Encode Python value to JSON text without ``UTF-8`` encoding it, with the
|
|
218
|
+
keys of an object in the order of the :class:`dict`, or sorted with
|
|
219
|
+
``sort_keys``.
|
|
220
|
+
"""
|
|
221
|
+
# Not in Mojolicious: the json module of the standard library
|
|
222
|
+
if JSON_STDLIB:
|
|
223
|
+
return _stdlib_encode(value, sort_keys)
|
|
224
|
+
|
|
225
|
+
return _encode_value(value, sort_keys)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
true = True
|
|
229
|
+
"""::
|
|
230
|
+
|
|
231
|
+
from Pyjo.JSON import true
|
|
232
|
+
|
|
233
|
+
True value, the same as ``True``.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _chars(data: bytes | bytearray | str, chars: bool) -> str:
|
|
238
|
+
if isinstance(data, str) and chars:
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
# UTF-8
|
|
242
|
+
text = decode('UTF-8', bytes(data) if isinstance(data, bytearray) else data)
|
|
243
|
+
if text is None:
|
|
244
|
+
raise ValueError('Input is not UTF-8 encoded')
|
|
245
|
+
return text
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _decode(data: bytes | bytearray | str, chars: bool = False) -> Any:
|
|
249
|
+
# Missing input
|
|
250
|
+
if not len(data):
|
|
251
|
+
raise ValueError('Missing or empty input at offset 0')
|
|
252
|
+
|
|
253
|
+
# UTF-8
|
|
254
|
+
decoder = _Decoder(_chars(data, chars))
|
|
255
|
+
|
|
256
|
+
# Value
|
|
257
|
+
value = decoder.decode_value(0)
|
|
258
|
+
|
|
259
|
+
# Leftover data
|
|
260
|
+
if not decoder.match(_RE_END):
|
|
261
|
+
decoder.throw('Unexpected data')
|
|
262
|
+
|
|
263
|
+
return value
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class _Decoder:
|
|
267
|
+
def __init__(self, text: str) -> None:
|
|
268
|
+
self.text = text
|
|
269
|
+
self.pos = 0
|
|
270
|
+
|
|
271
|
+
# Unlike Mojolicious: arrays and objects are decoded here, with one call for
|
|
272
|
+
# each level of nesting, as Python's recursion limit has no room for the two
|
|
273
|
+
# calls of _decode_value and _decode_array for each of MAX_DEPTH levels
|
|
274
|
+
def decode_value(self, depth: int) -> Any:
|
|
275
|
+
# Leading whitespace
|
|
276
|
+
self.match(_RE_WS)
|
|
277
|
+
text = self.text
|
|
278
|
+
pos = self.pos
|
|
279
|
+
|
|
280
|
+
# String
|
|
281
|
+
if text.startswith('"', pos):
|
|
282
|
+
self.pos += 1
|
|
283
|
+
return self.decode_string()
|
|
284
|
+
|
|
285
|
+
# Object
|
|
286
|
+
if text.startswith('{', pos):
|
|
287
|
+
self.pos += 1
|
|
288
|
+
|
|
289
|
+
# Nesting limit
|
|
290
|
+
if depth + 1 > MAX_DEPTH:
|
|
291
|
+
self.throw('Nesting too deep')
|
|
292
|
+
|
|
293
|
+
hash: dict[str, Any] = {}
|
|
294
|
+
if not self.match(_RE_OBJECT_END):
|
|
295
|
+
while True:
|
|
296
|
+
# Quote
|
|
297
|
+
if not self.match(_RE_QUOTE):
|
|
298
|
+
self.throw('Expected string while parsing object')
|
|
299
|
+
|
|
300
|
+
# Key
|
|
301
|
+
key = self.decode_string()
|
|
302
|
+
|
|
303
|
+
# Colon
|
|
304
|
+
if not self.match(_RE_COLON):
|
|
305
|
+
self.throw('Expected colon while parsing object')
|
|
306
|
+
|
|
307
|
+
# Value
|
|
308
|
+
hash[key] = self.decode_value(depth + 1)
|
|
309
|
+
|
|
310
|
+
# Separator
|
|
311
|
+
if self.match(_RE_COMMA):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
# End
|
|
315
|
+
if self.match(_RE_OBJECT_END):
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
# Invalid character
|
|
319
|
+
self.throw('Expected comma or right curly bracket while parsing object')
|
|
320
|
+
|
|
321
|
+
return hash
|
|
322
|
+
|
|
323
|
+
# Array
|
|
324
|
+
if text.startswith('[', pos):
|
|
325
|
+
self.pos += 1
|
|
326
|
+
|
|
327
|
+
# Nesting limit
|
|
328
|
+
if depth + 1 > MAX_DEPTH:
|
|
329
|
+
self.throw('Nesting too deep')
|
|
330
|
+
|
|
331
|
+
array: list[Any] = []
|
|
332
|
+
if not self.match(_RE_ARRAY_END):
|
|
333
|
+
while True:
|
|
334
|
+
# Value
|
|
335
|
+
array.append(self.decode_value(depth + 1))
|
|
336
|
+
|
|
337
|
+
# Separator
|
|
338
|
+
if self.match(_RE_COMMA):
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
# End
|
|
342
|
+
if self.match(_RE_ARRAY_END):
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
# Invalid character
|
|
346
|
+
self.throw('Expected comma or right square bracket while parsing array')
|
|
347
|
+
|
|
348
|
+
return array
|
|
349
|
+
|
|
350
|
+
# Number
|
|
351
|
+
if number := _RE_NUMBER.match(text, pos):
|
|
352
|
+
self.pos = number.end()
|
|
353
|
+
return float(number[0]) if any(c in number[0] for c in '.eE') else int(number[0])
|
|
354
|
+
|
|
355
|
+
# True
|
|
356
|
+
if text.startswith('true', pos):
|
|
357
|
+
self.pos += 4
|
|
358
|
+
return true
|
|
359
|
+
|
|
360
|
+
# False
|
|
361
|
+
if text.startswith('false', pos):
|
|
362
|
+
self.pos += 5
|
|
363
|
+
return false
|
|
364
|
+
|
|
365
|
+
# Null
|
|
366
|
+
if text.startswith('null', pos):
|
|
367
|
+
self.pos += 4
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
# Invalid character
|
|
371
|
+
self.throw('Expected string, array, object, number, boolean or null')
|
|
372
|
+
|
|
373
|
+
def decode_string(self) -> str:
|
|
374
|
+
pos = self.pos
|
|
375
|
+
|
|
376
|
+
# Extract string with escaped characters
|
|
377
|
+
self.match(_RE_STRING)
|
|
378
|
+
string = self.text[pos : self.pos]
|
|
379
|
+
|
|
380
|
+
# Invalid character
|
|
381
|
+
if not self.text.startswith('"', self.pos):
|
|
382
|
+
if _RE_INVALID.match(self.text, self.pos):
|
|
383
|
+
self.throw('Unexpected character or invalid escape while parsing string')
|
|
384
|
+
self.throw('Unterminated string')
|
|
385
|
+
self.pos += 1
|
|
386
|
+
|
|
387
|
+
# Unescape popular characters
|
|
388
|
+
if '\\u' not in string:
|
|
389
|
+
return _RE_ESCAPE.sub(lambda m: _ESCAPE[m[1]], string)
|
|
390
|
+
|
|
391
|
+
# Unescape everything else
|
|
392
|
+
buffer = ''
|
|
393
|
+
offset = 0
|
|
394
|
+
while m := _RE_UNESCAPE.match(string, offset):
|
|
395
|
+
offset = m.end()
|
|
396
|
+
buffer += m[1]
|
|
397
|
+
|
|
398
|
+
# Popular character
|
|
399
|
+
if m[2]:
|
|
400
|
+
buffer += _ESCAPE[m[2]]
|
|
401
|
+
|
|
402
|
+
# Escaped
|
|
403
|
+
else:
|
|
404
|
+
ord = int(m[3], 16)
|
|
405
|
+
|
|
406
|
+
# Surrogate pair
|
|
407
|
+
if (ord & 0xF800) == 0xD800:
|
|
408
|
+
# High surrogate
|
|
409
|
+
if (ord & 0xFC00) != 0xD800:
|
|
410
|
+
self.pos = pos + offset
|
|
411
|
+
self.throw('Missing high-surrogate')
|
|
412
|
+
|
|
413
|
+
# Low surrogate
|
|
414
|
+
low = _RE_LOW_SURROGATE.match(string, offset)
|
|
415
|
+
if not low:
|
|
416
|
+
self.pos = pos + offset
|
|
417
|
+
self.throw('Missing low-surrogate')
|
|
418
|
+
offset = low.end()
|
|
419
|
+
|
|
420
|
+
ord = 0x10000 + (ord - 0xD800) * 0x400 + (int(low[1], 16) - 0xDC00)
|
|
421
|
+
|
|
422
|
+
# Character
|
|
423
|
+
buffer += chr(ord)
|
|
424
|
+
|
|
425
|
+
# The rest
|
|
426
|
+
return buffer + string[offset:]
|
|
427
|
+
|
|
428
|
+
def match(self, regex: re.Pattern[str]) -> bool:
|
|
429
|
+
m = regex.match(self.text, self.pos)
|
|
430
|
+
if m:
|
|
431
|
+
self.pos = m.end()
|
|
432
|
+
return m is not None
|
|
433
|
+
|
|
434
|
+
def throw(self, message: str) -> NoReturn:
|
|
435
|
+
# Leading whitespace
|
|
436
|
+
pos = self.pos
|
|
437
|
+
self.match(_RE_WS)
|
|
438
|
+
|
|
439
|
+
# Context
|
|
440
|
+
context = 'Malformed JSON: ' + message
|
|
441
|
+
|
|
442
|
+
# Perl matches \z after a match of no length only when it has moved on,
|
|
443
|
+
# so "before end of data" needs whitespace before the end
|
|
444
|
+
if self.pos > pos and self.pos == len(self.text):
|
|
445
|
+
context += ' before end of data'
|
|
446
|
+
else:
|
|
447
|
+
lines = self.text[: self.pos].split('\n')
|
|
448
|
+
while lines and not lines[-1]:
|
|
449
|
+
lines.pop()
|
|
450
|
+
|
|
451
|
+
# Unlike Mojolicious: a last line "0" has its length, where Perl takes
|
|
452
|
+
# it for false and gives offset 0
|
|
453
|
+
context += f' at line {len(lines)}, offset {len(lines[-1]) if lines else 0}'
|
|
454
|
+
|
|
455
|
+
raise ValueError(context)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _encode_array(array: list[Any] | tuple[Any, ...], sort_keys: bool) -> str:
|
|
459
|
+
return '[' + ','.join(_encode_value(v, sort_keys) for v in array) + ']'
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _encode_key(key: object) -> str:
|
|
463
|
+
# Not in Mojolicious: keys that are not strings, converted as the json
|
|
464
|
+
# module of the standard library does, as Perl's hash keys are strings
|
|
465
|
+
if isinstance(key, str):
|
|
466
|
+
return key
|
|
467
|
+
if key is None or isinstance(key, bool):
|
|
468
|
+
return _encode_value(key, False)
|
|
469
|
+
if isinstance(key, (int, float)):
|
|
470
|
+
return _encode_number(key)
|
|
471
|
+
return str(key)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _encode_number(number: int | float) -> str:
|
|
475
|
+
if isinstance(number, int):
|
|
476
|
+
return str(int(number))
|
|
477
|
+
|
|
478
|
+
# Unlike Mojolicious: "inf" and "nan" are stringified the Python way, where
|
|
479
|
+
# Perl gives "Inf" and "NaN"
|
|
480
|
+
if not math.isfinite(number):
|
|
481
|
+
return _encode_string(str(number))
|
|
482
|
+
|
|
483
|
+
# As Perl renders numbers
|
|
484
|
+
return f'{number:.15g}'
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _encode_object(object: dict[Any, Any], sort_keys: bool) -> str:
|
|
488
|
+
# Unlike Mojolicious: the keys in the order of the dict, or sorted with sort_keys, as Mojo sorts them
|
|
489
|
+
pairs = [(_encode_key(k), v) for k, v in object.items()]
|
|
490
|
+
if sort_keys:
|
|
491
|
+
pairs.sort(key=lambda pair: pair[0])
|
|
492
|
+
return '{' + ','.join(_encode_string(k) + ':' + _encode_value(v, sort_keys) for k, v in pairs) + '}'
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _encode_string(string: str) -> str:
|
|
496
|
+
return '"' + string.translate(_TRANSLATE) + '"'
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _encode_value(value: object, sort_keys: bool = False) -> str:
|
|
500
|
+
# Null
|
|
501
|
+
if value is None:
|
|
502
|
+
return 'null'
|
|
503
|
+
|
|
504
|
+
# True or false
|
|
505
|
+
if isinstance(value, bool):
|
|
506
|
+
return 'true' if value else 'false'
|
|
507
|
+
|
|
508
|
+
# String
|
|
509
|
+
if isinstance(value, str):
|
|
510
|
+
return _encode_string(value)
|
|
511
|
+
|
|
512
|
+
# Number
|
|
513
|
+
if isinstance(value, (int, float)):
|
|
514
|
+
return _encode_number(value)
|
|
515
|
+
|
|
516
|
+
# Object
|
|
517
|
+
if isinstance(value, dict):
|
|
518
|
+
return _encode_object(cast(dict[Any, Any], value), sort_keys)
|
|
519
|
+
|
|
520
|
+
# Array
|
|
521
|
+
if isinstance(value, (list, tuple)):
|
|
522
|
+
return _encode_array(cast(list[Any] | tuple[Any, ...], value), sort_keys)
|
|
523
|
+
|
|
524
|
+
# Everything else
|
|
525
|
+
return _encode_value(default(value), sort_keys)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# Not in Mojolicious: the json module of the standard library, set up as Mojo
|
|
529
|
+
# sets up Cpanel::JSON::XS
|
|
530
|
+
def _stdlib_constant(name: str) -> NoReturn:
|
|
531
|
+
raise ValueError(f'Malformed JSON: Unexpected {name}')
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _stdlib_decode(data: bytes | bytearray | str, chars: bool = False) -> Any:
|
|
535
|
+
try:
|
|
536
|
+
return json.loads(_chars(data, chars), parse_constant=_stdlib_constant)
|
|
537
|
+
except RecursionError:
|
|
538
|
+
raise ValueError('Malformed JSON: Nesting too deep') from None
|
|
539
|
+
|
|
540
|
+
# The message of the json module, after the words of the pure Python one
|
|
541
|
+
except json.JSONDecodeError as e:
|
|
542
|
+
raise ValueError(f'Malformed JSON: {e}') from None
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _stdlib_dumps(value: object, sort_keys: bool) -> str:
|
|
546
|
+
return (
|
|
547
|
+
json.dumps(
|
|
548
|
+
value, default=default, ensure_ascii=False, allow_nan=False, separators=(',', ':'), sort_keys=sort_keys
|
|
549
|
+
)
|
|
550
|
+
.replace('/', '\\/')
|
|
551
|
+
.replace('<', '\\u003C')
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _stdlib_encode(value: object, sort_keys: bool = False) -> str:
|
|
556
|
+
try:
|
|
557
|
+
return _stdlib_dumps(value, sort_keys)
|
|
558
|
+
|
|
559
|
+
# "inf" and "nan", or keys that are not strings, numbers, booleans or None, or that cannot be sorted
|
|
560
|
+
except (TypeError, ValueError):
|
|
561
|
+
return _stdlib_dumps(_stdlib_plain(value), sort_keys)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _stdlib_plain(value: object) -> Any:
|
|
565
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
566
|
+
return str(value)
|
|
567
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
568
|
+
return value
|
|
569
|
+
if isinstance(value, dict):
|
|
570
|
+
return {_encode_key(k): _stdlib_plain(v) for k, v in cast(dict[Any, Any], value).items()}
|
|
571
|
+
if isinstance(value, (list, tuple)):
|
|
572
|
+
return [_stdlib_plain(v) for v in cast(list[Any] | tuple[Any, ...], value)]
|
|
573
|
+
return _stdlib_plain(default(value))
|