omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev493__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 (265) hide show
  1. omlish/.omlish-manifests.json +12 -0
  2. omlish/README.md +199 -0
  3. omlish/__about__.py +21 -16
  4. omlish/argparse/all.py +17 -9
  5. omlish/argparse/cli.py +16 -3
  6. omlish/argparse/utils.py +21 -0
  7. omlish/asyncs/asyncio/rlock.py +110 -0
  8. omlish/asyncs/asyncio/sync.py +43 -0
  9. omlish/asyncs/asyncio/utils.py +2 -0
  10. omlish/asyncs/sync.py +25 -0
  11. omlish/bootstrap/_marshal.py +1 -1
  12. omlish/bootstrap/diag.py +12 -21
  13. omlish/bootstrap/main.py +2 -5
  14. omlish/bootstrap/sys.py +27 -28
  15. omlish/cexts/__init__.py +0 -0
  16. omlish/cexts/include/omlish/omlish.hh +1 -0
  17. omlish/collections/__init__.py +13 -1
  18. omlish/collections/attrregistry.py +210 -0
  19. omlish/collections/cache/impl.py +1 -0
  20. omlish/collections/identity.py +1 -0
  21. omlish/collections/mappings.py +28 -0
  22. omlish/collections/trie.py +5 -1
  23. omlish/collections/utils.py +77 -0
  24. omlish/concurrent/all.py +2 -1
  25. omlish/concurrent/futures.py +25 -0
  26. omlish/concurrent/threadlets.py +1 -1
  27. omlish/daemons/reparent.py +2 -3
  28. omlish/daemons/spawning.py +2 -3
  29. omlish/dataclasses/__init__.py +2 -0
  30. omlish/dataclasses/impl/api/classes/decorator.py +3 -0
  31. omlish/dataclasses/impl/api/classes/make.py +3 -0
  32. omlish/dataclasses/impl/concerns/repr.py +15 -2
  33. omlish/dataclasses/impl/configs.py +97 -37
  34. omlish/dataclasses/impl/generation/compilation.py +21 -19
  35. omlish/dataclasses/impl/generation/globals.py +1 -0
  36. omlish/dataclasses/impl/generation/ops.py +1 -0
  37. omlish/dataclasses/impl/generation/plans.py +2 -17
  38. omlish/dataclasses/impl/generation/processor.py +106 -25
  39. omlish/dataclasses/impl/processing/base.py +8 -0
  40. omlish/dataclasses/impl/processing/driving.py +19 -7
  41. omlish/dataclasses/specs.py +1 -0
  42. omlish/dataclasses/tools/modifiers.py +5 -0
  43. omlish/diag/_pycharm/runhack.py +1 -1
  44. omlish/diag/cmds/__init__.py +0 -0
  45. omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
  46. omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
  47. omlish/diag/{ps.py → cmds/ps.py} +6 -6
  48. omlish/diag/pycharm.py +16 -2
  49. omlish/diag/pydevd.py +58 -40
  50. omlish/diag/replserver/console.py +1 -1
  51. omlish/dispatch/__init__.py +18 -12
  52. omlish/dispatch/methods.py +50 -140
  53. omlish/dom/rendering.py +1 -1
  54. omlish/formats/dotenv.py +1 -1
  55. omlish/formats/json/stream/__init__.py +13 -0
  56. omlish/funcs/guard.py +225 -0
  57. omlish/graphs/dot/rendering.py +1 -1
  58. omlish/http/all.py +44 -4
  59. omlish/http/clients/asyncs.py +33 -27
  60. omlish/http/clients/base.py +17 -1
  61. omlish/http/clients/coro/__init__.py +0 -0
  62. omlish/http/clients/coro/sync.py +171 -0
  63. omlish/http/clients/default.py +208 -29
  64. omlish/http/clients/executor.py +56 -0
  65. omlish/http/clients/httpx.py +72 -11
  66. omlish/http/clients/middleware.py +181 -0
  67. omlish/http/clients/sync.py +33 -27
  68. omlish/http/clients/syncasync.py +49 -0
  69. omlish/http/clients/urllib.py +6 -3
  70. omlish/http/coro/client/connection.py +15 -6
  71. omlish/http/coro/io.py +2 -0
  72. omlish/http/flasky/__init__.py +40 -0
  73. omlish/http/flasky/_compat.py +2 -0
  74. omlish/http/flasky/api.py +82 -0
  75. omlish/http/flasky/app.py +203 -0
  76. omlish/http/flasky/cvs.py +59 -0
  77. omlish/http/flasky/requests.py +20 -0
  78. omlish/http/flasky/responses.py +23 -0
  79. omlish/http/flasky/routes.py +23 -0
  80. omlish/http/flasky/types.py +15 -0
  81. omlish/http/urls.py +67 -0
  82. omlish/inject/__init__.py +57 -29
  83. omlish/inject/_dataclasses.py +5148 -0
  84. omlish/inject/binder.py +11 -52
  85. omlish/inject/eagers.py +2 -0
  86. omlish/inject/elements.py +27 -0
  87. omlish/inject/helpers/__init__.py +0 -0
  88. omlish/inject/{utils.py → helpers/constfn.py} +3 -3
  89. omlish/inject/{tags.py → helpers/id.py} +2 -2
  90. omlish/inject/helpers/late.py +76 -0
  91. omlish/inject/{managed.py → helpers/managed.py} +10 -10
  92. omlish/inject/helpers/multis.py +143 -0
  93. omlish/inject/helpers/wrappers.py +54 -0
  94. omlish/inject/impl/elements.py +54 -21
  95. omlish/inject/impl/injector.py +29 -25
  96. omlish/inject/impl/inspect.py +10 -1
  97. omlish/inject/impl/maysync.py +3 -4
  98. omlish/inject/impl/multis.py +3 -0
  99. omlish/inject/impl/sync.py +3 -4
  100. omlish/inject/injector.py +31 -2
  101. omlish/inject/inspect.py +35 -0
  102. omlish/inject/maysync.py +2 -4
  103. omlish/inject/multis.py +8 -0
  104. omlish/inject/overrides.py +3 -3
  105. omlish/inject/privates.py +6 -0
  106. omlish/inject/providers.py +3 -2
  107. omlish/inject/sync.py +5 -4
  108. omlish/io/buffers.py +118 -0
  109. omlish/io/readers.py +29 -0
  110. omlish/iterators/transforms.py +2 -2
  111. omlish/lang/__init__.py +180 -97
  112. omlish/lang/_asyncs.cc +186 -0
  113. omlish/lang/asyncs.py +17 -0
  114. omlish/lang/casing.py +11 -0
  115. omlish/lang/contextmanagers.py +28 -4
  116. omlish/lang/functions.py +31 -22
  117. omlish/lang/imports/_capture.cc +11 -9
  118. omlish/lang/imports/capture.py +363 -170
  119. omlish/lang/imports/proxy.py +455 -152
  120. omlish/lang/lazyglobals.py +22 -9
  121. omlish/lang/params.py +17 -0
  122. omlish/lang/recursion.py +0 -1
  123. omlish/lang/sequences.py +124 -0
  124. omlish/lifecycles/README.md +30 -0
  125. omlish/lifecycles/__init__.py +87 -13
  126. omlish/lifecycles/_dataclasses.py +1388 -0
  127. omlish/lifecycles/base.py +178 -64
  128. omlish/lifecycles/contextmanagers.py +113 -4
  129. omlish/lifecycles/controller.py +150 -87
  130. omlish/lifecycles/injection.py +143 -0
  131. omlish/lifecycles/listeners.py +56 -0
  132. omlish/lifecycles/managed.py +142 -0
  133. omlish/lifecycles/manager.py +218 -93
  134. omlish/lifecycles/states.py +2 -0
  135. omlish/lifecycles/transitions.py +3 -0
  136. omlish/lifecycles/unwrap.py +57 -0
  137. omlish/lite/abstract.py +54 -24
  138. omlish/lite/asyncs.py +2 -2
  139. omlish/lite/attrops.py +2 -0
  140. omlish/lite/contextmanagers.py +4 -4
  141. omlish/lite/dataclasses.py +44 -0
  142. omlish/lite/maybes.py +8 -0
  143. omlish/lite/maysync.py +1 -5
  144. omlish/lite/pycharm.py +1 -1
  145. omlish/lite/typing.py +24 -0
  146. omlish/logs/_amalg.py +1 -1
  147. omlish/logs/all.py +25 -11
  148. omlish/logs/asyncs.py +73 -0
  149. omlish/logs/base.py +101 -12
  150. omlish/logs/contexts.py +4 -1
  151. omlish/logs/lists.py +125 -0
  152. omlish/logs/modules.py +19 -1
  153. omlish/logs/std/loggers.py +6 -1
  154. omlish/logs/std/noisy.py +11 -9
  155. omlish/logs/{standard.py → std/standard.py} +3 -4
  156. omlish/logs/utils.py +17 -2
  157. omlish/manifests/loading.py +2 -1
  158. omlish/marshal/__init__.py +33 -13
  159. omlish/marshal/_dataclasses.py +2774 -0
  160. omlish/marshal/base/configs.py +12 -0
  161. omlish/marshal/base/contexts.py +36 -21
  162. omlish/marshal/base/funcs.py +8 -11
  163. omlish/marshal/base/options.py +8 -0
  164. omlish/marshal/base/registries.py +146 -44
  165. omlish/marshal/base/types.py +40 -16
  166. omlish/marshal/composite/iterables.py +33 -20
  167. omlish/marshal/composite/literals.py +20 -18
  168. omlish/marshal/composite/mappings.py +36 -23
  169. omlish/marshal/composite/maybes.py +29 -19
  170. omlish/marshal/composite/newtypes.py +16 -16
  171. omlish/marshal/composite/optionals.py +14 -14
  172. omlish/marshal/composite/special.py +15 -15
  173. omlish/marshal/composite/unions/__init__.py +0 -0
  174. omlish/marshal/composite/unions/literals.py +93 -0
  175. omlish/marshal/composite/unions/primitives.py +103 -0
  176. omlish/marshal/factories/invalidate.py +18 -68
  177. omlish/marshal/factories/method.py +26 -0
  178. omlish/marshal/factories/moduleimport/factories.py +22 -65
  179. omlish/marshal/factories/multi.py +13 -25
  180. omlish/marshal/factories/recursive.py +42 -56
  181. omlish/marshal/factories/typecache.py +29 -74
  182. omlish/marshal/factories/typemap.py +42 -43
  183. omlish/marshal/objects/dataclasses.py +129 -106
  184. omlish/marshal/objects/marshal.py +18 -14
  185. omlish/marshal/objects/namedtuples.py +48 -42
  186. omlish/marshal/objects/unmarshal.py +19 -15
  187. omlish/marshal/polymorphism/marshal.py +9 -11
  188. omlish/marshal/polymorphism/metadata.py +16 -5
  189. omlish/marshal/polymorphism/standard.py +13 -1
  190. omlish/marshal/polymorphism/unions.py +15 -105
  191. omlish/marshal/polymorphism/unmarshal.py +9 -10
  192. omlish/marshal/singular/enums.py +14 -18
  193. omlish/marshal/standard.py +10 -6
  194. omlish/marshal/trivial/any.py +1 -1
  195. omlish/marshal/trivial/forbidden.py +21 -26
  196. omlish/metadata.py +23 -1
  197. omlish/os/forkhooks.py +4 -0
  198. omlish/os/pidfiles/pinning.py +2 -2
  199. omlish/reflect/__init__.py +43 -26
  200. omlish/reflect/ops.py +10 -1
  201. omlish/reflect/types.py +1 -0
  202. omlish/secrets/marshal.py +1 -1
  203. omlish/specs/jmespath/__init__.py +12 -3
  204. omlish/specs/jmespath/_dataclasses.py +2893 -0
  205. omlish/specs/jmespath/ast.py +1 -1
  206. omlish/specs/jsonrpc/__init__.py +13 -0
  207. omlish/specs/jsonrpc/_marshal.py +32 -23
  208. omlish/specs/jsonrpc/conns.py +10 -7
  209. omlish/specs/jsonschema/_marshal.py +1 -1
  210. omlish/specs/jsonschema/keywords/__init__.py +7 -0
  211. omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
  212. omlish/specs/openapi/_marshal.py +31 -22
  213. omlish/sql/__init__.py +24 -5
  214. omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
  215. omlish/sql/api/dbapi.py +1 -1
  216. omlish/sql/dbapi/__init__.py +15 -0
  217. omlish/sql/{dbapi.py → dbapi/drivers.py} +2 -2
  218. omlish/sql/queries/__init__.py +3 -0
  219. omlish/sql/queries/_marshal.py +2 -2
  220. omlish/sql/queries/rendering.py +1 -1
  221. omlish/sql/tabledefs/_marshal.py +1 -1
  222. omlish/subprocesses/base.py +4 -0
  223. omlish/subprocesses/editor.py +1 -1
  224. omlish/sync.py +155 -21
  225. omlish/term/alt.py +60 -0
  226. omlish/term/confirm.py +8 -8
  227. omlish/term/pager.py +235 -0
  228. omlish/term/terminfo.py +935 -0
  229. omlish/term/termstate.py +67 -0
  230. omlish/term/vt100/terminal.py +0 -3
  231. omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
  232. omlish/testing/pytest/plugins/asyncs/plugin.py +2 -0
  233. omlish/testing/pytest/plugins/skips.py +2 -1
  234. omlish/testing/unittest/main.py +3 -3
  235. omlish/text/docwrap/__init__.py +3 -0
  236. omlish/text/docwrap/__main__.py +11 -0
  237. omlish/text/docwrap/api.py +83 -0
  238. omlish/text/docwrap/cli.py +91 -0
  239. omlish/text/docwrap/groups.py +84 -0
  240. omlish/text/docwrap/lists.py +167 -0
  241. omlish/text/docwrap/parts.py +146 -0
  242. omlish/text/docwrap/reflowing.py +106 -0
  243. omlish/text/docwrap/rendering.py +151 -0
  244. omlish/text/docwrap/utils.py +11 -0
  245. omlish/text/docwrap/wrapping.py +59 -0
  246. omlish/text/filecache.py +2 -2
  247. omlish/text/lorem.py +6 -0
  248. omlish/text/parts.py +2 -2
  249. omlish/text/textwrap.py +51 -0
  250. omlish/typedvalues/marshal.py +85 -59
  251. omlish/typedvalues/values.py +2 -1
  252. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/METADATA +36 -32
  253. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/RECORD +260 -199
  254. omlish/dataclasses/impl/generation/mangling.py +0 -18
  255. omlish/funcs/match.py +0 -227
  256. omlish/lifecycles/abstract.py +0 -86
  257. omlish/marshal/factories/match.py +0 -34
  258. omlish/marshal/factories/simple.py +0 -28
  259. /omlish/inject/{impl → helpers}/proxy.py +0 -0
  260. /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
  261. /omlish/sql/{abc.py → dbapi/abc.py} +0 -0
  262. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/WHEEL +0 -0
  263. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/entry_points.txt +0 -0
  264. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/licenses/LICENSE +0 -0
  265. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/top_level.txt +0 -0
