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/http/clients/default.py
CHANGED
|
@@ -1,60 +1,239 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import contextlib
|
|
1
3
|
import typing as ta
|
|
2
4
|
|
|
5
|
+
from ... import lang
|
|
3
6
|
from ..headers import CanHttpHeaders
|
|
7
|
+
from .asyncs import AsyncHttpClient
|
|
8
|
+
from .base import HttpClientContext
|
|
4
9
|
from .base import HttpRequest
|
|
5
10
|
from .base import HttpResponse
|
|
6
11
|
from .sync import HttpClient
|
|
7
|
-
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
with lang.auto_proxy_import(globals()):
|
|
15
|
+
from . import httpx as _httpx
|
|
16
|
+
from . import urllib as _urllib
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
C = ta.TypeVar('C')
|
|
20
|
+
R = ta.TypeVar('R')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _DefaultRequester(lang.Abstract, ta.Generic[C, R]):
|
|
27
|
+
def __call__(
|
|
28
|
+
self,
|
|
29
|
+
url: str,
|
|
30
|
+
method: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
headers: CanHttpHeaders | None = None,
|
|
33
|
+
data: bytes | str | None = None,
|
|
34
|
+
|
|
35
|
+
timeout_s: float | None = None,
|
|
36
|
+
|
|
37
|
+
context: HttpClientContext | None = None,
|
|
38
|
+
check: bool = False,
|
|
39
|
+
client: C | None = None, # noqa
|
|
40
|
+
|
|
41
|
+
**kwargs: ta.Any,
|
|
42
|
+
) -> R:
|
|
43
|
+
request = HttpRequest( # noqa
|
|
44
|
+
url,
|
|
45
|
+
method=method,
|
|
46
|
+
|
|
47
|
+
headers=headers,
|
|
48
|
+
data=data,
|
|
49
|
+
|
|
50
|
+
timeout_s=timeout_s,
|
|
51
|
+
|
|
52
|
+
**kwargs,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return self._do(
|
|
56
|
+
request,
|
|
57
|
+
context=context,
|
|
58
|
+
check=check,
|
|
59
|
+
client=client,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@abc.abstractmethod
|
|
63
|
+
def _do(
|
|
64
|
+
self,
|
|
65
|
+
request: HttpRequest, # noqa
|
|
66
|
+
*,
|
|
67
|
+
context: HttpClientContext | None = None,
|
|
68
|
+
check: bool = False,
|
|
69
|
+
client: C | None = None, # noqa
|
|
70
|
+
) -> R:
|
|
71
|
+
raise NotImplementedError
|
|
8
72
|
|
|
9
73
|
|
|
10
74
|
##
|
|
11
75
|
|
|
12
76
|
|
|
13
77
|
def _default_client() -> HttpClient:
|
|
14
|
-
return UrllibHttpClient()
|
|
78
|
+
return _urllib.UrllibHttpClient()
|
|
15
79
|
|
|
16
80
|
|
|
17
81
|
def client() -> HttpClient:
|
|
18
82
|
return _default_client()
|
|
19
83
|
|
|
20
84
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
85
|
+
@contextlib.contextmanager
|
|
86
|
+
def manage_client(client: HttpClient | None = None) -> ta.Generator[HttpClient]: # noqa
|
|
87
|
+
if client is not None:
|
|
88
|
+
yield client
|
|
89
|
+
|
|
90
|
+
else:
|
|
91
|
+
with _default_client() as client: # noqa
|
|
92
|
+
yield client
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _BaseSyncDefaultRequester(_DefaultRequester[HttpClient, R], lang.Abstract, ta.Generic[R]):
|
|
99
|
+
def _do(
|
|
100
|
+
self,
|
|
101
|
+
request: HttpRequest, # noqa
|
|
102
|
+
*,
|
|
103
|
+
context: HttpClientContext | None = None,
|
|
104
|
+
check: bool = False,
|
|
105
|
+
client: HttpClient | None = None, # noqa
|
|
106
|
+
) -> R:
|
|
107
|
+
if context is None:
|
|
108
|
+
context = HttpClientContext()
|
|
109
|
+
|
|
110
|
+
if client is not None:
|
|
111
|
+
return self._do_(
|
|
112
|
+
client,
|
|
113
|
+
context,
|
|
114
|
+
request,
|
|
115
|
+
check=check,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
with _default_client() as client: # noqa
|
|
120
|
+
return self._do_(
|
|
121
|
+
client,
|
|
122
|
+
context,
|
|
123
|
+
request,
|
|
124
|
+
check=check,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@abc.abstractmethod
|
|
128
|
+
def _do_(
|
|
129
|
+
self,
|
|
130
|
+
client: HttpClient, # noqa
|
|
131
|
+
context: HttpClientContext,
|
|
132
|
+
request: HttpRequest, # noqa
|
|
133
|
+
*,
|
|
134
|
+
check: bool = False, # noqa
|
|
135
|
+
) -> R:
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _SyncDefaultRequester(_BaseSyncDefaultRequester[HttpResponse]):
|
|
140
|
+
def _do_(
|
|
141
|
+
self,
|
|
142
|
+
client: HttpClient, # noqa
|
|
143
|
+
context: HttpClientContext,
|
|
144
|
+
request: HttpRequest, # noqa
|
|
145
|
+
*,
|
|
146
|
+
check: bool = False, # noqa
|
|
147
|
+
) -> HttpResponse:
|
|
148
|
+
return client.request(
|
|
149
|
+
request,
|
|
150
|
+
context=context,
|
|
151
|
+
check=check,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
request = _SyncDefaultRequester()
|
|
27
156
|
|
|
28
|
-
timeout_s: float | None = None,
|
|
29
157
|
|
|
30
|
-
|
|
158
|
+
##
|
|
31
159
|
|
|
32
|
-
client: HttpClient | None = None, # noqa
|
|
33
160
|
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
req = HttpRequest(
|
|
37
|
-
url,
|
|
38
|
-
method=method,
|
|
161
|
+
def _default_async_client() -> AsyncHttpClient:
|
|
162
|
+
return _httpx.HttpxAsyncHttpClient()
|
|
39
163
|
|
|
40
|
-
headers=headers,
|
|
41
|
-
data=data,
|
|
42
164
|
|
|
43
|
-
|
|
165
|
+
def async_client() -> AsyncHttpClient:
|
|
166
|
+
return _default_async_client()
|
|
44
167
|
|
|
45
|
-
**kwargs,
|
|
46
|
-
)
|
|
47
168
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
169
|
+
@contextlib.asynccontextmanager
|
|
170
|
+
async def manage_async_client(client: AsyncHttpClient | None = None) -> ta.AsyncGenerator[AsyncHttpClient]: # noqa
|
|
171
|
+
if client is not None:
|
|
172
|
+
yield client
|
|
51
173
|
|
|
174
|
+
else:
|
|
175
|
+
async with _default_async_client() as client: # noqa
|
|
176
|
+
yield client
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
#
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _BaseAsyncDefaultRequester(_DefaultRequester[AsyncHttpClient, ta.Awaitable[R]], lang.Abstract, ta.Generic[R]):
|
|
183
|
+
async def _do(
|
|
184
|
+
self,
|
|
185
|
+
request: HttpRequest, # noqa
|
|
186
|
+
*,
|
|
187
|
+
context: HttpClientContext | None = None,
|
|
188
|
+
check: bool = False,
|
|
189
|
+
client: AsyncHttpClient | None = None, # noqa
|
|
190
|
+
) -> R:
|
|
191
|
+
if context is None:
|
|
192
|
+
context = HttpClientContext()
|
|
193
|
+
|
|
194
|
+
if client is not None:
|
|
195
|
+
return await self._do_(
|
|
196
|
+
client,
|
|
197
|
+
context,
|
|
198
|
+
request,
|
|
199
|
+
check=check,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
else:
|
|
203
|
+
async with _default_async_client() as client: # noqa
|
|
204
|
+
return await self._do_(
|
|
205
|
+
client,
|
|
206
|
+
context,
|
|
207
|
+
request,
|
|
208
|
+
check=check,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@abc.abstractmethod
|
|
212
|
+
def _do_(
|
|
213
|
+
self,
|
|
214
|
+
client: AsyncHttpClient, # noqa
|
|
215
|
+
context: HttpClientContext,
|
|
216
|
+
request: HttpRequest, # noqa
|
|
217
|
+
*,
|
|
218
|
+
check: bool = False, # noqa
|
|
219
|
+
) -> ta.Awaitable[R]:
|
|
220
|
+
raise NotImplementedError
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _AsyncDefaultRequester(_BaseAsyncDefaultRequester[HttpResponse]):
|
|
224
|
+
async def _do_(
|
|
225
|
+
self,
|
|
226
|
+
client: AsyncHttpClient, # noqa
|
|
227
|
+
context: HttpClientContext,
|
|
228
|
+
request: HttpRequest, # noqa
|
|
229
|
+
*,
|
|
230
|
+
check: bool = False,
|
|
231
|
+
) -> HttpResponse: # noqa
|
|
232
|
+
return await client.request(
|
|
233
|
+
request,
|
|
234
|
+
context=context,
|
|
52
235
|
check=check,
|
|
53
236
|
)
|
|
54
237
|
|
|
55
|
-
if client is not None:
|
|
56
|
-
return do(client)
|
|
57
238
|
|
|
58
|
-
|
|
59
|
-
with _default_client() as cli:
|
|
60
|
-
return do(cli)
|
|
239
|
+
async_request = _AsyncDefaultRequester()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ruff: noqa: UP043 UP045
|
|
2
|
+
# @omlish-lite
|
|
3
|
+
import dataclasses as dc
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from .asyncs import AsyncHttpClient
|
|
7
|
+
from .asyncs import AsyncStreamHttpResponse
|
|
8
|
+
from .base import HttpClientContext
|
|
9
|
+
from .base import HttpRequest
|
|
10
|
+
from .sync import HttpClient
|
|
11
|
+
from .sync import StreamHttpResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecutorAsyncHttpClient(AsyncHttpClient):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
run_in_executor: ta.Callable[..., ta.Awaitable],
|
|
21
|
+
client: HttpClient,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
self._run_in_executor = run_in_executor
|
|
26
|
+
self._client = client
|
|
27
|
+
|
|
28
|
+
@dc.dataclass(frozen=True)
|
|
29
|
+
class _StreamAdapter:
|
|
30
|
+
owner: 'ExecutorAsyncHttpClient'
|
|
31
|
+
resp: StreamHttpResponse
|
|
32
|
+
|
|
33
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
34
|
+
return await self.owner._run_in_executor(self.resp.stream.read1, n) # noqa
|
|
35
|
+
|
|
36
|
+
async def read(self, n: int = -1, /) -> bytes:
|
|
37
|
+
return await self.owner._run_in_executor(self.resp.stream.read, n) # noqa
|
|
38
|
+
|
|
39
|
+
async def readall(self) -> bytes:
|
|
40
|
+
return await self.owner._run_in_executor(self.resp.stream.readall) # noqa
|
|
41
|
+
|
|
42
|
+
async def close(self) -> None:
|
|
43
|
+
return await self.owner._run_in_executor(self.resp.close) # noqa
|
|
44
|
+
|
|
45
|
+
async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
|
|
46
|
+
resp: StreamHttpResponse = await self._run_in_executor(lambda: self._client.stream_request(req, context=ctx))
|
|
47
|
+
return AsyncStreamHttpResponse(
|
|
48
|
+
status=resp.status,
|
|
49
|
+
headers=resp.headers,
|
|
50
|
+
request=req,
|
|
51
|
+
underlying=resp,
|
|
52
|
+
**(dict( # type: ignore
|
|
53
|
+
_stream=(adapter := self._StreamAdapter(self, resp)),
|
|
54
|
+
_closer=adapter.close,
|
|
55
|
+
) if resp.has_data else {}),
|
|
56
|
+
)
|
omlish/http/clients/httpx.py
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
TODO:
|
|
3
3
|
- standardize following redirects
|
|
4
4
|
"""
|
|
5
|
+
import contextlib
|
|
5
6
|
import functools
|
|
6
7
|
import typing as ta
|
|
7
8
|
|
|
8
9
|
from ... import dataclasses as dc
|
|
9
10
|
from ... import lang
|
|
11
|
+
from ...io.buffers import ReadableListBuffer
|
|
10
12
|
from ..headers import HttpHeaders
|
|
13
|
+
from .asyncs import AsyncHttpClient
|
|
14
|
+
from .asyncs import AsyncStreamHttpResponse
|
|
15
|
+
from .base import HttpClientContext
|
|
11
16
|
from .base import HttpClientError
|
|
12
17
|
from .base import HttpRequest
|
|
13
18
|
from .sync import HttpClient
|
|
@@ -28,16 +33,13 @@ class HttpxHttpClient(HttpClient):
|
|
|
28
33
|
class _StreamAdapter:
|
|
29
34
|
it: ta.Iterator[bytes]
|
|
30
35
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return next(self.it)
|
|
37
|
-
except StopIteration:
|
|
38
|
-
return b''
|
|
36
|
+
def read1(self, n: int = -1, /) -> bytes:
|
|
37
|
+
try:
|
|
38
|
+
return next(self.it)
|
|
39
|
+
except StopIteration:
|
|
40
|
+
return b''
|
|
39
41
|
|
|
40
|
-
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
|
42
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
|
|
41
43
|
try:
|
|
42
44
|
resp_cm = httpx.stream(
|
|
43
45
|
method=req.method_or_default,
|
|
@@ -59,7 +61,7 @@ class HttpxHttpClient(HttpClient):
|
|
|
59
61
|
headers=HttpHeaders(resp.headers.raw),
|
|
60
62
|
request=req,
|
|
61
63
|
underlying=resp,
|
|
62
|
-
|
|
64
|
+
_stream=ReadableListBuffer().new_buffered_reader(self._StreamAdapter(resp.iter_bytes())),
|
|
63
65
|
_closer=resp_close, # type: ignore
|
|
64
66
|
)
|
|
65
67
|
|
|
@@ -67,6 +69,65 @@ class HttpxHttpClient(HttpClient):
|
|
|
67
69
|
resp_close()
|
|
68
70
|
raise HttpClientError from e
|
|
69
71
|
|
|
70
|
-
except
|
|
72
|
+
except BaseException:
|
|
71
73
|
resp_close()
|
|
72
74
|
raise
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class HttpxAsyncHttpClient(AsyncHttpClient):
|
|
81
|
+
@dc.dataclass(frozen=True)
|
|
82
|
+
class _StreamAdapter:
|
|
83
|
+
it: ta.AsyncIterator[bytes]
|
|
84
|
+
|
|
85
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
86
|
+
try:
|
|
87
|
+
return await anext(self.it)
|
|
88
|
+
except StopAsyncIteration:
|
|
89
|
+
return b''
|
|
90
|
+
|
|
91
|
+
async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
|
|
92
|
+
es = contextlib.AsyncExitStack()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
client = await es.enter_async_context(httpx.AsyncClient())
|
|
96
|
+
|
|
97
|
+
resp = await es.enter_async_context(client.stream(
|
|
98
|
+
method=req.method_or_default,
|
|
99
|
+
url=req.url,
|
|
100
|
+
headers=req.headers_ or None, # type: ignore
|
|
101
|
+
content=req.data,
|
|
102
|
+
timeout=req.timeout_s,
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
it = resp.aiter_bytes()
|
|
106
|
+
|
|
107
|
+
# FIXME:
|
|
108
|
+
# this has a tendency to raise `RuntimeError: async generator ignored GeneratorExit` when all of the
|
|
109
|
+
# following conditions are met:
|
|
110
|
+
# - stopped iterating midway through
|
|
111
|
+
# - shutting down the event loop
|
|
112
|
+
# - debugging under pycharm / pydevd
|
|
113
|
+
# - running under asyncio
|
|
114
|
+
# it does not seem to happen unless all of these conditions are met. see:
|
|
115
|
+
# https://gist.github.com/wrmsr/a0578ee5d5371b53804cfb56aeb84cdf .
|
|
116
|
+
es.push_async_callback(it.aclose) # type: ignore[attr-defined]
|
|
117
|
+
|
|
118
|
+
return AsyncStreamHttpResponse(
|
|
119
|
+
status=resp.status_code,
|
|
120
|
+
headers=HttpHeaders(resp.headers.raw),
|
|
121
|
+
request=req,
|
|
122
|
+
underlying=resp,
|
|
123
|
+
_stream=ReadableListBuffer().new_async_buffered_reader(self._StreamAdapter(it)),
|
|
124
|
+
_closer=es.aclose,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
except httpx.HTTPError as e:
|
|
128
|
+
await es.aclose()
|
|
129
|
+
raise HttpClientError from e
|
|
130
|
+
|
|
131
|
+
except BaseException:
|
|
132
|
+
await es.aclose()
|
|
133
|
+
raise
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# ruff: noqa: UP007 UP043 UP045
|
|
2
|
+
# @omlish-lite
|
|
3
|
+
"""
|
|
4
|
+
TODO:
|
|
5
|
+
- redirect
|
|
6
|
+
- referrer header?
|
|
7
|
+
- non-forwarded headers, host check, etc lol
|
|
8
|
+
- 'check' kw becomes StatusCheckingMiddleware?
|
|
9
|
+
"""
|
|
10
|
+
import dataclasses as dc
|
|
11
|
+
import typing as ta
|
|
12
|
+
import urllib.parse
|
|
13
|
+
|
|
14
|
+
from ...lite.abstract import Abstract
|
|
15
|
+
from ...lite.check import check
|
|
16
|
+
from ..urls import parsed_url_replace
|
|
17
|
+
from .asyncs import AsyncHttpClient
|
|
18
|
+
from .asyncs import AsyncStreamHttpResponse
|
|
19
|
+
from .base import BaseHttpClient
|
|
20
|
+
from .base import BaseHttpResponse
|
|
21
|
+
from .base import HttpClientContext
|
|
22
|
+
from .base import HttpClientError
|
|
23
|
+
from .base import HttpRequest
|
|
24
|
+
from .sync import HttpClient
|
|
25
|
+
from .sync import StreamHttpResponse
|
|
26
|
+
from .sync import close_http_client_response
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
BaseHttpClientT = ta.TypeVar('BaseHttpClientT', bound=BaseHttpClient)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HttpClientMiddleware(Abstract):
|
|
36
|
+
def process_request(
|
|
37
|
+
self,
|
|
38
|
+
ctx: HttpClientContext,
|
|
39
|
+
req: HttpRequest,
|
|
40
|
+
) -> HttpRequest:
|
|
41
|
+
return req
|
|
42
|
+
|
|
43
|
+
def process_response(
|
|
44
|
+
self,
|
|
45
|
+
ctx: HttpClientContext,
|
|
46
|
+
req: HttpRequest,
|
|
47
|
+
resp: BaseHttpResponse,
|
|
48
|
+
) -> ta.Union[BaseHttpResponse, HttpRequest]:
|
|
49
|
+
return resp
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AbstractMiddlewareHttpClient(Abstract, ta.Generic[BaseHttpClientT]):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
client: BaseHttpClientT,
|
|
56
|
+
middlewares: ta.Iterable[HttpClientMiddleware],
|
|
57
|
+
) -> None:
|
|
58
|
+
super().__init__()
|
|
59
|
+
|
|
60
|
+
self._client = client
|
|
61
|
+
self._middlewares = list(middlewares)
|
|
62
|
+
|
|
63
|
+
def _process_request(
|
|
64
|
+
self,
|
|
65
|
+
ctx: HttpClientContext,
|
|
66
|
+
req: HttpRequest,
|
|
67
|
+
) -> HttpRequest:
|
|
68
|
+
for mw in self._middlewares:
|
|
69
|
+
req = mw.process_request(ctx, req)
|
|
70
|
+
return req
|
|
71
|
+
|
|
72
|
+
def _process_response(
|
|
73
|
+
self,
|
|
74
|
+
ctx: HttpClientContext,
|
|
75
|
+
req: HttpRequest,
|
|
76
|
+
resp: BaseHttpResponse,
|
|
77
|
+
) -> ta.Union[BaseHttpResponse, HttpRequest]:
|
|
78
|
+
for mw in self._middlewares:
|
|
79
|
+
nxt = mw.process_response(ctx, req, resp)
|
|
80
|
+
if isinstance(nxt, HttpRequest):
|
|
81
|
+
return nxt
|
|
82
|
+
else:
|
|
83
|
+
resp = nxt
|
|
84
|
+
return resp
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MiddlewareHttpClient(AbstractMiddlewareHttpClient[HttpClient], HttpClient):
|
|
91
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
|
|
92
|
+
while True:
|
|
93
|
+
req = self._process_request(ctx, req)
|
|
94
|
+
|
|
95
|
+
resp = self._client.stream_request(req, context=ctx)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
out = self._process_response(ctx, req, resp)
|
|
99
|
+
|
|
100
|
+
if isinstance(out, HttpRequest):
|
|
101
|
+
close_http_client_response(resp)
|
|
102
|
+
req = out
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
elif isinstance(out, StreamHttpResponse):
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
raise TypeError(out) # noqa
|
|
110
|
+
|
|
111
|
+
except Exception:
|
|
112
|
+
close_http_client_response(resp)
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
raise RuntimeError
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class MiddlewareAsyncHttpClient(AbstractMiddlewareHttpClient[AsyncHttpClient], AsyncHttpClient):
|
|
119
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
|
|
120
|
+
return self._client.stream_request(self._process_request(ctx, req))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TooManyRedirectsHttpClientError(HttpClientError):
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RedirectHandlingHttpClientMiddleware(HttpClientMiddleware):
|
|
131
|
+
DEFAULT_MAX_REDIRECTS: ta.ClassVar[int] = 5
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
max_redirects: ta.Optional[int] = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
super().__init__()
|
|
139
|
+
|
|
140
|
+
if max_redirects is None:
|
|
141
|
+
max_redirects = self.DEFAULT_MAX_REDIRECTS
|
|
142
|
+
self._max_redirects = max_redirects
|
|
143
|
+
|
|
144
|
+
@dc.dataclass()
|
|
145
|
+
class _State:
|
|
146
|
+
num_redirects: int = 0
|
|
147
|
+
|
|
148
|
+
def _get_state(self, ctx: HttpClientContext) -> _State:
|
|
149
|
+
try:
|
|
150
|
+
return ctx._dct[self._State] # noqa
|
|
151
|
+
except KeyError:
|
|
152
|
+
ret = ctx._dct[self._State] = self._State() # noqa
|
|
153
|
+
return ret
|
|
154
|
+
|
|
155
|
+
def process_response(
|
|
156
|
+
self,
|
|
157
|
+
ctx: HttpClientContext,
|
|
158
|
+
req: HttpRequest,
|
|
159
|
+
resp: BaseHttpResponse,
|
|
160
|
+
) -> ta.Union[BaseHttpResponse, HttpRequest]: # noqa
|
|
161
|
+
if resp.status == 302:
|
|
162
|
+
st = self._get_state(ctx)
|
|
163
|
+
if st.num_redirects >= self._max_redirects:
|
|
164
|
+
raise TooManyRedirectsHttpClientError
|
|
165
|
+
st.num_redirects += 1
|
|
166
|
+
|
|
167
|
+
rd_url = check.not_none(resp.headers).single_str_dct['location']
|
|
168
|
+
|
|
169
|
+
rd_purl = urllib.parse.urlparse(rd_url)
|
|
170
|
+
if not rd_purl.netloc:
|
|
171
|
+
rq_purl = urllib.parse.urlparse(req.url)
|
|
172
|
+
rd_purl = parsed_url_replace(
|
|
173
|
+
rd_purl,
|
|
174
|
+
scheme=rq_purl.scheme,
|
|
175
|
+
netloc=rq_purl.netloc,
|
|
176
|
+
)
|
|
177
|
+
rd_url = urllib.parse.urlunparse(rd_purl)
|
|
178
|
+
|
|
179
|
+
return dc.replace(req, url=rd_url)
|
|
180
|
+
|
|
181
|
+
return resp
|