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.

Files changed (86) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/__init__.py +1 -1
  3. omlish/asyncs/__init__.py +1 -4
  4. omlish/asyncs/anyio.py +66 -0
  5. omlish/asyncs/flavors.py +27 -1
  6. omlish/asyncs/trio_asyncio.py +24 -18
  7. omlish/c3.py +1 -1
  8. omlish/cached.py +1 -2
  9. omlish/collections/__init__.py +4 -1
  10. omlish/collections/cache/impl.py +1 -1
  11. omlish/collections/indexed.py +1 -1
  12. omlish/collections/utils.py +38 -6
  13. omlish/configs/__init__.py +5 -0
  14. omlish/configs/classes.py +53 -0
  15. omlish/configs/dotenv.py +586 -0
  16. omlish/configs/props.py +589 -49
  17. omlish/dataclasses/impl/api.py +1 -1
  18. omlish/dataclasses/impl/as_.py +1 -1
  19. omlish/dataclasses/impl/fields.py +1 -0
  20. omlish/dataclasses/impl/init.py +1 -1
  21. omlish/dataclasses/impl/main.py +1 -0
  22. omlish/dataclasses/impl/metaclass.py +6 -1
  23. omlish/dataclasses/impl/order.py +1 -1
  24. omlish/dataclasses/impl/reflect.py +15 -2
  25. omlish/defs.py +1 -1
  26. omlish/diag/procfs.py +29 -1
  27. omlish/diag/procstats.py +32 -0
  28. omlish/diag/replserver/console.py +3 -3
  29. omlish/diag/replserver/server.py +6 -5
  30. omlish/diag/threads.py +86 -0
  31. omlish/docker.py +19 -0
  32. omlish/fnpairs.py +26 -18
  33. omlish/graphs/dags.py +113 -0
  34. omlish/graphs/domination.py +268 -0
  35. omlish/graphs/trees.py +2 -2
  36. omlish/http/__init__.py +25 -0
  37. omlish/http/asgi.py +131 -0
  38. omlish/http/consts.py +31 -4
  39. omlish/http/cookies.py +194 -0
  40. omlish/http/dates.py +70 -0
  41. omlish/http/encodings.py +6 -0
  42. omlish/http/json.py +273 -0
  43. omlish/http/sessions.py +197 -0
  44. omlish/inject/__init__.py +8 -2
  45. omlish/inject/bindings.py +3 -3
  46. omlish/inject/exceptions.py +3 -3
  47. omlish/inject/impl/elements.py +33 -24
  48. omlish/inject/impl/injector.py +1 -0
  49. omlish/inject/impl/multis.py +74 -0
  50. omlish/inject/impl/providers.py +19 -39
  51. omlish/inject/{proxy.py → impl/proxy.py} +2 -2
  52. omlish/inject/impl/scopes.py +1 -0
  53. omlish/inject/injector.py +1 -0
  54. omlish/inject/keys.py +3 -9
  55. omlish/inject/multis.py +70 -0
  56. omlish/inject/providers.py +23 -23
  57. omlish/inject/scopes.py +7 -3
  58. omlish/inject/types.py +0 -8
  59. omlish/iterators.py +13 -0
  60. omlish/json.py +2 -1
  61. omlish/lang/__init__.py +4 -0
  62. omlish/lang/classes/restrict.py +1 -1
  63. omlish/lang/classes/virtual.py +2 -2
  64. omlish/lang/contextmanagers.py +64 -0
  65. omlish/lang/datetimes.py +6 -5
  66. omlish/lang/functions.py +10 -0
  67. omlish/lang/imports.py +11 -2
  68. omlish/lang/typing.py +1 -0
  69. omlish/logs/utils.py +1 -1
  70. omlish/marshal/datetimes.py +1 -1
  71. omlish/reflect.py +8 -2
  72. omlish/sync.py +70 -0
  73. omlish/term.py +6 -1
  74. omlish/testing/pytest/__init__.py +5 -0
  75. omlish/testing/pytest/helpers.py +0 -24
  76. omlish/testing/pytest/inject/harness.py +1 -1
  77. omlish/testing/pytest/marks.py +48 -0
  78. omlish/testing/pytest/plugins/__init__.py +2 -0
  79. omlish/testing/pytest/plugins/managermarks.py +60 -0
  80. omlish/testing/testing.py +10 -0
  81. omlish/text/delimit.py +4 -0
  82. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +1 -1
  83. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +86 -69
  84. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
  85. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
  86. {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
@@ -0,0 +1,6 @@
1
+ def latin1_decode(s: str) -> str:
2
+ return s.encode('latin1').decode(errors='replace')
3
+
4
+
5
+ def latin1_encode(s: str) -> str:
6
+ return s.encode().decode('latin1')
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()
@@ -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.provided_cls())), o) # type: ignore # noqa
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
- cls = type(o)
45
- return Binding(Key(cls), const(o, cls))
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:
@@ -17,17 +17,17 @@ class BaseKeyError(Exception):
17
17
 
18
18
 
19
19
  @dc.dataclass()
20
- class UnboundKeyError(KeyError):
20
+ class UnboundKeyError(BaseKeyError):
21
21
  pass
22
22
 
23
23
 
24
24
  @dc.dataclass()
25
- class DuplicateKeyError(KeyError):
25
+ class DuplicateKeyError(BaseKeyError):
26
26
  pass
27
27
 
28
28
 
29
29
  @dc.dataclass()
30
- class CyclicDependencyError(KeyError):
30
+ class CyclicDependencyError(BaseKeyError):
31
31
  pass
32
32
 
33
33