@@ -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
@@ -5,11 +5,13 @@ import contextlib
5
5
  import dataclasses as dc
6
6
  import typing as ta
7
7
 
8
+ from ...io.readers import BufferedBytesReader
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
@@ -25,22 +27,21 @@ HttpClientT = ta.TypeVar('HttpClientT', bound='HttpClient')
25
27
  @ta.final
26
28
  @dc.dataclass(frozen=True) # kw_only=True
27
29
  class StreamHttpResponse(BaseHttpResponse):
28
- class Stream(ta.Protocol):
29
- def read(self, /, n: int = -1) -> bytes: ...
30
+ _stream: ta.Optional[BufferedBytesReader] = None
30
31
 
31
- @ta.final
32
- class _NullStream:
33
- def read(self, /, n: int = -1) -> bytes:
34
- raise TypeError
32
+ @property
33
+ def stream(self) -> 'BufferedBytesReader':
34
+ if (st := self._stream) is None:
35
+ raise TypeError('No data')
36
+ return st
35
37
 
36
- stream: Stream = _NullStream()
38
+ @property
39
+ def has_data(self) -> bool:
40
+ return self._stream is not None
37
41
 
38
- _closer: ta.Optional[ta.Callable[[], None]] = None
42
+ #
39
43
 
40
- def __post_init__(self) -> None:
41
- dataclass_maybe_post_init(super())
42
- if isinstance(self.stream, StreamHttpResponse._NullStream):
43
- raise TypeError(self.stream)
44
+ _closer: ta.Optional[ta.Callable[[], None]] = None
44
45
 
