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
omlish/funcs/guard.py ADDED
@@ -0,0 +1,225 @@
1
+ import abc
2
+ import functools
3
+ import operator
4
+ import typing as ta
5
+
6
+ from .. import check
7
+ from .. import collections as col
8
+ from .. import lang
9
+
10
+
11
+ T = ta.TypeVar('T')
12
+ T_co = ta.TypeVar('T_co', covariant=True)
13
+ U = ta.TypeVar('U')
14
+ P = ta.ParamSpec('P')
15
+
16
+
17
+ ##
18
+
19
+
20
+ class GuardFn(ta.Protocol[P, T_co]):
21
+ def __get__(self, instance, owner=None): ...
22
+
23
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T_co] | None: ...
24
+
25
+
26
+ ##
27
+
28
+
29
+ @ta.final
30
+ class DumbGuardFn(ta.Generic[P, T]):
31
+ def __init__(self, fn: ta.Callable[P, T]) -> None:
32
+ self._fn = fn
33
+
34
+ def __get__(self, instance, owner=None):
35
+ return DumbGuardFn(self._fn.__get__(instance, owner)) # noqa
36
+
37
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T]:
38
+ return functools.partial(self._fn, *args, **kwargs)
39
+
40
+
41
+ dumb = DumbGuardFn
42
+
43
+
44
+ ##
45
+
46
+
47
+ class AmbiguousGuardFnError(Exception):
48
+ pass
49
+
50
+
51
+ @ta.final
52
+ class MultiGuardFn(ta.Generic[P, T]):
53
+ def __init__(
54
+ self,
55
+ *children: GuardFn[P, T],
56
+ default: GuardFn[P, T] | None = None,
57
+ strict: bool = False,
58
+ ) -> None:
59
+ self._children, self._default, self._strict = children, default, strict
60
+
61
+ lang.attr_ops(lambda self: (
62
+ self._children,
63
+ self._default,
64
+ self._strict,
65
+ )).install(locals())
66
+
67
+ def __get__(self, instance, owner=None):
68
+ return MultiGuardFn(*map(operator.methodcaller('__get__', instance, owner), self._children), default=self._default.__get__(instance, owner) if self._default is not None else None, strict=self._strict) # noqa
69
+
70
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T] | None:
71
+ matches = []
72
+ for c in self._children:
73
+ if (m := c(*args, **kwargs)) is not None:
74
+ if not self._strict:
75
+ return m
76
+ matches.append(m)
77
+ if not matches:
78
+ if (dfl := self._default) is not None:
79
+ return dfl(*args, **kwargs)
80
+ else:
81
+ return None
82
+ elif len(matches) > 1:
83
+ raise AmbiguousGuardFnError
84
+ else:
85
+ return matches[0]
86
+
87
+
88
+ multi = MultiGuardFn
89
+
90
+
91
+ ##
92
+
93
+
94
+ class _BaseGuardFnMethod(lang.Abstract, ta.Generic[P, T]):
95
+ def __init__(
96
+ self,
97
+ *,
98
+ strict: bool = False,
99
+ requires_override: bool = False,
100
+ instance_cache: bool = False,
101
+ default: GuardFn[P, T] | None = None,
102
+ ) -> None:
103
+ super().__init__()
104
+
105
+ self._strict = strict
106
+ self._instance_cache = instance_cache
107
+ self._default = default
108
+
109
+ self._registry: col.AttrRegistry[ta.Callable, None] = col.AttrRegistry(
110
+ requires_override=requires_override,
111
+ )
112
+
113
+ self._cache: col.AttrRegistryCache[ta.Callable, None, MultiGuardFn] = col.AttrRegistryCache(
114
+ self._registry,
115
+ self._prepare,
116
+ )
117
+
118
+ _owner: type | None = None
119
+ _name: str | None = None
120
+
121
+ def __set_name__(self, owner, name):
122
+ if self._owner is None:
123
+ self._owner = owner
124
+ if self._name is None:
125
+ self._name = name
126
+
127
+ def register(self, fn: U) -> U:
128
+ check.callable(fn)
129
+ self._registry.register(ta.cast(ta.Callable, fn), None)
130
+ return fn
131
+
132
+ def _prepare(self, instance_cls: type, collected: ta.Mapping[str, tuple[ta.Callable, None]]) -> MultiGuardFn:
133
+ return MultiGuardFn(
134
+ *[getattr(instance_cls, a) for a in collected],
135
+ default=self._default,
136
+ strict=self._strict,
137
+ )
138
+
139
+ @abc.abstractmethod
140
+ def _bind(self, instance, owner):
141
+ raise NotImplementedError
142
+
143
+ def __get__(self, instance, owner=None):
144
+ if instance is None:
145
+ return self
146
+
147
+ if self._instance_cache:
148
+ try:
149
+ return instance.__dict__[self._name]
150
+ except KeyError:
151
+ pass
152
+
153
+ bound = self._bind(instance, owner)
154
+
155
+ if self._instance_cache:
156
+ instance.__dict__[self._name] = bound
157
+
158
+ return bound
159
+
160
+ def _call(self, *args, **kwargs):
161
+ instance, *rest = args
162
+ return self.__get__(instance)(*rest, **kwargs)
163
+
164
+ #
165
+
166
+
167
+ @ta.final
168
+ class GuardFnMethod(_BaseGuardFnMethod[P, T]):
169
+ def _bind(self, instance, owner):
170
+ return self._cache.get(type(instance)).__get__(instance, owner) # noqa
171
+
172
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ta.Callable[[], T] | None:
173
+ return self._call(*args, **kwargs)
174
+
175
+
176
+ def method(
177
+ *,
178
+ strict: bool = False,
179
+ requires_override: bool = False,
180
+ instance_cache: bool = False,
181
+ default: bool = False,
182
+ ) -> ta.Callable[[ta.Callable[P, T]], GuardFnMethod[P, T]]: # noqa
183
+ def inner(fn):
184
+ return GuardFnMethod(
185
+ strict=strict,
186
+ requires_override=requires_override,
187
+ instance_cache=instance_cache,
188
+ default=fn if default else None,
189
+ )
190
+
191
+ return inner
192
+
193
+
194
+ #
195
+
196
+
197
+ @ta.final
198
+ class ImmediateGuardFnMethod(_BaseGuardFnMethod[P, T]):
199
+ def _bind(self, instance, owner):
200
+ gf = self._cache.get(type(instance)).__get__(instance, owner) # noqa
201
+
202
+ def inner(*args, **kwargs):
203
+ return gf(*args, **kwargs)() # Note: cannot be None due to non-optional default
204
+
205
+ return inner
206
+
207
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
208
+ return self._call(*args, **kwargs)
209
+
210
+
211
+ def immediate_method(
212
+ *,
213
+ strict: bool = False,
214
+ requires_override: bool = False,
215
+ instance_cache: bool = False,
216
+ ) -> ta.Callable[[ta.Callable[P, T]], ImmediateGuardFnMethod[P, T]]: # noqa
217
+ def inner(fn):
218
+ return ImmediateGuardFnMethod(
219
+ strict=strict,
220
+ requires_override=requires_override,
221
+ instance_cache=instance_cache,
222
+ default=(lambda *args, **kwargs: lambda: fn(*args, **kwargs)),
223
+ )
224
+
225
+ return inner
@@ -29,7 +29,7 @@ class Renderer:
29
29
 
