omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev484__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/.omlish-manifests.json +12 -0
- omlish/__about__.py +15 -15
- omlish/argparse/all.py +17 -9
- omlish/argparse/cli.py +16 -3
- omlish/argparse/utils.py +21 -0
- omlish/asyncs/asyncio/rlock.py +110 -0
- omlish/asyncs/asyncio/sync.py +43 -0
- omlish/asyncs/asyncio/utils.py +2 -0
- omlish/asyncs/sync.py +25 -0
- omlish/bootstrap/_marshal.py +1 -1
- omlish/bootstrap/diag.py +12 -21
- omlish/bootstrap/main.py +2 -5
- omlish/bootstrap/sys.py +27 -28
- omlish/cexts/__init__.py +0 -0
- omlish/cexts/include/omlish/omlish.hh +1 -0
- omlish/collections/__init__.py +13 -1
- omlish/collections/attrregistry.py +210 -0
- omlish/collections/cache/impl.py +1 -0
- omlish/collections/identity.py +1 -0
- omlish/collections/mappings.py +28 -0
- omlish/collections/trie.py +5 -1
- omlish/collections/utils.py +77 -0
- omlish/concurrent/all.py +2 -1
- omlish/concurrent/futures.py +25 -0
- omlish/concurrent/threadlets.py +1 -1
- omlish/daemons/reparent.py +2 -3
- omlish/daemons/spawning.py +2 -3
- omlish/dataclasses/__init__.py +2 -0
- omlish/dataclasses/impl/api/classes/decorator.py +3 -0
- omlish/dataclasses/impl/api/classes/make.py +3 -0
- omlish/dataclasses/impl/concerns/repr.py +15 -2
- omlish/dataclasses/impl/configs.py +97 -37
- omlish/dataclasses/impl/generation/compilation.py +21 -19
- omlish/dataclasses/impl/generation/globals.py +1 -0
- omlish/dataclasses/impl/generation/ops.py +1 -0
- omlish/dataclasses/impl/generation/processor.py +105 -24
- omlish/dataclasses/impl/processing/base.py +8 -0
- omlish/dataclasses/impl/processing/driving.py +8 -8
- omlish/dataclasses/specs.py +1 -0
- omlish/dataclasses/tools/modifiers.py +5 -0
- omlish/diag/cmds/__init__.py +0 -0
- omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
- omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
- omlish/diag/{ps.py → cmds/ps.py} +6 -6
- omlish/diag/pycharm.py +16 -2
- omlish/diag/pydevd.py +58 -40
- omlish/diag/replserver/console.py +1 -1
- omlish/dispatch/__init__.py +18 -12
- omlish/dispatch/methods.py +50 -140
- omlish/dom/rendering.py +1 -1
- omlish/formats/dotenv.py +1 -1
- omlish/formats/json/stream/__init__.py +13 -0
- omlish/funcs/guard.py +225 -0
- omlish/graphs/dot/rendering.py +1 -1
- omlish/http/all.py +44 -4
- omlish/http/clients/asyncs.py +33 -27
- omlish/http/clients/base.py +17 -1
- omlish/http/clients/coro/__init__.py +0 -0
- omlish/http/clients/coro/sync.py +171 -0
- omlish/http/clients/default.py +208 -29
- omlish/http/clients/executor.py +56 -0
- omlish/http/clients/httpx.py +72 -11
- omlish/http/clients/middleware.py +181 -0
- omlish/http/clients/sync.py +33 -27
- omlish/http/clients/syncasync.py +49 -0
- omlish/http/clients/urllib.py +6 -3
- omlish/http/coro/client/connection.py +15 -6
- omlish/http/coro/io.py +2 -0
- omlish/http/flasky/__init__.py +40 -0
- omlish/http/flasky/_compat.py +2 -0
- omlish/http/flasky/api.py +82 -0
- omlish/http/flasky/app.py +203 -0
- omlish/http/flasky/cvs.py +59 -0
- omlish/http/flasky/requests.py +20 -0
- omlish/http/flasky/responses.py +23 -0
- omlish/http/flasky/routes.py +23 -0
- omlish/http/flasky/types.py +15 -0
- omlish/http/urls.py +67 -0
- omlish/inject/__init__.py +38 -18
- omlish/inject/_dataclasses.py +4986 -0
- omlish/inject/binder.py +4 -48
- omlish/inject/elements.py +27 -0
- omlish/inject/helpers/__init__.py +0 -0
- omlish/inject/{utils.py → helpers/constfn.py} +3 -3
- omlish/inject/{tags.py → helpers/id.py} +2 -2
- omlish/inject/helpers/multis.py +143 -0
- omlish/inject/helpers/wrappers.py +54 -0
- omlish/inject/impl/elements.py +47 -17
- omlish/inject/impl/injector.py +20 -19
- omlish/inject/impl/inspect.py +10 -1
- omlish/inject/impl/maysync.py +3 -4
- omlish/inject/impl/multis.py +3 -0
- omlish/inject/impl/sync.py +3 -4
- omlish/inject/injector.py +31 -2
- omlish/inject/inspect.py +35 -0
- omlish/inject/maysync.py +2 -4
- omlish/inject/multis.py +8 -0
- omlish/inject/overrides.py +3 -3
- omlish/inject/privates.py +6 -0
- omlish/inject/providers.py +3 -2
- omlish/inject/sync.py +5 -4
- omlish/io/buffers.py +118 -0
- omlish/io/readers.py +29 -0
- omlish/iterators/transforms.py +2 -2
- omlish/lang/__init__.py +178 -97
- omlish/lang/_asyncs.cc +186 -0
- omlish/lang/asyncs.py +17 -0
- omlish/lang/casing.py +11 -0
- omlish/lang/contextmanagers.py +28 -4
- omlish/lang/functions.py +31 -22
- omlish/lang/imports/_capture.cc +11 -9
- omlish/lang/imports/capture.py +363 -170
- omlish/lang/imports/proxy.py +455 -152
- omlish/lang/lazyglobals.py +22 -9
- omlish/lang/params.py +17 -0
- omlish/lang/recursion.py +0 -1
- omlish/lang/sequences.py +124 -0
- omlish/lite/abstract.py +54 -24
- omlish/lite/asyncs.py +2 -2
- omlish/lite/attrops.py +2 -0
- omlish/lite/contextmanagers.py +4 -4
- omlish/lite/dataclasses.py +44 -0
- omlish/lite/maybes.py +8 -0
- omlish/lite/maysync.py +1 -5
- omlish/lite/pycharm.py +1 -1
- omlish/lite/typing.py +6 -0
- omlish/logs/all.py +1 -1
- omlish/logs/utils.py +1 -1
- omlish/manifests/loading.py +2 -1
- omlish/marshal/__init__.py +33 -13
- omlish/marshal/_dataclasses.py +2774 -0
- omlish/marshal/base/configs.py +12 -0
- omlish/marshal/base/contexts.py +36 -21
- omlish/marshal/base/funcs.py +8 -11
- omlish/marshal/base/options.py +8 -0
- omlish/marshal/base/registries.py +146 -44
- omlish/marshal/base/types.py +40 -16
- omlish/marshal/composite/iterables.py +33 -20
- omlish/marshal/composite/literals.py +20 -18
- omlish/marshal/composite/mappings.py +36 -23
- omlish/marshal/composite/maybes.py +29 -19
- omlish/marshal/composite/newtypes.py +16 -16
- omlish/marshal/composite/optionals.py +14 -14
- omlish/marshal/composite/special.py +15 -15
- omlish/marshal/composite/unions/__init__.py +0 -0
- omlish/marshal/composite/unions/literals.py +93 -0
- omlish/marshal/composite/unions/primitives.py +103 -0
- omlish/marshal/factories/invalidate.py +18 -68
- omlish/marshal/factories/method.py +26 -0
- omlish/marshal/factories/moduleimport/factories.py +22 -65
- omlish/marshal/factories/multi.py +13 -25
- omlish/marshal/factories/recursive.py +42 -56
- omlish/marshal/factories/typecache.py +29 -74
- omlish/marshal/factories/typemap.py +42 -43
- omlish/marshal/objects/dataclasses.py +129 -106
- omlish/marshal/objects/marshal.py +18 -14
- omlish/marshal/objects/namedtuples.py +48 -42
- omlish/marshal/objects/unmarshal.py +19 -15
- omlish/marshal/polymorphism/marshal.py +9 -11
- omlish/marshal/polymorphism/metadata.py +16 -5
- omlish/marshal/polymorphism/standard.py +13 -1
- omlish/marshal/polymorphism/unions.py +15 -105
- omlish/marshal/polymorphism/unmarshal.py +9 -10
- omlish/marshal/singular/enums.py +14 -18
- omlish/marshal/standard.py +10 -6
- omlish/marshal/trivial/any.py +1 -1
- omlish/marshal/trivial/forbidden.py +21 -26
- omlish/metadata.py +23 -1
- omlish/os/forkhooks.py +4 -0
- omlish/os/pidfiles/pinning.py +2 -2
- omlish/reflect/types.py +1 -0
- omlish/secrets/marshal.py +1 -1
- omlish/specs/jmespath/__init__.py +12 -3
- omlish/specs/jmespath/_dataclasses.py +2893 -0
- omlish/specs/jmespath/ast.py +1 -1
- omlish/specs/jsonrpc/__init__.py +13 -0
- omlish/specs/jsonrpc/_marshal.py +32 -23
- omlish/specs/jsonrpc/conns.py +10 -7
- omlish/specs/jsonschema/_marshal.py +1 -1
- omlish/specs/jsonschema/keywords/__init__.py +7 -0
- omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
- omlish/specs/openapi/_marshal.py +31 -22
- omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
- omlish/sql/queries/_marshal.py +2 -2
- omlish/sql/queries/rendering.py +1 -1
- omlish/sql/tabledefs/_marshal.py +1 -1
- omlish/subprocesses/base.py +4 -0
- omlish/subprocesses/editor.py +1 -1
- omlish/sync.py +155 -21
- omlish/term/alt.py +60 -0
- omlish/term/confirm.py +8 -8
- omlish/term/pager.py +235 -0
- omlish/term/terminfo.py +935 -0
- omlish/term/termstate.py +67 -0
- omlish/term/vt100/terminal.py +0 -3
- omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
- omlish/testing/pytest/plugins/skips.py +2 -1
- omlish/testing/unittest/main.py +3 -3
- omlish/text/docwrap/__init__.py +3 -0
- omlish/text/docwrap/__main__.py +11 -0
- omlish/text/docwrap/api.py +83 -0
- omlish/text/docwrap/cli.py +86 -0
- omlish/text/docwrap/groups.py +84 -0
- omlish/text/docwrap/lists.py +167 -0
- omlish/text/docwrap/parts.py +146 -0
- omlish/text/docwrap/reflowing.py +106 -0
- omlish/text/docwrap/rendering.py +151 -0
- omlish/text/docwrap/utils.py +11 -0
- omlish/text/docwrap/wrapping.py +59 -0
- omlish/text/filecache.py +2 -2
- omlish/text/lorem.py +6 -0
- omlish/text/parts.py +2 -2
- omlish/text/textwrap.py +51 -0
- omlish/typedvalues/marshal.py +85 -59
- omlish/typedvalues/values.py +2 -1
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/METADATA +29 -28
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/RECORD +222 -171
- omlish/dataclasses/impl/generation/mangling.py +0 -18
- omlish/funcs/match.py +0 -227
- omlish/marshal/factories/match.py +0 -34
- omlish/marshal/factories/simple.py +0 -28
- /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/top_level.txt +0 -0
omlish/funcs/guard.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import functools
|
|
3
|
+
import operator
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from .. import check
|
|
7
|
+
from .. import collections as col
|
|
8
|
+
from .. import lang
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = ta.TypeVar('T')
|
|
12
|
+
T_co = ta.TypeVar('T_co', covariant=True)
|
|
13
|
+
U = ta.TypeVar('U')
|
|
14
|
+
P = ta.ParamSpec('P')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GuardFn(ta.Protocol[P, T_co]):
|
|
21
|
+
def __get__(self, instance, owner=None): ...
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T_co] | None: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@ta.final
|
|
30
|
+
class DumbGuardFn(ta.Generic[P, T]):
|
|
31
|
+
def __init__(self, fn: ta.Callable[P, T]) -> None:
|
|
32
|
+
self._fn = fn
|
|
33
|
+
|
|
34
|
+
def __get__(self, instance, owner=None):
|
|
35
|
+
return DumbGuardFn(self._fn.__get__(instance, owner)) # noqa
|
|
36
|
+
|
|
37
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T]:
|
|
38
|
+
return functools.partial(self._fn, *args, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
dumb = DumbGuardFn
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AmbiguousGuardFnError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@ta.final
|
|
52
|
+
class MultiGuardFn(ta.Generic[P, T]):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*children: GuardFn[P, T],
|
|
56
|
+
default: GuardFn[P, T] | None = None,
|
|
57
|
+
strict: bool = False,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._children, self._default, self._strict = children, default, strict
|
|
60
|
+
|
|
61
|
+
lang.attr_ops(lambda self: (
|
|
62
|
+
self._children,
|
|
63
|
+
self._default,
|
|
64
|
+
self._strict,
|
|
65
|
+
)).install(locals())
|
|
66
|
+
|
|
67
|
+
def __get__(self, instance, owner=None):
|
|
68
|
+
return MultiGuardFn(*map(operator.methodcaller('__get__', instance, owner), self._children), default=self._default.__get__(instance, owner) if self._default is not None else None, strict=self._strict) # noqa
|
|
69
|
+
|
|
70
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T] | None:
|
|
71
|
+
matches = []
|
|
72
|
+
for c in self._children:
|
|
73
|
+
if (m := c(*args, **kwargs)) is not None:
|
|
74
|
+
if not self._strict:
|
|
75
|
+
return m
|
|
76
|
+
matches.append(m)
|
|
77
|
+
if not matches:
|
|
78
|
+
if (dfl := self._default) is not None:
|
|
79
|
+
return dfl(*args, **kwargs)
|
|
80
|
+
else:
|
|
81
|
+
return None
|
|
82
|
+
elif len(matches) > 1:
|
|
83
|
+
raise AmbiguousGuardFnError
|
|
84
|
+
else:
|
|
85
|
+
return matches[0]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
multi = MultiGuardFn
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _BaseGuardFnMethod(lang.Abstract, ta.Generic[P, T]):
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
strict: bool = False,
|
|
99
|
+
requires_override: bool = False,
|
|
100
|
+
instance_cache: bool = False,
|
|
101
|
+
default: GuardFn[P, T] | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__()
|
|
104
|
+
|
|
105
|
+
self._strict = strict
|
|
106
|
+
self._instance_cache = instance_cache
|
|
107
|
+
self._default = default
|
|
108
|
+
|
|
109
|
+
self._registry: col.AttrRegistry[ta.Callable, None] = col.AttrRegistry(
|
|
110
|
+
requires_override=requires_override,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._cache: col.AttrRegistryCache[ta.Callable, None, MultiGuardFn] = col.AttrRegistryCache(
|
|
114
|
+
self._registry,
|
|
115
|
+
self._prepare,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
_owner: type | None = None
|
|
119
|
+
_name: str | None = None
|
|
120
|
+
|
|
121
|
+
def __set_name__(self, owner, name):
|
|
122
|
+
if self._owner is None:
|
|
123
|
+
self._owner = owner
|
|
124
|
+
if self._name is None:
|
|
125
|
+
self._name = name
|
|
126
|
+
|
|
127
|
+
def register(self, fn: U) -> U:
|
|
128
|
+
check.callable(fn)
|
|
129
|
+
self._registry.register(ta.cast(ta.Callable, fn), None)
|
|
130
|
+
return fn
|
|
131
|
+
|
|
132
|
+
def _prepare(self, instance_cls: type, collected: ta.Mapping[str, tuple[ta.Callable, None]]) -> MultiGuardFn:
|
|
133
|
+
return MultiGuardFn(
|
|
134
|
+
*[getattr(instance_cls, a) for a in collected],
|
|
135
|
+
default=self._default,
|
|
136
|
+
strict=self._strict,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@abc.abstractmethod
|
|
140
|
+
def _bind(self, instance, owner):
|
|
141
|
+
raise NotImplementedError
|
|
142
|
+
|
|
143
|
+
def __get__(self, instance, owner=None):
|
|
144
|
+
if instance is None:
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
if self._instance_cache:
|
|
148
|
+
try:
|
|
149
|
+
return instance.__dict__[self._name]
|
|
150
|
+
except KeyError:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
bound = self._bind(instance, owner)
|
|
154
|
+
|
|
155
|
+
if self._instance_cache:
|
|
156
|
+
instance.__dict__[self._name] = bound
|
|
157
|
+
|
|
158
|
+
return bound
|
|
159
|
+
|
|
160
|
+
def _call(self, *args, **kwargs):
|
|
161
|
+
instance, *rest = args
|
|
162
|
+
return self.__get__(instance)(*rest, **kwargs)
|
|
163
|
+
|
|
164
|
+
#
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@ta.final
|
|
168
|
+
class GuardFnMethod(_BaseGuardFnMethod[P, T]):
|
|
169
|
+
def _bind(self, instance, owner):
|
|
170
|
+
return self._cache.get(type(instance)).__get__(instance, owner) # noqa
|
|
171
|
+
|
|
172
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T] | None:
|
|
173
|
+
return self._call(*args, **kwargs)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def method(
|
|
177
|
+
*,
|
|
178
|
+
strict: bool = False,
|
|
179
|
+
requires_override: bool = False,
|
|
180
|
+
instance_cache: bool = False,
|
|
181
|
+
default: bool = False,
|
|
182
|
+
) -> ta.Callable[[ta.Callable[P, T]], GuardFnMethod[P, T]]: # noqa
|
|
183
|
+
def inner(fn):
|
|
184
|
+
return GuardFnMethod(
|
|
185
|
+
strict=strict,
|
|
186
|
+
requires_override=requires_override,
|
|
187
|
+
instance_cache=instance_cache,
|
|
188
|
+
default=fn if default else None,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return inner
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
#
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@ta.final
|
|
198
|
+
class ImmediateGuardFnMethod(_BaseGuardFnMethod[P, T]):
|
|
199
|
+
def _bind(self, instance, owner):
|
|
200
|
+
gf = self._cache.get(type(instance)).__get__(instance, owner) # noqa
|
|
201
|
+
|
|
202
|
+
def inner(*args, **kwargs):
|
|
203
|
+
return gf(*args, **kwargs)() # Note: cannot be None due to non-optional default
|
|
204
|
+
|
|
205
|
+
return inner
|
|
206
|
+
|
|
207
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
|
208
|
+
return self._call(*args, **kwargs)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def immediate_method(
|
|
212
|
+
*,
|
|
213
|
+
strict: bool = False,
|
|
214
|
+
requires_override: bool = False,
|
|
215
|
+
instance_cache: bool = False,
|
|
216
|
+
) -> ta.Callable[[ta.Callable[P, T]], ImmediateGuardFnMethod[P, T]]: # noqa
|
|
217
|
+
def inner(fn):
|
|
218
|
+
return ImmediateGuardFnMethod(
|
|
219
|
+
strict=strict,
|
|
220
|
+
requires_override=requires_override,
|
|
221
|
+
instance_cache=instance_cache,
|
|
222
|
+
default=(lambda *args, **kwargs: lambda: fn(*args, **kwargs)),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return inner
|
omlish/graphs/dot/rendering.py
CHANGED
omlish/http/all.py
CHANGED
|
@@ -4,9 +4,18 @@ from .. import lang as _lang
|
|
|
4
4
|
with _lang.auto_proxy_init(globals()):
|
|
5
5
|
##
|
|
6
6
|
|
|
7
|
+
from .clients.asyncs import ( # noqa
|
|
8
|
+
AsyncStreamHttpResponse,
|
|
9
|
+
|
|
10
|
+
async_close_http_client_response,
|
|
11
|
+
async_closing_http_client_response,
|
|
12
|
+
async_read_http_client_response,
|
|
13
|
+
|
|
14
|
+
AsyncHttpClient,
|
|
15
|
+
)
|
|
16
|
+
|
|
7
17
|
from .clients.base import ( # noqa
|
|
8
18
|
DEFAULT_ENCODING,
|
|
9
|
-
|
|
10
19
|
is_success_status,
|
|
11
20
|
|
|
12
21
|
HttpRequest,
|
|
@@ -14,34 +23,63 @@ with _lang.auto_proxy_init(globals()):
|
|
|
14
23
|
BaseHttpResponse,
|
|
15
24
|
HttpResponse,
|
|
16
25
|
|
|
26
|
+
HttpClientContext,
|
|
27
|
+
|
|
17
28
|
HttpClientError,
|
|
18
29
|
HttpStatusError,
|
|
30
|
+
|
|
31
|
+
BaseHttpClient,
|
|
19
32
|
)
|
|
20
33
|
|
|
21
34
|
from .clients.default import ( # noqa
|
|
22
35
|
client,
|
|
36
|
+
manage_client,
|
|
23
37
|
|
|
24
38
|
request,
|
|
39
|
+
|
|
40
|
+
async_client,
|
|
41
|
+
manage_async_client,
|
|
42
|
+
|
|
43
|
+
async_request,
|
|
25
44
|
)
|
|
26
45
|
|
|
27
46
|
from .clients.httpx import ( # noqa
|
|
28
47
|
HttpxHttpClient,
|
|
48
|
+
|
|
49
|
+
HttpxAsyncHttpClient,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from .clients.middleware import ( # noqa
|
|
53
|
+
HttpClientMiddleware,
|
|
54
|
+
AbstractMiddlewareHttpClient,
|
|
55
|
+
|
|
56
|
+
MiddlewareHttpClient,
|
|
57
|
+
MiddlewareAsyncHttpClient,
|
|
58
|
+
|
|
59
|
+
TooManyRedirectsHttpClientError,
|
|
60
|
+
RedirectHandlingHttpClientMiddleware,
|
|
29
61
|
)
|
|
30
62
|
|
|
31
63
|
from .clients.sync import ( # noqa
|
|
32
64
|
StreamHttpResponse,
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
close_http_client_response,
|
|
67
|
+
closing_http_client_response,
|
|
68
|
+
read_http_client_response,
|
|
37
69
|
|
|
38
70
|
HttpClient,
|
|
39
71
|
)
|
|
40
72
|
|
|
73
|
+
from .clients.syncasync import ( # noqa
|
|
74
|
+
SyncAsyncHttpClient,
|
|
75
|
+
)
|
|
76
|
+
|
|
41
77
|
from .clients.urllib import ( # noqa
|
|
42
78
|
UrllibHttpClient,
|
|
43
79
|
)
|
|
44
80
|
|
|
81
|
+
from . import asgi # noqa
|
|
82
|
+
|
|
45
83
|
from . import consts # noqa
|
|
46
84
|
|
|
47
85
|
from .cookies import ( # noqa
|
|
@@ -80,3 +118,5 @@ with _lang.auto_proxy_init(globals()):
|
|
|
80
118
|
MultipartEncoder,
|
|
81
119
|
MultipartField,
|
|
82
120
|
)
|
|
121
|
+
|
|
122
|
+
from . import wsgi # noqa
|
omlish/http/clients/asyncs.py
CHANGED
|
@@ -5,11 +5,13 @@ import contextlib
|
|
|
5
5
|
import dataclasses as dc
|
|
6
6
|
import typing as ta
|
|
7
7
|
|
|
8
|
+
from ...io.readers import AsyncBufferedBytesReader
|
|
8
9
|
from ...lite.abstract import Abstract
|
|
9
|
-
from ...lite.dataclasses import dataclass_maybe_post_init
|
|
10
10
|
from ...lite.dataclasses import dataclass_shallow_asdict
|
|
11
|
+
from .base import BaseHttpClient
|
|
11
12
|
from .base import BaseHttpResponse
|
|
12
13
|
from .base import BaseHttpResponseT
|
|
14
|
+
from .base import HttpClientContext
|
|
13
15
|
from .base import HttpRequest
|
|
14
16
|
from .base import HttpResponse
|
|
15
17
|
from .base import HttpStatusError
|
|
@@ -25,22 +27,21 @@ AsyncHttpClientT = ta.TypeVar('AsyncHttpClientT', bound='AsyncHttpClient')
|
|
|
25
27
|
@ta.final
|
|
26
28
|
@dc.dataclass(frozen=True) # kw_only=True
|
|
27
29
|
class AsyncStreamHttpResponse(BaseHttpResponse):
|
|
28
|
-
|
|
29
|
-
def read(self, /, n: int = -1) -> ta.Awaitable[bytes]: ...
|
|
30
|
+
_stream: ta.Optional[AsyncBufferedBytesReader] = None
|
|
30
31
|
|
|
31
|
-
@
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
raise TypeError
|
|
32
|
+
@property
|
|
33
|
+
def stream(self) -> 'AsyncBufferedBytesReader':
|
|
34
|
+
if (st := self._stream) is None:
|
|
35
|
+
raise TypeError('No data')
|
|
36
|
+
return st
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
@property
|
|
39
|
+
def has_data(self) -> bool:
|
|
40
|
+
return self._stream is not None
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
#
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
dataclass_maybe_post_init(super())
|
|
42
|
-
if isinstance(self.stream, AsyncStreamHttpResponse._NullStream):
|
|
43
|
-
raise TypeError(self.stream)
|
|
44
|
+
_closer: ta.Optional[ta.Callable[[], ta.Awaitable[None]]] = None
|
|
44
45
|
|
|
45
46
|
async def __aenter__(self: AsyncStreamHttpResponseT) -> AsyncStreamHttpResponseT:
|
|
46
47
|
return self
|
|
@@ -50,13 +51,13 @@ class AsyncStreamHttpResponse(BaseHttpResponse):
|
|
|
50
51
|
|
|
51
52
|
async def close(self) -> None:
|
|
52
53
|
if (c := self._closer) is not None:
|
|
53
|
-
c()
|
|
54
|
+
await c() # noqa
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
#
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
async def
|
|
60
|
+
async def async_close_http_client_response(resp: BaseHttpResponse) -> None:
|
|
60
61
|
if isinstance(resp, HttpResponse):
|
|
61
62
|
pass
|
|
62
63
|
|
|
@@ -68,7 +69,7 @@ async def async_close_response(resp: BaseHttpResponse) -> None:
|
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
@contextlib.asynccontextmanager
|
|
71
|
-
async def
|
|
72
|
+
async def async_closing_http_client_response(resp: BaseHttpResponseT) -> ta.AsyncGenerator[BaseHttpResponseT, None]:
|
|
72
73
|
if isinstance(resp, HttpResponse):
|
|
73
74
|
yield resp
|
|
74
75
|
return
|
|
@@ -83,15 +84,14 @@ async def async_closing_response(resp: BaseHttpResponseT) -> ta.AsyncGenerator[B
|
|
|
83
84
|
raise TypeError(resp)
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
async def
|
|
87
|
+
async def async_read_http_client_response(resp: BaseHttpResponse) -> HttpResponse:
|
|
87
88
|
if isinstance(resp, HttpResponse):
|
|
88
89
|
return resp
|
|
89
90
|
|
|
90
91
|
elif isinstance(resp, AsyncStreamHttpResponse):
|
|
91
|
-
data = await resp.stream.read()
|
|
92
92
|
return HttpResponse(**{
|
|
93
|
-
**{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('
|
|
94
|
-
'data':
|
|
93
|
+
**{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('_stream', '_closer')},
|
|
94
|
+
**({'data': await resp.stream.readall()} if resp.has_data else {}),
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
else:
|
|
@@ -101,7 +101,7 @@ async def async_read_response(resp: BaseHttpResponse) -> HttpResponse:
|
|
|
101
101
|
##
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
class AsyncHttpClient(Abstract):
|
|
104
|
+
class AsyncHttpClient(BaseHttpClient, Abstract):
|
|
105
105
|
async def __aenter__(self: AsyncHttpClientT) -> AsyncHttpClientT:
|
|
106
106
|
return self
|
|
107
107
|
|
|
@@ -112,21 +112,27 @@ class AsyncHttpClient(Abstract):
|
|
|
112
112
|
self,
|
|
113
113
|
req: HttpRequest,
|
|
114
114
|
*,
|
|
115
|
+
context: ta.Optional[HttpClientContext] = None,
|
|
115
116
|
check: bool = False,
|
|
116
117
|
) -> HttpResponse:
|
|
117
|
-
async with
|
|
118
|
+
async with async_closing_http_client_response(await self.stream_request(
|
|
118
119
|
req,
|
|
120
|
+
context=context,
|
|
119
121
|
check=check,
|
|
120
122
|
)) as resp:
|
|
121
|
-
return await
|
|
123
|
+
return await async_read_http_client_response(resp)
|
|
122
124
|
|
|
123
125
|
async def stream_request(
|
|
124
126
|
self,
|
|
125
127
|
req: HttpRequest,
|
|
126
128
|
*,
|
|
129
|
+
context: ta.Optional[HttpClientContext] = None,
|
|
127
130
|
check: bool = False,
|
|
128
131
|
) -> AsyncStreamHttpResponse:
|
|
129
|
-
|
|
132
|
+
if context is None:
|
|
133
|
+
context = HttpClientContext()
|
|
134
|
+
|
|
135
|
+
resp = await self._stream_request(context, req)
|
|
130
136
|
|
|
131
137
|
try:
|
|
132
138
|
if check and not resp.is_success:
|
|
@@ -134,14 +140,14 @@ class AsyncHttpClient(Abstract):
|
|
|
134
140
|
cause = resp.underlying
|
|
135
141
|
else:
|
|
136
142
|
cause = None
|
|
137
|
-
raise HttpStatusError(await
|
|
143
|
+
raise HttpStatusError(await async_read_http_client_response(resp)) from cause # noqa
|
|
138
144
|
|
|
139
145
|
except Exception:
|
|
140
|
-
await
|
|
146
|
+
await async_close_http_client_response(resp)
|
|
141
147
|
raise
|
|
142
148
|
|
|
143
149
|
return resp
|
|
144
150
|
|
|
145
151
|
@abc.abstractmethod
|
|
146
|
-
def _stream_request(self, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
|
|
152
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
|
|
147
153
|
raise NotImplementedError
|
omlish/http/clients/base.py
CHANGED
|
@@ -119,12 +119,28 @@ class HttpResponse(BaseHttpResponse):
|
|
|
119
119
|
##
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
@ta.final
|
|
123
|
+
class HttpClientContext:
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self._dct: dict = {}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
|
|
130
|
+
|
|
122
131
|
class HttpClientError(Exception):
|
|
123
132
|
@property
|
|
124
133
|
def cause(self) -> ta.Optional[BaseException]:
|
|
125
134
|
return self.__cause__
|
|
126
135
|
|
|
127
136
|
|
|
128
|
-
@dc.dataclass(
|
|
137
|
+
@dc.dataclass()
|
|
129
138
|
class HttpStatusError(HttpClientError):
|
|
130
139
|
response: HttpResponse
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class BaseHttpClient(Abstract):
|
|
146
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# @omlish-lite
|
|
2
|
+
# ruff: noqa: UP045
|
|
3
|
+
import errno
|
|
4
|
+
import socket
|
|
5
|
+
import typing as ta
|
|
6
|
+
import urllib.parse
|
|
7
|
+
|
|
8
|
+
from ....io.buffers import ReadableListBuffer
|
|
9
|
+
from ....lite.check import check
|
|
10
|
+
from ...coro.client.connection import CoroHttpClientConnection
|
|
11
|
+
from ...coro.client.response import CoroHttpClientResponse
|
|
12
|
+
from ...coro.io import CoroHttpIo
|
|
13
|
+
from ...headers import HttpHeaders
|
|
14
|
+
from ...urls import unparse_url_request_path
|
|
15
|
+
from ..base import HttpClientContext
|
|
16
|
+
from ..base import HttpClientError
|
|
17
|
+
from ..base import HttpRequest
|
|
18
|
+
from ..sync import HttpClient
|
|
19
|
+
from ..sync import StreamHttpResponse
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
T = ta.TypeVar('T')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoroHttpClient(HttpClient):
|
|
29
|
+
class _Connection:
|
|
30
|
+
def __init__(self, req: HttpRequest) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
self._req = req
|
|
34
|
+
self._ups = urllib.parse.urlparse(req.url)
|
|
35
|
+
|
|
36
|
+
self._ssl = self._ups.scheme == 'https'
|
|
37
|
+
|
|
38
|
+
_cc: ta.Optional[CoroHttpClientConnection] = None
|
|
39
|
+
_resp: ta.Optional[CoroHttpClientResponse] = None
|
|
40
|
+
|
|
41
|
+
_sock: ta.Optional[socket.socket] = None
|
|
42
|
+
_sock_file: ta.Optional[ta.BinaryIO] = None
|
|
43
|
+
|
|
44
|
+
_ssl_context: ta.Any = None
|
|
45
|
+
|
|
46
|
+
#
|
|
47
|
+
|
|
48
|
+
def _create_https_context(self, http_version: int) -> ta.Any:
|
|
49
|
+
# https://github.com/python/cpython/blob/a7160912274003672dc116d918260c0a81551c21/Lib/http/client.py#L809
|
|
50
|
+
import ssl
|
|
51
|
+
|
|
52
|
+
# Function also used by urllib.request to be able to set the check_hostname attribute on a context object.
|
|
53
|
+
context = ssl.create_default_context()
|
|
54
|
+
|
|
55
|
+
# Send ALPN extension to indicate HTTP/1.1 protocol.
|
|
56
|
+
if http_version == 11:
|
|
57
|
+
context.set_alpn_protocols(['http/1.1'])
|
|
58
|
+
|
|
59
|
+
# Enable PHA for TLS 1.3 connections if available.
|
|
60
|
+
if context.post_handshake_auth is not None:
|
|
61
|
+
context.post_handshake_auth = True
|
|
62
|
+
|
|
63
|
+
return context
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
|
|
67
|
+
def setup(self) -> StreamHttpResponse:
|
|
68
|
+
check.none(self._sock)
|
|
69
|
+
check.none(self._ssl_context)
|
|
70
|
+
|
|
71
|
+
self._cc = cc = CoroHttpClientConnection(
|
|
72
|
+
check.not_none(self._ups.hostname),
|
|
73
|
+
default_port=CoroHttpClientConnection.HTTPS_PORT if self._ssl else CoroHttpClientConnection.HTTP_PORT,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if self._ssl:
|
|
77
|
+
self._ssl_context = self._create_https_context(self._cc.http_version)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
self._run_coro(cc.connect())
|
|
81
|
+
|
|
82
|
+
self._run_coro(cc.request(
|
|
83
|
+
self._req.method or 'GET',
|
|
84
|
+
unparse_url_request_path(self._ups) or '/',
|
|
85
|
+
self._req.data,
|
|
86
|
+
hh.single_str_dct if (hh := self._req.headers_) is not None else {},
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
self._resp = resp = self._run_coro(cc.get_response())
|
|
90
|
+
|
|
91
|
+
return StreamHttpResponse(
|
|
92
|
+
status=resp._state.status, # noqa
|
|
93
|
+
headers=HttpHeaders(resp._state.headers.items()), # noqa
|
|
94
|
+
request=self._req,
|
|
95
|
+
underlying=self,
|
|
96
|
+
_stream=ReadableListBuffer().new_buffered_reader(self),
|
|
97
|
+
_closer=self.close,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
except Exception:
|
|
101
|
+
self.close()
|
|
102
|
+
raise
|
|
103
|
+
|
|
104
|
+
def _run_coro(self, g: ta.Generator[ta.Any, ta.Any, T]) -> T:
|
|
105
|
+
i = None
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
try:
|
|
109
|
+
o = g.send(i)
|
|
110
|
+
except StopIteration as e:
|
|
111
|
+
return e.value
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
i = self._handle_io(o)
|
|
115
|
+
except OSError as e:
|
|
116
|
+
raise HttpClientError from e
|
|
117
|
+
|
|
118
|
+
def _handle_io(self, o: CoroHttpIo.Io) -> ta.Any:
|
|
119
|
+
if isinstance(o, CoroHttpIo.ConnectIo):
|
|
120
|
+
check.none(self._sock)
|
|
121
|
+
self._sock = socket.create_connection(*o.args, **(o.kwargs or {}))
|
|
122
|
+
|
|
123
|
+
if self._ssl_context is not None:
|
|
124
|
+
self._sock = self._ssl_context.wrap_socket(
|
|
125
|
+
self._sock,
|
|
126
|
+
server_hostname=check.not_none(o.server_hostname),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Might fail in OSs that don't implement TCP_NODELAY
|
|
130
|
+
try:
|
|
131
|
+
self._sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
132
|
+
except OSError as e:
|
|
133
|
+
if e.errno != errno.ENOPROTOOPT:
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
self._sock_file = self._sock.makefile('rb')
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
elif isinstance(o, CoroHttpIo.CloseIo):
|
|
141
|
+
check.not_none(self._sock).close()
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
elif isinstance(o, CoroHttpIo.WriteIo):
|
|
145
|
+
check.not_none(self._sock).sendall(o.data)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
elif isinstance(o, CoroHttpIo.ReadIo):
|
|
149
|
+
if (sz := o.sz) is not None:
|
|
150
|
+
return check.not_none(self._sock_file).read(sz)
|
|
151
|
+
else:
|
|
152
|
+
return check.not_none(self._sock_file).read()
|
|
153
|
+
|
|
154
|
+
elif isinstance(o, CoroHttpIo.ReadLineIo):
|
|
155
|
+
return check.not_none(self._sock_file).readline(o.sz)
|
|
156
|
+
|
|
157
|
+
else:
|
|
158
|
+
raise TypeError(o)
|
|
159
|
+
|
|
160
|
+
def read1(self, n: int = -1, /) -> bytes:
|
|
161
|
+
return self._run_coro(check.not_none(self._resp).read(n if n >= 0 else None))
|
|
162
|
+
|
|
163
|
+
def close(self) -> None:
|
|
164
|
+
if self._resp is not None:
|
|
165
|
+
self._resp.close()
|
|
166
|
+
if self._sock is not None:
|
|
167
|
+
self._sock.close()
|
|
168
|
+
|
|
169
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
|
|
170
|
+
conn = CoroHttpClient._Connection(req)
|
|
171
|
+
return conn.setup()
|