omlish 0.0.0.dev278__py3-none-any.whl → 0.0.0.dev280__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/anyio/subprocesses.py +85 -0
- omlish/collections/identity.py +6 -1
- omlish/dataclasses/impl/main.py +5 -1
- omlish/dataclasses/impl/metaclass.py +0 -9
- omlish/dataclasses/impl/slots.py +1 -0
- omlish/http/clients/__init__.py +34 -0
- omlish/http/clients/base.py +205 -0
- omlish/http/clients/default.py +60 -0
- omlish/http/clients/httpx.py +68 -0
- omlish/http/clients/urllib.py +79 -0
- omlish/lang/cached/function.py +1 -0
- {omlish-0.0.0.dev278.dist-info → omlish-0.0.0.dev280.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev278.dist-info → omlish-0.0.0.dev280.dist-info}/RECORD +18 -13
- omlish/http/clients.py +0 -388
- {omlish-0.0.0.dev278.dist-info → omlish-0.0.0.dev280.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev278.dist-info → omlish-0.0.0.dev280.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev278.dist-info → omlish-0.0.0.dev280.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev278.dist-info → omlish-0.0.0.dev280.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
import io
|
2
|
+
import subprocess
|
3
|
+
import sys
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
import anyio.abc
|
7
|
+
|
8
|
+
from ... import check
|
9
|
+
from ...lite.timeouts import Timeout
|
10
|
+
from ...subprocesses.async_ import AbstractAsyncSubprocesses
|
11
|
+
from ...subprocesses.run import SubprocessRun
|
12
|
+
from ...subprocesses.run import SubprocessRunOutput
|
13
|
+
|
14
|
+
|
15
|
+
T = ta.TypeVar('T')
|
16
|
+
|
17
|
+
|
18
|
+
##
|
19
|
+
|
20
|
+
|
21
|
+
class AnyioSubprocesses(AbstractAsyncSubprocesses):
|
22
|
+
async def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
|
23
|
+
kwargs = dict(run.kwargs or {})
|
24
|
+
|
25
|
+
if run.capture_output:
|
26
|
+
kwargs.setdefault('stdout', subprocess.PIPE)
|
27
|
+
kwargs.setdefault('stderr', subprocess.PIPE)
|
28
|
+
|
29
|
+
with anyio.fail_after(Timeout.of(run.timeout).or_(None)):
|
30
|
+
async with await anyio.open_process(
|
31
|
+
run.cmd,
|
32
|
+
**kwargs,
|
33
|
+
) as proc:
|
34
|
+
async def read_output(stream: anyio.abc.ByteReceiveStream, writer: ta.IO) -> None:
|
35
|
+
while True:
|
36
|
+
try:
|
37
|
+
data = await stream.receive()
|
38
|
+
except anyio.EndOfStream:
|
39
|
+
return
|
40
|
+
writer.write(data)
|
41
|
+
|
42
|
+
stdout: io.BytesIO | None = None
|
43
|
+
stderr: io.BytesIO | None = None
|
44
|
+
async with anyio.create_task_group() as tg:
|
45
|
+
if proc.stdout is not None:
|
46
|
+
stdout = io.BytesIO()
|
47
|
+
tg.start_soon(read_output, proc.stdout, stdout)
|
48
|
+
|
49
|
+
if proc.stderr is not None:
|
50
|
+
stderr = io.BytesIO()
|
51
|
+
tg.start_soon(read_output, proc.stderr, stderr)
|
52
|
+
|
53
|
+
if proc.stdin and run.input is not None:
|
54
|
+
await proc.stdin.send(run.input)
|
55
|
+
await proc.stdin.aclose()
|
56
|
+
|
57
|
+
await proc.wait()
|
58
|
+
|
59
|
+
if run.check and proc.returncode != 0:
|
60
|
+
raise subprocess.CalledProcessError(
|
61
|
+
ta.cast(int, proc.returncode),
|
62
|
+
run.cmd,
|
63
|
+
stdout.getvalue() if stdout is not None else None,
|
64
|
+
stderr.getvalue() if stderr is not None else None,
|
65
|
+
)
|
66
|
+
|
67
|
+
return SubprocessRunOutput(
|
68
|
+
proc=proc,
|
69
|
+
|
70
|
+
returncode=check.isinstance(proc.returncode, int),
|
71
|
+
|
72
|
+
stdout=stdout.getvalue() if stdout is not None else None,
|
73
|
+
stderr=stderr.getvalue() if stderr is not None else None,
|
74
|
+
)
|
75
|
+
|
76
|
+
async def check_call(self, *cmd: str, stdout: ta.Any = sys.stderr, **kwargs: ta.Any) -> None:
|
77
|
+
with self.prepare_and_wrap(*cmd, stdout=stdout, check=True, **kwargs) as (cmd, kwargs): # noqa
|
78
|
+
await self.run(*cmd, **kwargs)
|
79
|
+
|
80
|
+
async def check_output(self, *cmd: str, **kwargs: ta.Any) -> bytes:
|
81
|
+
with self.prepare_and_wrap(*cmd, stdout=subprocess.PIPE, check=True, **kwargs) as (cmd, kwargs): # noqa
|
82
|
+
return check.not_none((await self.run(*cmd, **kwargs)).stdout)
|
83
|
+
|
84
|
+
|
85
|
+
anyio_subprocesses = AnyioSubprocesses()
|
omlish/collections/identity.py
CHANGED
@@ -122,6 +122,7 @@ class IdentityWeakSet(ta.MutableSet[T]):
|
|
122
122
|
self._dict: weakref.WeakValueDictionary[int, T] = weakref.WeakValueDictionary()
|
123
123
|
|
124
124
|
def add(self, value):
|
125
|
+
# FIXME: race with weakref callback?
|
125
126
|
self._dict[id(value)] = value
|
126
127
|
|
127
128
|
def discard(self, value):
|
@@ -131,7 +132,11 @@ class IdentityWeakSet(ta.MutableSet[T]):
|
|
131
132
|
pass
|
132
133
|
|
133
134
|
def __contains__(self, x):
|
134
|
-
|
135
|
+
try:
|
136
|
+
o = self._dict[id(x)]
|
137
|
+
except KeyError:
|
138
|
+
return False
|
139
|
+
return x is o
|
135
140
|
|
136
141
|
def __len__(self):
|
137
142
|
return len(self._dict)
|
omlish/dataclasses/impl/main.py
CHANGED
@@ -49,7 +49,11 @@ class MainProcessor:
|
|
49
49
|
raise TypeError('weakref_slot is True but slots is False')
|
50
50
|
if not self._info.params.slots:
|
51
51
|
return
|
52
|
-
self._cls = add_slots(
|
52
|
+
self._cls = add_slots(
|
53
|
+
self._cls,
|
54
|
+
is_frozen=self._info.params.frozen,
|
55
|
+
weakref_slot=self._info.params.weakref_slot,
|
56
|
+
)
|
53
57
|
|
54
58
|
PROCESSOR_TYPES: ta.ClassVar[ta.Sequence[type[Processor]]] = [
|
55
59
|
FieldsProcessor,
|
@@ -196,15 +196,6 @@ class Data(
|
|
196
196
|
# Typechecking barrier
|
197
197
|
super().__init_subclass__(**kwargs)
|
198
198
|
|
199
|
-
def __post_init__(self, *args, **kwargs) -> None:
|
200
|
-
try:
|
201
|
-
spi = super().__post_init__ # type: ignore # noqa
|
202
|
-
except AttributeError:
|
203
|
-
if args or kwargs:
|
204
|
-
raise TypeError(args, kwargs) from None
|
205
|
-
else:
|
206
|
-
spi(*args, **kwargs)
|
207
|
-
|
208
199
|
|
209
200
|
class Frozen(
|
210
201
|
Data,
|
omlish/dataclasses/impl/slots.py
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
from .base import ( # noqa
|
2
|
+
DEFAULT_ENCODING,
|
3
|
+
|
4
|
+
is_success_status,
|
5
|
+
|
6
|
+
HttpRequest,
|
7
|
+
|
8
|
+
BaseHttpResponse,
|
9
|
+
HttpResponse,
|
10
|
+
StreamHttpResponse,
|
11
|
+
|
12
|
+
close_response,
|
13
|
+
closing_response,
|
14
|
+
read_response,
|
15
|
+
|
16
|
+
HttpClientError,
|
17
|
+
HttpStatusError,
|
18
|
+
|
19
|
+
HttpClient,
|
20
|
+
)
|
21
|
+
|
22
|
+
from .default import ( # noqa
|
23
|
+
client,
|
24
|
+
|
25
|
+
request,
|
26
|
+
)
|
27
|
+
|
28
|
+
from .httpx import ( # noqa
|
29
|
+
HttpxHttpClient,
|
30
|
+
)
|
31
|
+
|
32
|
+
from .urllib import ( # noqa
|
33
|
+
UrllibHttpClient,
|
34
|
+
)
|
@@ -0,0 +1,205 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- stream
|
4
|
+
- chunk size - httpx interface is awful, punch through?
|
5
|
+
- httpx catch
|
6
|
+
- return non-200 HttpResponses
|
7
|
+
- async
|
8
|
+
"""
|
9
|
+
import abc
|
10
|
+
import contextlib
|
11
|
+
import typing as ta
|
12
|
+
|
13
|
+
from ... import cached
|
14
|
+
from ... import dataclasses as dc
|
15
|
+
from ... import lang
|
16
|
+
from ..headers import CanHttpHeaders
|
17
|
+
from ..headers import HttpHeaders
|
18
|
+
|
19
|
+
|
20
|
+
BaseHttpResponseT = ta.TypeVar('BaseHttpResponseT', bound='BaseHttpResponse')
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
|
25
|
+
|
26
|
+
DEFAULT_ENCODING = 'utf-8'
|
27
|
+
|
28
|
+
|
29
|
+
def is_success_status(status: int) -> bool:
|
30
|
+
return 200 <= status < 300
|
31
|
+
|
32
|
+
|
33
|
+
##
|
34
|
+
|
35
|
+
|
36
|
+
@dc.dataclass(frozen=True)
|
37
|
+
class HttpRequest(lang.Final):
|
38
|
+
url: str
|
39
|
+
method: str | None = None # noqa
|
40
|
+
|
41
|
+
_: dc.KW_ONLY
|
42
|
+
|
43
|
+
headers: CanHttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
|
44
|
+
data: bytes | str | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
|
45
|
+
|
46
|
+
timeout_s: float | None = None
|
47
|
+
|
48
|
+
#
|
49
|
+
|
50
|
+
@property
|
51
|
+
def method_or_default(self) -> str:
|
52
|
+
if self.method is not None:
|
53
|
+
return self.method
|
54
|
+
if self.data is not None:
|
55
|
+
return 'POST'
|
56
|
+
return 'GET'
|
57
|
+
|
58
|
+
@cached.property
|
59
|
+
def headers_(self) -> HttpHeaders | None:
|
60
|
+
return HttpHeaders(self.headers) if self.headers is not None else None
|
61
|
+
|
62
|
+
|
63
|
+
#
|
64
|
+
|
65
|
+
|
66
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
67
|
+
class BaseHttpResponse(lang.Abstract):
|
68
|
+
status: int
|
69
|
+
|
70
|
+
headers: HttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
|
71
|
+
|
72
|
+
request: HttpRequest
|
73
|
+
underlying: ta.Any = dc.field(default=None, repr=False)
|
74
|
+
|
75
|
+
@property
|
76
|
+
def is_success(self) -> bool:
|
77
|
+
return is_success_status(self.status)
|
78
|
+
|
79
|
+
|
80
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
81
|
+
class HttpResponse(BaseHttpResponse, lang.Final):
|
82
|
+
data: bytes | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
|
83
|
+
|
84
|
+
|
85
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
86
|
+
class StreamHttpResponse(BaseHttpResponse, lang.Final):
|
87
|
+
class Stream(ta.Protocol):
|
88
|
+
def read(self, /, n: int = -1) -> bytes: ...
|
89
|
+
|
90
|
+
stream: Stream
|
91
|
+
|
92
|
+
_closer: ta.Callable[[], None] | None = dc.field(default=None, repr=False)
|
93
|
+
|
94
|
+
def __enter__(self) -> ta.Self:
|
95
|
+
return self
|
96
|
+
|
97
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
98
|
+
self.close()
|
99
|
+
|
100
|
+
def close(self) -> None:
|
101
|
+
if (c := self._closer) is not None:
|
102
|
+
c()
|
103
|
+
|
104
|
+
|
105
|
+
def close_response(resp: BaseHttpResponse) -> None:
|
106
|
+
if isinstance(resp, HttpResponse):
|
107
|
+
pass
|
108
|
+
|
109
|
+
elif isinstance(resp, StreamHttpResponse):
|
110
|
+
resp.close()
|
111
|
+
|
112
|
+
else:
|
113
|
+
raise TypeError(resp)
|
114
|
+
|
115
|
+
|
116
|
+
@contextlib.contextmanager
|
117
|
+
def closing_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
|
118
|
+
if isinstance(resp, HttpResponse):
|
119
|
+
yield resp # type: ignore
|
120
|
+
return
|
121
|
+
|
122
|
+
elif isinstance(resp, StreamHttpResponse):
|
123
|
+
with contextlib.closing(resp):
|
124
|
+
yield resp # type: ignore
|
125
|
+
|
126
|
+
else:
|
127
|
+
raise TypeError(resp)
|
128
|
+
|
129
|
+
|
130
|
+
def read_response(resp: BaseHttpResponse) -> HttpResponse:
|
131
|
+
if isinstance(resp, HttpResponse):
|
132
|
+
return resp
|
133
|
+
|
134
|
+
elif isinstance(resp, StreamHttpResponse):
|
135
|
+
data = resp.stream.read()
|
136
|
+
return HttpResponse(**{
|
137
|
+
**{k: v for k, v in dc.shallow_asdict(resp).items() if k not in ('stream', '_closer')},
|
138
|
+
'data': data,
|
139
|
+
})
|
140
|
+
|
141
|
+
else:
|
142
|
+
raise TypeError(resp)
|
143
|
+
|
144
|
+
|
145
|
+
#
|
146
|
+
|
147
|
+
|
148
|
+
class HttpClientError(Exception):
|
149
|
+
@property
|
150
|
+
def cause(self) -> BaseException | None:
|
151
|
+
return self.__cause__
|
152
|
+
|
153
|
+
|
154
|
+
@dc.dataclass(frozen=True)
|
155
|
+
class HttpStatusError(HttpClientError):
|
156
|
+
response: HttpResponse
|
157
|
+
|
158
|
+
|
159
|
+
#
|
160
|
+
|
161
|
+
|
162
|
+
class HttpClient(lang.Abstract):
|
163
|
+
def __enter__(self) -> ta.Self:
|
164
|
+
return self
|
165
|
+
|
166
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
167
|
+
pass
|
168
|
+
|
169
|
+
def request(
|
170
|
+
self,
|
171
|
+
req: HttpRequest,
|
172
|
+
*,
|
173
|
+
check: bool = False,
|
174
|
+
) -> HttpResponse:
|
175
|
+
with closing_response(self.stream_request(
|
176
|
+
req,
|
177
|
+
check=check,
|
178
|
+
)) as resp:
|
179
|
+
return read_response(resp)
|
180
|
+
|
181
|
+
def stream_request(
|
182
|
+
self,
|
183
|
+
req: HttpRequest,
|
184
|
+
*,
|
185
|
+
check: bool = False,
|
186
|
+
) -> StreamHttpResponse:
|
187
|
+
resp = self._stream_request(req)
|
188
|
+
|
189
|
+
try:
|
190
|
+
if check and not resp.is_success:
|
191
|
+
if isinstance(resp.underlying, Exception):
|
192
|
+
cause = resp.underlying
|
193
|
+
else:
|
194
|
+
cause = None
|
195
|
+
raise HttpStatusError(read_response(resp)) from cause # noqa
|
196
|
+
|
197
|
+
except Exception:
|
198
|
+
close_response(resp)
|
199
|
+
raise
|
200
|
+
|
201
|
+
return resp
|
202
|
+
|
203
|
+
@abc.abstractmethod
|
204
|
+
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
205
|
+
raise NotImplementedError
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
from ..headers import CanHttpHeaders
|
4
|
+
from .base import HttpClient
|
5
|
+
from .base import HttpRequest
|
6
|
+
from .base import HttpResponse
|
7
|
+
from .urllib import UrllibHttpClient
|
8
|
+
|
9
|
+
|
10
|
+
##
|
11
|
+
|
12
|
+
|
13
|
+
def _default_client() -> HttpClient:
|
14
|
+
return UrllibHttpClient()
|
15
|
+
|
16
|
+
|
17
|
+
def client() -> HttpClient:
|
18
|
+
return _default_client()
|
19
|
+
|
20
|
+
|
21
|
+
def request(
|
22
|
+
url: str,
|
23
|
+
method: str | None = None,
|
24
|
+
*,
|
25
|
+
headers: CanHttpHeaders | None = None,
|
26
|
+
data: bytes | str | None = None,
|
27
|
+
|
28
|
+
timeout_s: float | None = None,
|
29
|
+
|
30
|
+
check: bool = False,
|
31
|
+
|
32
|
+
client: HttpClient | None = None, # noqa
|
33
|
+
|
34
|
+
**kwargs: ta.Any,
|
35
|
+
) -> HttpResponse:
|
36
|
+
req = HttpRequest(
|
37
|
+
url,
|
38
|
+
method=method,
|
39
|
+
|
40
|
+
headers=headers,
|
41
|
+
data=data,
|
42
|
+
|
43
|
+
timeout_s=timeout_s,
|
44
|
+
|
45
|
+
**kwargs,
|
46
|
+
)
|
47
|
+
|
48
|
+
def do(cli: HttpClient) -> HttpResponse:
|
49
|
+
return cli.request(
|
50
|
+
req,
|
51
|
+
|
52
|
+
check=check,
|
53
|
+
)
|
54
|
+
|
55
|
+
if client is not None:
|
56
|
+
return do(client)
|
57
|
+
|
58
|
+
else:
|
59
|
+
with _default_client() as cli:
|
60
|
+
return do(cli)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import functools
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from ... import dataclasses as dc
|
5
|
+
from ... import lang
|
6
|
+
from ..headers import HttpHeaders
|
7
|
+
from .base import HttpClient
|
8
|
+
from .base import HttpClientError
|
9
|
+
from .base import HttpRequest
|
10
|
+
from .base import StreamHttpResponse
|
11
|
+
|
12
|
+
|
13
|
+
if ta.TYPE_CHECKING:
|
14
|
+
import httpx
|
15
|
+
else:
|
16
|
+
httpx = lang.proxy_import('httpx')
|
17
|
+
|
18
|
+
|
19
|
+
##
|
20
|
+
|
21
|
+
|
22
|
+
class HttpxHttpClient(HttpClient):
|
23
|
+
@dc.dataclass(frozen=True)
|
24
|
+
class _StreamAdapter:
|
25
|
+
it: ta.Iterator[bytes]
|
26
|
+
|
27
|
+
def read(self, /, n: int = -1) -> bytes:
|
28
|
+
if n < 0:
|
29
|
+
return b''.join(self.it)
|
30
|
+
else:
|
31
|
+
try:
|
32
|
+
return next(self.it)
|
33
|
+
except StopIteration:
|
34
|
+
return b''
|
35
|
+
|
36
|
+
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
37
|
+
try:
|
38
|
+
resp_cm = httpx.stream(
|
39
|
+
method=req.method_or_default,
|
40
|
+
url=req.url,
|
41
|
+
headers=req.headers_ or None, # type: ignore
|
42
|
+
content=req.data,
|
43
|
+
timeout=req.timeout_s,
|
44
|
+
)
|
45
|
+
|
46
|
+
except httpx.HTTPError as e:
|
47
|
+
raise HttpClientError from e
|
48
|
+
|
49
|
+
resp_close = functools.partial(resp_cm.__exit__, None, None, None)
|
50
|
+
|
51
|
+
try:
|
52
|
+
resp = resp_cm.__enter__()
|
53
|
+
return StreamHttpResponse(
|
54
|
+
status=resp.status_code,
|
55
|
+
headers=HttpHeaders(resp.headers.raw),
|
56
|
+
request=req,
|
57
|
+
underlying=resp,
|
58
|
+
stream=self._StreamAdapter(resp.iter_bytes()),
|
59
|
+
_closer=resp_close, # type: ignore
|
60
|
+
)
|
61
|
+
|
62
|
+
except httpx.HTTPError as e:
|
63
|
+
resp_close()
|
64
|
+
raise HttpClientError from e
|
65
|
+
|
66
|
+
except Exception:
|
67
|
+
resp_close()
|
68
|
+
raise
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import http.client
|
2
|
+
import typing as ta
|
3
|
+
import urllib.error
|
4
|
+
import urllib.request
|
5
|
+
|
6
|
+
from ..headers import HttpHeaders
|
7
|
+
from .base import DEFAULT_ENCODING
|
8
|
+
from .base import HttpClient
|
9
|
+
from .base import HttpClientError
|
10
|
+
from .base import HttpRequest
|
11
|
+
from .base import StreamHttpResponse
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
class UrllibHttpClient(HttpClient):
|
18
|
+
def _build_request(self, req: HttpRequest) -> urllib.request.Request:
|
19
|
+
d: ta.Any
|
20
|
+
if (d := req.data) is not None:
|
21
|
+
if isinstance(d, str):
|
22
|
+
d = d.encode(DEFAULT_ENCODING)
|
23
|
+
|
24
|
+
# urllib headers are dumb dicts [1], and keys *must* be strings or it will automatically add problematic default
|
25
|
+
# headers because it doesn't see string keys in its header dict [2]. frustratingly it has no problem accepting
|
26
|
+
# bytes values though [3].
|
27
|
+
# [1]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/urllib/request.py#L319-L325 # noqa
|
28
|
+
# [2]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/urllib/request.py#L1276-L1279 # noqa
|
29
|
+
# [3]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/http/client.py#L1300-L1301 # noqa
|
30
|
+
h: dict[str, str] = {}
|
31
|
+
if hs := req.headers_:
|
32
|
+
for k, v in hs.strict_dct.items():
|
33
|
+
h[k.decode('ascii')] = v.decode('ascii')
|
34
|
+
|
35
|
+
return urllib.request.Request( # noqa
|
36
|
+
req.url,
|
37
|
+
method=req.method_or_default,
|
38
|
+
headers=h,
|
39
|
+
data=d,
|
40
|
+
)
|
41
|
+
|
42
|
+
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
43
|
+
try:
|
44
|
+
resp = urllib.request.urlopen( # noqa
|
45
|
+
self._build_request(req),
|
46
|
+
timeout=req.timeout_s,
|
47
|
+
)
|
48
|
+
|
49
|
+
except urllib.error.HTTPError as e:
|
50
|
+
try:
|
51
|
+
return StreamHttpResponse(
|
52
|
+
status=e.code,
|
53
|
+
headers=HttpHeaders(e.headers.items()),
|
54
|
+
request=req,
|
55
|
+
underlying=e,
|
56
|
+
stream=e, # noqa
|
57
|
+
_closer=e.close,
|
58
|
+
)
|
59
|
+
|
60
|
+
except Exception:
|
61
|
+
e.close()
|
62
|
+
raise
|
63
|
+
|
64
|
+
except (urllib.error.URLError, http.client.HTTPException) as e:
|
65
|
+
raise HttpClientError from e
|
66
|
+
|
67
|
+
try:
|
68
|
+
return StreamHttpResponse(
|
69
|
+
status=resp.status,
|
70
|
+
headers=HttpHeaders(resp.headers.items()),
|
71
|
+
request=req,
|
72
|
+
underlying=resp,
|
73
|
+
stream=resp,
|
74
|
+
_closer=resp.close,
|
75
|
+
)
|
76
|
+
|
77
|
+
except Exception: # noqa
|
78
|
+
resp.close()
|
79
|
+
raise
|
omlish/lang/cached/function.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
omlish/.manifests.json,sha256=pjGUyLHaoWpPqRP3jz2u1fC1qoRc2lvrEcpU_Ax2tdg,8253
|
2
|
-
omlish/__about__.py,sha256=
|
2
|
+
omlish/__about__.py,sha256=tomhvMbjsT0jtVBAtPgoqWQWYTlkWH2K_sC_ndp9_lI,3380
|
3
3
|
omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
|
4
4
|
omlish/c3.py,sha256=rer-TPOFDU6fYq_AWio_AmA-ckZ8JDY5shIzQ_yXfzA,8414
|
5
5
|
omlish/cached.py,sha256=MLap_p0rdGoDIMVhXVHm1tsbcWobJF0OanoodV03Ju8,542
|
@@ -102,6 +102,7 @@ omlish/asyncs/anyio/backends.py,sha256=jJIymWoiedaEJJm82gvKiJ41EWLQZ-bcyNHpbDpKK
|
|
102
102
|
omlish/asyncs/anyio/futures.py,sha256=Nm1gLerZEnHk-rlsmr0UfK168IWIK6zA8EebZFtoY_E,2052
|
103
103
|
omlish/asyncs/anyio/signals.py,sha256=ySSut5prdnoy0-5Ws5V1M4cC2ON_vY550vU10d2NHk8,893
|
104
104
|
omlish/asyncs/anyio/streams.py,sha256=gNRAcHR0L8OtNioqKFbq0Z_apYAWKHFipZ2MUBp8Vg0,2228
|
105
|
+
omlish/asyncs/anyio/subprocesses.py,sha256=jjMjlcwtIiy-_y-spPn3eTC5dzrqFNSAMTPaIcXH9S8,3002
|
105
106
|
omlish/asyncs/anyio/sync.py,sha256=ZmSNhSsEkPwlXThrpefhtVTxw4GJ9F0P-yKyo5vbbSk,1574
|
106
107
|
omlish/asyncs/anyio/utils.py,sha256=X2Rz1DGrCJ0zkt1O5cHoMRaYKTPndBj6dzLhb09mVtE,1672
|
107
108
|
omlish/asyncs/asyncio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -149,7 +150,7 @@ omlish/collections/coerce.py,sha256=tAls15v_7p5bUN33R7Zbko87KW5toWHl9fRialCqyNY,
|
|
149
150
|
omlish/collections/exceptions.py,sha256=shcS-NCnEUudF8qC_SmO2TQyjivKlS4TDjaz_faqQ0c,44
|
150
151
|
omlish/collections/frozen.py,sha256=LMbAHYDENIQk1hvjCTvpnx66m1TalrHa4CSn8n_tsXQ,4142
|
151
152
|
omlish/collections/hasheq.py,sha256=swOBPEnU_C0SU3VqWJX9mr0BfZLD0A-4Ke9Vahj3fE4,3669
|
152
|
-
omlish/collections/identity.py,sha256=
|
153
|
+
omlish/collections/identity.py,sha256=xtoczgBPYzr6r2lJS-eti2kEnN8rVDvNGDCG3TA6vRo,3405
|
153
154
|
omlish/collections/mappings.py,sha256=u0UrB550nBM_RNXQV0YnBTbRZEWrplZe9ZxcCN3H65M,2789
|
154
155
|
omlish/collections/ordered.py,sha256=7zTbrAt12rf6i33XHkQERKar258fJacaw_WbtGEBgWo,2338
|
155
156
|
omlish/collections/ranked.py,sha256=rg6DL36oOUiG5JQEAkGnT8b6f9mSndQlIovtt8GQj_w,2229
|
@@ -216,8 +217,8 @@ omlish/dataclasses/impl/frozen.py,sha256=x87DSM8FIMZ3c_BIUE8NooCkExFjPsabeqIueEP
|
|
216
217
|
omlish/dataclasses/impl/hashing.py,sha256=0Gr6XIRkKy4pr-mdHblIlQCy3mBxycjMqJk3oZDw43s,3215
|
217
218
|
omlish/dataclasses/impl/init.py,sha256=CUM8Gnx171D3NO6FN4mlrIBoTcYiDqj_tqE9_NKKicg,6409
|
218
219
|
omlish/dataclasses/impl/internals.py,sha256=UvZYjrLT1S8ntyxJ_vRPIkPOF00K8HatGAygErgoXTU,2990
|
219
|
-
omlish/dataclasses/impl/main.py,sha256=
|
220
|
-
omlish/dataclasses/impl/metaclass.py,sha256=
|
220
|
+
omlish/dataclasses/impl/main.py,sha256=bWnqEDOfITjEwkLokTvOegp88KaQXJFun3krgxt3aE0,2647
|
221
|
+
omlish/dataclasses/impl/metaclass.py,sha256=rhcMHNJYISgMkC95Yq14aLEs48iK9Rzma5yb7-4mPIk,4965
|
221
222
|
omlish/dataclasses/impl/metadata.py,sha256=4veWwTr-aA0KP-Y1cPEeOcXHup9EKJTYNJ0ozIxtzD4,1401
|
222
223
|
omlish/dataclasses/impl/order.py,sha256=zWvWDkSTym8cc7vO1cLHqcBhhjOlucHOCUVJcdh4jt0,1369
|
223
224
|
omlish/dataclasses/impl/overrides.py,sha256=g9aCzaDDKyek8-yXRvtAcu1B1nCphWDYr4InHDlgbKk,1732
|
@@ -227,7 +228,7 @@ omlish/dataclasses/impl/reflect.py,sha256=ndX22d9bd9m_GAajPFQdnrw98AGw75lH9_tb-k
|
|
227
228
|
omlish/dataclasses/impl/replace.py,sha256=wS9GHX4fIwaPv1JBJzIewdBfXyK3X3V7_t55Da87dYo,1217
|
228
229
|
omlish/dataclasses/impl/repr.py,sha256=hk6HwKTLA0tb698CfiO8RYAqtpHQbGCpymm_Eo-B-2Y,1876
|
229
230
|
omlish/dataclasses/impl/simple.py,sha256=Q272TYXifB5iKtydByxyzraeQHX6aXDY0VKO1-AKBF4,1771
|
230
|
-
omlish/dataclasses/impl/slots.py,sha256=
|
231
|
+
omlish/dataclasses/impl/slots.py,sha256=uiUB391b2WG9Xak8ckXeGawhpc38dZEhEp60uQgiY-w,5275
|
231
232
|
omlish/dataclasses/impl/utils.py,sha256=aER2iL3UAtgS1BdLuEvTr9Tr2wC28wk1kiOeO-jIymw,6138
|
232
233
|
omlish/diag/__init__.py,sha256=4S8v0myJM4Zld6_FV6cPe_nSv0aJb6kXftEit0HkiGE,1141
|
233
234
|
omlish/diag/asts.py,sha256=MWh9XAG3m9L10FIJCyoNT2aU4Eft6tun_x9K0riq6Dk,3332
|
@@ -331,7 +332,6 @@ omlish/graphs/dot/utils.py,sha256=_FMwn77WfiiAfLsRTOKWm4IYbNv5kQN22YJ5psw6CWg,80
|
|
331
332
|
omlish/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
332
333
|
omlish/http/all.py,sha256=4dSBbitsrQIadivSo2rBLg8dgdmC0efpboylGUZgKKo,829
|
333
334
|
omlish/http/asgi.py,sha256=wXhBZ21bEl32Kv9yBrRwUR_7pHEgVtHP8ZZwbasQ6-4,3307
|
334
|
-
omlish/http/clients.py,sha256=gbR4Fl2C1fYpcfDEg_W9De3ImRARKFkRHoQkkG1AwRs,9558
|
335
335
|
omlish/http/consts.py,sha256=7BJ4D1MdIvqBcepkgCfBFHolgTwbOlqsOEiee_IjxOA,2289
|
336
336
|
omlish/http/cookies.py,sha256=uuOYlHR6e2SC3GM41V0aozK10nef9tYg83Scqpn5-HM,6351
|
337
337
|
omlish/http/dates.py,sha256=Otgp8wRxPgNGyzx8LFowu1vC4EKJYARCiAwLFncpfHM,2875
|
@@ -346,6 +346,11 @@ omlish/http/sessions.py,sha256=TfTJ_j-6c9PelG_RmijEwozfaVm3O7YzgtFvp8VzQqM,4799
|
|
346
346
|
omlish/http/sse.py,sha256=NwJnQj-hFXAkadXKhUuHSnbXHwDVJjhzfdkkHQ-prQo,2320
|
347
347
|
omlish/http/versions.py,sha256=wSiOXPiClVjkVgSU_VmxkoD1SUYGaoPbP0U5Aw-Ufg8,409
|
348
348
|
omlish/http/wsgi.py,sha256=czZsVUX-l2YTlMrUjKN49wRoP4rVpS0qpeBn4O5BoMY,948
|
349
|
+
omlish/http/clients/__init__.py,sha256=SeH3ofjQvk7VuV9OE1uJir9QMZwvEuDl7fptkKgGQUU,449
|
350
|
+
omlish/http/clients/base.py,sha256=8dQGHJyRxH9GFefdoG6HR-VAMsr1rCOti_M27NLAdJU,4658
|
351
|
+
omlish/http/clients/default.py,sha256=fO8So3pI2z8ateLqt9Sv50UoOJFkUZbgMdoDyWsjBNw,1070
|
352
|
+
omlish/http/clients/httpx.py,sha256=Grl9_sG7QNIZj8HqaU7XduAsBDyfXl36RjbYQzQqal0,1787
|
353
|
+
omlish/http/clients/urllib.py,sha256=DVKFPLQzANiZmwnlbFw5pWs5ZPLLvvgCb3UInvCSqPE,2701
|
349
354
|
omlish/http/coro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
350
355
|
omlish/http/coro/fdio.py,sha256=bd9K4EYVWbXV3e3npDPXI9DuDAruJiyDmrgFpgNcjzY,4035
|
351
356
|
omlish/http/coro/server.py,sha256=30FTcJG8kuFeThf0HJYpTzMZN-giLTBP7wr5Wl3b9X0,18285
|
@@ -438,7 +443,7 @@ omlish/lang/strings.py,sha256=egdv8PxLNG40-5V93agP5j2rBUDIsahCx048zV7uEbU,4690
|
|
438
443
|
omlish/lang/sys.py,sha256=b4qOPiJZQru_mbb04FNfOjYWUxlV2becZOoc-yya_rQ,411
|
439
444
|
omlish/lang/typing.py,sha256=Zdad9Zv0sa-hIaUXPrzPidT7sDVpRcussAI7D-j-I1c,3296
|
440
445
|
omlish/lang/cached/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
441
|
-
omlish/lang/cached/function.py,sha256=
|
446
|
+
omlish/lang/cached/function.py,sha256=CqxYhpl1JK7eBkha_xPQ17c946W1PgnfMNpRaIPgRGc,9229
|
442
447
|
omlish/lang/cached/property.py,sha256=kzbao_35PlszdK_9oJBWrMmFFlVK_Xhx7YczHhTJ6cc,2764
|
443
448
|
omlish/lang/classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
444
449
|
omlish/lang/classes/abstract.py,sha256=n4rDlDraUKxPF0GtOWEFZ6mEzEDmP7Z8LSI6Jww_thw,3715
|
@@ -784,9 +789,9 @@ omlish/typedvalues/holder.py,sha256=4SwRezsmuDDEO5gENGx8kTm30pblF5UktoEAu02i-Gk,
|
|
784
789
|
omlish/typedvalues/marshal.py,sha256=eWMrmuzPk3pX5AlILc5YBvuJBUHRQA_vwkxRm5aHiGs,4209
|
785
790
|
omlish/typedvalues/reflect.py,sha256=y_7IY8_4cLVRvD3ug-_-cDaO5RtzC1rLVFzkeAPALf8,683
|
786
791
|
omlish/typedvalues/values.py,sha256=Acyf6xSdNHxrkRXLXrFqJouk35YOveso1VqTbyPwQW4,1223
|
787
|
-
omlish-0.0.0.
|
788
|
-
omlish-0.0.0.
|
789
|
-
omlish-0.0.0.
|
790
|
-
omlish-0.0.0.
|
791
|
-
omlish-0.0.0.
|
792
|
-
omlish-0.0.0.
|
792
|
+
omlish-0.0.0.dev280.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
793
|
+
omlish-0.0.0.dev280.dist-info/METADATA,sha256=cB-W0A9GSdEwu5Hr5ccj4cqL_r-KCNy3JQdUC85fo3A,4198
|
794
|
+
omlish-0.0.0.dev280.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
795
|
+
omlish-0.0.0.dev280.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
|
796
|
+
omlish-0.0.0.dev280.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
|
797
|
+
omlish-0.0.0.dev280.dist-info/RECORD,,
|
omlish/http/clients.py
DELETED
@@ -1,388 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
TODO:
|
3
|
-
- stream
|
4
|
-
- chunk size - httpx interface is awful, punch through?
|
5
|
-
- httpx catch
|
6
|
-
- return non-200 HttpResponses
|
7
|
-
- async
|
8
|
-
"""
|
9
|
-
import abc
|
10
|
-
import contextlib
|
11
|
-
import functools
|
12
|
-
import http.client
|
13
|
-
import typing as ta
|
14
|
-
import urllib.error
|
15
|
-
import urllib.request
|
16
|
-
|
17
|
-
from .. import cached
|
18
|
-
from .. import dataclasses as dc
|
19
|
-
from .. import lang
|
20
|
-
from .headers import CanHttpHeaders
|
21
|
-
from .headers import HttpHeaders
|
22
|
-
|
23
|
-
|
24
|
-
if ta.TYPE_CHECKING:
|
25
|
-
import httpx
|
26
|
-
else:
|
27
|
-
httpx = lang.proxy_import('httpx')
|
28
|
-
|
29
|
-
|
30
|
-
BaseHttpResponseT = ta.TypeVar('BaseHttpResponseT', bound='BaseHttpResponse')
|
31
|
-
|
32
|
-
|
33
|
-
##
|
34
|
-
|
35
|
-
|
36
|
-
DEFAULT_ENCODING = 'utf-8'
|
37
|
-
|
38
|
-
|
39
|
-
def is_success_status(status: int) -> bool:
|
40
|
-
return 200 <= status < 300
|
41
|
-
|
42
|
-
|
43
|
-
##
|
44
|
-
|
45
|
-
|
46
|
-
@dc.dataclass(frozen=True)
|
47
|
-
class HttpRequest(lang.Final):
|
48
|
-
url: str
|
49
|
-
method: str | None = None # noqa
|
50
|
-
|
51
|
-
_: dc.KW_ONLY
|
52
|
-
|
53
|
-
headers: CanHttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
|
54
|
-
data: bytes | str | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
|
55
|
-
|
56
|
-
timeout_s: float | None = None
|
57
|
-
|
58
|
-
#
|
59
|
-
|
60
|
-
@property
|
61
|
-
def method_or_default(self) -> str:
|
62
|
-
if self.method is not None:
|
63
|
-
return self.method
|
64
|
-
if self.data is not None:
|
65
|
-
return 'POST'
|
66
|
-
return 'GET'
|
67
|
-
|
68
|
-
@cached.property
|
69
|
-
def headers_(self) -> HttpHeaders | None:
|
70
|
-
return HttpHeaders(self.headers) if self.headers is not None else None
|
71
|
-
|
72
|
-
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
@dc.dataclass(frozen=True, kw_only=True)
|
77
|
-
class BaseHttpResponse(lang.Abstract):
|
78
|
-
status: int
|
79
|
-
|
80
|
-
headers: HttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
|
81
|
-
|
82
|
-
request: HttpRequest
|
83
|
-
underlying: ta.Any = dc.field(default=None, repr=False)
|
84
|
-
|
85
|
-
@property
|
86
|
-
def is_success(self) -> bool:
|
87
|
-
return is_success_status(self.status)
|
88
|
-
|
89
|
-
|
90
|
-
@dc.dataclass(frozen=True, kw_only=True)
|
91
|
-
class HttpResponse(BaseHttpResponse, lang.Final):
|
92
|
-
data: bytes | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
|
93
|
-
|
94
|
-
|
95
|
-
@dc.dataclass(frozen=True, kw_only=True)
|
96
|
-
class StreamHttpResponse(BaseHttpResponse, lang.Final):
|
97
|
-
class Stream(ta.Protocol):
|
98
|
-
def read(self, /, n: int = -1) -> bytes: ...
|
99
|
-
|
100
|
-
stream: Stream
|
101
|
-
|
102
|
-
_closer: ta.Callable[[], None] | None = dc.field(default=None, repr=False)
|
103
|
-
|
104
|
-
def __enter__(self) -> ta.Self:
|
105
|
-
return self
|
106
|
-
|
107
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
108
|
-
self.close()
|
109
|
-
|
110
|
-
def close(self) -> None:
|
111
|
-
if (c := self._closer) is not None:
|
112
|
-
c()
|
113
|
-
|
114
|
-
|
115
|
-
def close_response(resp: BaseHttpResponse) -> None:
|
116
|
-
if isinstance(resp, HttpResponse):
|
117
|
-
pass
|
118
|
-
|
119
|
-
elif isinstance(resp, StreamHttpResponse):
|
120
|
-
resp.close()
|
121
|
-
|
122
|
-
else:
|
123
|
-
raise TypeError(resp)
|
124
|
-
|
125
|
-
|
126
|
-
@contextlib.contextmanager
|
127
|
-
def closing_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
|
128
|
-
if isinstance(resp, HttpResponse):
|
129
|
-
yield resp # type: ignore
|
130
|
-
return
|
131
|
-
|
132
|
-
elif isinstance(resp, StreamHttpResponse):
|
133
|
-
with contextlib.closing(resp):
|
134
|
-
yield resp # type: ignore
|
135
|
-
|
136
|
-
else:
|
137
|
-
raise TypeError(resp)
|
138
|
-
|
139
|
-
|
140
|
-
def read_response(resp: BaseHttpResponse) -> HttpResponse:
|
141
|
-
if isinstance(resp, HttpResponse):
|
142
|
-
return resp
|
143
|
-
|
144
|
-
elif isinstance(resp, StreamHttpResponse):
|
145
|
-
data = resp.stream.read()
|
146
|
-
return HttpResponse(**{
|
147
|
-
**{k: v for k, v in dc.shallow_asdict(resp).items() if k not in ('stream', '_closer')},
|
148
|
-
'data': data,
|
149
|
-
})
|
150
|
-
|
151
|
-
else:
|
152
|
-
raise TypeError(resp)
|
153
|
-
|
154
|
-
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
class HttpClientError(Exception):
|
159
|
-
@property
|
160
|
-
def cause(self) -> BaseException | None:
|
161
|
-
return self.__cause__
|
162
|
-
|
163
|
-
|
164
|
-
@dc.dataclass(frozen=True)
|
165
|
-
class HttpStatusError(HttpClientError):
|
166
|
-
response: HttpResponse
|
167
|
-
|
168
|
-
|
169
|
-
#
|
170
|
-
|
171
|
-
|
172
|
-
class HttpClient(lang.Abstract):
|
173
|
-
def __enter__(self) -> ta.Self:
|
174
|
-
return self
|
175
|
-
|
176
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
177
|
-
pass
|
178
|
-
|
179
|
-
def request(
|
180
|
-
self,
|
181
|
-
req: HttpRequest,
|
182
|
-
*,
|
183
|
-
check: bool = False,
|
184
|
-
) -> HttpResponse:
|
185
|
-
with closing_response(self.stream_request(
|
186
|
-
req,
|
187
|
-
check=check,
|
188
|
-
)) as resp:
|
189
|
-
return read_response(resp)
|
190
|
-
|
191
|
-
def stream_request(
|
192
|
-
self,
|
193
|
-
req: HttpRequest,
|
194
|
-
*,
|
195
|
-
check: bool = False,
|
196
|
-
) -> StreamHttpResponse:
|
197
|
-
resp = self._stream_request(req)
|
198
|
-
|
199
|
-
try:
|
200
|
-
if check and not resp.is_success:
|
201
|
-
if isinstance(resp.underlying, Exception):
|
202
|
-
cause = resp.underlying
|
203
|
-
else:
|
204
|
-
cause = None
|
205
|
-
raise HttpStatusError(read_response(resp)) from cause # noqa
|
206
|
-
|
207
|
-
except Exception:
|
208
|
-
close_response(resp)
|
209
|
-
raise
|
210
|
-
|
211
|
-
return resp
|
212
|
-
|
213
|
-
@abc.abstractmethod
|
214
|
-
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
215
|
-
raise NotImplementedError
|
216
|
-
|
217
|
-
|
218
|
-
##
|
219
|
-
|
220
|
-
|
221
|
-
class UrllibHttpClient(HttpClient):
|
222
|
-
def _build_request(self, req: HttpRequest) -> urllib.request.Request:
|
223
|
-
d: ta.Any
|
224
|
-
if (d := req.data) is not None:
|
225
|
-
if isinstance(d, str):
|
226
|
-
d = d.encode(DEFAULT_ENCODING)
|
227
|
-
|
228
|
-
# urllib headers are dumb dicts [1], and keys *must* be strings or it will automatically add problematic default
|
229
|
-
# headers because it doesn't see string keys in its header dict [2]. frustratingly it has no problem accepting
|
230
|
-
# bytes values though [3].
|
231
|
-
# [1]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/urllib/request.py#L319-L325 # noqa
|
232
|
-
# [2]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/urllib/request.py#L1276-L1279 # noqa
|
233
|
-
# [3]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/http/client.py#L1300-L1301 # noqa
|
234
|
-
h: dict[str, str] = {}
|
235
|
-
if hs := req.headers_:
|
236
|
-
for k, v in hs.strict_dct.items():
|
237
|
-
h[k.decode('ascii')] = v.decode('ascii')
|
238
|
-
|
239
|
-
return urllib.request.Request( # noqa
|
240
|
-
req.url,
|
241
|
-
method=req.method_or_default,
|
242
|
-
headers=h,
|
243
|
-
data=d,
|
244
|
-
)
|
245
|
-
|
246
|
-
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
247
|
-
try:
|
248
|
-
resp = urllib.request.urlopen( # noqa
|
249
|
-
self._build_request(req),
|
250
|
-
timeout=req.timeout_s,
|
251
|
-
)
|
252
|
-
|
253
|
-
except urllib.error.HTTPError as e:
|
254
|
-
try:
|
255
|
-
return StreamHttpResponse(
|
256
|
-
status=e.code,
|
257
|
-
headers=HttpHeaders(e.headers.items()),
|
258
|
-
request=req,
|
259
|
-
underlying=e,
|
260
|
-
stream=e,
|
261
|
-
_closer=e.close,
|
262
|
-
)
|
263
|
-
|
264
|
-
except Exception:
|
265
|
-
e.close()
|
266
|
-
raise
|
267
|
-
|
268
|
-
except (urllib.error.URLError, http.client.HTTPException) as e:
|
269
|
-
raise HttpClientError from e
|
270
|
-
|
271
|
-
try:
|
272
|
-
return StreamHttpResponse(
|
273
|
-
status=resp.status,
|
274
|
-
headers=HttpHeaders(resp.headers.items()),
|
275
|
-
request=req,
|
276
|
-
underlying=resp,
|
277
|
-
stream=resp,
|
278
|
-
_closer=resp.close,
|
279
|
-
)
|
280
|
-
|
281
|
-
except Exception: # noqa
|
282
|
-
resp.close()
|
283
|
-
raise
|
284
|
-
|
285
|
-
|
286
|
-
##
|
287
|
-
|
288
|
-
|
289
|
-
class HttpxHttpClient(HttpClient):
|
290
|
-
@dc.dataclass(frozen=True)
|
291
|
-
class _StreamAdapter:
|
292
|
-
it: ta.Iterator[bytes]
|
293
|
-
|
294
|
-
def read(self, /, n: int = -1) -> bytes:
|
295
|
-
if n < 0:
|
296
|
-
return b''.join(self.it)
|
297
|
-
else:
|
298
|
-
try:
|
299
|
-
return next(self.it)
|
300
|
-
except StopIteration:
|
301
|
-
return b''
|
302
|
-
|
303
|
-
def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
|
304
|
-
try:
|
305
|
-
resp_cm = httpx.stream(
|
306
|
-
method=req.method_or_default,
|
307
|
-
url=req.url,
|
308
|
-
headers=req.headers_ or None, # type: ignore
|
309
|
-
content=req.data,
|
310
|
-
timeout=req.timeout_s,
|
311
|
-
)
|
312
|
-
|
313
|
-
except httpx.HTTPError as e:
|
314
|
-
raise HttpClientError from e
|
315
|
-
|
316
|
-
resp_close = functools.partial(resp_cm.__exit__, None, None, None)
|
317
|
-
|
318
|
-
try:
|
319
|
-
resp = resp_cm.__enter__()
|
320
|
-
return StreamHttpResponse(
|
321
|
-
status=resp.status_code,
|
322
|
-
headers=HttpHeaders(resp.headers.raw),
|
323
|
-
request=req,
|
324
|
-
underlying=resp,
|
325
|
-
stream=self._StreamAdapter(resp.iter_bytes()),
|
326
|
-
_closer=resp_close, # type: ignore
|
327
|
-
)
|
328
|
-
|
329
|
-
except httpx.HTTPError as e:
|
330
|
-
resp_close()
|
331
|
-
raise HttpClientError from e
|
332
|
-
|
333
|
-
except Exception:
|
334
|
-
resp_close()
|
335
|
-
raise
|
336
|
-
|
337
|
-
|
338
|
-
##
|
339
|
-
|
340
|
-
|
341
|
-
def _default_client() -> HttpClient:
|
342
|
-
return UrllibHttpClient()
|
343
|
-
|
344
|
-
|
345
|
-
def client() -> HttpClient:
|
346
|
-
return _default_client()
|
347
|
-
|
348
|
-
|
349
|
-
def request(
|
350
|
-
url: str,
|
351
|
-
method: str | None = None,
|
352
|
-
*,
|
353
|
-
headers: CanHttpHeaders | None = None,
|
354
|
-
data: bytes | str | None = None,
|
355
|
-
|
356
|
-
timeout_s: float | None = None,
|
357
|
-
|
358
|
-
check: bool = False,
|
359
|
-
|
360
|
-
client: HttpClient | None = None, # noqa
|
361
|
-
|
362
|
-
**kwargs: ta.Any,
|
363
|
-
) -> HttpResponse:
|
364
|
-
req = HttpRequest(
|
365
|
-
url,
|
366
|
-
method=method,
|
367
|
-
|
368
|
-
headers=headers,
|
369
|
-
data=data,
|
370
|
-
|
371
|
-
timeout_s=timeout_s,
|
372
|
-
|
373
|
-
**kwargs,
|
374
|
-
)
|
375
|
-
|
376
|
-
def do(cli: HttpClient) -> HttpResponse:
|
377
|
-
return cli.request(
|
378
|
-
req,
|
379
|
-
|
380
|
-
check=check,
|
381
|
-
)
|
382
|
-
|
383
|
-
if client is not None:
|
384
|
-
return do(client)
|
385
|
-
|
386
|
-
else:
|
387
|
-
with _default_client() as cli:
|
388
|
-
return do(cli)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|