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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. 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))