omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev484__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (226) hide show
  1. omlish/.omlish-manifests.json +12 -0
  2. omlish/__about__.py +15 -15
  3. omlish/argparse/all.py +17 -9
  4. omlish/argparse/cli.py +16 -3
  5. omlish/argparse/utils.py +21 -0
  6. omlish/asyncs/asyncio/rlock.py +110 -0
  7. omlish/asyncs/asyncio/sync.py +43 -0
  8. omlish/asyncs/asyncio/utils.py +2 -0
  9. omlish/asyncs/sync.py +25 -0
  10. omlish/bootstrap/_marshal.py +1 -1
  11. omlish/bootstrap/diag.py +12 -21
  12. omlish/bootstrap/main.py +2 -5
  13. omlish/bootstrap/sys.py +27 -28
  14. omlish/cexts/__init__.py +0 -0
  15. omlish/cexts/include/omlish/omlish.hh +1 -0
  16. omlish/collections/__init__.py +13 -1
  17. omlish/collections/attrregistry.py +210 -0
  18. omlish/collections/cache/impl.py +1 -0
  19. omlish/collections/identity.py +1 -0
  20. omlish/collections/mappings.py +28 -0
  21. omlish/collections/trie.py +5 -1
  22. omlish/collections/utils.py +77 -0
  23. omlish/concurrent/all.py +2 -1
  24. omlish/concurrent/futures.py +25 -0
  25. omlish/concurrent/threadlets.py +1 -1
  26. omlish/daemons/reparent.py +2 -3
  27. omlish/daemons/spawning.py +2 -3
  28. omlish/dataclasses/__init__.py +2 -0
  29. omlish/dataclasses/impl/api/classes/decorator.py +3 -0
  30. omlish/dataclasses/impl/api/classes/make.py +3 -0
  31. omlish/dataclasses/impl/concerns/repr.py +15 -2
  32. omlish/dataclasses/impl/configs.py +97 -37
  33. omlish/dataclasses/impl/generation/compilation.py +21 -19
  34. omlish/dataclasses/impl/generation/globals.py +1 -0
  35. omlish/dataclasses/impl/generation/ops.py +1 -0
  36. omlish/dataclasses/impl/generation/processor.py +105 -24
  37. omlish/dataclasses/impl/processing/base.py +8 -0
  38. omlish/dataclasses/impl/processing/driving.py +8 -8
  39. omlish/dataclasses/specs.py +1 -0
  40. omlish/dataclasses/tools/modifiers.py +5 -0
  41. omlish/diag/cmds/__init__.py +0 -0
  42. omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
  43. omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
  44. omlish/diag/{ps.py → cmds/ps.py} +6 -6
  45. omlish/diag/pycharm.py +16 -2
  46. omlish/diag/pydevd.py +58 -40
  47. omlish/diag/replserver/console.py +1 -1
  48. omlish/dispatch/__init__.py +18 -12
  49. omlish/dispatch/methods.py +50 -140
  50. omlish/dom/rendering.py +1 -1
  51. omlish/formats/dotenv.py +1 -1
  52. omlish/formats/json/stream/__init__.py +13 -0
  53. omlish/funcs/guard.py +225 -0
  54. omlish/graphs/dot/rendering.py +1 -1
  55. omlish/http/all.py +44 -4
  56. omlish/http/clients/asyncs.py +33 -27
  57. omlish/http/clients/base.py +17 -1
  58. omlish/http/clients/coro/__init__.py +0 -0
  59. omlish/http/clients/coro/sync.py +171 -0
  60. omlish/http/clients/default.py +208 -29
  61. omlish/http/clients/executor.py +56 -0
  62. omlish/http/clients/httpx.py +72 -11
  63. omlish/http/clients/middleware.py +181 -0
  64. omlish/http/clients/sync.py +33 -27
  65. omlish/http/clients/syncasync.py +49 -0
  66. omlish/http/clients/urllib.py +6 -3
  67. omlish/http/coro/client/connection.py +15 -6
  68. omlish/http/coro/io.py +2 -0
  69. omlish/http/flasky/__init__.py +40 -0
  70. omlish/http/flasky/_compat.py +2 -0
  71. omlish/http/flasky/api.py +82 -0
  72. omlish/http/flasky/app.py +203 -0
  73. omlish/http/flasky/cvs.py +59 -0
  74. omlish/http/flasky/requests.py +20 -0
  75. omlish/http/flasky/responses.py +23 -0
  76. omlish/http/flasky/routes.py +23 -0
  77. omlish/http/flasky/types.py +15 -0
  78. omlish/http/urls.py +67 -0
  79. omlish/inject/__init__.py +38 -18
  80. omlish/inject/_dataclasses.py +4986 -0
  81. omlish/inject/binder.py +4 -48
  82. omlish/inject/elements.py +27 -0
  83. omlish/inject/helpers/__init__.py +0 -0
  84. omlish/inject/{utils.py → helpers/constfn.py} +3 -3
  85. omlish/inject/{tags.py → helpers/id.py} +2 -2
  86. omlish/inject/helpers/multis.py +143 -0
  87. omlish/inject/helpers/wrappers.py +54 -0
  88. omlish/inject/impl/elements.py +47 -17
  89. omlish/inject/impl/injector.py +20 -19
  90. omlish/inject/impl/inspect.py +10 -1
  91. omlish/inject/impl/maysync.py +3 -4
  92. omlish/inject/impl/multis.py +3 -0
  93. omlish/inject/impl/sync.py +3 -4
  94. omlish/inject/injector.py +31 -2
  95. omlish/inject/inspect.py +35 -0
  96. omlish/inject/maysync.py +2 -4
  97. omlish/inject/multis.py +8 -0
  98. omlish/inject/overrides.py +3 -3
  99. omlish/inject/privates.py +6 -0
  100. omlish/inject/providers.py +3 -2
  101. omlish/inject/sync.py +5 -4
  102. omlish/io/buffers.py +118 -0
  103. omlish/io/readers.py +29 -0
  104. omlish/iterators/transforms.py +2 -2
  105. omlish/lang/__init__.py +178 -97
  106. omlish/lang/_asyncs.cc +186 -0
  107. omlish/lang/asyncs.py +17 -0
  108. omlish/lang/casing.py +11 -0
  109. omlish/lang/contextmanagers.py +28 -4
  110. omlish/lang/functions.py +31 -22
  111. omlish/lang/imports/_capture.cc +11 -9
  112. omlish/lang/imports/capture.py +363 -170
  113. omlish/lang/imports/proxy.py +455 -152
  114. omlish/lang/lazyglobals.py +22 -9
  115. omlish/lang/params.py +17 -0
  116. omlish/lang/recursion.py +0 -1
  117. omlish/lang/sequences.py +124 -0
  118. omlish/lite/abstract.py +54 -24
  119. omlish/lite/asyncs.py +2 -2
  120. omlish/lite/attrops.py +2 -0
  121. omlish/lite/contextmanagers.py +4 -4
  122. omlish/lite/dataclasses.py +44 -0
  123. omlish/lite/maybes.py +8 -0
  124. omlish/lite/maysync.py +1 -5
  125. omlish/lite/pycharm.py +1 -1
  126. omlish/lite/typing.py +6 -0
  127. omlish/logs/all.py +1 -1
  128. omlish/logs/utils.py +1 -1
  129. omlish/manifests/loading.py +2 -1
  130. omlish/marshal/__init__.py +33 -13
  131. omlish/marshal/_dataclasses.py +2774 -0
  132. omlish/marshal/base/configs.py +12 -0
  133. omlish/marshal/base/contexts.py +36 -21
  134. omlish/marshal/base/funcs.py +8 -11
  135. omlish/marshal/base/options.py +8 -0
  136. omlish/marshal/base/registries.py +146 -44
  137. omlish/marshal/base/types.py +40 -16
  138. omlish/marshal/composite/iterables.py +33 -20
  139. omlish/marshal/composite/literals.py +20 -18
  140. omlish/marshal/composite/mappings.py +36 -23
  141. omlish/marshal/composite/maybes.py +29 -19
  142. omlish/marshal/composite/newtypes.py +16 -16
  143. omlish/marshal/composite/optionals.py +14 -14
  144. omlish/marshal/composite/special.py +15 -15
  145. omlish/marshal/composite/unions/__init__.py +0 -0
  146. omlish/marshal/composite/unions/literals.py +93 -0
  147. omlish/marshal/composite/unions/primitives.py +103 -0
  148. omlish/marshal/factories/invalidate.py +18 -68
  149. omlish/marshal/factories/method.py +26 -0
  150. omlish/marshal/factories/moduleimport/factories.py +22 -65
  151. omlish/marshal/factories/multi.py +13 -25
  152. omlish/marshal/factories/recursive.py +42 -56
  153. omlish/marshal/factories/typecache.py +29 -74
  154. omlish/marshal/factories/typemap.py +42 -43
  155. omlish/marshal/objects/dataclasses.py +129 -106
  156. omlish/marshal/objects/marshal.py +18 -14
  157. omlish/marshal/objects/namedtuples.py +48 -42
  158. omlish/marshal/objects/unmarshal.py +19 -15
  159. omlish/marshal/polymorphism/marshal.py +9 -11
  160. omlish/marshal/polymorphism/metadata.py +16 -5
  161. omlish/marshal/polymorphism/standard.py +13 -1
  162. omlish/marshal/polymorphism/unions.py +15 -105
  163. omlish/marshal/polymorphism/unmarshal.py +9 -10
  164. omlish/marshal/singular/enums.py +14 -18
  165. omlish/marshal/standard.py +10 -6
  166. omlish/marshal/trivial/any.py +1 -1
  167. omlish/marshal/trivial/forbidden.py +21 -26
  168. omlish/metadata.py +23 -1
  169. omlish/os/forkhooks.py +4 -0
  170. omlish/os/pidfiles/pinning.py +2 -2
  171. omlish/reflect/types.py +1 -0
  172. omlish/secrets/marshal.py +1 -1
  173. omlish/specs/jmespath/__init__.py +12 -3
  174. omlish/specs/jmespath/_dataclasses.py +2893 -0
  175. omlish/specs/jmespath/ast.py +1 -1
  176. omlish/specs/jsonrpc/__init__.py +13 -0
  177. omlish/specs/jsonrpc/_marshal.py +32 -23
  178. omlish/specs/jsonrpc/conns.py +10 -7
  179. omlish/specs/jsonschema/_marshal.py +1 -1
  180. omlish/specs/jsonschema/keywords/__init__.py +7 -0
  181. omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
  182. omlish/specs/openapi/_marshal.py +31 -22
  183. omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
  184. omlish/sql/queries/_marshal.py +2 -2
  185. omlish/sql/queries/rendering.py +1 -1
  186. omlish/sql/tabledefs/_marshal.py +1 -1
  187. omlish/subprocesses/base.py +4 -0
  188. omlish/subprocesses/editor.py +1 -1
  189. omlish/sync.py +155 -21
  190. omlish/term/alt.py +60 -0
  191. omlish/term/confirm.py +8 -8
  192. omlish/term/pager.py +235 -0
  193. omlish/term/terminfo.py +935 -0
  194. omlish/term/termstate.py +67 -0
  195. omlish/term/vt100/terminal.py +0 -3
  196. omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
  197. omlish/testing/pytest/plugins/skips.py +2 -1
  198. omlish/testing/unittest/main.py +3 -3
  199. omlish/text/docwrap/__init__.py +3 -0
  200. omlish/text/docwrap/__main__.py +11 -0
  201. omlish/text/docwrap/api.py +83 -0
  202. omlish/text/docwrap/cli.py +86 -0
  203. omlish/text/docwrap/groups.py +84 -0
  204. omlish/text/docwrap/lists.py +167 -0
  205. omlish/text/docwrap/parts.py +146 -0
  206. omlish/text/docwrap/reflowing.py +106 -0
  207. omlish/text/docwrap/rendering.py +151 -0
  208. omlish/text/docwrap/utils.py +11 -0
  209. omlish/text/docwrap/wrapping.py +59 -0
  210. omlish/text/filecache.py +2 -2
  211. omlish/text/lorem.py +6 -0
  212. omlish/text/parts.py +2 -2
  213. omlish/text/textwrap.py +51 -0
  214. omlish/typedvalues/marshal.py +85 -59
  215. omlish/typedvalues/values.py +2 -1
  216. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/METADATA +29 -28
  217. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/RECORD +222 -171
  218. omlish/dataclasses/impl/generation/mangling.py +0 -18
  219. omlish/funcs/match.py +0 -227
  220. omlish/marshal/factories/match.py +0 -34
  221. omlish/marshal/factories/simple.py +0 -28
  222. /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
  223. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/WHEEL +0 -0
  224. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/entry_points.txt +0 -0
  225. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/licenses/LICENSE +0 -0
  226. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/top_level.txt +0 -0