45
46
  def __enter__(self: StreamHttpResponseT) -> StreamHttpResponseT:
46
47
  return self
@@ -50,13 +51,13 @@ class StreamHttpResponse(BaseHttpResponse):
50
51
 
51
52
  def close(self) -> None:
52
53
  if (c := self._closer) is not None:
53
- c()
54
+ c() # noqa
54
55
 
55
56
 
56
57
  #
57
58
 
58
59
 
59
- def close_response(resp: BaseHttpResponse) -> None:
60
+ def close_http_client_response(resp: BaseHttpResponse) -> None:
60
61
  if isinstance(resp, HttpResponse):
61
62
  pass
62
63
 
@@ -68,7 +69,7 @@ def close_response(resp: BaseHttpResponse) -> None:
68
69
 
69
70
 
70
71
  @contextlib.contextmanager
71
- def closing_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
72
+ def closing_http_client_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
72
73
  if isinstance(resp, HttpResponse):
73
74
  yield resp
74
75
  return
@@ -81,15 +82,14 @@ def closing_response(resp: BaseHttpResponseT) -> ta.Iterator[BaseHttpResponseT]:
81
82
  raise TypeError(resp)
82
83
 
83
84
 
84
- def read_response(resp: BaseHttpResponse) -> HttpResponse:
85
+ def read_http_client_response(resp: BaseHttpResponse) -> HttpResponse:
85
86
  if isinstance(resp, HttpResponse):
