yta-httpx 0.0.1__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.
- yta_httpx/__init__.py +3 -0
- yta_httpx/client/__init__.py +596 -0
- yta_httpx/client/utils.py +62 -0
- yta_httpx/enums.py +65 -0
- yta_httpx/exceptions.py +15 -0
- yta_httpx/types.py +34 -0
- yta_httpx-0.0.1.dist-info/METADATA +22 -0
- yta_httpx-0.0.1.dist-info/RECORD +10 -0
- yta_httpx-0.0.1.dist-info/WHEEL +4 -0
- yta_httpx-0.0.1.dist-info/licenses/LICENSE +19 -0
yta_httpx/__init__.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
from yta_httpx.client.utils import _raise_exception_if_status_code, content_type_to_parse_mode
|
|
2
|
+
from yta_httpx.enums import RequestMethod, ResponseParseMode, ResponseTransportMode
|
|
3
|
+
from yta_httpx.exceptions import HttpRequestError
|
|
4
|
+
from yta_httpx.types import ParsedResponseType, ParsedCompleteResponseType, ParsedStreamResponseType
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpClient:
|
|
12
|
+
"""
|
|
13
|
+
HttpClient to send requests.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
timeout: float = 60.0,
|
|
19
|
+
default_headers: Union[dict, None] = None
|
|
20
|
+
):
|
|
21
|
+
self._client = httpx.AsyncClient(
|
|
22
|
+
timeout = timeout,
|
|
23
|
+
headers = default_headers
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
*For internal use only*
|
|
27
|
+
|
|
28
|
+
The `httpx.AsyncClient` instance to be used to
|
|
29
|
+
send the reqeusts we need.
|
|
30
|
+
"""
|
|
31
|
+
self.get: _HttpClientMethodNamespace = _HttpClientMethodNamespace(
|
|
32
|
+
http_client = self,
|
|
33
|
+
method = RequestMethod.GET
|
|
34
|
+
)
|
|
35
|
+
"""
|
|
36
|
+
Shortcut to send `GET` requests.
|
|
37
|
+
"""
|
|
38
|
+
self.post: _HttpClientMethodNamespace = _HttpClientMethodNamespace(
|
|
39
|
+
http_client = self,
|
|
40
|
+
method = RequestMethod.POST
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
Shortcut to send `POST` requests.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
async def close(
|
|
47
|
+
self
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
*Asynchronous method*
|
|
51
|
+
|
|
52
|
+
Force the client to be closed asynchronously.
|
|
53
|
+
"""
|
|
54
|
+
await self._client.aclose()
|
|
55
|
+
|
|
56
|
+
def __del__(
|
|
57
|
+
self
|
|
58
|
+
):
|
|
59
|
+
if not self._client.is_closed:
|
|
60
|
+
warnings.warn(
|
|
61
|
+
'HttpClient was not properly closed'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def __aenter__(
|
|
65
|
+
self
|
|
66
|
+
):
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
async def __aexit__(
|
|
70
|
+
self,
|
|
71
|
+
exc_type,
|
|
72
|
+
exc,
|
|
73
|
+
tb
|
|
74
|
+
):
|
|
75
|
+
await self.close()
|
|
76
|
+
|
|
77
|
+
async def request_complete(
|
|
78
|
+
self,
|
|
79
|
+
method: RequestMethod,
|
|
80
|
+
url: str,
|
|
81
|
+
*,
|
|
82
|
+
headers: Union[dict, None] = None,
|
|
83
|
+
params: Union[dict, None] = None,
|
|
84
|
+
json: Union[dict, None] = None,
|
|
85
|
+
data: Any = None,
|
|
86
|
+
content: Union[bytes, None] = None,
|
|
87
|
+
response_parse_mode: ResponseParseMode = ResponseParseMode.RESPONSE,
|
|
88
|
+
timeout: int = 60
|
|
89
|
+
) -> ParsedCompleteResponseType:
|
|
90
|
+
"""
|
|
91
|
+
*Asynchronous method*
|
|
92
|
+
|
|
93
|
+
Method to send a completely customizable request
|
|
94
|
+
that will return the whole response and will not
|
|
95
|
+
be streamed (using not the `stream=True` option).
|
|
96
|
+
"""
|
|
97
|
+
return request_complete(
|
|
98
|
+
client = self._client,
|
|
99
|
+
method = method,
|
|
100
|
+
url = url,
|
|
101
|
+
# TODO: What about the '*' (?)
|
|
102
|
+
headers = headers,
|
|
103
|
+
params = params,
|
|
104
|
+
json = json,
|
|
105
|
+
data = data,
|
|
106
|
+
content = content,
|
|
107
|
+
response_parse_mode = response_parse_mode,
|
|
108
|
+
timeout = timeout
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def request_stream(
|
|
112
|
+
self,
|
|
113
|
+
method: RequestMethod,
|
|
114
|
+
url: str,
|
|
115
|
+
*,
|
|
116
|
+
headers: Union[dict, None] = None,
|
|
117
|
+
params: Union[dict, None] = None,
|
|
118
|
+
json: Union[dict, None] = None,
|
|
119
|
+
data: Any = None,
|
|
120
|
+
content: Union[bytes, None] = None,
|
|
121
|
+
response_parse_mode: ResponseParseMode = ResponseParseMode.RESPONSE,
|
|
122
|
+
chunk_size: int = 8192,
|
|
123
|
+
timeout: int = 60
|
|
124
|
+
) -> ParsedCompleteResponseType:
|
|
125
|
+
"""
|
|
126
|
+
*Asynchronous method*
|
|
127
|
+
|
|
128
|
+
Method to send a completely customizable request
|
|
129
|
+
that will return the response by chunks, as a
|
|
130
|
+
streamed response (using the `stream=True` option).
|
|
131
|
+
"""
|
|
132
|
+
return request_stream(
|
|
133
|
+
client = self._client,
|
|
134
|
+
method = method,
|
|
135
|
+
url = url,
|
|
136
|
+
# TODO: What about the '*' (?)
|
|
137
|
+
headers = headers,
|
|
138
|
+
params = params,
|
|
139
|
+
json = json,
|
|
140
|
+
data = data,
|
|
141
|
+
content = content,
|
|
142
|
+
response_parse_mode = response_parse_mode,
|
|
143
|
+
chunk_size = chunk_size,
|
|
144
|
+
timeout = timeout
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
class _HttpClientMethodNamespace:
|
|
148
|
+
"""
|
|
149
|
+
*For internal use only*
|
|
150
|
+
|
|
151
|
+
Class to include the funcionality related to
|
|
152
|
+
the method of the `HttpClient` requests.
|
|
153
|
+
|
|
154
|
+
Class to be used as a hierarchical system.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
http_client: HttpClient,
|
|
160
|
+
method: RequestMethod
|
|
161
|
+
):
|
|
162
|
+
method = RequestMethod.to_enum(method)
|
|
163
|
+
|
|
164
|
+
self._http_client: 'HttpClient' = http_client
|
|
165
|
+
"""
|
|
166
|
+
*For internal use only*
|
|
167
|
+
|
|
168
|
+
The reference to the father `HttpClient` instance.
|
|
169
|
+
"""
|
|
170
|
+
self._method: RequestMethod = method
|
|
171
|
+
"""
|
|
172
|
+
*For internal use only*
|
|
173
|
+
|
|
174
|
+
The method that will be used for the requests that
|
|
175
|
+
are made by this instance.
|
|
176
|
+
"""
|
|
177
|
+
self.stream = _HttpClientTransportModeNamespace(
|
|
178
|
+
http_client = http_client,
|
|
179
|
+
method = method,
|
|
180
|
+
transport_mode = ResponseTransportMode.STREAM
|
|
181
|
+
)
|
|
182
|
+
"""
|
|
183
|
+
Attribute to send a request that will be streamed.
|
|
184
|
+
|
|
185
|
+
If you are using a `stream`, check this code below
|
|
186
|
+
to handle the response:
|
|
187
|
+
```
|
|
188
|
+
async for chunk in http_client.get.stream.as_bytes(...):
|
|
189
|
+
print(chunk)
|
|
190
|
+
```
|
|
191
|
+
"""
|
|
192
|
+
self.complete = _HttpClientTransportModeNamespace(
|
|
193
|
+
http_client = http_client,
|
|
194
|
+
method = method,
|
|
195
|
+
transport_mode = ResponseTransportMode.COMPLETE
|
|
196
|
+
)
|
|
197
|
+
"""
|
|
198
|
+
Attribute to send a request that will not be streamed.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
class _HttpClientTransportModeNamespace:
|
|
202
|
+
"""
|
|
203
|
+
*For internal use only*
|
|
204
|
+
|
|
205
|
+
Class to include the funcionality related to
|
|
206
|
+
the transport mode of the `HttpClient` requests.
|
|
207
|
+
|
|
208
|
+
Class to be used as a hierarchical system.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
http_client: HttpClient,
|
|
214
|
+
method: RequestMethod,
|
|
215
|
+
transport_mode: ResponseTransportMode
|
|
216
|
+
):
|
|
217
|
+
method = RequestMethod.to_enum(method)
|
|
218
|
+
transport_mode = ResponseTransportMode.to_enum(transport_mode)
|
|
219
|
+
|
|
220
|
+
self._http_client: 'HttpClient' = http_client
|
|
221
|
+
"""
|
|
222
|
+
*For internal use only*
|
|
223
|
+
|
|
224
|
+
The reference to the father `HttpClient` instance.
|
|
225
|
+
"""
|
|
226
|
+
self._method: RequestMethod = method
|
|
227
|
+
"""
|
|
228
|
+
*For internal use only*
|
|
229
|
+
|
|
230
|
+
The method that will be used for the requests that
|
|
231
|
+
are made by this instance.
|
|
232
|
+
"""
|
|
233
|
+
self._transport_mode: ResponseTransportMode = transport_mode
|
|
234
|
+
"""
|
|
235
|
+
*For internal use only*
|
|
236
|
+
|
|
237
|
+
The transport mode that will be used for the requests
|
|
238
|
+
that are made by this instance.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
self.as_json = _HttpClientParseModeNamespace(
|
|
242
|
+
http_client = http_client,
|
|
243
|
+
method = method,
|
|
244
|
+
transport_mode = transport_mode,
|
|
245
|
+
parse_mode = ResponseParseMode.JSON
|
|
246
|
+
)
|
|
247
|
+
"""
|
|
248
|
+
Attribute to send a request that will parse the result
|
|
249
|
+
as a json.
|
|
250
|
+
"""
|
|
251
|
+
self.as_text = _HttpClientParseModeNamespace(
|
|
252
|
+
http_client = http_client,
|
|
253
|
+
method = method,
|
|
254
|
+
transport_mode = transport_mode,
|
|
255
|
+
parse_mode = ResponseParseMode.TEXT
|
|
256
|
+
)
|
|
257
|
+
"""
|
|
258
|
+
Attribute to send a request that will parse the result
|
|
259
|
+
as text.
|
|
260
|
+
"""
|
|
261
|
+
self.as_bytes = _HttpClientParseModeNamespace(
|
|
262
|
+
http_client = http_client,
|
|
263
|
+
method = method,
|
|
264
|
+
transport_mode = transport_mode,
|
|
265
|
+
parse_mode = ResponseParseMode.BYTES
|
|
266
|
+
)
|
|
267
|
+
"""
|
|
268
|
+
Attribute to send a request that will parse the result
|
|
269
|
+
as bytes.
|
|
270
|
+
"""
|
|
271
|
+
self.as_lines = _HttpClientParseModeNamespace(
|
|
272
|
+
http_client = http_client,
|
|
273
|
+
method = method,
|
|
274
|
+
transport_mode = transport_mode,
|
|
275
|
+
parse_mode = ResponseParseMode.LINES
|
|
276
|
+
)
|
|
277
|
+
"""
|
|
278
|
+
Attribute to send a request that will parse the result
|
|
279
|
+
as lines.
|
|
280
|
+
"""
|
|
281
|
+
self.as_response = _HttpClientParseModeNamespace(
|
|
282
|
+
http_client = http_client,
|
|
283
|
+
method = method,
|
|
284
|
+
transport_mode = transport_mode,
|
|
285
|
+
parse_mode = ResponseParseMode.RESPONSE
|
|
286
|
+
)
|
|
287
|
+
"""
|
|
288
|
+
Attribute to send a request that will return the
|
|
289
|
+
whole response as it is, as an object, parsed not.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
class _HttpClientParseModeNamespace:
|
|
293
|
+
"""
|
|
294
|
+
*For internal use only*
|
|
295
|
+
|
|
296
|
+
Class to include the funcionality related to
|
|
297
|
+
the parse mode of the `HttpClient` requests.
|
|
298
|
+
|
|
299
|
+
Class to be used as a hierarchical system.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(
|
|
303
|
+
self,
|
|
304
|
+
http_client: HttpClient,
|
|
305
|
+
method: RequestMethod,
|
|
306
|
+
transport_mode: ResponseTransportMode,
|
|
307
|
+
parse_mode: ResponseParseMode
|
|
308
|
+
):
|
|
309
|
+
method = RequestMethod.to_enum(method)
|
|
310
|
+
transport_mode = ResponseTransportMode.to_enum(transport_mode)
|
|
311
|
+
parse_mode = ResponseParseMode.to_enum(parse_mode)
|
|
312
|
+
|
|
313
|
+
self._http_client: 'HttpClient' = http_client
|
|
314
|
+
"""
|
|
315
|
+
*For internal use only*
|
|
316
|
+
|
|
317
|
+
The reference to the father `HttpClient` instance.
|
|
318
|
+
"""
|
|
319
|
+
self._method: RequestMethod = method
|
|
320
|
+
"""
|
|
321
|
+
*For internal use only*
|
|
322
|
+
|
|
323
|
+
The method that will be used for the requests that
|
|
324
|
+
are made by this instance.
|
|
325
|
+
"""
|
|
326
|
+
self._transport_mode: ResponseTransportMode = transport_mode
|
|
327
|
+
"""
|
|
328
|
+
*For internal use only*
|
|
329
|
+
|
|
330
|
+
The transport mode that will be used for the requests
|
|
331
|
+
that are made by this instance.
|
|
332
|
+
"""
|
|
333
|
+
self._parse_mode: ResponseParseMode = parse_mode
|
|
334
|
+
"""
|
|
335
|
+
*For internal use only*
|
|
336
|
+
|
|
337
|
+
The parse mode that will be used for the requests
|
|
338
|
+
that are made by this instance.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
async def __call__(
|
|
342
|
+
self,
|
|
343
|
+
url: str,
|
|
344
|
+
headers: Union[dict, None] = None,
|
|
345
|
+
params: Union[dict, None] = None,
|
|
346
|
+
json: Union[dict, None] = None,
|
|
347
|
+
data: Any = None,
|
|
348
|
+
content: Union[bytes, None] = None,
|
|
349
|
+
chunk_size: int = 8196,
|
|
350
|
+
timeout: int = 60,
|
|
351
|
+
# TODO: No more specific args (?)
|
|
352
|
+
**kwargs
|
|
353
|
+
):
|
|
354
|
+
if self._transport_mode == ResponseTransportMode.COMPLETE:
|
|
355
|
+
return await request_complete(
|
|
356
|
+
client = self._http_client._client,
|
|
357
|
+
method = self._method,
|
|
358
|
+
url = url,
|
|
359
|
+
headers = headers,
|
|
360
|
+
params = params,
|
|
361
|
+
json = json,
|
|
362
|
+
data = data,
|
|
363
|
+
content = content,
|
|
364
|
+
response_parse_mode = self._parse_mode,
|
|
365
|
+
timeout = timeout,
|
|
366
|
+
**kwargs
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return request_stream(
|
|
370
|
+
client = self._http_client._client,
|
|
371
|
+
method = self._method,
|
|
372
|
+
url = url,
|
|
373
|
+
headers = headers,
|
|
374
|
+
params = params,
|
|
375
|
+
json = json,
|
|
376
|
+
data = data,
|
|
377
|
+
content = content,
|
|
378
|
+
response_parse_mode = self._parse_mode,
|
|
379
|
+
chunk_size = chunk_size,
|
|
380
|
+
timeout = timeout
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# Utils below
|
|
385
|
+
async def request_complete(
|
|
386
|
+
client: 'AsyncClient',
|
|
387
|
+
method: RequestMethod,
|
|
388
|
+
url: str,
|
|
389
|
+
*,
|
|
390
|
+
headers: Union[dict, None] = None,
|
|
391
|
+
params: Union[dict, None] = None,
|
|
392
|
+
json: Union[dict, None] = None,
|
|
393
|
+
data: Any = None,
|
|
394
|
+
content: Union[bytes, None] = None,
|
|
395
|
+
response_parse_mode: ResponseParseMode = ResponseParseMode.RESPONSE,
|
|
396
|
+
timeout: int = 60
|
|
397
|
+
) -> ParsedCompleteResponseType:
|
|
398
|
+
response = await client.request(
|
|
399
|
+
method = method.value,
|
|
400
|
+
url = url,
|
|
401
|
+
content = content,
|
|
402
|
+
data = data,
|
|
403
|
+
json = json,
|
|
404
|
+
params = params,
|
|
405
|
+
headers = headers,
|
|
406
|
+
timeout = timeout
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
await _raise_exception_if_status_code(response)
|
|
410
|
+
|
|
411
|
+
return await parse_response_content(
|
|
412
|
+
response = response,
|
|
413
|
+
transport_mode = ResponseTransportMode.COMPLETE,
|
|
414
|
+
parse_mode = response_parse_mode
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
async def request_stream(
|
|
419
|
+
client: 'AsyncClient',
|
|
420
|
+
method: RequestMethod,
|
|
421
|
+
url: str,
|
|
422
|
+
*,
|
|
423
|
+
headers: Union[dict, None] = None,
|
|
424
|
+
params: Union[dict, None] = None,
|
|
425
|
+
json: Union[dict, None] = None,
|
|
426
|
+
data: Any = None,
|
|
427
|
+
content: Union[bytes, None] = None,
|
|
428
|
+
response_parse_mode: ResponseParseMode = ResponseParseMode.RESPONSE,
|
|
429
|
+
chunk_size: int = 8192,
|
|
430
|
+
timeout: int = 60
|
|
431
|
+
) -> ParsedStreamResponseType:
|
|
432
|
+
async with client.stream(
|
|
433
|
+
method = method.value,
|
|
434
|
+
url = url,
|
|
435
|
+
content = content,
|
|
436
|
+
data = data,
|
|
437
|
+
json = json,
|
|
438
|
+
params = params,
|
|
439
|
+
headers = headers,
|
|
440
|
+
timeout = timeout
|
|
441
|
+
) as response:
|
|
442
|
+
await _raise_exception_if_status_code(response)
|
|
443
|
+
|
|
444
|
+
parsed_stream = await parse_response_content(
|
|
445
|
+
response = response,
|
|
446
|
+
transport_mode = ResponseTransportMode.STREAM,
|
|
447
|
+
parse_mode = response_parse_mode,
|
|
448
|
+
chunk_size = chunk_size
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
async for chunk in parsed_stream:
|
|
452
|
+
yield chunk
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def _parse_stream_response(
|
|
456
|
+
response: httpx.Response,
|
|
457
|
+
parse_mode: ResponseParseMode,
|
|
458
|
+
chunk_size: int = 8192
|
|
459
|
+
) -> ParsedStreamResponseType:
|
|
460
|
+
"""
|
|
461
|
+
*For internal use only*
|
|
462
|
+
|
|
463
|
+
Method used by the `parse_response_content`
|
|
464
|
+
to parse the content when the request has
|
|
465
|
+
been done as a stream request and not as
|
|
466
|
+
a complete request.
|
|
467
|
+
"""
|
|
468
|
+
# TODO: Can we validate if it was streaming (?)
|
|
469
|
+
parse_mode = ResponseParseMode.to_enum(parse_mode)
|
|
470
|
+
|
|
471
|
+
if parse_mode == ResponseParseMode.BYTES:
|
|
472
|
+
async for chunk in response.aiter_bytes(chunk_size):
|
|
473
|
+
yield chunk
|
|
474
|
+
elif parse_mode == ResponseParseMode.TEXT:
|
|
475
|
+
async for chunk in response.aiter_text():
|
|
476
|
+
yield chunk
|
|
477
|
+
elif parse_mode == ResponseParseMode.LINES:
|
|
478
|
+
async for line in response.aiter_lines():
|
|
479
|
+
yield line
|
|
480
|
+
elif parse_mode == ResponseParseMode.JSON:
|
|
481
|
+
async for line in response.aiter_lines():
|
|
482
|
+
if not line:
|
|
483
|
+
continue
|
|
484
|
+
# TODO: Check this...
|
|
485
|
+
import json
|
|
486
|
+
# TODO: Check if possible or not (?)
|
|
487
|
+
yield json.loads(line)
|
|
488
|
+
elif parse_mode == ResponseParseMode.RESPONSE:
|
|
489
|
+
# TODO: Non-sense, but here it is, maybe exc instead (?)
|
|
490
|
+
yield response
|
|
491
|
+
|
|
492
|
+
async def _parse_complete_response(
|
|
493
|
+
response: httpx.Response,
|
|
494
|
+
parse_mode: ResponseParseMode,
|
|
495
|
+
) -> ParsedCompleteResponseType:
|
|
496
|
+
"""
|
|
497
|
+
*For internal use only*
|
|
498
|
+
|
|
499
|
+
Method used by the `parse_response_content`
|
|
500
|
+
to parse the content when the request has
|
|
501
|
+
been done as a complete request and not as
|
|
502
|
+
a stream request.
|
|
503
|
+
"""
|
|
504
|
+
parse_mode = ResponseParseMode.to_enum(parse_mode)
|
|
505
|
+
|
|
506
|
+
if parse_mode == ResponseParseMode.JSON:
|
|
507
|
+
return _parse_response_content_as_json(response = response)
|
|
508
|
+
elif parse_mode == ResponseParseMode.TEXT:
|
|
509
|
+
return response.text
|
|
510
|
+
elif parse_mode == ResponseParseMode.BYTES:
|
|
511
|
+
return response.content
|
|
512
|
+
elif parse_mode == ResponseParseMode.LINES:
|
|
513
|
+
# TODO: Non-sense, but here it is, maybe exc instead (?)
|
|
514
|
+
return response.text.splitlines()
|
|
515
|
+
elif parse_mode == ResponseParseMode.RESPONSE:
|
|
516
|
+
return response
|
|
517
|
+
|
|
518
|
+
async def parse_response_content(
|
|
519
|
+
response: httpx.Response,
|
|
520
|
+
transport_mode: ResponseTransportMode = ResponseTransportMode.COMPLETE,
|
|
521
|
+
parse_mode: ResponseParseMode = ResponseParseMode.AUTO,
|
|
522
|
+
chunk_size: int = 8192
|
|
523
|
+
) -> ParsedResponseType:
|
|
524
|
+
"""
|
|
525
|
+
Parse the given `response` according to the
|
|
526
|
+
`transport_mode` given and the `parse_mode` also
|
|
527
|
+
provided.
|
|
528
|
+
|
|
529
|
+
If the `parse_mode` is `AUTO`, we need to detect
|
|
530
|
+
the real `parse_mode` we need to apply based on
|
|
531
|
+
the `content_type` of the `response` provided,
|
|
532
|
+
which will be autodetected by us.
|
|
533
|
+
"""
|
|
534
|
+
parse_mode = ResponseParseMode.to_enum(parse_mode)
|
|
535
|
+
transport_mode = ResponseTransportMode.to_enum(transport_mode)
|
|
536
|
+
|
|
537
|
+
if parse_mode == ResponseParseMode.AUTO:
|
|
538
|
+
parse_mode = _resolve_parse_mode(
|
|
539
|
+
response = response,
|
|
540
|
+
parse_mode = parse_mode
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Complete
|
|
544
|
+
if transport_mode == ResponseTransportMode.COMPLETE:
|
|
545
|
+
return await _parse_complete_response(
|
|
546
|
+
response = response,
|
|
547
|
+
parse_mode = parse_mode
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Stream
|
|
551
|
+
return _parse_stream_response(
|
|
552
|
+
response = response,
|
|
553
|
+
parse_mode = parse_mode,
|
|
554
|
+
chunk_size = chunk_size
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def _resolve_parse_mode(
|
|
558
|
+
response: 'Response',
|
|
559
|
+
parse_mode: ResponseParseMode
|
|
560
|
+
) -> ResponseParseMode:
|
|
561
|
+
"""
|
|
562
|
+
Method to resolve the `ResponseParseMode`
|
|
563
|
+
according to the `parse_mode` option provided
|
|
564
|
+
and the `content_type` of the `response`,
|
|
565
|
+
because if `ResponseParseMode.AUTO` is given it
|
|
566
|
+
will be autoselected based on it.
|
|
567
|
+
"""
|
|
568
|
+
if parse_mode != ResponseParseMode.AUTO:
|
|
569
|
+
return parse_mode
|
|
570
|
+
|
|
571
|
+
content_type = response.headers.get(
|
|
572
|
+
'content-type',
|
|
573
|
+
''
|
|
574
|
+
).lower()
|
|
575
|
+
|
|
576
|
+
return content_type_to_parse_mode(content_type)
|
|
577
|
+
|
|
578
|
+
def _parse_response_content_as_json(
|
|
579
|
+
response: httpx.Response
|
|
580
|
+
) -> dict:
|
|
581
|
+
"""
|
|
582
|
+
*For internal use only*
|
|
583
|
+
|
|
584
|
+
Method to try to obtain the `response` as a `JSON`,
|
|
585
|
+
raising an exception if something is wrong.
|
|
586
|
+
"""
|
|
587
|
+
try:
|
|
588
|
+
return response.json()
|
|
589
|
+
|
|
590
|
+
except Exception as e:
|
|
591
|
+
raise HttpRequestError(
|
|
592
|
+
status_code = response.status_code,
|
|
593
|
+
message = f'Invalid JSON response: {e}',
|
|
594
|
+
response_text = response.text
|
|
595
|
+
)
|
|
596
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from yta_httpx.enums import ResponseContentType, ResponseParseMode
|
|
2
|
+
from yta_httpx.exceptions import HttpRequestError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def content_type_to_parse_mode(
|
|
6
|
+
content_type: ResponseContentType
|
|
7
|
+
) -> ResponseParseMode:
|
|
8
|
+
"""
|
|
9
|
+
Transform the `content_type` provided into
|
|
10
|
+
the `ResponseParseMode` that has to be used
|
|
11
|
+
to parse that type of response.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
content_type = ResponseContentType.to_enum(content_type)
|
|
15
|
+
except:
|
|
16
|
+
print(f'The {content_type} is not registered in our system, ask the administrator to add it. Using "ResponseParseMode.BYTES" by default.')
|
|
17
|
+
|
|
18
|
+
return ResponseParseMode.BYTES
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
We must have all the `ResponseContentType` items
|
|
22
|
+
here with their corresponding mode, and if it is
|
|
23
|
+
not registered it will be detected above.
|
|
24
|
+
"""
|
|
25
|
+
return {
|
|
26
|
+
ResponseContentType.APPLICATION_JSON: ResponseParseMode.JSON,
|
|
27
|
+
ResponseContentType.TEXT_PLAIN: ResponseParseMode.TEXT,
|
|
28
|
+
ResponseContentType.TEXT_HTML: ResponseParseMode.TEXT,
|
|
29
|
+
ResponseContentType.APPLICATION_XML: ResponseParseMode.TEXT,
|
|
30
|
+
ResponseContentType.IMAGE_JPEG: ResponseParseMode.BYTES,
|
|
31
|
+
ResponseContentType.VIDEO_MP4: ResponseParseMode.BYTES,
|
|
32
|
+
ResponseContentType.AUDIO_WAV: ResponseParseMode.BYTES,
|
|
33
|
+
ResponseContentType.TEXT_EVENT_STREAM: ResponseParseMode.LINES,
|
|
34
|
+
ResponseContentType.APPLICATION_XHTML_XML: ResponseParseMode.TEXT,
|
|
35
|
+
ResponseContentType.APPLICATION_JAVASCRIPT: ResponseParseMode.TEXT,
|
|
36
|
+
ResponseContentType.APPLICATION_PROBLEM_JSON: ResponseParseMode.JSON
|
|
37
|
+
}[content_type]
|
|
38
|
+
|
|
39
|
+
async def _raise_exception_if_status_code(
|
|
40
|
+
response: 'httpx.Response'
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
*Asynchronous method*
|
|
44
|
+
|
|
45
|
+
*For internal use only*
|
|
46
|
+
|
|
47
|
+
Raise an exception if the `status_code` is
|
|
48
|
+
indicating the error extracted from the
|
|
49
|
+
response itself.
|
|
50
|
+
"""
|
|
51
|
+
if response.status_code >= 400:
|
|
52
|
+
try:
|
|
53
|
+
text = await response.aread()
|
|
54
|
+
text = text.decode()
|
|
55
|
+
except:
|
|
56
|
+
text = None
|
|
57
|
+
|
|
58
|
+
raise HttpRequestError(
|
|
59
|
+
status_code = response.status_code,
|
|
60
|
+
message = 'Request failed',
|
|
61
|
+
response_text = text
|
|
62
|
+
)
|
yta_httpx/enums.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from yta_constants.enum import YTAEnum as Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RequestMethod(Enum):
|
|
5
|
+
"""
|
|
6
|
+
The different methods of a request.
|
|
7
|
+
"""
|
|
8
|
+
GET = 'GET'
|
|
9
|
+
POST = 'POST'
|
|
10
|
+
|
|
11
|
+
class ResponseTransportMode(Enum):
|
|
12
|
+
"""
|
|
13
|
+
Enum class to indicate the way you want to load
|
|
14
|
+
and receive the content of the response, that
|
|
15
|
+
can be at once, or by chunks through an iterator
|
|
16
|
+
that is caused by an streamed response.
|
|
17
|
+
"""
|
|
18
|
+
STREAM = 'stream'
|
|
19
|
+
"""
|
|
20
|
+
The response content will be given chunk by chunk
|
|
21
|
+
with an iterator.
|
|
22
|
+
"""
|
|
23
|
+
COMPLETE = 'complete'
|
|
24
|
+
"""
|
|
25
|
+
The response content will be given all of it at
|
|
26
|
+
once.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
class ResponseParseMode(Enum):
|
|
30
|
+
"""
|
|
31
|
+
The way we want to parse the content that comes
|
|
32
|
+
as a result of a request.
|
|
33
|
+
"""
|
|
34
|
+
AUTO = 'auto'
|
|
35
|
+
"""
|
|
36
|
+
The way to parse the content will be chosen
|
|
37
|
+
according to the response content type.
|
|
38
|
+
"""
|
|
39
|
+
JSON = 'json'
|
|
40
|
+
TEXT = 'text'
|
|
41
|
+
BYTES = 'bytes'
|
|
42
|
+
LINES = 'lines'
|
|
43
|
+
RESPONSE = 'response'
|
|
44
|
+
"""
|
|
45
|
+
This mode will return the whole response and as
|
|
46
|
+
it was received, not only the content.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
class ResponseContentType(Enum):
|
|
50
|
+
"""
|
|
51
|
+
The `content_type` field that could come in a
|
|
52
|
+
request.
|
|
53
|
+
"""
|
|
54
|
+
APPLICATION_JSON = 'application/json'
|
|
55
|
+
TEXT_PLAIN = 'text/plain'
|
|
56
|
+
TEXT_HTML = 'text/html'
|
|
57
|
+
APPLICATION_XML = 'application/xml'
|
|
58
|
+
IMAGE_JPEG = 'image/jpeg'
|
|
59
|
+
VIDEO_MP4 = 'video/mp4'
|
|
60
|
+
AUDIO_WAV = 'audio/wav'
|
|
61
|
+
APPLICATION_OCTET_STREAM = 'application/octet-stream'
|
|
62
|
+
TEXT_EVENT_STREAM = 'text/event-stream'
|
|
63
|
+
APPLICATION_XHTML_XML = 'application/xhtml+xml'
|
|
64
|
+
APPLICATION_JAVASCRIPT = 'application/javascript'
|
|
65
|
+
APPLICATION_PROBLEM_JSON = 'application/problem+json'
|
yta_httpx/exceptions.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HttpRequestError(Exception):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
status_code: int,
|
|
8
|
+
message: str,
|
|
9
|
+
response_text: Union[str, None] = None
|
|
10
|
+
):
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
self.message = message
|
|
13
|
+
self.response_text = response_text
|
|
14
|
+
|
|
15
|
+
super().__init__(f'{status_code} - {message}')
|
yta_httpx/types.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Union, Any
|
|
2
|
+
from typing import Any, AsyncGenerator, Union
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
JsonType = Union[
|
|
8
|
+
dict[str, Any],
|
|
9
|
+
list[Any]
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
StreamChunkType = Union[
|
|
13
|
+
bytes,
|
|
14
|
+
str,
|
|
15
|
+
JsonType
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
ParsedCompleteResponseType = Union[
|
|
19
|
+
JsonType,
|
|
20
|
+
str,
|
|
21
|
+
bytes,
|
|
22
|
+
httpx.Response
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
ParsedStreamResponseType = AsyncGenerator[
|
|
26
|
+
StreamChunkType,
|
|
27
|
+
None
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
ParsedResponseType = Union[
|
|
31
|
+
ParsedCompleteResponseType,
|
|
32
|
+
ParsedStreamResponseType
|
|
33
|
+
]
|
|
34
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yta-httpx
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Youtube Autonomous Httpx Module.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: danialcala94
|
|
7
|
+
Author-email: danielalcalavalera@gmail.com
|
|
8
|
+
Requires-Python: >=3.8,<3.14
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: httpx (>=0.0.1,<9999.9.9)
|
|
17
|
+
Requires-Dist: yta_constants (>=0.0.1,<1.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Youtube Autonomous Httpx Module
|
|
21
|
+
|
|
22
|
+
The module to implement Http clients to make requests easily.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
yta_httpx/__init__.py,sha256=_kbCgTevMudl-5sKvCSG8BiUoqmkCVt4jSf7GaLranE,45
|
|
2
|
+
yta_httpx/client/__init__.py,sha256=uwZAkbGuIUsOeOEf_1iOOvlL2QgZbU8AaJwD6zlwNlQ,18056
|
|
3
|
+
yta_httpx/client/utils.py,sha256=fZh0-6xvXTD7q1vKDIU5d-b-QyNAiK3OzYmFGXUviSA,2244
|
|
4
|
+
yta_httpx/enums.py,sha256=-WU7oT8f3Dq1a_QX9WjAwuR2N5VRdfnnfduLEajTmuI,1772
|
|
5
|
+
yta_httpx/exceptions.py,sha256=eQXzOZjMVMOGiD4MyuIbZAZ-nimVkUHHVYaVtCAkUL0,378
|
|
6
|
+
yta_httpx/types.py,sha256=ZoExXSXgZQTMiQg2d69MyarmfXp5XPcG4Myei21DVwg,502
|
|
7
|
+
yta_httpx-0.0.1.dist-info/licenses/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
|
8
|
+
yta_httpx-0.0.1.dist-info/METADATA,sha256=WMO7BGB_KILBeYne9gasoJlSbAwFbsVwXhAM1EKdgek,787
|
|
9
|
+
yta_httpx-0.0.1.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
|
|
10
|
+
yta_httpx-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|