@@ -1,60 +1,239 @@
1
+ import abc
2
+ import contextlib
1
3
  import typing as ta
2
4
 
5
+ from ... import lang
3
6
  from ..headers import CanHttpHeaders
7
+ from .asyncs import AsyncHttpClient
8
+ from .base import HttpClientContext
4
9
  from .base import HttpRequest
5
10
  from .base import HttpResponse
6
11
  from .sync import HttpClient
7
- from .urllib import UrllibHttpClient
12
+
13
+
14
+ with lang.auto_proxy_import(globals()):
15
+ from . import httpx as _httpx
16
+ from . import urllib as _urllib
17
+
18
+
19
+ C = ta.TypeVar('C')
20
+ R = ta.TypeVar('R')
21
+
22
+
23
+ ##
24
+
25
+
26
+ class _DefaultRequester(lang.Abstract, ta.Generic[C, R]):
27
+ def __call__(
28
+ self,
29
+ url: str,
30
+ method: str | None = None,
31
+ *,
32
+ headers: CanHttpHeaders | None = None,
33
+ data: bytes | str | None = None,
34
+
35
+ timeout_s: float | None = None,
36
+
37
+ context: HttpClientContext | None = None,
38
+ check: bool = False,
39
+ client: C | None = None, # noqa
40
+
41
+ **kwargs: ta.Any,
42
+ ) -> R:
43
+ request = HttpRequest( # noqa
44
+ url,
45
+ method=method,
46
+
47
+ headers=headers,
48
+ data=data,
49
+
50
+ timeout_s=timeout_s,
51
+
52
+ **kwargs,
53
+ )
54
+
55
+ return self._do(
56
+ request,
57
+ context=context,
58
+ check=check,
59
+ client=client,
60
+ )
61
+
62
+ @abc.abstractmethod
63
+ def _do(
64
+ self,
65
+ request: HttpRequest, # noqa
66
+ *,
67
+ context: HttpClientContext | None = None,
68
+ check: bool = False,
69
+ client: C | None = None, # noqa
70
+ ) -> R:
71
+ raise NotImplementedError
8
72
 
