omlish 0.0.0.dev277__py3-none-any.whl → 0.0.0.dev279__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/dataclasses/impl/main.py +5 -1
- 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/typedvalues/collection.py +0 -7
- omlish/typedvalues/reflect.py +1 -1
- {omlish-0.0.0.dev277.dist-info → omlish-0.0.0.dev279.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev277.dist-info → omlish-0.0.0.dev279.dist-info}/RECORD +17 -13
- omlish/http/clients.py +0 -388
- {omlish-0.0.0.dev277.dist-info → omlish-0.0.0.dev279.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev277.dist-info → omlish-0.0.0.dev279.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev277.dist-info → omlish-0.0.0.dev279.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev277.dist-info → omlish-0.0.0.dev279.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
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,
|
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
omlish/typedvalues/collection.py
CHANGED
@@ -129,13 +129,6 @@ class TypedValues(
|
|
129
129
|
|
130
130
|
#
|
131
131
|
|
132
|
-
def check_all_isinstance(self, ty: type | tuple[type, ...]) -> ta.Self:
|
133
|
-
for tv in self._tup:
|
134
|
-
check.isinstance(tv, ty)
|
135
|
-
return self
|
136
|
-
|
137
|
-
#
|
138
|
-
|
139
132
|
_hash: int
|
140
133
|
|
141
134
|
def __hash__(self) -> int:
|
omlish/typedvalues/reflect.py
CHANGED
@@ -23,7 +23,7 @@ def reflect_typed_values_impls(rty: rfl.Type) -> set[type[TypedValue]]:
|
|
23
23
|
if isinstance(cur, rfl.Union):
|
24
24
|
todo.extend(cur.args)
|
25
25
|
elif isinstance(cur, ta.TypeVar):
|
26
|
-
todo.append(rfl.get_type_var_bound(cur))
|
26
|
+
todo.append(rfl.type_(rfl.get_type_var_bound(cur)))
|
27
27
|
else:
|
28
28
|
tv_cls_set.add(check.issubclass(check.isinstance(cur, type), TypedValue))
|
29
29
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
omlish/.manifests.json,sha256=pjGUyLHaoWpPqRP3jz2u1fC1qoRc2lvrEcpU_Ax2tdg,8253
|
2
|
-
omlish/__about__.py,sha256
|
2
|
+
omlish/__about__.py,sha256=-gs989IcqffQxsEg0eeKFRHhbx_CtVvUkKesXecfclk,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
|
@@ -216,7 +216,7 @@ omlish/dataclasses/impl/frozen.py,sha256=x87DSM8FIMZ3c_BIUE8NooCkExFjPsabeqIueEP
|
|
216
216
|
omlish/dataclasses/impl/hashing.py,sha256=0Gr6XIRkKy4pr-mdHblIlQCy3mBxycjMqJk3oZDw43s,3215
|
217
217
|
omlish/dataclasses/impl/init.py,sha256=CUM8Gnx171D3NO6FN4mlrIBoTcYiDqj_tqE9_NKKicg,6409
|
218
218
|
omlish/dataclasses/impl/internals.py,sha256=UvZYjrLT1S8ntyxJ_vRPIkPOF00K8HatGAygErgoXTU,2990
|
219
|
-
omlish/dataclasses/impl/main.py,sha256=
|
219
|
+
omlish/dataclasses/impl/main.py,sha256=bWnqEDOfITjEwkLokTvOegp88KaQXJFun3krgxt3aE0,2647
|
220
220
|
omlish/dataclasses/impl/metaclass.py,sha256=ebxJr3j8d_wz-fc7mvhWtJ6wJgnxVV6rVOQdqZ5avKw,5262
|
221
221
|
omlish/dataclasses/impl/metadata.py,sha256=4veWwTr-aA0KP-Y1cPEeOcXHup9EKJTYNJ0ozIxtzD4,1401
|
222
222
|
omlish/dataclasses/impl/order.py,sha256=zWvWDkSTym8cc7vO1cLHqcBhhjOlucHOCUVJcdh4jt0,1369
|
@@ -227,7 +227,7 @@ omlish/dataclasses/impl/reflect.py,sha256=ndX22d9bd9m_GAajPFQdnrw98AGw75lH9_tb-k
|
|
227
227
|
omlish/dataclasses/impl/replace.py,sha256=wS9GHX4fIwaPv1JBJzIewdBfXyK3X3V7_t55Da87dYo,1217
|
228
228
|
omlish/dataclasses/impl/repr.py,sha256=hk6HwKTLA0tb698CfiO8RYAqtpHQbGCpymm_Eo-B-2Y,1876
|
229
229
|
omlish/dataclasses/impl/simple.py,sha256=Q272TYXifB5iKtydByxyzraeQHX6aXDY0VKO1-AKBF4,1771
|
230
|
-
omlish/dataclasses/impl/slots.py,sha256=
|
230
|
+
omlish/dataclasses/impl/slots.py,sha256=uiUB391b2WG9Xak8ckXeGawhpc38dZEhEp60uQgiY-w,5275
|
231
231
|
omlish/dataclasses/impl/utils.py,sha256=aER2iL3UAtgS1BdLuEvTr9Tr2wC28wk1kiOeO-jIymw,6138
|
232
232
|
omlish/diag/__init__.py,sha256=4S8v0myJM4Zld6_FV6cPe_nSv0aJb6kXftEit0HkiGE,1141
|
233
233
|
omlish/diag/asts.py,sha256=MWh9XAG3m9L10FIJCyoNT2aU4Eft6tun_x9K0riq6Dk,3332
|
@@ -331,7 +331,6 @@ omlish/graphs/dot/utils.py,sha256=_FMwn77WfiiAfLsRTOKWm4IYbNv5kQN22YJ5psw6CWg,80
|
|
331
331
|
omlish/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
332
332
|
omlish/http/all.py,sha256=4dSBbitsrQIadivSo2rBLg8dgdmC0efpboylGUZgKKo,829
|
333
333
|
omlish/http/asgi.py,sha256=wXhBZ21bEl32Kv9yBrRwUR_7pHEgVtHP8ZZwbasQ6-4,3307
|
334
|
-
omlish/http/clients.py,sha256=gbR4Fl2C1fYpcfDEg_W9De3ImRARKFkRHoQkkG1AwRs,9558
|
335
334
|
omlish/http/consts.py,sha256=7BJ4D1MdIvqBcepkgCfBFHolgTwbOlqsOEiee_IjxOA,2289
|
336
335
|
omlish/http/cookies.py,sha256=uuOYlHR6e2SC3GM41V0aozK10nef9tYg83Scqpn5-HM,6351
|
337
336
|
omlish/http/dates.py,sha256=Otgp8wRxPgNGyzx8LFowu1vC4EKJYARCiAwLFncpfHM,2875
|
@@ -346,6 +345,11 @@ omlish/http/sessions.py,sha256=TfTJ_j-6c9PelG_RmijEwozfaVm3O7YzgtFvp8VzQqM,4799
|
|
346
345
|
omlish/http/sse.py,sha256=NwJnQj-hFXAkadXKhUuHSnbXHwDVJjhzfdkkHQ-prQo,2320
|
347
346
|
omlish/http/versions.py,sha256=wSiOXPiClVjkVgSU_VmxkoD1SUYGaoPbP0U5Aw-Ufg8,409
|
348
347
|
omlish/http/wsgi.py,sha256=czZsVUX-l2YTlMrUjKN49wRoP4rVpS0qpeBn4O5BoMY,948
|
348
|
+
omlish/http/clients/__init__.py,sha256=SeH3ofjQvk7VuV9OE1uJir9QMZwvEuDl7fptkKgGQUU,449
|
349
|
+
omlish/http/clients/base.py,sha256=8dQGHJyRxH9GFefdoG6HR-VAMsr1rCOti_M27NLAdJU,4658
|
350
|
+
omlish/http/clients/default.py,sha256=fO8So3pI2z8ateLqt9Sv50UoOJFkUZbgMdoDyWsjBNw,1070
|
351
|
+
omlish/http/clients/httpx.py,sha256=Grl9_sG7QNIZj8HqaU7XduAsBDyfXl36RjbYQzQqal0,1787
|
352
|
+
omlish/http/clients/urllib.py,sha256=DVKFPLQzANiZmwnlbFw5pWs5ZPLLvvgCb3UInvCSqPE,2701
|
349
353
|
omlish/http/coro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
350
354
|
omlish/http/coro/fdio.py,sha256=bd9K4EYVWbXV3e3npDPXI9DuDAruJiyDmrgFpgNcjzY,4035
|
351
355
|
omlish/http/coro/server.py,sha256=30FTcJG8kuFeThf0HJYpTzMZN-giLTBP7wr5Wl3b9X0,18285
|
@@ -438,7 +442,7 @@ omlish/lang/strings.py,sha256=egdv8PxLNG40-5V93agP5j2rBUDIsahCx048zV7uEbU,4690
|
|
438
442
|
omlish/lang/sys.py,sha256=b4qOPiJZQru_mbb04FNfOjYWUxlV2becZOoc-yya_rQ,411
|
439
443
|
omlish/lang/typing.py,sha256=Zdad9Zv0sa-hIaUXPrzPidT7sDVpRcussAI7D-j-I1c,3296
|
440
444
|
omlish/lang/cached/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
441
|
-
omlish/lang/cached/function.py,sha256=
|
445
|
+
omlish/lang/cached/function.py,sha256=CqxYhpl1JK7eBkha_xPQ17c946W1PgnfMNpRaIPgRGc,9229
|
442
446
|
omlish/lang/cached/property.py,sha256=kzbao_35PlszdK_9oJBWrMmFFlVK_Xhx7YczHhTJ6cc,2764
|
443
447
|
omlish/lang/classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
444
448
|
omlish/lang/classes/abstract.py,sha256=n4rDlDraUKxPF0GtOWEFZ6mEzEDmP7Z8LSI6Jww_thw,3715
|
@@ -778,15 +782,15 @@ omlish/text/go/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
778
782
|
omlish/text/go/quoting.py,sha256=N9EYdnFdEX_A8fOviH-1w4jwV3XOQ7VU2WsoUNubYVY,9137
|
779
783
|
omlish/typedvalues/__init__.py,sha256=c3IQmRneMmH6JRcafprqmBILWD89b-IyIll6MgahGCI,562
|
780
784
|
omlish/typedvalues/accessor.py,sha256=0k21N-CkjGaY6zCwugsRfOC_CDkqk7wNz4oxO1_6EEA,2919
|
781
|
-
omlish/typedvalues/collection.py,sha256=
|
785
|
+
omlish/typedvalues/collection.py,sha256=jsXSggmMMvGATcJgQkUXt5Guwq8aquw73_OIC-e6U0I,5300
|
782
786
|
omlish/typedvalues/generic.py,sha256=byWG_gMXhNelckUwdmOoJE9FKkL71Q4BSi4ZLyy0XZ0,788
|
783
787
|
omlish/typedvalues/holder.py,sha256=4SwRezsmuDDEO5gENGx8kTm30pblF5UktoEAu02i-Gk,1554
|
784
788
|
omlish/typedvalues/marshal.py,sha256=eWMrmuzPk3pX5AlILc5YBvuJBUHRQA_vwkxRm5aHiGs,4209
|
785
|
-
omlish/typedvalues/reflect.py,sha256=
|
789
|
+
omlish/typedvalues/reflect.py,sha256=y_7IY8_4cLVRvD3ug-_-cDaO5RtzC1rLVFzkeAPALf8,683
|
786
790
|
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.
|
791
|
+
omlish-0.0.0.dev279.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
792
|
+
omlish-0.0.0.dev279.dist-info/METADATA,sha256=tJQPgukBhxbtW45qs1JlUKCv7Dlr2McN2CfhnzKfjqM,4198
|
793
|
+
omlish-0.0.0.dev279.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
794
|
+
omlish-0.0.0.dev279.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
|
795
|
+
omlish-0.0.0.dev279.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
|
796
|
+
omlish-0.0.0.dev279.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
|