omlish 0.0.0.dev4__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 +1 -1
- omlish/asyncs/__init__.py +1 -4
- omlish/asyncs/anyio.py +66 -0
- omlish/asyncs/flavors.py +27 -1
- omlish/asyncs/trio_asyncio.py +24 -18
- 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/fnpairs.py +26 -18
- 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 +33 -24
- omlish/inject/impl/injector.py +1 -0
- 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 +1 -0
- 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 +2 -1
- omlish/lang/__init__.py +4 -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/typing.py +1 -0
- omlish/logs/utils.py +1 -1
- omlish/marshal/datetimes.py +1 -1
- omlish/reflect.py +8 -2
- omlish/sync.py +70 -0
- omlish/term.py +6 -1
- 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.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +86 -69
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
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()
|
omlish/http/sessions.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import dataclasses as dc
|
|
3
|
+
import datetime
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import struct
|
|
7
|
+
import time
|
|
8
|
+
import typing as ta
|
|
9
|
+
import zlib
|
|
10
|
+
|
|
11
|
+
from .. import fnpairs as fps
|
|
12
|
+
from .. import lang
|
|
13
|
+
from .cookies import dump_cookie
|
|
14
|
+
from .cookies import parse_cookie
|
|
15
|
+
from .json import JSON_TAGGER
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Session: ta.TypeAlias = dict[str, ta.Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def base64_encode(b: bytes) -> bytes:
|
|
25
|
+
return base64.urlsafe_b64encode(b).rstrip(b'=')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def base64_decode(b: bytes) -> bytes:
|
|
29
|
+
b += b'=' * (-len(b) % 4)
|
|
30
|
+
return base64.urlsafe_b64decode(b)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def int_to_bytes(num: int) -> bytes:
|
|
34
|
+
return struct.pack('>Q', num).lstrip(b'\0')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bytes_to_int(bytestr: bytes) -> int:
|
|
38
|
+
return struct.unpack('>Q', bytestr.rjust(8, b'\0'))[0]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Signer:
|
|
45
|
+
@dc.dataclass(frozen=True)
|
|
46
|
+
class Config:
|
|
47
|
+
secret_key: str
|
|
48
|
+
salt: str = 'cookie-session'
|
|
49
|
+
|
|
50
|
+
def __init__(self, config: Config) -> None:
|
|
51
|
+
super().__init__()
|
|
52
|
+
|
|
53
|
+
self._config = config
|
|
54
|
+
|
|
55
|
+
@lang.cached_function
|
|
56
|
+
def digest(self) -> ta.Any:
|
|
57
|
+
return hashlib.sha1
|
|
58
|
+
|
|
59
|
+
@lang.cached_function
|
|
60
|
+
def derive_key(self) -> bytes:
|
|
61
|
+
mac = hmac.new(self._config.secret_key.encode(), digestmod=self.digest())
|
|
62
|
+
mac.update(self._config.salt.encode())
|
|
63
|
+
return mac.digest()
|
|
64
|
+
|
|
65
|
+
def get_signature(self, value: bytes) -> bytes:
|
|
66
|
+
mac = hmac.new(self.derive_key(), msg=value, digestmod=self.digest())
|
|
67
|
+
return mac.digest()
|
|
68
|
+
|
|
69
|
+
def verify_signature(self, value: bytes, sig: bytes) -> bool:
|
|
70
|
+
return hmac.compare_digest(sig, self.get_signature(value))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SessionExpiredError(Exception):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SessionVerificationError(Exception):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SessionMarshal:
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
signer: Signer,
|
|
88
|
+
serializer: fps.ObjectStr = fps.of(JSON_TAGGER.dumps, JSON_TAGGER.loads),
|
|
89
|
+
) -> None:
|
|
90
|
+
super().__init__()
|
|
91
|
+
|
|
92
|
+
self._signer = signer
|
|
93
|
+
self._serializer = serializer
|
|
94
|
+
|
|
95
|
+
SEP = b'.'
|
|
96
|
+
|
|
97
|
+
def load(self, bs: bytes) -> ta.Any:
|
|
98
|
+
value, sig = bs.rsplit(self.SEP, 1)
|
|
99
|
+
|
|
100
|
+
sig_b = base64_decode(sig)
|
|
101
|
+
|
|
102
|
+
if not self._signer.verify_signature(value, sig_b):
|
|
103
|
+
raise SessionVerificationError
|
|
104
|
+
|
|
105
|
+
value, ts_bytes = value.rsplit(self.SEP, 1)
|
|
106
|
+
ts_int = bytes_to_int(base64_decode(ts_bytes))
|
|
107
|
+
|
|
108
|
+
max_age = 31 * 24 * 60 * 60
|
|
109
|
+
age = int(time.time()) - ts_int
|
|
110
|
+
|
|
111
|
+
if age > max_age:
|
|
112
|
+
raise SessionExpiredError
|
|
113
|
+
if age < 0:
|
|
114
|
+
raise SessionExpiredError
|
|
115
|
+
|
|
116
|
+
payload = value
|
|
117
|
+
|
|
118
|
+
decompress = False
|
|
119
|
+
if payload.startswith(b'.'):
|
|
120
|
+
payload = payload[1:]
|
|
121
|
+
decompress = True
|
|
122
|
+
|
|
123
|
+
jb = base64_decode(payload)
|
|
124
|
+
|
|
125
|
+
if decompress:
|
|
126
|
+
jb = zlib.decompress(jb)
|
|
127
|
+
|
|
128
|
+
jbs = jb.decode()
|
|
129
|
+
|
|
130
|
+
obj = self._serializer.backward(jbs)
|
|
131
|
+
|
|
132
|
+
return obj
|
|
133
|
+
|
|
134
|
+
def dump(self, obj: ta.Any) -> bytes:
|
|
135
|
+
jbs = self._serializer.forward(obj)
|
|
136
|
+
|
|
137
|
+
jb = jbs.encode()
|
|
138
|
+
|
|
139
|
+
is_compressed = False
|
|
140
|
+
compressed = zlib.compress(jb)
|
|
141
|
+
|
|
142
|
+
if len(compressed) < (len(jb) - 1):
|
|
143
|
+
jb = compressed
|
|
144
|
+
is_compressed = True
|
|
145
|
+
|
|
146
|
+
base64d = base64_encode(jb)
|
|
147
|
+
|
|
148
|
+
if is_compressed:
|
|
149
|
+
base64d = b'.' + base64d
|
|
150
|
+
|
|
151
|
+
payload = base64d
|
|
152
|
+
|
|
153
|
+
timestamp = base64_encode(int_to_bytes(int(time.time())))
|
|
154
|
+
|
|
155
|
+
value = payload + self.SEP + timestamp
|
|
156
|
+
return value + self.SEP + base64_encode(self._signer.get_signature(value))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
##
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CookieSessionStore:
|
|
163
|
+
@dc.dataclass(frozen=True)
|
|
164
|
+
class Config:
|
|
165
|
+
key: str = 'session'
|
|
166
|
+
max_age: datetime.timedelta | int | None = None
|
|
167
|
+
|
|
168
|
+
def __init__(self, marshal: SessionMarshal, config: Config = Config()) -> None:
|
|
169
|
+
super().__init__()
|
|
170
|
+
|
|
171
|
+
self._marshal = marshal
|
|
172
|
+
self._config = config
|
|
173
|
+
|
|
174
|
+
def extract(self, scope) -> Session:
|
|
175
|
+
for k, v in scope['headers']:
|
|
176
|
+
if k == b'cookie':
|
|
177
|
+
cks = parse_cookie(v.decode('latin-1', 'strict'))
|
|
178
|
+
sk = cks.get(self._config.key)
|
|
179
|
+
if sk:
|
|
180
|
+
return self._marshal.load(sk[0].encode('latin-1', 'strict'))
|
|
181
|
+
|
|
182
|
+
return {}
|
|
183
|
+
|
|
184
|
+
def build_headers(self, session: Session) -> list[tuple[bytes, bytes]]:
|
|
185
|
+
d = self._marshal.dump(session)
|
|
186
|
+
|
|
187
|
+
c = dump_cookie(
|
|
188
|
+
self._config.key,
|
|
189
|
+
d.decode('latin-1', 'strict'),
|
|
190
|
+
max_age=self._config.max_age,
|
|
191
|
+
httponly=True,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return [
|
|
195
|
+
(b'Vary', b'Cookie'),
|
|
196
|
+
(b'Set-Cookie', c.encode('latin-1', 'strict')),
|
|
197
|
+
]
|
omlish/inject/__init__.py
CHANGED
|
@@ -40,7 +40,6 @@ from .inspect import ( # noqa
|
|
|
40
40
|
from .keys import ( # noqa
|
|
41
41
|
Key,
|
|
42
42
|
as_key,
|
|
43
|
-
multi,
|
|
44
43
|
tag,
|
|
45
44
|
)
|
|
46
45
|
|
|
@@ -48,6 +47,14 @@ from .managed import ( # noqa
|
|
|
48
47
|
create_managed_injector,
|
|
49
48
|
)
|
|
50
49
|
|
|
50
|
+
from .multis import ( # noqa
|
|
51
|
+
MapBinding,
|
|
52
|
+
SetBinding,
|
|
53
|
+
bind_map_provider,
|
|
54
|
+
bind_set_provider,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
51
58
|
from .overrides import ( # noqa
|
|
52
59
|
override,
|
|
53
60
|
)
|
|
@@ -79,7 +86,6 @@ from .scopes import ( # noqa
|
|
|
79
86
|
)
|
|
80
87
|
|
|
81
88
|
from .types import ( # noqa
|
|
82
|
-
Cls,
|
|
83
89
|
Scope,
|
|
84
90
|
Unscoped,
|
|
85
91
|
)
|
omlish/inject/bindings.py
CHANGED
|
@@ -36,13 +36,13 @@ def as_binding(o: ta.Any) -> Binding:
|
|
|
36
36
|
return o
|
|
37
37
|
check.not_isinstance(o, (Element, Elements))
|
|
38
38
|
if isinstance(o, Provider):
|
|
39
|
-
return Binding(Key(check.not_none(o.
|
|
39
|
+
return Binding(Key(check.not_none(o.provided_ty())), o)
|
|
40
40
|
if isinstance(o, type):
|
|
41
41
|
return as_binding(ctor(o))
|
|
42
42
|
if callable(o):
|
|
43
43
|
return as_binding(fn(o))
|
|
44
|
-
|
|
45
|
-
return Binding(Key(
|
|
44
|
+
ty = type(o)
|
|
45
|
+
return Binding(Key(ty), const(o, ty))
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def as_(k: ta.Any, p: ta.Any) -> Binding:
|
omlish/inject/exceptions.py
CHANGED
|
@@ -17,17 +17,17 @@ class BaseKeyError(Exception):
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@dc.dataclass()
|
|
20
|
-
class UnboundKeyError(
|
|
20
|
+
class UnboundKeyError(BaseKeyError):
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dc.dataclass()
|
|
25
|
-
class DuplicateKeyError(
|
|
25
|
+
class DuplicateKeyError(BaseKeyError):
|
|
26
26
|
pass
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dc.dataclass()
|
|
30
|
-
class CyclicDependencyError(
|
|
30
|
+
class CyclicDependencyError(BaseKeyError):
|
|
31
31
|
pass
|
|
32
32
|
|
|
33
33
|
|