86
87
  return resp
87
88
 
88
89
  elif isinstance(resp, StreamHttpResponse):
89
- data = resp.stream.read()
90
90
  return HttpResponse(**{
91
- **{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('stream', '_closer')},
92
- 'data': data,
91
+ **{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('_stream', '_closer')},
92
+ **({'data': resp.stream.readall()} if resp.has_data else {}),
93
93
  })
94
94
 
95
95
  else:
@@ -99,7 +99,7 @@ def read_response(resp: BaseHttpResponse) -> HttpResponse:
99
99
  ##
100
100
 
101
101
 
102
- class HttpClient(Abstract):
102
+ class HttpClient(BaseHttpClient, Abstract):
103
103
  def __enter__(self: HttpClientT) -> HttpClientT:
104
104
  return self
105
105
 
@@ -110,21 +110,27 @@ class HttpClient(Abstract):
110
110
  self,
111
111
  req: HttpRequest,
112
112
  *,
113
+ context: ta.Optional[HttpClientContext] = None,
113
114
  check: bool = False,
114
115
  ) -> HttpResponse:
115
- with closing_response(self.stream_request(
116
+ with closing_http_client_response(self.stream_request(
116
117
  req,
118
+ context=context,
117
119
  check=check,
118
120
  )) as resp:
119
- return read_response(resp)
121
+ return read_http_client_response(resp)
120
122
 
121
123
  def stream_request(
122
124
  self,
123
125
  req: HttpRequest,
124
126
  *,
127
+ context: ta.Optional[HttpClientContext] = None,
125
128
  check: bool = False,
126
129
  ) -> StreamHttpResponse:
127
- resp = self._stream_request(req)
130
+ if context is None:
131
+ context = HttpClientContext()
132
+
133
+ resp = self._stream_request(context, req)
128
134
 
129
135
  try:
130
136
  if check and not resp.is_success:
@@ -132,14 +138,14 @@ class HttpClient(Abstract):
132
138
  cause = resp.underlying
133
139
  else:
134
140
  cause = None
135
- raise HttpStatusError(read_response(resp)) from cause # noqa
141
+ raise HttpStatusError(read_http_client_response(resp)) from cause # noqa
136
142
 
137
143
  except Exception:
138
- close_response(resp)
144
+ close_http_client_response(resp)
139
145
  raise
140
146
 
141
147
  return resp
142
148
 
143
149
  @abc.abstractmethod
144
- def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
150
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
145
151
  raise NotImplementedError
@@ -0,0 +1,49 @@
1
+ # ruff: noqa: UP043 UP045
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+
5
+ from .asyncs import AsyncHttpClient
6
+ from .asyncs import AsyncStreamHttpResponse
7
+ from .base import HttpClientContext
8
+ from .base import HttpRequest
9
+ from .sync import HttpClient
10
+ from .sync import StreamHttpResponse
11
+
12
+
13
+ ##
14
+
15
+
16
+ class SyncAsyncHttpClient(AsyncHttpClient):
17
+ def __init__(self, client: HttpClient) -> None:
18
+ super().__init__()
19
+
20
+ self._client = client
21
+
22
+ @dc.dataclass(frozen=True)
23
+ class _StreamAdapter:
24
+ ul: StreamHttpResponse
25
+
26
+ async def read1(self, n: int = -1, /) -> bytes:
27
+ return self.ul.stream.read1(n)
28
+
29
+ async def read(self, n: int = -1, /) -> bytes:
30
+ return self.ul.stream.read(n)
31
+
32
+ async def readall(self) -> bytes:
33
+ return self.ul.stream.readall()
34
+
35
+ async def close(self) -> None:
36
+ self.ul.close()
37
+
38
+ async def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> AsyncStreamHttpResponse:
39
+ resp = self._client.stream_request(req, context=ctx)
40
+ return AsyncStreamHttpResponse(
41
+ status=resp.status,
42
+ headers=resp.headers,
43
+ request=req,
44
+ underlying=resp,
45
+ **(dict( # type: ignore
46
+ _stream=(adapter := self._StreamAdapter(resp)),
47
+ _closer=adapter.close,
48
+ ) if resp.has_data else {}),
49
+ )
@@ -1,10 +1,14 @@
1
+ # ruff: noqa: UP043 UP045
2
+ # @omlish-lite
1
3
  import http.client
2
4
  import typing as ta
3
5
  import urllib.error
4
6
  import urllib.request
5
7
 
8
+ from ...io.buffers import ReadableListBuffer
6
9
  from ..headers import HttpHeaders
7
10
  from .base import DEFAULT_ENCODING
11
+ from .base import HttpClientContext
8
12
  from .base import HttpClientError
9
13
  from .base import HttpRequest
10
14
  from .sync import HttpClient
@@ -39,7 +43,7 @@ class UrllibHttpClient(HttpClient):
39
43
  data=d,
40
44
  )
41
45
 
42
- def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
46
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
43
47
  try:
44
48
  resp = urllib.request.urlopen( # noqa
45
49
  self._build_request(req),
@@ -53,7 +57,6 @@ class UrllibHttpClient(HttpClient):
53
57
  headers=HttpHeaders(e.headers.items()),
54
58
  request=req,
55
59
  underlying=e,
56
- stream=e, # noqa
57
60
  _closer=e.close,
58
61
  )
59
62
 
@@ -70,7 +73,7 @@ class UrllibHttpClient(HttpClient):
70
73
  headers=HttpHeaders(resp.headers.items()),
71
74
  request=req,
72
75
  underlying=resp,
73
- stream=resp,
76
+ _stream=ReadableListBuffer().new_buffered_reader(resp),
74
77
  _closer=resp.close,
75
78
  )