9
73
 
10
74
  ##
11
75
 
12
76
 
13
77
  def _default_client() -> HttpClient:
14
- return UrllibHttpClient()
78
+ return _urllib.UrllibHttpClient()
15
79
 
16
80
 
17
81
  def client() -> HttpClient:
18
82
  return _default_client()
19
83
 
20
84
 
21
- def request(
22
- url: str,
23
- method: str | None = None,
24
- *,
25
- headers: CanHttpHeaders | None = None,
26
- data: bytes | str | None = None,
85
+ @contextlib.contextmanager
86
+ def manage_client(client: HttpClient | None = 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
+
95
+ #
96
+
97
+
98
+ class _BaseSyncDefaultRequester(_DefaultRequester[HttpClient, R], lang.Abstract, ta.Generic[R]):
99
+ def _do(
100
+ self,
101
+ request: HttpRequest, # noqa
102
+ *,
103
+ context: HttpClientContext | None = None,
104
+ check: bool = False,
105
+ client: HttpClient | None = None, # noqa
106
+ ) -> R:
107
+ if context is None:
108
+ context = HttpClientContext()
109
+
110
+ if client is not None:
111
+ return self._do_(
112
+ client,
113
+ context,
114
+ request,
115
+ check=check,
116
+ )
117
+
118
+ else:
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
+ )
153
+
154
+
155
+ request = _SyncDefaultRequester()
27
156
 
28
- timeout_s: float | None = None,
29
157
 
30
- check: bool = False,
158
+ ##
31
159
 
32
- client: HttpClient | None = None, # noqa
33
160
 
34
- **kwargs: ta.Any,
35
- ) -> HttpResponse:
36
- req = HttpRequest(
37
- url,
38
- method=method,
161
+ def _default_async_client() -> AsyncHttpClient:
162
+ return _httpx.HttpxAsyncHttpClient()
39
163
 
40
- headers=headers,
41
- data=data,
42
164
 
43
- timeout_s=timeout_s,
165
+ def async_client() -> AsyncHttpClient:
166
+ return _default_async_client()
44
167
 
45
- **kwargs,
46
- )
47
168
 
