omlish 0.0.0.dev71__py3-none-any.whl → 0.0.0.dev72__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev71'
2
- __revision__ = '293695b11eeed255848dcf6beee9c4b4383d5c91'
1
+ __version__ = '0.0.0.dev72'
2
+ __revision__ = '4448e03bbb77cb149e46eeefb0e9e61faed2a494'
3
3
 
4
4
 
5
5
  #
@@ -13,6 +13,7 @@ from .render import JsonRenderer
13
13
 
14
14
 
15
15
  if ta.TYPE_CHECKING:
16
+ import ast
16
17
  import tomllib
17
18
 
18
19
  import yaml
@@ -21,6 +22,7 @@ if ta.TYPE_CHECKING:
21
22
  from .. import props
22
23
 
23
24
  else:
25
+ ast = lang.proxy_import('ast')
24
26
  tomllib = lang.proxy_import('tomllib')
25
27
 
26
28
  yaml = lang.proxy_import('yaml')
@@ -50,6 +52,7 @@ class Formats(enum.Enum):
50
52
  TOML = Format(['toml'], lambda f: tomllib.loads(f.read()))
51
53
  ENV = Format(['env', 'dotenv'], lambda f: dotenv.dotenv_values(stream=f))
52
54
  PROPS = Format(['properties', 'props'], lambda f: dict(props.Properties().load(f.read())))
55
+ PY = Format(['py', 'python', 'repr'], lambda f: ast.literal_eval(f.read()))
53
56
 
54
57
 