76
79
 
@@ -116,10 +116,10 @@ class CoroHttpClientConnection:
116
116
  _http_version = 11
117
117
  _http_version_str = 'HTTP/1.1'
118
118
 
119
- http_port: ta.ClassVar[int] = 80
120
- https_port: ta.ClassVar[int] = 443
119
+ HTTP_PORT: ta.ClassVar[int] = 80
120
+ HTTPS_PORT: ta.ClassVar[int] = 443
121
121
 
122
- default_port = http_port
122
+ DEFAULT_PORT: ta.ClassVar[int] = HTTP_PORT
123
123
 
124
124
  class _NOT_SET: # noqa
125
125
  def __new__(cls, *args, **kwargs): # noqa
@@ -139,6 +139,7 @@ class CoroHttpClientConnection:
139
139
  source_address: ta.Optional[str] = None,
140
140
  block_size: int = 8192,
141
141
  auto_open: bool = True,
142
+ default_port: ta.Optional[int] = None,
142
143
  ) -> None:
143
144
  super().__init__()
144
145
 
@@ -146,6 +147,9 @@ class CoroHttpClientConnection:
146
147
  self._source_address = source_address
147
148
  self._block_size = block_size
148
149
  self._auto_open = auto_open
150
+ if default_port is None:
151
+ default_port = self.DEFAULT_PORT
152
+ self._default_port = default_port
149
153
 
