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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev277'
2
- __revision__ = 'b8087c9a0ca9d46a8ce3bfc58ba6bef8d3370196'
1
+ __version__ = '0.0.0.dev279'
2
+ __revision__ = '854e384ef0fb5b5687a88d1cad43b4b118bc9fd3'
3
3
 
4
4
 
5
5
  #
@@ -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(self._cls, self._info.params.frozen, self._info.params.weakref_slot)
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,
@@ -61,6 +61,7 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
61
61
 
62
62
  def add_slots(
63
63
  cls: type,
64
+ *,
64
65
  is_frozen: bool,
65
66
  weakref_slot: bool,
66
67
  ) -> type:
@@ -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
@@ -1,5 +1,6 @@
1
1
  """
2
2
  TODO:
3
+ - !! specialize nullary, explicit kwarg
3
4
  - !! reconcile A().f() with A.f(A())
4
5
  - unbound descriptor *should* still hit instance cache
5
6
  - integrate / expose with collections.cache
@@ -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:
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omlish
3
- Version: 0.0.0.dev277
3
+ Version: 0.0.0.dev279
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=pjGUyLHaoWpPqRP3jz2u1fC1qoRc2lvrEcpU_Ax2tdg,8253
2
- omlish/__about__.py,sha256=LsJ6JjvAcEVPpuFaB33e_08fA_C2VKyJfcYRyZuU2RM,3380
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=LyWr8IBfoL-bBfweR4OqJgi842REDw20WRYcGSYNfMg,2577
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=qXRLbtFWUs_2UV1fFUdv53_6fBLKJ_8McjNiP9YQlGM,5264
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=3AeTVfpzovMMWbnjFuT5mM7kT1dBuk7fa66FOJKikYw,9188
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=OUFL1Ggj5ozxO4C3eRPfDPzGwNitpRuoSJPc_QbmMNI,5470
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=JO3gMpJm1XAFGAlXglE_U1MCXFV-L_geImyb3CGgFIU,672
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.dev277.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
788
- omlish-0.0.0.dev277.dist-info/METADATA,sha256=WJ5keRK7rdyZD3UZsZQt2ybc4foSZ3L1GSJLZI6zJXY,4198
789
- omlish-0.0.0.dev277.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
790
- omlish-0.0.0.dev277.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
791
- omlish-0.0.0.dev277.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
792
- omlish-0.0.0.dev277.dist-info/RECORD,,
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)