55
58
  FORMATS_BY_NAME: ta.Mapping[str, Format] = {
omlish/http/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from . import consts # noqa
2
2
 
3
- from .client import ( # noqa
3
+ from .clients import ( # noqa
4
4
  HttpClient,
5
5
  HttpClientError,
6
6
  HttpRequest,
omlish/http/clients.py ADDED
@@ -0,0 +1,239 @@
1
+ """
2
+ TODO:
3
+ - check=False
4
+ - return non-200 HttpResponses
5
+ - async
6
+ - stream
7
+ """
8
+ import abc
9
+ import http.client
10
+ import typing as ta
11
+ import urllib.error
12
+ import urllib.request
13
+
14
+ from .. import cached
15
+ from .. import dataclasses as dc
16
+ from .. import lang
17
+ from .headers import CanHttpHeaders
18
+ from .headers import HttpHeaders
19
+
20
+
21
+ if ta.TYPE_CHECKING:
22
+ import httpx
23
+ else:
24
+ httpx = lang.proxy_import('httpx')
25
+
26
+
27
+ ##
28
+
29
+
30
+ DEFAULT_ENCODING = 'utf-8'
31
+
32
+
33
+ def is_success_status(status: int) -> bool:
34
+ return 200 <= status < 300
35
+
36
+
37
+ ##
38
+
39
+
40
+ @dc.dataclass(frozen=True)
41
+ class HttpRequest(lang.Final):
42
+ url: str
43
+ method: str | None = None # noqa
44
+
45
+ _: dc.KW_ONLY
46
+
47
+ headers: CanHttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
48
+ data: bytes | str | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
49
+
50
+ timeout_s: float | None = None
51
+
52
+ #
53
+
54
+ @property
55
+ def method_or_default(self) -> str:
56
+ if self.method is not None:
57
+ return self.method
58
+ if self.data is not None:
59
+ return 'POST'
60
+ return 'GET'
61
+
62
+ @cached.property
63
+ def headers_(self) -> HttpHeaders | None:
64
+ return HttpHeaders(self.headers) if self.headers is not None else None
65
+
66
+
67
+ @dc.dataclass(frozen=True, kw_only=True)
68
+ class HttpResponse(lang.Final):
69
+ status: int
70
+
71
+ headers: HttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
72
+ data: bytes | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
73
+
74
+ request: HttpRequest
75
+ underlying: ta.Any = dc.field(default=None, repr=False)
76
+
77
+ #
78
+
79
+ @property
80
+ def is_success(self) -> bool:
81
+ return is_success_status(self.status)
82
+
83
+
84
+ class HttpClientError(Exception):
85
+ @property
86
+ def cause(self) -> BaseException | None:
87
+ return self.__cause__
88
+
89
+
90
+ @dc.dataclass(frozen=True)
91
+ class HttpStatusError(HttpClientError):
92
+ response: HttpResponse
93
+
94
+
95
+ class HttpClient(lang.Abstract):
96
+ def __enter__(self) -> ta.Self:
97
+ return self
98
+
99
+ def __exit__(self, exc_type, exc_val, exc_tb):
100
+ pass
101
+
102
+ def request(
103
+ self,
104
+ req: HttpRequest,
105
+ *,
106
+ check: bool = False,
107
+ ) -> HttpResponse:
108
+ resp = self._request(req)
109
+
110
+ if check and not resp.is_success:
111
+ if isinstance(resp.underlying, Exception):
112
+ cause = resp.underlying
113
+ else:
114
+ cause = None
115
+ raise HttpStatusError(resp) from cause
116
+
117
+ return resp
118
+
119
+ @abc.abstractmethod
120
+ def _request(self, req: HttpRequest) -> HttpResponse:
121
+ raise NotImplementedError
122
+
123
+
124
+ ##
125
+
126
+
127
+ class UrllibHttpClient(HttpClient):
128
+ def _request(self, req: HttpRequest) -> HttpResponse:
129
+ d: ta.Any
130
+ if (d := req.data) is not None:
131
+ if isinstance(d, str):
132
+ d = d.encode(DEFAULT_ENCODING)
133
+
134
+ # urllib headers are dumb dicts [1], and keys *must* be strings or it will automatically add problematic default
135
+ # headers because it doesn't see string keys in its header dict [2]. frustratingly it has no problem accepting
136
+ # bytes keys though [3].
137
+ # [1]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/urllib/request.py#L319-L325 # noqa
138
+ # [2]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/urllib/request.py#L1276-L1279 # noqa
139
+ # [3]: https://github.com/python/cpython/blob/232b303e4ca47892f544294bf42e31dc34f0ec72/Lib/http/client.py#L1300-L1301 # noqa
140
+ h: dict[str, str] = {}
141
+ if hs := req.headers_:
142
+ for k, v in hs.strict_dct.items():
143
+ h[k.decode('ascii')] = v.decode('ascii')
144
+
145
+ try:
146
+ with urllib.request.urlopen( # noqa
147
+ urllib.request.Request( # noqa
148
+ req.url,
149
+ method=req.method_or_default,
150
+ headers=h,
151
+ data=d,
152
+ ),
153
+ timeout=req.timeout_s,
154
+ ) as resp:
155
+ return HttpResponse(
156
+ status=resp.status,
157
+ headers=HttpHeaders(resp.headers.items()),
158
+ data=resp.read(),
159
+ request=req,
160
+ underlying=resp,
161
+ )
162
+
163
+ except urllib.error.HTTPError as e:
164
+ return HttpResponse(
165
+ status=e.code,
166
+ headers=HttpHeaders(e.headers.items()),
167
+ data=e.read(),
168
+ request=req,
169
+ underlying=e,
170
+ )
171
+
172
+ except (urllib.error.URLError, http.client.HTTPException) as e:
173
+ raise HttpClientError from e
174
+
175
+
176
+ ##
177
+
178
+
179
+ class HttpxHttpClient(HttpClient):
180
+ def _request(self, req: HttpRequest) -> HttpResponse:
181
+ try:
182
+ response = httpx.request(
183
+ method=req.method_or_default,
184
+ url=req.url,
185
+ headers=req.headers_ or None, # type: ignore
186
+ content=req.data,
187
+ timeout=req.timeout_s,
188
+ )
189
+
190
+ return HttpResponse(
191
+ status=response.status_code,
192
+ headers=HttpHeaders(response.headers.raw),
193
+ data=response.content,
194
+ request=req,
195
+ underlying=response,
196
+ )
197
+
198
+ except httpx.HTTPError as e:
199
+ raise HttpClientError from e
200
+
201
+
202
+ ##
203
+
204
+
205
+ def client() -> HttpClient:
206
+ return UrllibHttpClient()
207
+
208
+
209
+ def request(
210
+ url: str,
211
+ method: str | None = None,
212
+ *,
213
+ headers: CanHttpHeaders | None = None,
214
+ data: bytes | str | None = None,
215
+
216
+ timeout_s: float | None = None,
217
+
218
+ check: bool = False,
219
+
220
+ **kwargs: ta.Any,
221
+ ) -> HttpResponse:
222
+ req = HttpRequest(
223
+ url,
224
+ method=method,
225
+
226
+ headers=headers,
227
+ data=data,
228
+
229
+ timeout_s=timeout_s,
230
+
231
+ **kwargs,
232
+ )
233
+
234
+ with client() as cli:
235
+ return cli.request(
236
+ req,
237
+
238
+ check=check,
239
+ )
omlish/http/consts.py CHANGED
@@ -64,5 +64,9 @@ BEARER_AUTH_HEADER_PREFIX = b'Bearer '
64
64
  BASIC_AUTH_HEADER_PREFIX = b'Basic '
65
65
 
66
66
 
67
+ def format_bearer_auth_header(token: str | bytes) -> bytes:
68
+ return BEARER_AUTH_HEADER_PREFIX + (token.encode('ascii') if isinstance(token, str) else token)
69
+
70
+
67
71
  def format_basic_auth_header(username: str, password: str) -> bytes:
68
72
  return BASIC_AUTH_HEADER_PREFIX + base64.b64encode(':'.join([username, password]).encode())
omlish/http/headers.py CHANGED
@@ -9,9 +9,20 @@ StrOrBytes: ta.TypeAlias = str | bytes
9
9
 
10
10
  CanHttpHeaders: ta.TypeAlias = ta.Union[
11
11
  'HttpHeaders',
12
+
13
+ ta.Mapping[str, str],
14
+ ta.Mapping[str, ta.Sequence[str]],
15
+
16
+ ta.Mapping[bytes, bytes],
17
+ ta.Mapping[bytes, ta.Sequence[bytes]],
18
+
12
19
  ta.Mapping[StrOrBytes, StrOrBytes],
13
20
  ta.Mapping[StrOrBytes, ta.Sequence[StrOrBytes]],
21
+
14
22
  ta.Mapping[StrOrBytes, StrOrBytes | ta.Sequence[StrOrBytes]],
23
+
24
+ ta.Sequence[tuple[str, str]],
25
+ ta.Sequence[tuple[bytes, bytes]],
15
26
  ta.Sequence[tuple[StrOrBytes, StrOrBytes]],
16
27
  ]
17
28
 
@@ -38,7 +49,11 @@ class HttpHeaders:
38
49
  raise TypeError(src)
39
50
 
40
51
  elif isinstance(src, ta.Sequence):
41
- for k, v in src:
52
+ for t in src:
53
+ if isinstance(t, (str, bytes)):
54
+ raise TypeError(t)
55
+
56
+ k, v = t
42
57
  lst.append((self._as_bytes(k), self._as_bytes(v)))
43
58
 
44
59
  else:
@@ -74,9 +89,23 @@ class HttpHeaders:
74
89
 
75
90
  #
76
91
 
92
+ @property
93
+ def raw(self) -> ta.Sequence[tuple[bytes, bytes]]:
94
+ return self._lst
95
+
96
+ @classmethod
97
+ def _as_key(cls, o: StrOrBytes) -> bytes:
98
+ return cls._as_bytes(o).lower()
99
+
100
+ @cached.property
101
+ def normalized(self) -> ta.Sequence[tuple[bytes, bytes]]:
102
+ return [(self._as_key(k), v) for k, v in self._lst]
103
+
104
+ #
105
+
77
106
  @cached.property
78
107
  def multi_dct(self) -> ta.Mapping[bytes, ta.Sequence[bytes]]:
79
- return col.multi_map(self._lst)
108
+ return col.multi_map(self.normalized)
80
109
 
81
110
  @cached.property
82
111
  def single_dct(self) -> ta.Mapping[bytes, bytes]:
@@ -84,13 +113,13 @@ class HttpHeaders:
84
113
 
85
114
  @cached.property
86
115
  def strict_dct(self) -> ta.Mapping[bytes, bytes]:
87
- return col.make_map(self._lst, strict=True)
116
+ return col.make_map(self.normalized, strict=True)
88
117
 
89
118
  #
90
119
 
91
120
  @cached.property
92
121
  def strs(self) -> ta.Sequence[tuple[str, str]]:
93
- return tuple((k.decode(self.ENCODING), v.decode(self.ENCODING)) for k, v in self._lst)
122
+ return tuple((k.decode(self.ENCODING), v.decode(self.ENCODING)) for k, v in self.normalized)
94
123
 
95
124
  @cached.property
96
125
  def multi_str_dct(self) -> ta.Mapping[str, ta.Sequence[str]]:
@@ -116,7 +145,11 @@ class HttpHeaders:
116
145
  return iter(self._lst)
117
146
 
118
147
  @ta.overload
119
- def __getitem__(self, item: StrOrBytes) -> ta.Sequence[StrOrBytes]:
148
+ def __getitem__(self, item: str) -> ta.Sequence[str]:
149
+ ...
150
+
151
+ @ta.overload
152
+ def __getitem__(self, item: bytes) -> ta.Sequence[bytes]:
120
153
  ...
121
154
 
122
155
  @ta.overload
@@ -130,8 +163,10 @@ class HttpHeaders:
130
163
  def __getitem__(self, item):
131
164
  if isinstance(item, (int, slice)):
132
165
  return self._lst[item]
133
- elif isinstance(item, (str, bytes)):
134
- return self.multi_dct[self._as_bytes(item)]
166
+ elif isinstance(item, str):
167
+ return self.multi_str_dct[item.lower()]
168
+ elif isinstance(item, bytes):
169
+ return self.multi_dct[self._as_key(item)]
135
170
  else:
136
171
  raise TypeError(item)
137
172
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev71
3
+ Version: 0.0.0.dev72
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=TXvFdkAU0Zr2FKdo7fyvt9nr3UjCtrnAZ0diZXSAteE,1430
2
- omlish/__about__.py,sha256=UA4amXjbtRmwpmcjop8hOCA8ytGC55dHDjgDm1toVmM,3420
2
+ omlish/__about__.py,sha256=Xtux9lxQlFpaTqHV9Xq7BumGnu8QP3C6l4R7wQX9F_w,3420
3
3
  omlish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  omlish/argparse.py,sha256=Dc73G8lyoQBLvXhMYUbzQUh4SJu_OTvKUXjSUxq_ang,7499
5
5
  omlish/c3.py,sha256=4vogWgwPb8TbNS2KkZxpoWbwjj7MuHG2lQG-hdtkvjI,8062
@@ -184,7 +184,7 @@ omlish/formats/props.py,sha256=JwFJbKblqzqnzXf7YKFzQSDfcAXzkKsfoYvad6FPy98,18945
184
184
  omlish/formats/yaml.py,sha256=DSJXUq9yanfxdS6ufNTyBHMtIZO57LRnJj4w9fLY1aM,6852
185
185
  omlish/formats/json/__init__.py,sha256=moSR67Qkju2eYb_qVDtaivepe44mxAnYuC8OCSbtETg,298
186
186
  omlish/formats/json/__main__.py,sha256=1wxxKZVkj_u7HCcewwMIbGuZj_Wph95yrUbm474Op9M,188
187
- omlish/formats/json/cli.py,sha256=4zftNijlIOnGUHYn5J1s4yRDRM1K4udBzS3Kh8R2vNc,3374
187
+ omlish/formats/json/cli.py,sha256=pHFvYji6h_kMUyTgHCuDFofeDVY_5Em0wBqqVOJzDmI,3504
188
188
  omlish/formats/json/json.py,sha256=y8d8WWgzGZDTjzYc_xe9v4T0foXHI-UP7gjCwnHzUIA,828
189
189
  omlish/formats/json/render.py,sha256=6edhSrxXWW3nzRfokp5qaldT0_YAj-HVEan_rErf-vo,3208
190
190
  omlish/formats/json/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -200,15 +200,15 @@ omlish/graphs/dot/items.py,sha256=OWPf0-hjBgS1uyy2QgAEn4IgFHJcEg7sHVWeTx1ghZc,40
200
200
  omlish/graphs/dot/make.py,sha256=RN30gHfJPiXx5Q51kbDdhVJYf59Fr84Lz9J-mXRt9sI,360
201
201
  omlish/graphs/dot/rendering.py,sha256=2UgXvMRN4Z9cfIqLlC7Iu_8bWbwUDEL4opHHkFfSqTw,3630
202
202
  omlish/graphs/dot/utils.py,sha256=_FMwn77WfiiAfLsRTOKWm4IYbNv5kQN22YJ5psw6CWg,801
203
- omlish/http/__init__.py,sha256=zv9D9PPGS49W8fKpU2k12AmUL5vE-vYxStwkz_3yxZ4,624
203
+ omlish/http/__init__.py,sha256=-ENDALr8ehHvivRD6cxIbEC94t0RHhrakf6CQRDTc8o,625
204
204
  omlish/http/asgi.py,sha256=wXhBZ21bEl32Kv9yBrRwUR_7pHEgVtHP8ZZwbasQ6-4,3307
205
- omlish/http/client.py,sha256=Zr5QzjTEo_iILQ0Ypkjr2RZQGPWa6q57BDqkpeT3qsQ,3587
205
+ omlish/http/clients.py,sha256=KPWDw251eBmiYSXw7KafMP9UQHEiLiLdh9ZLPV7jydc,6002
206
206
  omlish/http/collections.py,sha256=s8w5s4Gewgxxhe2Ai0R45PgJYYifrLgTbU3VXVflHj4,260
207
- omlish/http/consts.py,sha256=-O0F6qiVWGGT18j8TMP7UNfHCECg1MmByx05oc7Ae9Q,1985
207
+ omlish/http/consts.py,sha256=FTolezLknKU6WJjk_x2T3a5LEMlnZSqv7gzTq55lxcU,2147
208
208
  omlish/http/cookies.py,sha256=uuOYlHR6e2SC3GM41V0aozK10nef9tYg83Scqpn5-HM,6351
209
209
  omlish/http/dates.py,sha256=Otgp8wRxPgNGyzx8LFowu1vC4EKJYARCiAwLFncpfHM,2875
210
210
  omlish/http/encodings.py,sha256=w2WoKajpaZnQH8j-IBvk5ZFL2O2pAU_iBvZnkocaTlw,164
211
- omlish/http/headers.py,sha256=_1d4cdRoh9rWP8QpMCVLyo5sGEDqLInzJ7GNSlsXY5s,4045
211
+ omlish/http/headers.py,sha256=MO5uDwbViY6Z371Hl5OwTvh2DGl76_PnWHlurNwIsOs,4895
212
212
  omlish/http/json.py,sha256=9XwAsl4966Mxrv-1ytyCqhcE6lbBJw-0_tFZzGszgHE,7440
213
213
  omlish/http/sessions.py,sha256=VZ_WS5uiQG5y7i3u8oKuQMqf8dPKUOjFm_qk_0OvI8c,4793
214
214
  omlish/http/wsgi.py,sha256=czZsVUX-l2YTlMrUjKN49wRoP4rVpS0qpeBn4O5BoMY,948
@@ -433,9 +433,9 @@ omlish/text/delimit.py,sha256=ubPXcXQmtbOVrUsNh5gH1mDq5H-n1y2R4cPL5_DQf68,4928
433
433
  omlish/text/glyphsplit.py,sha256=Ug-dPRO7x-OrNNr8g1y6DotSZ2KH0S-VcOmUobwa4B0,3296
434
434
  omlish/text/indent.py,sha256=6Jj6TFY9unaPa4xPzrnZemJ-fHsV53IamP93XGjSUHs,1274
435
435
  omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
436
- omlish-0.0.0.dev71.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
437
- omlish-0.0.0.dev71.dist-info/METADATA,sha256=UWQoK0TuRSj5SbCN-2CeBmXPBL5rCJUeBJg_qLHv6Q0,4167
438
- omlish-0.0.0.dev71.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
439
- omlish-0.0.0.dev71.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
440
- omlish-0.0.0.dev71.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
441
- omlish-0.0.0.dev71.dist-info/RECORD,,
436
+ omlish-0.0.0.dev72.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
437
+ omlish-0.0.0.dev72.dist-info/METADATA,sha256=fC-RJQd1zeLsNghRMhMtxbjiAbWfSE6bbIBTkW85Rxg,4167
438
+ omlish-0.0.0.dev72.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
439
+ omlish-0.0.0.dev72.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
440
+ omlish-0.0.0.dev72.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
441
+ omlish-0.0.0.dev72.dist-info/RECORD,,
omlish/http/client.py DELETED
@@ -1,142 +0,0 @@
1
- """
2
- TODO:
3
- - return non-200 HttpResponses
4
- - async
5
- - stream
6
- """
7
- import abc
8
- import http.client
9
- import typing as ta
10
- import urllib.error
11
- import urllib.request
12
-
13
- from .. import cached
14
- from .. import dataclasses as dc
15
- from .. import lang
16
- from .headers import CanHttpHeaders
17
- from .headers import HttpHeaders
18
-
19
-
20
- if ta.TYPE_CHECKING:
21
- import httpx
22
- else:
23
- httpx = lang.proxy_import('httpx')
24
-
25
-
26
- @dc.dataclass(frozen=True)
27
- class HttpRequest(lang.Final):
28
- url: str
29
- method: str = 'GET' # noqa
30
-
31
- _: dc.KW_ONLY
32
-
33
- headers: CanHttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
34
- data: bytes | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
35
-
36
- timeout_s: float | None = None
37
-
38
- @cached.property
39
- def headers_(self) -> HttpHeaders | None:
40
- return HttpHeaders(self.headers) if self.headers is not None else None
41
-
42
-
43
- @dc.dataclass(frozen=True, kw_only=True)
44
- class HttpResponse(lang.Final):
45
- code: int
46
-
47
- headers: HttpHeaders | None = dc.xfield(None, repr=dc.truthy_repr)
48
- data: bytes | None = dc.xfield(None, repr_fn=lambda v: '...' if v is not None else None)
49
-
50
- request: HttpRequest
51
- underlying: ta.Any = dc.field(default=None, repr=False)
52
-
53
-
54
- class HttpClientError(Exception):
55
- pass
56
-
57
-
58
- class HttpClient(lang.Abstract):
59
- def __enter__(self) -> ta.Self:
60
- return self
61
-
62
- def __exit__(self, exc_type, exc_val, exc_tb):
63
- pass
64
-
65
- @abc.abstractmethod
66
- def request(self, req: HttpRequest) -> HttpResponse:
67
- raise NotImplementedError
68
-
69
-
70
- class UrllibHttpClient(HttpClient):
71
- def request(self, req: HttpRequest) -> HttpResponse:
72
- try:
73
- with urllib.request.urlopen( # noqa
74
- urllib.request.Request( # noqa
75
- req.url,
76
- method=req.method,
77
- headers=req.headers_ or {}, # type: ignore
78
- data=req.data,
79
- ),
80
- timeout=req.timeout_s,
81
- ) as resp:
82
- return HttpResponse(
83
- code=resp.status,
84
- headers=HttpHeaders(resp.headers.items()),
85
- data=resp.read(),
86
- request=req,
87
- underlying=resp,
88
- )
89
- except (urllib.error.URLError, http.client.HTTPException) as e:
90
- raise HttpClientError from e
91
-
92
-
93
- class HttpxHttpClient(HttpClient):
94
- def request(self, req: HttpRequest) -> HttpResponse:
95
- try:
96
- response = httpx.request(
97
- method=req.method,
98
- url=req.url,
99
- headers=req.headers_ or None, # type: ignore
100
- content=req.data,
101
- timeout=req.timeout_s,
102
- )
103
- return HttpResponse(
104
- code=response.status_code,
105
- headers=HttpHeaders(response.headers.raw),
106
- data=response.content,
107
- request=req,
108
- underlying=response,
109
- )
110
- except httpx.HTTPError as e:
111
- raise HttpClientError from e
112
-
113
-
114
- def client() -> HttpClient:
115
- return UrllibHttpClient()
116
-
117
-
118
- def request(
119
- url: str,
120
- method: str = 'GET',
121
- *,
122
- headers: CanHttpHeaders | None = None,
123
- data: bytes | None = None,
124
-
125
- timeout_s: float | None = None,
126
-
127
- **kwargs: ta.Any,
128
- ) -> HttpResponse:
129
- req = HttpRequest(
130
- url,
131
- method=method,
132
-
133
- headers=headers,
134
- data=data,
135
-
136
- timeout_s=timeout_s,
137
-
138
- **kwargs,
139
- )
140
-
141
- with client() as cli:
142
- return cli.request(req)