150
154
  self._connected = False
151
155
  self._buffer: ta.List[bytes] = []
@@ -162,6 +166,10 @@ class CoroHttpClientConnection:
162
166
 
163
167
  CoroHttpClientValidation.validate_host(self._host)
164
168
 
169
+ @property
170
+ def http_version(self) -> int:
171
+ return self._http_version
172
+
165
173
  #
166
174
 
167
175
  def _get_hostport(self, host: str, port: ta.Optional[int]) -> ta.Tuple[str, int]:
@@ -173,12 +181,12 @@ class CoroHttpClientConnection:
173
181
  port = int(host[i + 1:])
174
182
  except ValueError:
175
183
  if host[i + 1:] == '': # http://foo.com:/ == http://foo.com/
176
- port = self.default_port
184
+ port = self._default_port
177
185
  else:
178
186
  raise CoroHttpClientErrors.InvalidUrlError(f"non-numeric port: '{host[i + 1:]}'") from None
179
187
  host = host[:i]
180
188
  else:
181
- port = self.default_port
189
+ port = self._default_port
182
190
 
183
191
  if host and host[0] == '[' and host[-1] == ']':
184
192
  host = host[1:-1]
@@ -286,6 +294,7 @@ class CoroHttpClientConnection:
286
294
  source_address=self._source_address,
