omlish 0.0.0.dev468__py3-none-any.whl → 0.0.0.dev469__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.
- omlish/__about__.py +2 -2
- omlish/asyncs/asyncio/sync.py +43 -0
- omlish/asyncs/sync.py +25 -0
- omlish/http/all.py +4 -0
- omlish/http/clients/asyncs.py +26 -14
- omlish/http/clients/base.py +17 -1
- omlish/http/clients/coro/__init__.py +0 -0
- omlish/http/clients/coro/sync.py +170 -0
- omlish/http/clients/default.py +118 -25
- omlish/http/clients/executor.py +50 -0
- omlish/http/clients/httpx.py +35 -15
- omlish/http/clients/middleware.py +178 -0
- omlish/http/clients/sync.py +25 -13
- omlish/http/clients/urllib.py +4 -2
- omlish/http/coro/client/connection.py +15 -6
- omlish/http/coro/io.py +2 -0
- omlish/http/urls.py +67 -0
- omlish/io/buffers.py +3 -0
- omlish/lang/__init__.py +3 -0
- omlish/lang/functions.py +9 -4
- omlish/lang/params.py +17 -0
- omlish/sync.py +26 -24
- {omlish-0.0.0.dev468.dist-info → omlish-0.0.0.dev469.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev468.dist-info → omlish-0.0.0.dev469.dist-info}/RECORD +28 -21
- {omlish-0.0.0.dev468.dist-info → omlish-0.0.0.dev469.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev468.dist-info → omlish-0.0.0.dev469.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev468.dist-info → omlish-0.0.0.dev469.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev468.dist-info → omlish-0.0.0.dev469.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# ruff: noqa: UP045
|
|
2
|
+
# @omlish-lite
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from ...sync import SyncBufferRelay
|
|
7
|
+
from ..sync import AsyncBufferRelay
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = ta.TypeVar('T')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@ta.final
|
|
17
|
+
class AsyncioBufferRelay(AsyncBufferRelay[T]):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
event: ta.Optional[asyncio.Event] = None,
|
|
22
|
+
loop: ta.Optional[ta.Any] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
if event is None:
|
|
25
|
+
event = asyncio.Event()
|
|
26
|
+
self._event = event
|
|
27
|
+
if loop is None:
|
|
28
|
+
loop = asyncio.get_running_loop()
|
|
29
|
+
self._loop = loop
|
|
30
|
+
|
|
31
|
+
self._relay: SyncBufferRelay[T] = SyncBufferRelay(
|
|
32
|
+
wake_fn=lambda: loop.call_soon_threadsafe(event.set), # type: ignore[arg-type]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def push(self, *vs: T) -> None:
|
|
36
|
+
self._relay.push(*vs)
|
|
37
|
+
|
|
38
|
+
def swap(self) -> ta.Sequence[T]:
|
|
39
|
+
return self._relay.swap()
|
|
40
|
+
|
|
41
|
+
async def wait(self) -> None:
|
|
42
|
+
await self._event.wait()
|
|
43
|
+
self._event.clear()
|
omlish/asyncs/sync.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @omlish-lite
|
|
2
|
+
import abc
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
from ..lite.abstract import Abstract
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
T = ta.TypeVar('T')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncBufferRelay(Abstract, ta.Generic[T]):
|
|
15
|
+
@abc.abstractmethod
|
|
16
|
+
def push(self, *vs: T) -> None:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def swap(self) -> ta.Sequence[T]:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
async def wait(self) -> None:
|
|
25
|
+
raise NotImplementedError
|
omlish/http/all.py
CHANGED
omlish/http/clients/asyncs.py
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
import abc
|
|
4
4
|
import contextlib
|
|
5
5
|
import dataclasses as dc
|
|
6
|
+
import io
|
|
6
7
|
import typing as ta
|
|
7
8
|
|
|
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
|
|
@@ -26,21 +28,26 @@ AsyncHttpClientT = ta.TypeVar('AsyncHttpClientT', bound='AsyncHttpClient')
|
|
|
26
28
|
@dc.dataclass(frozen=True) # kw_only=True
|
|
27
29
|
class AsyncStreamHttpResponse(BaseHttpResponse):
|
|
28
30
|
class Stream(ta.Protocol):
|
|
29
|
-
def
|
|
31
|
+
def read1(self, /, n: int = -1) -> ta.Awaitable[bytes]: ...
|
|
30
32
|
|
|
31
33
|
@ta.final
|
|
32
34
|
class _NullStream:
|
|
33
|
-
def
|
|
35
|
+
def read1(self, /, n: int = -1) -> ta.Awaitable[bytes]:
|
|
34
36
|
raise TypeError
|
|
35
37
|
|
|
36
38
|
stream: Stream = _NullStream()
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
@property
|
|
41
|
+
def has_data(self) -> bool:
|
|
42
|
+
return not isinstance(self.stream, AsyncStreamHttpResponse._NullStream)
|
|
43
|
+
|
|
44
|
+
async def read_all(self) -> bytes:
|
|
45
|
+
buf = io.BytesIO()
|
|
46
|
+
while (b := await self.stream.read1()):
|
|
47
|
+
buf.write(b)
|
|
48
|
+
return buf.getvalue()
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
dataclass_maybe_post_init(super())
|
|
42
|
-
if isinstance(self.stream, AsyncStreamHttpResponse._NullStream):
|
|
43
|
-
raise TypeError(self.stream)
|
|
50
|
+
_closer: ta.Optional[ta.Callable[[], ta.Awaitable[None]]] = None
|
|
44
51
|
|
|
45
52
|
async def __aenter__(self: AsyncStreamHttpResponseT) -> AsyncStreamHttpResponseT:
|
|
46
53
|
return self
|
|
@@ -50,7 +57,7 @@ class AsyncStreamHttpResponse(BaseHttpResponse):
|
|
|
50
57
|
|
|
51
58
|
async def close(self) -> None:
|
|
52
59
|
if (c := self._closer) is not None:
|
|
53
|
-
c()
|
|
60
|
+
await c()
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
#
|
|
@@ -88,10 +95,9 @@ async def async_read_response(resp: BaseHttpResponse) -> HttpResponse:
|
|
|
88
95
|
return resp
|
|
89
96
|
|
|
90
97
|
elif isinstance(resp, AsyncStreamHttpResponse):
|
|
91
|
-
data = await resp.stream.read()
|
|
92
98
|
return HttpResponse(**{
|
|
93
99
|
**{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('stream', '_closer')},
|
|
94
|
-
'data':
|
|
100
|
+
**({'data': await resp.read_all()} if resp.has_data else {}),
|
|
95
101
|
})
|
|
96
102
|
|
|
97
103
|
else:
|
|
@@ -101,7 +107,7 @@ async def async_read_response(resp: BaseHttpResponse) -> HttpResponse:
|
|
|
101
107
|
##
|
|
102
108
|
|
|
103
109
|
|
|
104
|
-
class AsyncHttpClient(Abstract):
|
|
110
|
+
class AsyncHttpClient(BaseHttpClient, Abstract):
|
|
105
111
|
async def __aenter__(self: AsyncHttpClientT) -> AsyncHttpClientT:
|
|
106
112
|
return self
|
|
107
113
|
|
|
@@ -112,10 +118,12 @@ class AsyncHttpClient(Abstract):
|
|
|
112
118
|
self,
|
|
113
119
|
req: HttpRequest,
|
|
114
120
|
*,
|
|
121
|
+
context: ta.Optional[HttpClientContext] = None,
|
|
115
122
|
check: bool = False,
|
|
116
123
|
) -> HttpResponse:
|
|
117
124
|
async with async_closing_response(await self.stream_request(
|
|
118
125
|
req,
|
|
126
|
+
context=context,
|
|
119
127
|
check=check,
|
|
120
128
|
)) as resp:
|
|
121
129
|
return await async_read_response(resp)
|
|
@@ -124,9 +132,13 @@ class AsyncHttpClient(Abstract):
|
|
|
124
132
|
self,
|
|
125
133
|
req: HttpRequest,
|
|
126
134
|
*,
|
|
135
|
+
context: ta.Optional[HttpClientContext] = None,
|
|
127
136
|
check: bool = False,
|
|
128
137
|
) -> AsyncStreamHttpResponse:
|
|
129
|
-
|
|
138
|
+
if context is None:
|
|
139
|
+
context = HttpClientContext()
|
|
140
|
+
|
|
141
|
+
resp = await self._stream_request(context, req)
|
|
130
142
|
|
|
131
143
|
try:
|
|
132
144
|
if check and not resp.is_success:
|
|
@@ -143,5 +155,5 @@ class AsyncHttpClient(Abstract):
|
|
|
143
155
|
return resp
|
|
144
156
|
|
|
145
157
|
@abc.abstractmethod
|
|
146
|
-
def _stream_request(self, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
|
|
158
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
|
|
147
159
|
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,170 @@
|
|
|
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 ....lite.check import check
|
|
9
|
+
from ...coro.client.connection import CoroHttpClientConnection
|
|
10
|
+
from ...coro.client.response import CoroHttpClientResponse
|
|
11
|
+
from ...coro.io import CoroHttpIo
|
|
12
|
+
from ...headers import HttpHeaders
|
|
13
|
+
from ...urls import unparse_url_request_path
|
|
14
|
+
from ..base import HttpClientContext
|
|
15
|
+
from ..base import HttpClientError
|
|
16
|
+
from ..base import HttpRequest
|
|
17
|
+
from ..sync import HttpClient
|
|
18
|
+
from ..sync import StreamHttpResponse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
T = ta.TypeVar('T')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CoroHttpClient(HttpClient):
|
|
28
|
+
class _Connection:
|
|
29
|
+
def __init__(self, req: HttpRequest) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
self._req = req
|
|
33
|
+
self._ups = urllib.parse.urlparse(req.url)
|
|
34
|
+
|
|
35
|
+
self._ssl = self._ups.scheme == 'https'
|
|
36
|
+
|
|
37
|
+
_cc: ta.Optional[CoroHttpClientConnection] = None
|
|
38
|
+
_resp: ta.Optional[CoroHttpClientResponse] = None
|
|
39
|
+
|
|
40
|
+
_sock: ta.Optional[socket.socket] = None
|
|
41
|
+
_sock_file: ta.Optional[ta.BinaryIO] = None
|
|
42
|
+
|
|
43
|
+
_ssl_context: ta.Any = None
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
|
|
47
|
+
def _create_https_context(self, http_version: int) -> ta.Any:
|
|
48
|
+
# https://github.com/python/cpython/blob/a7160912274003672dc116d918260c0a81551c21/Lib/http/client.py#L809
|
|
49
|
+
import ssl
|
|
50
|
+
|
|
51
|
+
# Function also used by urllib.request to be able to set the check_hostname attribute on a context object.
|
|
52
|
+
context = ssl.create_default_context()
|
|
53
|
+
|
|
54
|
+
# Send ALPN extension to indicate HTTP/1.1 protocol.
|
|
55
|
+
if http_version == 11:
|
|
56
|
+
context.set_alpn_protocols(['http/1.1'])
|
|
57
|
+
|
|
58
|
+
# Enable PHA for TLS 1.3 connections if available.
|
|
59
|
+
if context.post_handshake_auth is not None:
|
|
60
|
+
context.post_handshake_auth = True
|
|
61
|
+
|
|
62
|
+
return context
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
|
|
66
|
+
def setup(self) -> StreamHttpResponse:
|
|
67
|
+
check.none(self._sock)
|
|
68
|
+
check.none(self._ssl_context)
|
|
69
|
+
|
|
70
|
+
self._cc = cc = CoroHttpClientConnection(
|
|
71
|
+
check.not_none(self._ups.hostname),
|
|
72
|
+
default_port=CoroHttpClientConnection.HTTPS_PORT if self._ssl else CoroHttpClientConnection.HTTP_PORT,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if self._ssl:
|
|
76
|
+
self._ssl_context = self._create_https_context(self._cc.http_version)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
self._run_coro(cc.connect())
|
|
80
|
+
|
|
81
|
+
self._run_coro(cc.request(
|
|
82
|
+
self._req.method or 'GET',
|
|
83
|
+
unparse_url_request_path(self._ups) or '/',
|
|
84
|
+
self._req.data,
|
|
85
|
+
hh.single_str_dct if (hh := self._req.headers_) is not None else {},
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
self._resp = resp = self._run_coro(cc.get_response())
|
|
89
|
+
|
|
90
|
+
return StreamHttpResponse(
|
|
91
|
+
status=resp._state.status, # noqa
|
|
92
|
+
headers=HttpHeaders(resp._state.headers.items()), # noqa
|
|
93
|
+
request=self._req,
|
|
94
|
+
underlying=self,
|
|
95
|
+
stream=self,
|
|
96
|
+
_closer=self.close,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
except Exception:
|
|
100
|
+
self.close()
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
def _run_coro(self, g: ta.Generator[ta.Any, ta.Any, T]) -> T:
|
|
104
|
+
i = None
|
|
105
|
+
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
o = g.send(i)
|
|
109
|
+
except StopIteration as e:
|
|
110
|
+
return e.value
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
i = self._handle_io(o)
|
|
114
|
+
except OSError as e:
|
|
115
|
+
raise HttpClientError from e
|
|
116
|
+
|
|
117
|
+
def _handle_io(self, o: CoroHttpIo.Io) -> ta.Any:
|
|
118
|
+
if isinstance(o, CoroHttpIo.ConnectIo):
|
|
119
|
+
check.none(self._sock)
|
|
120
|
+
self._sock = socket.create_connection(*o.args, **(o.kwargs or {}))
|
|
121
|
+
|
|
122
|
+
if self._ssl_context is not None:
|
|
123
|
+
self._sock = self._ssl_context.wrap_socket(
|
|
124
|
+
self._sock,
|
|
125
|
+
server_hostname=check.not_none(o.server_hostname),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Might fail in OSs that don't implement TCP_NODELAY
|
|
129
|
+
try:
|
|
130
|
+
self._sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
131
|
+
except OSError as e:
|
|
132
|
+
if e.errno != errno.ENOPROTOOPT:
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
self._sock_file = self._sock.makefile('rb')
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
elif isinstance(o, CoroHttpIo.CloseIo):
|
|
140
|
+
check.not_none(self._sock).close()
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
elif isinstance(o, CoroHttpIo.WriteIo):
|
|
144
|
+
check.not_none(self._sock).sendall(o.data)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
elif isinstance(o, CoroHttpIo.ReadIo):
|
|
148
|
+
if (sz := o.sz) is not None:
|
|
149
|
+
return check.not_none(self._sock_file).read(sz)
|
|
150
|
+
else:
|
|
151
|
+
return check.not_none(self._sock_file).read()
|
|
152
|
+
|
|
153
|
+
elif isinstance(o, CoroHttpIo.ReadLineIo):
|
|
154
|
+
return check.not_none(self._sock_file).readline(o.sz)
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
raise TypeError(o)
|
|
158
|
+
|
|
159
|
+
def read1(self, /, n: int = -1) -> bytes:
|
|
160
|
+
return self._run_coro(check.not_none(self._resp).read(n if n >= 0 else None))
|
|
161
|
+
|
|
162
|
+
def close(self) -> None:
|
|
163
|
+
if self._resp is not None:
|
|
164
|
+
self._resp.close()
|
|
165
|
+
if self._sock is not None:
|
|
166
|
+
self._sock.close()
|
|
167
|
+
|
|
168
|
+
def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
|
|
169
|
+
conn = CoroHttpClient._Connection(req)
|
|
170
|
+
return conn.setup()
|
omlish/http/clients/default.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
import contextlib
|
|
2
3
|
import typing as ta
|
|
3
4
|
|
|
4
5
|
from ... import lang
|
|
5
6
|
from ..headers import CanHttpHeaders
|
|
6
7
|
from .asyncs import AsyncHttpClient
|
|
8
|
+
from .base import HttpClientContext
|
|
7
9
|
from .base import HttpRequest
|
|
8
10
|
from .base import HttpResponse
|
|
9
11
|
from .sync import HttpClient
|
|
@@ -32,12 +34,13 @@ class _DefaultRequester(lang.Abstract, ta.Generic[C, R]):
|
|
|
32
34
|
|
|
33
35
|
timeout_s: float | None = None,
|
|
34
36
|
|
|
37
|
+
context: HttpClientContext | None = None,
|
|
35
38
|
check: bool = False,
|
|
36
39
|
client: C | None = None, # noqa
|
|
37
40
|
|
|
38
41
|
**kwargs: ta.Any,
|
|
39
42
|
) -> R:
|
|
40
|
-
|
|
43
|
+
request = HttpRequest( # noqa
|
|
41
44
|
url,
|
|
42
45
|
method=method,
|
|
43
46
|
|
|
@@ -50,7 +53,8 @@ class _DefaultRequester(lang.Abstract, ta.Generic[C, R]):
|
|
|
50
53
|
)
|
|
51
54
|
|
|
52
55
|
return self._do(
|
|
53
|
-
|
|
56
|
+
request,
|
|
57
|
+
context=context,
|
|
54
58
|
check=check,
|
|
55
59
|
client=client,
|
|
56
60
|
)
|
|
@@ -58,8 +62,9 @@ class _DefaultRequester(lang.Abstract, ta.Generic[C, R]):
|
|
|
58
62
|
@abc.abstractmethod
|
|
59
63
|
def _do(
|
|
60
64
|
self,
|
|
61
|
-
|
|
65
|
+
request: HttpRequest, # noqa
|
|
62
66
|
*,
|
|
67
|
+
context: HttpClientContext | None = None,
|
|
63
68
|
check: bool = False,
|
|
64
69
|
client: C | None = None, # noqa
|
|
65
70
|
) -> R:
|
|
@@ -77,30 +82,74 @@ def client() -> HttpClient:
|
|
|
77
82
|
return _default_client()
|
|
78
83
|
|
|
79
84
|
|
|
85
|
+
@contextlib.contextmanager
|
|
86
|
+
def manage_client(client: HttpClient | 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
|
+
|
|
80
95
|
#
|
|
81
96
|
|
|
82
97
|
|
|
83
|
-
class
|
|
98
|
+
class _BaseSyncDefaultRequester(_DefaultRequester[HttpClient, R], lang.Abstract, ta.Generic[R]):
|
|
84
99
|
def _do(
|
|
85
100
|
self,
|
|
86
|
-
|
|
101
|
+
request: HttpRequest, # noqa
|
|
87
102
|
*,
|
|
103
|
+
context: HttpClientContext | None = None,
|
|
88
104
|
check: bool = False,
|
|
89
105
|
client: HttpClient | None = None, # noqa
|
|
90
|
-
) ->
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
req,
|
|
106
|
+
) -> R:
|
|
107
|
+
if context is None:
|
|
108
|
+
context = HttpClientContext()
|
|
94
109
|
|
|
110
|
+
if client is not None:
|
|
111
|
+
return self._do_(
|
|
112
|
+
client,
|
|
113
|
+
context,
|
|
114
|
+
request,
|
|
95
115
|
check=check,
|
|
96
116
|
)
|
|
97
117
|
|
|
98
|
-
if client is not None:
|
|
99
|
-
return do(client)
|
|
100
|
-
|
|
101
118
|
else:
|
|
102
|
-
with _default_client() as
|
|
103
|
-
return
|
|
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
|
+
)
|
|
104
153
|
|
|
105
154
|
|
|
106
155
|
request = _SyncDefaultRequester()
|
|
@@ -117,30 +166,74 @@ def async_client() -> AsyncHttpClient:
|
|
|
117
166
|
return _default_async_client()
|
|
118
167
|
|
|
119
168
|
|
|
169
|
+
@contextlib.asynccontextmanager
|
|
170
|
+
async def manage_async_client(client: AsyncHttpClient | None) -> ta.AsyncGenerator[AsyncHttpClient]: # noqa
|
|
171
|
+
if client is not None:
|
|
172
|
+
yield client
|
|
173
|
+
|
|
174
|
+
else:
|
|
175
|
+
async with _default_async_client() as client: # noqa
|
|
176
|
+
yield client
|
|
177
|
+
|
|
178
|
+
|
|
120
179
|
#
|
|
121
180
|
|
|
122
181
|
|
|
123
|
-
class
|
|
182
|
+
class _BaseAsyncDefaultRequester(_DefaultRequester[AsyncHttpClient, ta.Awaitable[R]], lang.Abstract, ta.Generic[R]):
|
|
124
183
|
async def _do(
|
|
125
184
|
self,
|
|
126
|
-
|
|
185
|
+
request: HttpRequest, # noqa
|
|
127
186
|
*,
|
|
187
|
+
context: HttpClientContext | None = None,
|
|
128
188
|
check: bool = False,
|
|
129
189
|
client: AsyncHttpClient | None = None, # noqa
|
|
130
|
-
) ->
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
req,
|
|
190
|
+
) -> R:
|
|
191
|
+
if context is None:
|
|
192
|
+
context = HttpClientContext()
|
|
134
193
|
|
|
194
|
+
if client is not None:
|
|
195
|
+
return await self._do_(
|
|
196
|
+
client,
|
|
197
|
+
context,
|
|
198
|
+
request,
|
|
135
199
|
check=check,
|
|
136
200
|
)
|
|
137
201
|
|
|
138
|
-
if client is not None:
|
|
139
|
-
return await do(client)
|
|
140
|
-
|
|
141
202
|
else:
|
|
142
|
-
async with _default_async_client() as
|
|
143
|
-
return await
|
|
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,
|
|
235
|
+
check=check,
|
|
236
|
+
)
|
|
144
237
|
|
|
145
238
|
|
|
146
239
|
async_request = _AsyncDefaultRequester()
|
|
@@ -0,0 +1,50 @@
|
|
|
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 close(self) -> None:
|
|
37
|
+
return await self.owner._run_in_executor(self.resp.close) # noqa
|
|
38
|
+
|
|
39
|
+
async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
|
|
40
|
+
resp: StreamHttpResponse = await self._run_in_executor(lambda: self._client.stream_request(req, context=ctx))
|
|
41
|
+
return AsyncStreamHttpResponse(
|
|
42
|
+
status=resp.status,
|
|
43
|
+
headers=resp.headers,
|
|
44
|
+
request=req,
|
|
45
|
+
underlying=resp,
|
|
46
|
+
**(dict( # type: ignore
|
|
47
|
+
stream=(adapter := self._StreamAdapter(self, resp)),
|
|
48
|
+
_closer=adapter.close,
|
|
49
|
+
) if resp.has_data else {}),
|
|
50
|
+
)
|