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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev278'
2
- __revision__ = '8c2d6a9db875d1122353ed124741768f37aeaa66'
1
+ __version__ = '0.0.0.dev280'
2
+ __revision__ = '7ffe9145bdd2895b005e12042a6b5d4437d4e634'
3
3
 
4
4
 
5
5
  #
@@ -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()
@@ -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
- return id(x) in self._dict
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)
@@ -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,
@@ -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,
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omlish
3
- Version: 0.0.0.dev278
3
+ Version: 0.0.0.dev280
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=Dmy-Gwt1Dg_ioGn-8nv0aWBtdA4mRgseFv3sCSvWR5g,3380
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=cIcvGORpKAWpNxEleAiPxJJsMA9RmZ7LUSKacexgCxE,3276
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=LyWr8IBfoL-bBfweR4OqJgi842REDw20WRYcGSYNfMg,2577
220
- omlish/dataclasses/impl/metaclass.py,sha256=ebxJr3j8d_wz-fc7mvhWtJ6wJgnxVV6rVOQdqZ5avKw,5262
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=qXRLbtFWUs_2UV1fFUdv53_6fBLKJ_8McjNiP9YQlGM,5264
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=3AeTVfpzovMMWbnjFuT5mM7kT1dBuk7fa66FOJKikYw,9188
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.dev278.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
788
- omlish-0.0.0.dev278.dist-info/METADATA,sha256=f1crVlpqO_vjVM0zC27geFcdLVJM_mYDfGA8y0Wnqxk,4198
789
- omlish-0.0.0.dev278.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
790
- omlish-0.0.0.dev278.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
791
- omlish-0.0.0.dev278.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
792
- omlish-0.0.0.dev278.dist-info/RECORD,,
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)