48
- def do(cli: HttpClient) -> HttpResponse:
49
- return cli.request(
50
- req,
169
+ @contextlib.asynccontextmanager
170
+ async def manage_async_client(client: AsyncHttpClient | None = None) -> ta.AsyncGenerator[AsyncHttpClient]: # noqa
171
+ if client is not None:
172
+ yield client
51
173
 
174
+ else:
175
+ async with _default_async_client() as client: # noqa
176
+ yield client
177
+
178
+
179
+ #
180
+
181
+
182
+ class _BaseAsyncDefaultRequester(_DefaultRequester[AsyncHttpClient, ta.Awaitable[R]], lang.Abstract, ta.Generic[R]):
183
+ async def _do(
184
+ self,
185
+ request: HttpRequest, # noqa
186
+ *,
187
+ context: HttpClientContext | None = None,
188
+ check: bool = False,
189
+ client: AsyncHttpClient | None = None, # noqa
190
+ ) -> R:
191
+ if context is None:
192
+ context = HttpClientContext()
193
+
194
+ if client is not None:
195
+ return await self._do_(
196
+ client,
197
+ context,
198
+ request,
199
+ check=check,
200
+ )
201
+
202
+ else:
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,
52
235
  check=check,
53
236
  )
54
237
 
55
- if client is not None:
56
- return do(client)
57
238
 
