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
@@ -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 AsyncBufferedBytesReader
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 @@ AsyncHttpClientT = ta.TypeVar('AsyncHttpClientT', bound='AsyncHttpClient')
25
27
  @ta.final
26
28
  @dc.dataclass(frozen=True) # kw_only=True
27
29
  class AsyncStreamHttpResponse(BaseHttpResponse):
28
- class Stream(ta.Protocol):
29
- def read(self, /, n: int = -1) -> ta.Awaitable[bytes]: ...
30
+ _stream: ta.Optional[AsyncBufferedBytesReader] = None
30
31
 
31
- @ta.final
32
- class _NullStream:
33
- def read(self, /, n: int = -1) -> ta.Awaitable[bytes]:
34
- raise TypeError
32
+ @property
33
+ def stream(self) -> 'AsyncBufferedBytesReader':
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[[], ta.Awaitable[None]]] = None
42
+ #
39
43
 
40
- def __post_init__(self) -> None:
41
- dataclass_maybe_post_init(super())
42
- if isinstance(self.stream, AsyncStreamHttpResponse._NullStream):
43
- raise TypeError(self.stream)
44
+ _closer: ta.Optional[ta.Callable[[], ta.Awaitable[None]]] = None
44
45
 
45
46
  async def __aenter__(self: AsyncStreamHttpResponseT) -> AsyncStreamHttpResponseT:
46
47
  return self
@@ -50,13 +51,13 @@ class AsyncStreamHttpResponse(BaseHttpResponse):
50
51
 
51
52
  async def close(self) -> None:
52
53
  if (c := self._closer) is not None:
53
- c()
54
+ await c() # noqa
54
55
 
55
56
 
56
57
  #
57
58
 
58
59
 
59
- async def async_close_response(resp: BaseHttpResponse) -> None:
60
+ async def async_close_http_client_response(resp: BaseHttpResponse) -> None:
60
61
  if isinstance(resp, HttpResponse):
61
62
  pass
62
63
 
@@ -68,7 +69,7 @@ async def async_close_response(resp: BaseHttpResponse) -> None:
68
69
 
69
70
 
70
71
  @contextlib.asynccontextmanager
71
- async def async_closing_response(resp: BaseHttpResponseT) -> ta.AsyncGenerator[BaseHttpResponseT, None]:
72
+ async def async_closing_http_client_response(resp: BaseHttpResponseT) -> ta.AsyncGenerator[BaseHttpResponseT, None]:
72
73
  if isinstance(resp, HttpResponse):
73
74
  yield resp
74
75
  return
@@ -83,15 +84,14 @@ async def async_closing_response(resp: BaseHttpResponseT) -> ta.AsyncGenerator[B
83
84
  raise TypeError(resp)
84
85
 
85
86
 
86
- async def async_read_response(resp: BaseHttpResponse) -> HttpResponse:
87
+ async def async_read_http_client_response(resp: BaseHttpResponse) -> HttpResponse:
87
88
  if isinstance(resp, HttpResponse):
88
89
  return resp
89
90
 
90
91
  elif isinstance(resp, AsyncStreamHttpResponse):
91
- data = await resp.stream.read()
92
92
  return HttpResponse(**{
93
- **{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('stream', '_closer')},
94
- 'data': data,
93
+ **{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('_stream', '_closer')},
94
+ **({'data': await resp.stream.readall()} if resp.has_data else {}),
95
95
  })
96
96
 
97
97
  else:
@@ -101,7 +101,7 @@ async def async_read_response(resp: BaseHttpResponse) -> HttpResponse:
101
101
  ##
102
102
 
103
103
 
104
- class AsyncHttpClient(Abstract):
104
+ class AsyncHttpClient(BaseHttpClient, Abstract):
105
105
  async def __aenter__(self: AsyncHttpClientT) -> AsyncHttpClientT:
106
106
  return self
107
107
 
@@ -112,21 +112,27 @@ class AsyncHttpClient(Abstract):
112
112
  self,
113
113
  req: HttpRequest,
114
114
  *,
115
+ context: ta.Optional[HttpClientContext] = None,
115
116
  check: bool = False,
116
117
  ) -> HttpResponse:
