omlish 0.0.0.dev3__py3-none-any.whl → 0.0.0.dev5__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.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/__about__.py +1 -1
- omlish/__init__.py +8 -0
- omlish/asyncs/__init__.py +18 -0
- omlish/asyncs/anyio.py +66 -0
- omlish/asyncs/flavors.py +227 -0
- omlish/asyncs/trio_asyncio.py +47 -0
- omlish/c3.py +1 -1
- omlish/cached.py +1 -2
- omlish/collections/__init__.py +4 -1
- omlish/collections/cache/impl.py +1 -1
- omlish/collections/indexed.py +1 -1
- omlish/collections/utils.py +38 -6
- omlish/configs/__init__.py +5 -0
- omlish/configs/classes.py +53 -0
- omlish/configs/dotenv.py +586 -0
- omlish/configs/props.py +589 -49
- omlish/dataclasses/impl/api.py +1 -1
- omlish/dataclasses/impl/as_.py +1 -1
- omlish/dataclasses/impl/fields.py +1 -0
- omlish/dataclasses/impl/init.py +1 -1
- omlish/dataclasses/impl/main.py +1 -0
- omlish/dataclasses/impl/metaclass.py +6 -1
- omlish/dataclasses/impl/order.py +1 -1
- omlish/dataclasses/impl/reflect.py +15 -2
- omlish/defs.py +1 -1
- omlish/diag/procfs.py +29 -1
- omlish/diag/procstats.py +32 -0
- omlish/diag/replserver/console.py +3 -3
- omlish/diag/replserver/server.py +6 -5
- omlish/diag/threads.py +86 -0
- omlish/docker.py +19 -0
- omlish/dynamic.py +2 -2
- omlish/fnpairs.py +121 -24
- omlish/graphs/dags.py +113 -0
- omlish/graphs/domination.py +268 -0
- omlish/graphs/trees.py +2 -2
- omlish/http/__init__.py +25 -0
- omlish/http/asgi.py +131 -0
- omlish/http/consts.py +31 -4
- omlish/http/cookies.py +194 -0
- omlish/http/dates.py +70 -0
- omlish/http/encodings.py +6 -0
- omlish/http/json.py +273 -0
- omlish/http/sessions.py +197 -0
- omlish/inject/__init__.py +8 -2
- omlish/inject/bindings.py +3 -3
- omlish/inject/exceptions.py +3 -3
- omlish/inject/impl/elements.py +46 -25
- omlish/inject/impl/injector.py +8 -5
- omlish/inject/impl/multis.py +74 -0
- omlish/inject/impl/providers.py +19 -39
- omlish/inject/{proxy.py → impl/proxy.py} +2 -2
- omlish/inject/impl/scopes.py +4 -2
- omlish/inject/injector.py +1 -0
- omlish/inject/keys.py +3 -9
- omlish/inject/multis.py +70 -0
- omlish/inject/providers.py +23 -23
- omlish/inject/scopes.py +7 -3
- omlish/inject/types.py +0 -8
- omlish/iterators.py +13 -0
- omlish/json.py +138 -1
- omlish/lang/__init__.py +8 -0
- omlish/lang/classes/restrict.py +1 -1
- omlish/lang/classes/virtual.py +2 -2
- omlish/lang/contextmanagers.py +64 -0
- omlish/lang/datetimes.py +6 -5
- omlish/lang/functions.py +10 -0
- omlish/lang/imports.py +11 -2
- omlish/lang/sys.py +7 -0
- omlish/lang/typing.py +1 -0
- omlish/logs/utils.py +1 -1
- omlish/marshal/datetimes.py +1 -1
- omlish/reflect.py +8 -2
- omlish/sql/__init__.py +9 -0
- omlish/sql/asyncs.py +148 -0
- omlish/sync.py +70 -0
- omlish/term.py +6 -1
- omlish/testing/pydevd.py +2 -0
- omlish/testing/pytest/__init__.py +5 -0
- omlish/testing/pytest/helpers.py +0 -24
- omlish/testing/pytest/inject/harness.py +1 -1
- omlish/testing/pytest/marks.py +48 -0
- omlish/testing/pytest/plugins/__init__.py +2 -0
- omlish/testing/pytest/plugins/managermarks.py +60 -0
- omlish/testing/testing.py +10 -0
- omlish/text/delimit.py +4 -0
- {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +4 -1
- {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +91 -70
- {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
- {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev3.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
omlish/http/cookies.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
https://github.com/pallets/werkzeug/blob/9e050f7750214d6779636813b8d661250804e811/src/werkzeug/http.py
|
|
3
|
+
https://github.com/pallets/werkzeug/blob/9e050f7750214d6779636813b8d661250804e811/src/werkzeug/sansio/http.py
|
|
4
|
+
"""
|
|
5
|
+
# Copyright 2007 Pallets
|
|
6
|
+
#
|
|
7
|
+
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
8
|
+
# following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
11
|
+
# disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
|
|
14
|
+
# following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
|
|
17
|
+
# products derived from this software without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
20
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
22
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
23
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
24
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
import datetime
|
|
27
|
+
import re
|
|
28
|
+
import typing as ta
|
|
29
|
+
import urllib.parse
|
|
30
|
+
|
|
31
|
+
from .. import collections as col
|
|
32
|
+
from .dates import http_date
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_COOKIE_RE = re.compile(
|
|
39
|
+
r"""
|
|
40
|
+
([^=;]*)
|
|
41
|
+
(?:\s*=\s*
|
|
42
|
+
(
|
|
43
|
+
"(?:[^\\"]|\\.)*"
|
|
44
|
+
|
|
|
45
|
+
.*?
|
|
46
|
+
)
|
|
47
|
+
)?
|
|
48
|
+
\s*;\s*
|
|
49
|
+
""",
|
|
50
|
+
flags=re.ASCII | re.VERBOSE,
|
|
51
|
+
)
|
|
52
|
+
_COOKIE_UNSLASH_RE = re.compile(rb'\\([0-3][0-7]{2}|.)')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cookie_unslash_replace(m: ta.Match[bytes]) -> bytes:
|
|
56
|
+
v = m.group(1)
|
|
57
|
+
|
|
58
|
+
if len(v) == 1:
|
|
59
|
+
return v
|
|
60
|
+
|
|
61
|
+
return int(v, 8).to_bytes(1, 'big')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_cookie(
|
|
65
|
+
cookie: str | None = None,
|
|
66
|
+
*,
|
|
67
|
+
no_latin1: bool = False,
|
|
68
|
+
) -> ta.MutableMapping[str, list[str]]:
|
|
69
|
+
if (not no_latin1) and cookie:
|
|
70
|
+
cookie = cookie.encode('latin1').decode()
|
|
71
|
+
|
|
72
|
+
if not cookie:
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
cookie = f'{cookie};'
|
|
76
|
+
out = []
|
|
77
|
+
|
|
78
|
+
for ck, cv in _COOKIE_RE.findall(cookie):
|
|
79
|
+
ck = ck.strip()
|
|
80
|
+
cv = cv.strip()
|
|
81
|
+
|
|
82
|
+
if not ck:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if len(cv) >= 2 and cv[0] == cv[-1] == '"':
|
|
86
|
+
# Work with bytes here, since a UTF-8 character could be multiple bytes.
|
|
87
|
+
cv = _COOKIE_UNSLASH_RE.sub(
|
|
88
|
+
_cookie_unslash_replace,
|
|
89
|
+
cv[1:-1].encode(),
|
|
90
|
+
).decode(errors='replace')
|
|
91
|
+
|
|
92
|
+
out.append((ck, cv))
|
|
93
|
+
|
|
94
|
+
return col.multi_map(out)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_COOKIE_NO_QUOTE_RE = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.ASCII)
|
|
101
|
+
_COOKIE_SLASH_RE = re.compile(rb'[\x00-\x19\",;\\\x7f-\xff]', re.ASCII)
|
|
102
|
+
_COOKIE_SLASH_MAP = {b'"': b'\\"', b'\\': b'\\\\'}
|
|
103
|
+
_COOKIE_SLASH_MAP.update(
|
|
104
|
+
(v.to_bytes(1, 'big'), b'\\%03o' % v)
|
|
105
|
+
for v in [*range(0x20), *b',;', *range(0x7F, 256)]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CookieTooBigError(Exception):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def dump_cookie(
|
|
114
|
+
key: str,
|
|
115
|
+
value: str = '',
|
|
116
|
+
*,
|
|
117
|
+
max_age: datetime.timedelta | int | None = None,
|
|
118
|
+
expires: str | datetime.datetime | float | None = None,
|
|
119
|
+
path: str | None = '/',
|
|
120
|
+
domain: str | None = None,
|
|
121
|
+
secure: bool = False,
|
|
122
|
+
httponly: bool = False,
|
|
123
|
+
sync_expires: bool = True,
|
|
124
|
+
max_size: int = 4093,
|
|
125
|
+
samesite: str | None = None,
|
|
126
|
+
partitioned: bool = False,
|
|
127
|
+
) -> str:
|
|
128
|
+
if path is not None:
|
|
129
|
+
# safe = https://url.spec.whatwg.org/#url-path-segment-string as well as percent for things that are already
|
|
130
|
+
# quoted excluding semicolon since it's part of the header syntax
|
|
131
|
+
path = urllib.parse.quote(path, safe="%!$&'()*+,/:=@")
|
|
132
|
+
|
|
133
|
+
if domain:
|
|
134
|
+
domain = domain.partition(':')[0].lstrip('.').encode('idna').decode('ascii')
|
|
135
|
+
|
|
136
|
+
if isinstance(max_age, datetime.timedelta):
|
|
137
|
+
max_age = int(max_age.total_seconds())
|
|
138
|
+
|
|
139
|
+
if expires is not None:
|
|
140
|
+
if not isinstance(expires, str):
|
|
141
|
+
expires = http_date(expires)
|
|
142
|
+
elif max_age is not None and sync_expires:
|
|
143
|
+
expires = http_date(datetime.datetime.now(tz=datetime.UTC).timestamp() + max_age)
|
|
144
|
+
|
|
145
|
+
if samesite is not None:
|
|
146
|
+
samesite = samesite.title()
|
|
147
|
+
|
|
148
|
+
if samesite not in {'Strict', 'Lax', 'None'}:
|
|
149
|
+
raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
|
|
150
|
+
|
|
151
|
+
if partitioned:
|
|
152
|
+
secure = True
|
|
153
|
+
|
|
154
|
+
# Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
|
|
155
|
+
# three octal digits, which matches http.cookies, although the RFC suggests base64.
|
|
156
|
+
if not _COOKIE_NO_QUOTE_RE.fullmatch(value):
|
|
157
|
+
# Work with bytes here, since a UTF-8 character could be multiple bytes.
|
|
158
|
+
value = _COOKIE_SLASH_RE.sub(
|
|
159
|
+
lambda m: _COOKIE_SLASH_MAP[m.group()], value.encode(),
|
|
160
|
+
).decode('ascii')
|
|
161
|
+
value = f'"{value}"'
|
|
162
|
+
|
|
163
|
+
# Send a non-ASCII key as mojibake. Everything else should already be ASCII.
|
|
164
|
+
# TODO Remove encoding dance, it seems like clients accept UTF-8 keys
|
|
165
|
+
buf = [f"{key.encode().decode('latin1')}={value}"]
|
|
166
|
+
|
|
167
|
+
for k, v in (
|
|
168
|
+
('Domain', domain),
|
|
169
|
+
('Expires', expires),
|
|
170
|
+
('Max-Age', max_age),
|
|
171
|
+
('Secure', secure),
|
|
172
|
+
('HttpOnly', httponly),
|
|
173
|
+
('Path', path),
|
|
174
|
+
('SameSite', samesite),
|
|
175
|
+
('Partitioned', partitioned),
|
|
176
|
+
):
|
|
177
|
+
if v is None or v is False:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if v is True:
|
|
181
|
+
buf.append(k)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
buf.append(f'{k}={v}')
|
|
185
|
+
|
|
186
|
+
rv = '; '.join(buf)
|
|
187
|
+
|
|
188
|
+
# Warn if the final value of the cookie is larger than the limit. If the cookie is too large, then it may be
|
|
189
|
+
# silently ignored by the browser, which can be quite hard to debug.
|
|
190
|
+
cookie_size = len(rv)
|
|
191
|
+
if max_size and cookie_size > max_size:
|
|
192
|
+
raise CookieTooBigError(cookie_size)
|
|
193
|
+
|
|
194
|
+
return rv
|
omlish/http/dates.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright 2007 Pallets
|
|
2
|
+
#
|
|
3
|
+
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
4
|
+
# following conditions are met:
|
|
5
|
+
#
|
|
6
|
+
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
7
|
+
# disclaimer.
|
|
8
|
+
#
|
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
|
|
10
|
+
# following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
11
|
+
#
|
|
12
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
|
|
13
|
+
# products derived from this software without specific prior written permission.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
16
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
18
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
19
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
20
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22
|
+
import datetime
|
|
23
|
+
import email.utils
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
from .. import check
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _dt_as_utc(dt: datetime.datetime | None) -> datetime.datetime | None:
|
|
30
|
+
if dt is None:
|
|
31
|
+
return dt
|
|
32
|
+
|
|
33
|
+
if dt.tzinfo is None:
|
|
34
|
+
return dt.replace(tzinfo=datetime.UTC)
|
|
35
|
+
elif dt.tzinfo != datetime.UTC:
|
|
36
|
+
return dt.astimezone(datetime.UTC)
|
|
37
|
+
|
|
38
|
+
return dt
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def http_date(timestamp: datetime.datetime | datetime.date | float | time.struct_time | None = None) -> str:
|
|
42
|
+
if isinstance(timestamp, datetime.date):
|
|
43
|
+
if not isinstance(timestamp, datetime.datetime):
|
|
44
|
+
# Assume plain date is midnight UTC.
|
|
45
|
+
timestamp = datetime.datetime.combine(timestamp, datetime.time(), tzinfo=datetime.UTC)
|
|
46
|
+
else:
|
|
47
|
+
# Ensure datetime is timezone-aware.
|
|
48
|
+
timestamp = _dt_as_utc(timestamp)
|
|
49
|
+
|
|
50
|
+
return email.utils.format_datetime(check.not_none(timestamp), usegmt=True)
|
|
51
|
+
|
|
52
|
+
if isinstance(timestamp, time.struct_time):
|
|
53
|
+
timestamp = time.mktime(timestamp)
|
|
54
|
+
|
|
55
|
+
return email.utils.formatdate(timestamp, usegmt=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_date(value: str | None) -> datetime.datetime | None:
|
|
59
|
+
if value is None:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
dt = email.utils.parsedate_to_datetime(value)
|
|
64
|
+
except (TypeError, ValueError):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
if dt.tzinfo is None:
|
|
68
|
+
return dt.replace(tzinfo=datetime.UTC)
|
|
69
|
+
|
|
70
|
+
return dt
|
omlish/http/encodings.py
ADDED
omlish/http/json.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Copyright 2010 Pallets
|
|
2
|
+
#
|
|
3
|
+
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
4
|
+
# following conditions are met:
|
|
5
|
+
#
|
|
6
|
+
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
7
|
+
# disclaimer.
|
|
8
|
+
#
|
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
|
|
10
|
+
# following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
11
|
+
#
|
|
12
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
|
|
13
|
+
# products derived from this software without specific prior written permission.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
16
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
18
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
19
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
20
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22
|
+
import base64
|
|
23
|
+
import dataclasses as dc
|
|
24
|
+
import datetime
|
|
25
|
+
import decimal
|
|
26
|
+
import json as _json
|
|
27
|
+
import typing as ta
|
|
28
|
+
import uuid
|
|
29
|
+
|
|
30
|
+
from .. import lang
|
|
31
|
+
from .dates import http_date
|
|
32
|
+
from .dates import parse_date
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if ta.TYPE_CHECKING:
|
|
36
|
+
import markupsafe
|
|
37
|
+
else:
|
|
38
|
+
markupsafe = lang.proxy_import('markupsafe')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _default(o: ta.Any) -> ta.Any:
|
|
45
|
+
if isinstance(o, datetime.date):
|
|
46
|
+
return http_date(o)
|
|
47
|
+
|
|
48
|
+
if isinstance(o, (decimal.Decimal, uuid.UUID)):
|
|
49
|
+
return str(o)
|
|
50
|
+
|
|
51
|
+
if dc.is_dataclass(o):
|
|
52
|
+
return dc.asdict(o) # type: ignore
|
|
53
|
+
|
|
54
|
+
if hasattr(o, '__html__'):
|
|
55
|
+
return str(o.__html__())
|
|
56
|
+
|
|
57
|
+
raise TypeError(f'Object of type {type(o).__name__} is not Json serializable')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def json_dumps(obj: ta.Any, **kwargs: ta.Any) -> str:
|
|
61
|
+
kwargs.setdefault('default', _default)
|
|
62
|
+
return _json.dumps(obj, **kwargs)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def json_loads(s: str | bytes, **kwargs: ta.Any) -> ta.Any:
|
|
66
|
+
return _json.loads(s, **kwargs)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class JsonTag:
|
|
73
|
+
key: str = ''
|
|
74
|
+
|
|
75
|
+
def __init__(self, tagger: 'JsonTagger') -> None:
|
|
76
|
+
super().__init__()
|
|
77
|
+
|
|
78
|
+
self.tagger = tagger
|
|
79
|
+
|
|
80
|
+
def check(self, value: ta.Any) -> bool:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
def tag(self, value: ta.Any) -> dict[str, ta.Any]:
|
|
90
|
+
return {self.key: self.to_json(value)}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TagDict(JsonTag):
|
|
94
|
+
key = ' di'
|
|
95
|
+
|
|
96
|
+
def check(self, value: ta.Any) -> bool:
|
|
97
|
+
return (
|
|
98
|
+
isinstance(value, dict)
|
|
99
|
+
and len(value) == 1
|
|
100
|
+
and next(iter(value)) in self.tagger.tags
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
104
|
+
key = next(iter(value))
|
|
105
|
+
return {f'{key}__': self.tagger.tag(value[key])}
|
|
106
|
+
|
|
107
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
108
|
+
key = next(iter(value))
|
|
109
|
+
return {key[:-2]: value[key]}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PassDict(JsonTag):
|
|
113
|
+
def check(self, value: ta.Any) -> bool:
|
|
114
|
+
return isinstance(value, dict)
|
|
115
|
+
|
|
116
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
117
|
+
return {k: self.tagger.tag(v) for k, v in value.items()}
|
|
118
|
+
|
|
119
|
+
tag = to_json
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TagTuple(JsonTag):
|
|
123
|
+
key = ' t'
|
|
124
|
+
|
|
125
|
+
def check(self, value: ta.Any) -> bool:
|
|
126
|
+
return isinstance(value, tuple)
|
|
127
|
+
|
|
128
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
129
|
+
return [self.tagger.tag(item) for item in value]
|
|
130
|
+
|
|
131
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
132
|
+
return tuple(value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PassList(JsonTag):
|
|
136
|
+
def check(self, value: ta.Any) -> bool:
|
|
137
|
+
return isinstance(value, list)
|
|
138
|
+
|
|
139
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
140
|
+
return [self.tagger.tag(item) for item in value]
|
|
141
|
+
|
|
142
|
+
tag = to_json
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TagBytes(JsonTag):
|
|
146
|
+
key = ' b'
|
|
147
|
+
|
|
148
|
+
def check(self, value: ta.Any) -> bool:
|
|
149
|
+
return isinstance(value, bytes)
|
|
150
|
+
|
|
151
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
152
|
+
return base64.b64encode(value).decode('ascii')
|
|
153
|
+
|
|
154
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
155
|
+
return base64.b64decode(value)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TagMarkup(JsonTag):
|
|
159
|
+
key = ' m'
|
|
160
|
+
|
|
161
|
+
def check(self, value: ta.Any) -> bool:
|
|
162
|
+
return callable(getattr(value, '__html__', None))
|
|
163
|
+
|
|
164
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
165
|
+
return str(value.__html__())
|
|
166
|
+
|
|
167
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
168
|
+
return markupsafe.Markup(value)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TagUuid(JsonTag):
|
|
172
|
+
key = ' u'
|
|
173
|
+
|
|
174
|
+
def check(self, value: ta.Any) -> bool:
|
|
175
|
+
return isinstance(value, uuid.UUID)
|
|
176
|
+
|
|
177
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
178
|
+
return value.hex
|
|
179
|
+
|
|
180
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
181
|
+
return uuid.UUID(value)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TagDatetime(JsonTag):
|
|
185
|
+
key = ' dt'
|
|
186
|
+
|
|
187
|
+
def check(self, value: ta.Any) -> bool:
|
|
188
|
+
return isinstance(value, datetime.datetime)
|
|
189
|
+
|
|
190
|
+
def to_json(self, value: ta.Any) -> ta.Any:
|
|
191
|
+
return http_date(value)
|
|
192
|
+
|
|
193
|
+
def to_python(self, value: ta.Any) -> ta.Any:
|
|
194
|
+
return parse_date(value)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class JsonTagger:
|
|
198
|
+
default_tags: ta.ClassVar[ta.Sequence[type[JsonTag]]] = [
|
|
199
|
+
TagDict,
|
|
200
|
+
PassDict,
|
|
201
|
+
TagTuple,
|
|
202
|
+
PassList,
|
|
203
|
+
TagBytes,
|
|
204
|
+
*([TagMarkup] if lang.can_import('markupsafe') else []),
|
|
205
|
+
TagUuid,
|
|
206
|
+
TagDatetime,
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
def __init__(self) -> None:
|
|
210
|
+
super().__init__()
|
|
211
|
+
|
|
212
|
+
self.tags: dict[str, JsonTag] = {}
|
|
213
|
+
self.order: list[JsonTag] = []
|
|
214
|
+
|
|
215
|
+
for cls in self.default_tags:
|
|
216
|
+
self.register(cls)
|
|
217
|
+
|
|
218
|
+
def register(
|
|
219
|
+
self,
|
|
220
|
+
tag_class: type[JsonTag],
|
|
221
|
+
force: bool = False,
|
|
222
|
+
index: int | None = None,
|
|
223
|
+
) -> None:
|
|
224
|
+
tag = tag_class(self)
|
|
225
|
+
key = tag.key
|
|
226
|
+
|
|
227
|
+
if key:
|
|
228
|
+
if not force and key in self.tags:
|
|
229
|
+
raise KeyError(f"Tag '{key}' is already registered.")
|
|
230
|
+
|
|
231
|
+
self.tags[key] = tag
|
|
232
|
+
|
|
233
|
+
if index is None:
|
|
234
|
+
self.order.append(tag)
|
|
235
|
+
else:
|
|
236
|
+
self.order.insert(index, tag)
|
|
237
|
+
|
|
238
|
+
def tag(self, value: ta.Any) -> ta.Any:
|
|
239
|
+
for tag in self.order:
|
|
240
|
+
if tag.check(value):
|
|
241
|
+
return tag.tag(value)
|
|
242
|
+
|
|
243
|
+
return value
|
|
244
|
+
|
|
245
|
+
def untag(self, value: dict[str, ta.Any]) -> ta.Any:
|
|
246
|
+
if len(value) != 1:
|
|
247
|
+
return value
|
|
248
|
+
|
|
249
|
+
key = next(iter(value))
|
|
250
|
+
|
|
251
|
+
if key not in self.tags:
|
|
252
|
+
return value
|
|
253
|
+
|
|
254
|
+
return self.tags[key].to_python(value[key])
|
|
255
|
+
|
|
256
|
+
def untag_scan(self, value: ta.Any) -> ta.Any:
|
|
257
|
+
if isinstance(value, dict):
|
|
258
|
+
value = {k: self.untag_scan(v) for k, v in value.items()}
|
|
259
|
+
value = self.untag(value)
|
|
260
|
+
|
|
261
|
+
elif isinstance(value, list):
|
|
262
|
+
value = [self.untag_scan(item) for item in value]
|
|
263
|
+
|
|
264
|
+
return value
|
|
265
|
+
|
|
266
|
+
def dumps(self, value: ta.Any) -> str:
|
|
267
|
+
return json_dumps(self.tag(value), separators=(',', ':'))
|
|
268
|
+
|
|
269
|
+
def loads(self, value: str) -> ta.Any:
|
|
270
|
+
return self.untag_scan(json_loads(value))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
JSON_TAGGER = JsonTagger()
|