30
30
  self._out = out
31
31
 
32
- @dispatch.method
32
+ @dispatch.method(instance_cache=True)
33
33
  def render(self, item: Item) -> None:
34
34
  raise TypeError(item)
35
35
 
omlish/http/all.py CHANGED
@@ -4,9 +4,18 @@ from .. import lang as _lang
4
4
  with _lang.auto_proxy_init(globals()):
5
5
  ##
6
6
 
7
+ from .clients.asyncs import ( # noqa
8
+ AsyncStreamHttpResponse,
9
+
10
+ async_close_http_client_response,
11
+ async_closing_http_client_response,
12
+ async_read_http_client_response,
13
+
14
+ AsyncHttpClient,
15
+ )
16
+
7
17
  from .clients.base import ( # noqa
8
18
  DEFAULT_ENCODING,
9
-
10
19
  is_success_status,
11
20
 
12
21
  HttpRequest,
@@ -14,34 +23,63 @@ with _lang.auto_proxy_init(globals()):
14
23
  BaseHttpResponse,
15
24
  HttpResponse,
16
25
 
26
+ HttpClientContext,
27
+
17
28
  HttpClientError,
18
29
  HttpStatusError,
30
+
31
+ BaseHttpClient,
19
32
  )
20
33
 
21
34
  from .clients.default import ( # noqa
22
35
  client,
36
+ manage_client,
23
37
 
24
38
  request,
39
+
40
+ async_client,
41
+ manage_async_client,
42
+
43
+ async_request,
25
44
  )
26
45
 
27
46
  from .clients.httpx import ( # noqa
28
47
  HttpxHttpClient,
48
+
49
+ HttpxAsyncHttpClient,
50
+ )
51
+
52
+ from .clients.middleware import ( # noqa
53
+ HttpClientMiddleware,
54
+ AbstractMiddlewareHttpClient,
55
+
56
+ MiddlewareHttpClient,
57
+ MiddlewareAsyncHttpClient,
58
+
59
+ TooManyRedirectsHttpClientError,
60
+ RedirectHandlingHttpClientMiddleware,
29
61
  )
30
62
 
31
63
  from .clients.sync import ( # noqa
32
64
  StreamHttpResponse,
33
65
 
34
- close_response,
35
- closing_response,
36
- read_response,
66
+ close_http_client_response,
67
+ closing_http_client_response,
68
+ read_http_client_response,
37
69
 
38
70
  HttpClient,
39
71
  )
40
72
 
73
+ from .clients.syncasync import ( # noqa
74
+ SyncAsyncHttpClient,
75
+ )
76
+
41
77
  from .clients.urllib import ( # noqa
42
78
  UrllibHttpClient,
43
79
  )
44
80
 
81
+ from . import asgi # noqa
82
+
45
83
  from . import consts # noqa
46
84
 
47
85
  from .cookies import ( # noqa
@@ -80,3 +118,5 @@ with _lang.auto_proxy_init(globals()):
80
118
  MultipartEncoder,
81
119
  MultipartField,
82
120
  )
121
+
122
+ from . import wsgi # noqa
@@ -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()