117
- async with async_closing_response(await self.stream_request(
118
+ async with async_closing_http_client_response(await self.stream_request(
118
119
  req,
120
+ context=context,
119
121
  check=check,
120
122
  )) as resp:
121
- return await async_read_response(resp)
123
+ return await async_read_http_client_response(resp)
122
124
 
123
125
  async def stream_request(
124
126
  self,
125
127
  req: HttpRequest,
126
128
  *,
129
+ context: ta.Optional[HttpClientContext] = None,
127
130
  check: bool = False,
128
131
  ) -> AsyncStreamHttpResponse:
129
- resp = await self._stream_request(req)
132
+ if context is None:
133
+ context = HttpClientContext()
134
+
135
+ resp = await self._stream_request(context, req)
130
136
 
131
137
  try:
132
138
  if check and not resp.is_success:
@@ -134,14 +140,14 @@ class AsyncHttpClient(Abstract):
134
140
  cause = resp.underlying
135
141
  else:
136
142
  cause = None
137
- raise HttpStatusError(await async_read_response(resp)) from cause # noqa
143
+ raise HttpStatusError(await async_read_http_client_response(resp)) from cause # noqa
138
144
 
139
145
  except Exception:
140
- await async_close_response(resp)
146
+ await async_close_http_client_response(resp)
141
147
  raise
142
148
 
143
149
  return resp
144
150
 
145
151
  @abc.abstractmethod
146
- def _stream_request(self, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
152
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> ta.Awaitable[AsyncStreamHttpResponse]:
147
153
  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,171 @@
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 ....io.buffers import ReadableListBuffer
9
+ from ....lite.check import check
10
+ from ...coro.client.connection import CoroHttpClientConnection
11
+ from ...coro.client.response import CoroHttpClientResponse
12
+ from ...coro.io import CoroHttpIo
13
+ from ...headers import HttpHeaders
14
+ from ...urls import unparse_url_request_path
15
+ from ..base import HttpClientContext
16
+ from ..base import HttpClientError
17
+ from ..base import HttpRequest
18
+ from ..sync import HttpClient
19
+ from ..sync import StreamHttpResponse
20
+
21
+
22
+ T = ta.TypeVar('T')
23
+
24
+
25
+ ##
26
+
27
+
28
+ class CoroHttpClient(HttpClient):
29
+ class _Connection:
30
+ def __init__(self, req: HttpRequest) -> None:
31
+ super().__init__()
32
+
33
+ self._req = req
34
+ self._ups = urllib.parse.urlparse(req.url)
35
+
36
+ self._ssl = self._ups.scheme == 'https'
37
+
38
+ _cc: ta.Optional[CoroHttpClientConnection] = None
39
+ _resp: ta.Optional[CoroHttpClientResponse] = None
40
+
41
+ _sock: ta.Optional[socket.socket] = None
42
+ _sock_file: ta.Optional[ta.BinaryIO] = None
43
+
44
+ _ssl_context: ta.Any = None
45
+
46
+ #
47
+
48
+ def _create_https_context(self, http_version: int) -> ta.Any:
49
+ # https://github.com/python/cpython/blob/a7160912274003672dc116d918260c0a81551c21/Lib/http/client.py#L809
50
+ import ssl
51
+
52
+ # Function also used by urllib.request to be able to set the check_hostname attribute on a context object.
53
+ context = ssl.create_default_context()
54
+
55
+ # Send ALPN extension to indicate HTTP/1.1 protocol.
56
+ if http_version == 11:
57
+ context.set_alpn_protocols(['http/1.1'])
58
+
59
+ # Enable PHA for TLS 1.3 connections if available.
60
+ if context.post_handshake_auth is not None:
61
+ context.post_handshake_auth = True
62
+
63
+ return context
64
+
65
+ #
66
+
67
+ def setup(self) -> StreamHttpResponse:
68
+ check.none(self._sock)
69
+ check.none(self._ssl_context)
70
+
71
+ self._cc = cc = CoroHttpClientConnection(
72
+ check.not_none(self._ups.hostname),
73
+ default_port=CoroHttpClientConnection.HTTPS_PORT if self._ssl else CoroHttpClientConnection.HTTP_PORT,
74
+ )
75
+
76
+ if self._ssl:
77
+ self._ssl_context = self._create_https_context(self._cc.http_version)
78
+
79
+ try:
80
+ self._run_coro(cc.connect())
81
+
82
+ self._run_coro(cc.request(
83
+ self._req.method or 'GET',
84
+ unparse_url_request_path(self._ups) or '/',
85
+ self._req.data,
86
+ hh.single_str_dct if (hh := self._req.headers_) is not None else {},
87
+ ))
88
+
89
+ self._resp = resp = self._run_coro(cc.get_response())
90
+
91
+ return StreamHttpResponse(
92
+ status=resp._state.status, # noqa
93
+ headers=HttpHeaders(resp._state.headers.items()), # noqa
94
+ request=self._req,
95
+ underlying=self,
96
+ _stream=ReadableListBuffer().new_buffered_reader(self),
97
+ _closer=self.close,
98
+ )
99
+
100
+ except Exception:
101
+ self.close()
102
+ raise
103
+
104
+ def _run_coro(self, g: ta.Generator[ta.Any, ta.Any, T]) -> T:
105
+ i = None
106
+
107
+ while True:
108
+ try:
109
+ o = g.send(i)
110
+ except StopIteration as e:
111
+ return e.value
112
+
113
+ try:
114
+ i = self._handle_io(o)
115
+ except OSError as e:
116
+ raise HttpClientError from e
117
+
118
+ def _handle_io(self, o: CoroHttpIo.Io) -> ta.Any:
119
+ if isinstance(o, CoroHttpIo.ConnectIo):
120
+ check.none(self._sock)
121
+ self._sock = socket.create_connection(*o.args, **(o.kwargs or {}))
122
+
123
+ if self._ssl_context is not None:
124
+ self._sock = self._ssl_context.wrap_socket(
125
+ self._sock,
126
+ server_hostname=check.not_none(o.server_hostname),
127
+ )
128
+
129
+ # Might fail in OSs that don't implement TCP_NODELAY
130
+ try:
131
+ self._sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
132
+ except OSError as e:
133
+ if e.errno != errno.ENOPROTOOPT:
134
+ raise
135
+
136
+ self._sock_file = self._sock.makefile('rb')
137
+
138
+ return None
139
+
140
+ elif isinstance(o, CoroHttpIo.CloseIo):
141
+ check.not_none(self._sock).close()
142
+ return None
143
+
144
+ elif isinstance(o, CoroHttpIo.WriteIo):
145
+ check.not_none(self._sock).sendall(o.data)
146
+ return None
147
+
148
+ elif isinstance(o, CoroHttpIo.ReadIo):
149
+ if (sz := o.sz) is not None:
150
+ return check.not_none(self._sock_file).read(sz)
151
+ else:
152
+ return check.not_none(self._sock_file).read()
153
+
154
+ elif isinstance(o, CoroHttpIo.ReadLineIo):
155
+ return check.not_none(self._sock_file).readline(o.sz)
156
+
157
+ else:
158
+ raise TypeError(o)
159
+
160
+ def read1(self, n: int = -1, /) -> bytes:
161
+ return self._run_coro(check.not_none(self._resp).read(n if n >= 0 else None))
162
+
163
+ def close(self) -> None:
164
+ if self._resp is not None:
165
+ self._resp.close()
166
+ if self._sock is not None:
167
+ self._sock.close()
168
+
169
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
170
+ conn = CoroHttpClient._Connection(req)
171
+ return conn.setup()
@@ -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
+ )