58
- else:
59
- with _default_client() as cli:
60
- return do(cli)
239
+ async_request = _AsyncDefaultRequester()
@@ -0,0 +1,56 @@
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 read(self, n: int = -1, /) -> bytes:
37
+ return await self.owner._run_in_executor(self.resp.stream.read, n) # noqa
38
+
39
+ async def readall(self) -> bytes:
40
+ return await self.owner._run_in_executor(self.resp.stream.readall) # noqa
41
+
42
+ async def close(self) -> None:
43
+ return await self.owner._run_in_executor(self.resp.close) # noqa
44
+
45
+ async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
46
+ resp: StreamHttpResponse = await self._run_in_executor(lambda: self._client.stream_request(req, context=ctx))
47
+ return AsyncStreamHttpResponse(
48
+ status=resp.status,
49
+ headers=resp.headers,
50
+ request=req,
51
+ underlying=resp,
52
+ **(dict( # type: ignore
53
+ _stream=(adapter := self._StreamAdapter(self, resp)),
54
+ _closer=adapter.close,
55
+ ) if resp.has_data else {}),
56
+ )
@@ -2,12 +2,17 @@
2
2
  TODO:
3
3
  - standardize following redirects
4
4
  """
5
+ import contextlib
5
6
  import functools
6
7
  import typing as ta
7
8
 
8
9
  from ... import dataclasses as dc
9
10
  from ... import lang
11
+ from ...io.buffers import ReadableListBuffer
10
12
  from ..headers import HttpHeaders
13
+ from .asyncs import AsyncHttpClient
14
+ from .asyncs import AsyncStreamHttpResponse
15
+ from .base import HttpClientContext
11
16
  from .base import HttpClientError
12
17
  from .base import HttpRequest
13
18
  from .sync import HttpClient
@@ -28,16 +33,13 @@ class HttpxHttpClient(HttpClient):
28
33
  class _StreamAdapter:
29
34
  it: ta.Iterator[bytes]
30
35
 
31
- def read(self, /, n: int = -1) -> bytes:
32
- if n < 0:
33
- return b''.join(self.it)
34
- else:
35
- try:
36
- return next(self.it)
37
- except StopIteration:
38
- return b''
36
+ def read1(self, n: int = -1, /) -> bytes:
37
+ try:
38
+ return next(self.it)
39
+ except StopIteration:
40
+ return b''
39
41
 
40
- def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
42
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
41
43
  try:
42
44
  resp_cm = httpx.stream(
43
45
  method=req.method_or_default,
@@ -59,7 +61,7 @@ class HttpxHttpClient(HttpClient):
59
61
  headers=HttpHeaders(resp.headers.raw),
60
62
  request=req,
61
63
  underlying=resp,
62
- stream=self._StreamAdapter(resp.iter_bytes()),
64
+ _stream=ReadableListBuffer().new_buffered_reader(self._StreamAdapter(resp.iter_bytes())),
63
65
  _closer=resp_close, # type: ignore
64
66
  )
65
67
 
@@ -67,6 +69,65 @@ class HttpxHttpClient(HttpClient):
67
69
  resp_close()
68
70
  raise HttpClientError from e
69
71
 
70
- except Exception:
72
+ except BaseException:
71
73
  resp_close()
72
74
  raise
75
+
76
+
77
+ ##
78
+
79
+
80
+ class HttpxAsyncHttpClient(AsyncHttpClient):
81
+ @dc.dataclass(frozen=True)
82
+ class _StreamAdapter:
83
+ it: ta.AsyncIterator[bytes]
84
+
85
+ async def read1(self, n: int = -1, /) -> bytes:
86
+ try:
87
+ return await anext(self.it)
88
+ except StopAsyncIteration:
89
+ return b''
90
+
91
+ async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
92
+ es = contextlib.AsyncExitStack()
93
+
94
+ try:
95
+ client = await es.enter_async_context(httpx.AsyncClient())
96
+
97
+ resp = await es.enter_async_context(client.stream(
98
+ method=req.method_or_default,
99
+ url=req.url,
100
+ headers=req.headers_ or None, # type: ignore
101
+ content=req.data,
102
+ timeout=req.timeout_s,
103
+ ))
104
+
105
+ it = resp.aiter_bytes()
106
+
107
+ # FIXME:
108
+ # this has a tendency to raise `RuntimeError: async generator ignored GeneratorExit` when all of the
109
+ # following conditions are met:
110
+ # - stopped iterating midway through
111
+ # - shutting down the event loop
112
+ # - debugging under pycharm / pydevd
113
+ # - running under asyncio
114
+ # it does not seem to happen unless all of these conditions are met. see:
115
+ # https://gist.github.com/wrmsr/a0578ee5d5371b53804cfb56aeb84cdf .
116
+ es.push_async_callback(it.aclose) # type: ignore[attr-defined]
117
+
118
+ return AsyncStreamHttpResponse(
119
+ status=resp.status_code,
120
+ headers=HttpHeaders(resp.headers.raw),
121
+ request=req,
122
+ underlying=resp,
123
+ _stream=ReadableListBuffer().new_async_buffered_reader(self._StreamAdapter(it)),
124
+ _closer=es.aclose,
125
+ )
126
+
127
+ except httpx.HTTPError as e:
128
+ await es.aclose()
129
+ raise HttpClientError from e
130
+
131
+ except BaseException:
132
+ await es.aclose()
133
+ raise
@@ -0,0 +1,181 @@
1
+ # ruff: noqa: UP007 UP043 UP045
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - redirect
6
+ - referrer header?
7
+ - non-forwarded headers, host check, etc lol
8
+ - 'check' kw becomes StatusCheckingMiddleware?
9
+ """
10
+ import dataclasses as dc
11
+ import typing as ta
12
+ import urllib.parse
13
+
14
+ from ...lite.abstract import Abstract
15
+ from ...lite.check import check
16
+ from ..urls import parsed_url_replace
17
+ from .asyncs import AsyncHttpClient
18
+ from .asyncs import AsyncStreamHttpResponse
19
+ from .base import BaseHttpClient
20
+ from .base import BaseHttpResponse
21
+ from .base import HttpClientContext
22
+ from .base import HttpClientError
23
+ from .base import HttpRequest
24
+ from .sync import HttpClient
25
+ from .sync import StreamHttpResponse
26
+ from .sync import close_http_client_response
27
+
28
+
29
+ BaseHttpClientT = ta.TypeVar('BaseHttpClientT', bound=BaseHttpClient)
30
+
31
+
32
+ ##
33
+
34
+
35
+ class HttpClientMiddleware(Abstract):
36
+ def process_request(
37
+ self,
38
+ ctx: HttpClientContext,
39
+ req: HttpRequest,
40
+ ) -> HttpRequest:
41
+ return req
42
+
43
+ def process_response(
44
+ self,
45
+ ctx: HttpClientContext,
46
+ req: HttpRequest,
47
+ resp: BaseHttpResponse,
48
+ ) -> ta.Union[BaseHttpResponse, HttpRequest]:
49
+ return resp
50
+
51
+
52
+ class AbstractMiddlewareHttpClient(Abstract, ta.Generic[BaseHttpClientT]):
53
+ def __init__(
54
+ self,
55
+ client: BaseHttpClientT,
56
+ middlewares: ta.Iterable[HttpClientMiddleware],
57
+ ) -> None:
58
+ super().__init__()
59
+
60
+ self._client = client
61
+ self._middlewares = list(middlewares)
62
+
63
+ def _process_request(
64
+ self,
65
+ ctx: HttpClientContext,
66
+ req: HttpRequest,
67
+ ) -> HttpRequest:
68
+ for mw in self._middlewares:
69
+ req = mw.process_request(ctx, req)
70
+ return req
71
+
72
+ def _process_response(
73
+ self,
74
+ ctx: HttpClientContext,
75
+ req: HttpRequest,
76
+ resp: BaseHttpResponse,
77
+ ) -> ta.Union[BaseHttpResponse, HttpRequest]:
78
+ for mw in self._middlewares:
79
+ nxt = mw.process_response(ctx, req, resp)
80
+ if isinstance(nxt, HttpRequest):
81
+ return nxt
82
+ else:
83
+ resp = nxt
84
+ return resp
85
+
86
+
87
+ #
88
+
89
+
90
+ class MiddlewareHttpClient(AbstractMiddlewareHttpClient[HttpClient], HttpClient):
91
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
92
+ while True:
93
+ req = self._process_request(ctx, req)
94
+
95
+ resp = self._client.stream_request(req, context=ctx)
96
+
97
+ try:
98
+ out = self._process_response(ctx, req, resp)
99
+
100
+ if isinstance(out, HttpRequest):
101
+ close_http_client_response(resp)
102
+ req = out
103
+ continue
104
+
105
+ elif isinstance(out, StreamHttpResponse):
106
+ return out
107
+
108
+ else:
109
+ raise TypeError(out) # noqa
110
+
111
+ except Exception:
112
+ close_http_client_response(resp)
113
+ raise
114
+
115
+ raise RuntimeError
116
+
117
+
118
+ class MiddlewareAsyncHttpClient(AbstractMiddlewareHttpClient[AsyncHttpClient], AsyncHttpClient):
119
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
120
+ return self._client.stream_request(self._process_request(ctx, req))
121
+
122
+
123
+ ##
124
+
125
+
126
+ class TooManyRedirectsHttpClientError(HttpClientError):
127
+ pass
128
+
129
+
130
+ class RedirectHandlingHttpClientMiddleware(HttpClientMiddleware):
131
+ DEFAULT_MAX_REDIRECTS: ta.ClassVar[int] = 5
132
+
133
+ def __init__(
134
+ self,
135
+ *,
136
+ max_redirects: ta.Optional[int] = None,
137
+ ) -> None:
138
+ super().__init__()
139
+
140
+ if max_redirects is None:
141
+ max_redirects = self.DEFAULT_MAX_REDIRECTS
142
+ self._max_redirects = max_redirects
143
+
144
+ @dc.dataclass()
145
+ class _State:
146
+ num_redirects: int = 0
147
+
148
+ def _get_state(self, ctx: HttpClientContext) -> _State:
149
+ try:
150
+ return ctx._dct[self._State] # noqa
151
+ except KeyError:
152
+ ret = ctx._dct[self._State] = self._State() # noqa
153
+ return ret
154
+
155
+ def process_response(
156
+ self,
157
+ ctx: HttpClientContext,
158
+ req: HttpRequest,
159
+ resp: BaseHttpResponse,
160
+ ) -> ta.Union[BaseHttpResponse, HttpRequest]: # noqa
161
+ if resp.status == 302:
162
+ st = self._get_state(ctx)
163
+ if st.num_redirects >= self._max_redirects:
164
+ raise TooManyRedirectsHttpClientError
165
+ st.num_redirects += 1
166
+
167
+ rd_url = check.not_none(resp.headers).single_str_dct['location']
168
+
169
+ rd_purl = urllib.parse.urlparse(rd_url)
170
+ if not rd_purl.netloc:
171
+ rq_purl = urllib.parse.urlparse(req.url)
172
+ rd_purl = parsed_url_replace(
173
+ rd_purl,
174
+ scheme=rq_purl.scheme,
175
+ netloc=rq_purl.netloc,
176
+ )
177
+ rd_url = urllib.parse.urlunparse(rd_purl)
178
+
179
+ return dc.replace(req, url=rd_url)
180
+
181
+ return resp