287
295
  **(dict(timeout=self._timeout) if self._timeout is not self._NOT_SET else {}),
288
296
  ),
297
+ server_hostname=self._tunnel_host if self._tunnel_host else self._host,
289
298
  )))
290
299
 
291
300
  self._connected = True
@@ -526,7 +535,7 @@ class CoroHttpClientConnection:
526
535
  if ':' in host:
527
536
  host_enc = self._strip_ipv6_iface(host_enc)
528
537
 
529
- if port == self.default_port:
538
+ if port == self._default_port:
530
539
  self.put_header('Host', host_enc)
531
540
  else:
532
541
  self.put_header('Host', f"{host_enc.decode('ascii')}:{port}")
omlish/http/coro/io.py CHANGED
@@ -37,6 +37,8 @@ class CoroHttpIo:
37
37
  args: ta.Tuple[ta.Any, ...]
38
38
  kwargs: ta.Optional[ta.Dict[str, ta.Any]] = None
39
39
 
40
+ server_hostname: ta.Optional[str] = None
41
+
40
42
  #
41
43
 
42
44
  class CloseIo(Io):
@@ -0,0 +1,40 @@
1
+ from .api import ( # noqa
2
+ Api,
3
+ )
4
+
5
+ from .app import ( # noqa
6
+ AppRunParams,
7
+ AppRunner,
8
+
9
+ ViewFunc,
10
+ BeforeRequestFunc,
11
+ AfterRequestFunc,
12
+ App,
13
+ )
14
+
15
+ from .cvs import ( # noqa
16
+ CvLookupError,
17
+
18
+ Cvs,
19
+ )
20
+
21
+ from .requests import ( # noqa
22
+ Request,
23
+ )
24
+
25
+ from .responses import ( # noqa
26
+ ResponseData,
27
+ ResponseStatus,
28
+ ResponseHeaders,
29
+
30
+ Response,
31
+ )
32
+
33
+ from .routes import ( # noqa
34
+ RouteKey,
35
+ Route,
36
+ )
37
+
38
+ from .types import ( # noqa
39
+ ImmutableMultiDict,
40
+ )
@@ -0,0 +1,2 @@
1
+ def compat(obj):
2
+ return obj