omlish 0.0.0.dev468__py3-none-any.whl → 0.0.0.dev469__py3-none-any.whl

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