omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev493__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/README.md +199 -0
- omlish/__about__.py +21 -16
- 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/plans.py +2 -17
- omlish/dataclasses/impl/generation/processor.py +106 -25
- omlish/dataclasses/impl/processing/base.py +8 -0
- omlish/dataclasses/impl/processing/driving.py +19 -7
- omlish/dataclasses/specs.py +1 -0
- omlish/dataclasses/tools/modifiers.py +5 -0
- omlish/diag/_pycharm/runhack.py +1 -1
- 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 +57 -29
- omlish/inject/_dataclasses.py +5148 -0
- omlish/inject/binder.py +11 -52
- omlish/inject/eagers.py +2 -0
- 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/late.py +76 -0
- omlish/inject/{managed.py → helpers/managed.py} +10 -10
- omlish/inject/helpers/multis.py +143 -0
- omlish/inject/helpers/wrappers.py +54 -0
- omlish/inject/impl/elements.py +54 -21
- omlish/inject/impl/injector.py +29 -25
- 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 +180 -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/lifecycles/README.md +30 -0
- omlish/lifecycles/__init__.py +87 -13
- omlish/lifecycles/_dataclasses.py +1388 -0
- omlish/lifecycles/base.py +178 -64
- omlish/lifecycles/contextmanagers.py +113 -4
- omlish/lifecycles/controller.py +150 -87
- omlish/lifecycles/injection.py +143 -0
- omlish/lifecycles/listeners.py +56 -0
- omlish/lifecycles/managed.py +142 -0
- omlish/lifecycles/manager.py +218 -93
- omlish/lifecycles/states.py +2 -0
- omlish/lifecycles/transitions.py +3 -0
- omlish/lifecycles/unwrap.py +57 -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 +24 -0
- omlish/logs/_amalg.py +1 -1
- omlish/logs/all.py +25 -11
- omlish/logs/asyncs.py +73 -0
- omlish/logs/base.py +101 -12
- omlish/logs/contexts.py +4 -1
- omlish/logs/lists.py +125 -0
- omlish/logs/modules.py +19 -1
- omlish/logs/std/loggers.py +6 -1
- omlish/logs/std/noisy.py +11 -9
- omlish/logs/{standard.py → std/standard.py} +3 -4
- omlish/logs/utils.py +17 -2
- 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/__init__.py +43 -26
- omlish/reflect/ops.py +10 -1
- 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/__init__.py +24 -5
- omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
- omlish/sql/api/dbapi.py +1 -1
- omlish/sql/dbapi/__init__.py +15 -0
- omlish/sql/{dbapi.py → dbapi/drivers.py} +2 -2
- omlish/sql/queries/__init__.py +3 -0
- 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/asyncs/plugin.py +2 -0
- 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 +91 -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.dev493.dist-info}/METADATA +36 -32
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/RECORD +260 -199
- omlish/dataclasses/impl/generation/mangling.py +0 -18
- omlish/funcs/match.py +0 -227
- omlish/lifecycles/abstract.py +0 -86
- omlish/marshal/factories/match.py +0 -34
- omlish/marshal/factories/simple.py +0 -28
- /omlish/inject/{impl → helpers}/proxy.py +0 -0
- /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
- /omlish/sql/{abc.py → dbapi/abc.py} +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/top_level.txt +0 -0
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
|
omlish/http/clients/sync.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 BufferedBytesReader
|
|
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 @@ HttpClientT = ta.TypeVar('HttpClientT', bound='HttpClient')
|
|
|
25
27
|
@ta.final
|
|
26
28
|
@dc.dataclass(frozen=True) # kw_only=True
|
|
27
29
|
class StreamHttpResponse(BaseHttpResponse):
|
|
28
|
-
|
|
29
|
-
def read(self, /, n: int = -1) -> bytes: ...
|
|
30
|
+
_stream: ta.Optional[BufferedBytesReader] = None
|
|
30
31
|
|
|
31
|
-
@
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
raise TypeError
|
|
32
|
+
@property
|
|
33
|
+
def stream(self) -> 'BufferedBytesReader':
|
|
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, StreamHttpResponse._NullStream):
|
|
43
|
-
raise TypeError(self.stream)
|
|
44
|
+
_closer: ta.Optional[ta.Callable[[], None]] = None
|
|
44
45
|
|
|
45
46
|
def __enter__(self: StreamHttpResponseT) -> StreamHttpResponseT:
|
|
46
47
|
return self
|
|
@@ -50,13 +51,13 @@ class StreamHttpResponse(BaseHttpResponse):
|
|
|
50
51
|
|
|
51
52
|
def close(self) -> None:
|
|
52
53
|
if (c := self._closer) is not None:
|
|
53
|
-
c()
|
|
54
|
+
c() # noqa
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
#
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
+
def close_http_client_response(resp: BaseHttpResponse) -> None:
|
|
60
61
|
if isinstance(resp, HttpResponse):
|
|
61
62
|
pass
|
|
62
63
|
|
|
@@ -68,7 +69,7 @@ def close_response(resp: BaseHttpResponse) -> None:
|
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
@contextlib.contextmanager
|
|
71
|
-
def
|
|
72
|
+
def closing_http_client_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
|
|
72
73
|
if isinstance(resp, HttpResponse):
|
|
73
74
|
yield resp
|
|
74
75
|
return
|
|
@@ -81,15 +82,14 @@ def closing_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
|
|
|
81
82
|
raise TypeError(resp)
|
|
82
83
|
|
|
83
84
|
|
|
84
|
-
def
|
|
85
|
+
def read_http_client_response(resp: BaseHttpResponse) -> HttpResponse:
|
|
85
86
|
if isinstance(resp, HttpResponse):
|
|
86
87
|
return resp
|
|
87
88
|
|
|
88
89
|
elif isinstance(resp, StreamHttpResponse):
|
|
89
|
-
data = resp.stream.read()
|
|
90
90
|
return HttpResponse(**{
|
|
91
|
-
**{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('
|
|
92
|
-
'data':
|
|
91
|
+
**{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('_stream', '_closer')},
|
|
92
|
+
**({'data': resp.stream.readall()} if resp.has_data else {}),
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
else:
|
|
@@ -99,7 +99,7 @@ def read_response(resp: BaseHttpResponse) -> HttpResponse:
|
|
|
99
99
|
##
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
class HttpClient(Abstract):
|
|
102
|
+
class HttpClient(BaseHttpClient, Abstract):
|
|
103
103
|
def __enter__(self: HttpClientT) -> HttpClientT:
|
|
104
104
|
return self
|
|
105
105
|
|
|
@@ -110,21 +110,27 @@ class HttpClient(Abstract):
|
|
|
110
110
|
self,
|
|
111
111
|
req: HttpRequest,
|
|
112
112
|
*,
|
|
113
|
+
context: ta.Optional[HttpClientContext] = None,
|
|
113
114
|
check: bool = False,
|
|
114
115
|
) -> HttpResponse:
|
|
115
|
-
with
|
|
116
|
+
with closing_http_client_response(self.stream_request(
|
|
116
117
|
req,
|
|
118
|
+
context=context,
|
|
117
119
|
check=check,
|
|
118
120
|
)) as resp:
|
|
119
|
-
return
|
|
121
|
+
return read_http_client_response(resp)
|
|
120
122
|
|
|
121
123
|
def stream_request(
|
|
122
124
|
self,
|
|
123
125
|
req: HttpRequest,
|
|
124
126
|
*,
|
|
127
|
+
context: ta.Optional[HttpClientContext] = None,
|
|
125
128
|
check: bool = False,
|
|
126
129
|
) -> StreamHttpResponse:
|
|
127
|
-
|
|
130
|
+
if context is None:
|
|
131
|
+
context = HttpClientContext()
|
|
132
|
+
|
|
133
|
+
resp = self._stream_request(context, req)
|
|
128
134
|
|
|
129
135
|
try:
|
|
130
136
|
if check and not resp.is_success:
|
|
@@ -132,14 +138,14 @@ class HttpClient(Abstract):
|
|
|
132
138
|
cause = resp.underlying
|
|
133
139
|
else:
|
|
134
140
|
cause = None
|
|
135
|
-
raise HttpStatusError(
|
|
141
|
+
raise HttpStatusError(read_http_client_response(resp)) from cause # noqa
|
|
136
142
|
|
|
137
143
|
except Exception:
|
|
138
|
-
|
|
144
|
+
close_http_client_response(resp)
|
|
139
145
|
raise
|
|
140
146
|
|
|
141
147
|
return resp
|
|
142
148
|
|
|
143
149
|
@abc.abstractmethod
|
|
144
|
-
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
|
150
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
|
|
145
151
|
raise NotImplementedError
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ruff: noqa: UP043 UP045
|
|
2
|
+
# @omlish-lite
|
|
3
|
+
import dataclasses as dc
|
|
4
|
+
|
|
5
|
+
from .asyncs import AsyncHttpClient
|
|
6
|
+
from .asyncs import AsyncStreamHttpResponse
|
|
7
|
+
from .base import HttpClientContext
|
|
8
|
+
from .base import HttpRequest
|
|
9
|
+
from .sync import HttpClient
|
|
10
|
+
from .sync import StreamHttpResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SyncAsyncHttpClient(AsyncHttpClient):
|
|
17
|
+
def __init__(self, client: HttpClient) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
@dc.dataclass(frozen=True)
|
|
23
|
+
class _StreamAdapter:
|
|
24
|
+
ul: StreamHttpResponse
|
|
25
|
+
|
|
26
|
+
async def read1(self, n: int = -1, /) -> bytes:
|
|
27
|
+
return self.ul.stream.read1(n)
|
|
28
|
+
|
|
29
|
+
async def read(self, n: int = -1, /) -> bytes:
|
|
30
|
+
return self.ul.stream.read(n)
|
|
31
|
+
|
|
32
|
+
async def readall(self) -> bytes:
|
|
33
|
+
return self.ul.stream.readall()
|
|
34
|
+
|
|
35
|
+
async def close(self) -> None:
|
|
36
|
+
self.ul.close()
|
|
37
|
+
|
|
38
|
+
async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
|
|
39
|
+
resp = self._client.stream_request(req, context=ctx)
|
|
40
|
+
return AsyncStreamHttpResponse(
|
|
41
|
+
status=resp.status,
|
|
42
|
+
headers=resp.headers,
|
|
43
|
+
request=req,
|
|
44
|
+
underlying=resp,
|
|
45
|
+
**(dict( # type: ignore
|
|
46
|
+
_stream=(adapter := self._StreamAdapter(resp)),
|
|
47
|
+
_closer=adapter.close,
|
|
48
|
+
) if resp.has_data else {}),
|
|
49
|
+
)
|
omlish/http/clients/urllib.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
# ruff: noqa: UP043 UP045
|
|
2
|
+
# @omlish-lite
|
|
1
3
|
import http.client
|
|
2
4
|
import typing as ta
|
|
3
5
|
import urllib.error
|
|
4
6
|
import urllib.request
|
|
5
7
|
|
|
8
|
+
from ...io.buffers import ReadableListBuffer
|
|
6
9
|
from ..headers import HttpHeaders
|
|
7
10
|
from .base import DEFAULT_ENCODING
|
|
11
|
+
from .base import HttpClientContext
|
|
8
12
|
from .base import HttpClientError
|
|
9
13
|
from .base import HttpRequest
|
|
10
14
|
from .sync import HttpClient
|
|
@@ -39,7 +43,7 @@ class UrllibHttpClient(HttpClient):
|
|
|
39
43
|
data=d,
|
|
40
44
|
)
|
|
41
45
|
|
|
42
|
-
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
|
46
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
|
|
43
47
|
try:
|
|
44
48
|
resp = urllib.request.urlopen( # noqa
|
|
45
49
|
self._build_request(req),
|
|
@@ -53,7 +57,6 @@ class UrllibHttpClient(HttpClient):
|
|
|
53
57
|
headers=HttpHeaders(e.headers.items()),
|
|
54
58
|
request=req,
|
|
55
59
|
underlying=e,
|
|
56
|
-
stream=e, # noqa
|
|
57
60
|
_closer=e.close,
|
|
58
61
|
)
|
|
59
62
|
|
|
@@ -70,7 +73,7 @@ class UrllibHttpClient(HttpClient):
|
|
|
70
73
|
headers=HttpHeaders(resp.headers.items()),
|
|
71
74
|
request=req,
|
|
72
75
|
underlying=resp,
|
|
73
|
-
|
|
76
|
+
_stream=ReadableListBuffer().new_buffered_reader(resp),
|
|
74
77
|
_closer=resp.close,
|
|
75
78
|
)
|
|
76
79
|
|
|
@@ -116,10 +116,10 @@ class CoroHttpClientConnection:
|
|
|
116
116
|
_http_version = 11
|
|
117
117
|
_http_version_str = 'HTTP/1.1'
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
HTTP_PORT: ta.ClassVar[int] = 80
|
|
120
|
+
HTTPS_PORT: ta.ClassVar[int] = 443
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
DEFAULT_PORT: ta.ClassVar[int] = HTTP_PORT
|
|
123
123
|
|
|
124
124
|
class _NOT_SET: # noqa
|
|
125
125
|
def __new__(cls, *args, **kwargs): # noqa
|
|
@@ -139,6 +139,7 @@ class CoroHttpClientConnection:
|
|
|
139
139
|
source_address: ta.Optional[str] = None,
|
|
140
140
|
block_size: int = 8192,
|
|
141
141
|
auto_open: bool = True,
|
|
142
|
+
default_port: ta.Optional[int] = None,
|
|
142
143
|
) -> None:
|
|
143
144
|
super().__init__()
|
|
144
145
|
|
|
@@ -146,6 +147,9 @@ class CoroHttpClientConnection:
|
|
|
146
147
|
self._source_address = source_address
|
|
147
148
|
self._block_size = block_size
|
|
148
149
|
self._auto_open = auto_open
|
|
150
|
+
if default_port is None:
|
|
151
|
+
default_port = self.DEFAULT_PORT
|
|
152
|
+
self._default_port = default_port
|
|
149
153
|
|
|
150
154
|
self._connected = False
|
|
151
155
|
self._buffer: ta.List[bytes] = []
|
|
@@ -162,6 +166,10 @@ class CoroHttpClientConnection:
|
|
|
162
166
|
|
|
163
167
|
CoroHttpClientValidation.validate_host(self._host)
|
|
164
168
|
|
|
169
|
+
@property
|
|
170
|
+
def http_version(self) -> int:
|
|
171
|
+
return self._http_version
|
|
172
|
+
|
|
165
173
|
#
|
|
166
174
|
|
|
167
175
|
def _get_hostport(self, host: str, port: ta.Optional[int]) -> ta.Tuple[str, int]:
|
|
@@ -173,12 +181,12 @@ class CoroHttpClientConnection:
|
|
|
173
181
|
port = int(host[i + 1:])
|
|
174
182
|
except ValueError:
|
|
175
183
|
if host[i + 1:] == '': # http://foo.com:/ == http://foo.com/
|
|
176
|
-
port = self.
|
|
184
|
+
port = self._default_port
|
|
177
185
|
else:
|
|
178
186
|
raise CoroHttpClientErrors.InvalidUrlError(f"non-numeric port: '{host[i + 1:]}'") from None
|
|
179
187
|
host = host[:i]
|
|
180
188
|
else:
|
|
181
|
-
port = self.
|
|
189
|
+
port = self._default_port
|
|
182
190
|
|
|
183
191
|
if host and host[0] == '[' and host[-1] == ']':
|
|
184
192
|
host = host[1:-1]
|
|
@@ -286,6 +294,7 @@ class CoroHttpClientConnection:
|
|
|
286
294
|
source_address=self._source_address,
|
|
287
295
|
**(dict(timeout=self._timeout) if self._timeout is not self._NOT_SET else {}),
|
|
288
296
|
),
|
|
297
|
+
server_hostname=self._tunnel_host if self._tunnel_host else self._host,
|
|
289
298
|
)))
|
|
290
299
|
|
|
291
300
|
self._connected = True
|
|
@@ -526,7 +535,7 @@ class CoroHttpClientConnection:
|
|
|
526
535
|
if ':' in host:
|
|
527
536
|
host_enc = self._strip_ipv6_iface(host_enc)
|
|
528
537
|
|
|
529
|
-
if port == self.
|
|
538
|
+
if port == self._default_port:
|
|
530
539
|
self.put_header('Host', host_enc)
|
|
531
540
|
else:
|
|
532
541
|
self.put_header('Host', f"{host_enc.decode('ascii')}:{port}")
|
omlish/http/coro/io.py
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .api import ( # noqa
|
|
2
|
+
Api,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from .app import ( # noqa
|
|
6
|
+
AppRunParams,
|
|
7
|
+
AppRunner,
|
|
8
|
+
|
|
9
|
+
ViewFunc,
|
|
10
|
+
BeforeRequestFunc,
|
|
11
|
+
AfterRequestFunc,
|
|
12
|
+
App,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .cvs import ( # noqa
|
|
16
|
+
CvLookupError,
|
|
17
|
+
|
|
18
|
+
Cvs,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .requests import ( # noqa
|
|
22
|
+
Request,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .responses import ( # noqa
|
|
26
|
+
ResponseData,
|
|
27
|
+
ResponseStatus,
|
|
28
|
+
ResponseHeaders,
|
|
29
|
+
|
|
30
|
+
Response,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .routes import ( # noqa
|
|
34
|
+
RouteKey,
|
|
35
|
+
Route,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .types import ( # noqa
|
|
39
|
+
ImmutableMultiDict,
|
|
40
|
+
)
|