python-http_request 0.0.8__tar.gz → 0.0.9__tar.gz

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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-http_request
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: Python http response utils.
5
- Home-page: https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-http_request
5
+ Home-page: https://github.com/ChenyangGao/python-modules/tree/main/python-http_request
6
6
  License: MIT
7
7
  Keywords: http,request
8
8
  Author: ChenyangGao
@@ -20,12 +20,14 @@ Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Classifier: Topic :: Software Development
21
21
  Classifier: Topic :: Software Development :: Libraries
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
- Requires-Dist: integer_tool (>=0.0.5)
23
+ Requires-Dist: http_response (>=0.0.4)
24
+ Requires-Dist: orjson
24
25
  Requires-Dist: python-asynctools (>=0.1.3)
25
26
  Requires-Dist: python-dicttools (>=0.0.1)
27
+ Requires-Dist: python-ensure (>=0.0.1)
26
28
  Requires-Dist: python-filewrap (>=0.2.8)
27
29
  Requires-Dist: python-texttools (>=0.0.4)
28
- Project-URL: Repository, https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-http_request
30
+ Project-URL: Repository, https://github.com/ChenyangGao/python-modules/tree/main/python-http_request
29
31
  Description-Content-Type: text/markdown
30
32
 
31
33
  # Python http response utils.
@@ -2,41 +2,58 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
- __version__ = (0, 0, 8)
5
+ __version__ = (0, 0, 9)
6
6
  __all__ = [
7
- "SupportsGeturl", "url_origin", "complete_url", "cookies_str_to_dict",
8
- "headers_str_to_dict_by_lines", "headers_str_to_dict",
9
- "encode_multipart_data", "encode_multipart_data_async",
7
+ "SupportsGeturl", "url_origin", "complete_url", "ensure_ascii_url",
8
+ "urlencode", "cookies_str_to_dict", "headers_str_to_dict_by_lines",
9
+ "headers_str_to_dict", "encode_multipart_data", "encode_multipart_data_async",
10
+ "normalize_request_args",
10
11
  ]
11
12
 
12
13
  from collections import UserString
13
14
  from collections.abc import (
14
- AsyncIterable, AsyncIterator, Buffer, Iterable, Iterator, Mapping,
15
+ AsyncIterable, AsyncIterator, Buffer, Iterable, Iterator,
16
+ Mapping, Sequence,
15
17
  )
18
+ from decimal import Decimal
19
+ from fractions import Fraction
16
20
  from io import TextIOWrapper
17
21
  from itertools import batched
18
22
  from mimetypes import guess_type
23
+ from numbers import Integral, Real
19
24
  from os import PathLike
20
25
  from os.path import basename
21
26
  from re import compile as re_compile, Pattern
22
- from typing import runtime_checkable, Any, Final, Protocol, TypeVar
23
- from urllib.parse import quote, urlsplit, urlunsplit
27
+ from string import punctuation
28
+ from typing import runtime_checkable, Any, Final, Protocol, TypedDict
29
+ from urllib.parse import quote, urlparse, urlunparse
24
30
  from uuid import uuid4
31
+ from yarl import URL
25
32
 
26
33
  from asynctools import async_map
27
- from dicttools import iter_items
34
+ from dicttools import dict_map, iter_items
35
+ from ensure import ensure_bytes, ensure_buffer, ensure_str
28
36
  from filewrap import bio_chunk_iter, bio_chunk_async_iter, SupportsRead
29
- from integer_tool import int_to_bytes
37
+ from http_response import get_charset, get_mimetype
38
+ from orjson import dumps as json_dumps
30
39
  from texttools import text_to_dict
31
40
 
32
41
 
33
- AnyStr = TypeVar("AnyStr", bytes, str, covariant=True)
42
+ type string = Buffer | str | UserString
34
43
 
44
+ QUERY_KEY_TRANSTAB: Final = {k: f"%{k:02X}" for k in b"&="}
35
45
  CRE_URL_SCHEME_match: Final = re_compile(r"(?i:[a-z][a-z0-9.+-]*)://").match
36
46
 
37
47
 
48
+ class RequestArgs(TypedDict):
49
+ method: str
50
+ url: str
51
+ data: Buffer | Iterable[Buffer] | AsyncIterable[Buffer]
52
+ headers: dict[str, str]
53
+
54
+
38
55
  @runtime_checkable
39
- class SupportsGeturl(Protocol[AnyStr]):
56
+ class SupportsGeturl[AnyStr: (bytes, str)](Protocol):
40
57
  def geturl(self) -> AnyStr: ...
41
58
 
42
59
 
@@ -49,7 +66,7 @@ def url_origin(url: str, /, default_port: int = 0) -> str:
49
66
  url = "http" + url
50
67
  elif not CRE_URL_SCHEME_match(url):
51
68
  url = "http://" + url
52
- urlp = urlsplit(url)
69
+ urlp = urlparse(url)
53
70
  scheme, netloc = urlp.scheme or "http", urlp.netloc or "localhost"
54
71
  if default_port and not urlp.port:
55
72
  netloc = netloc.removesuffix(":") + f":{default_port}"
@@ -65,7 +82,7 @@ def complete_url(url: str, /, default_port: int = 0) -> str:
65
82
  url = "http" + url
66
83
  elif not CRE_URL_SCHEME_match(url):
67
84
  url = "http://" + url
68
- urlp = urlsplit(url)
85
+ urlp = urlparse(url)
69
86
  repl = {"query": "", "fragment": ""}
70
87
  if not urlp.scheme:
71
88
  repl["scheme"] = "http"
@@ -75,7 +92,53 @@ def complete_url(url: str, /, default_port: int = 0) -> str:
75
92
  if default_port and not urlp.port:
76
93
  netloc = netloc.removesuffix(":") + f":{default_port}"
77
94
  repl["netloc"] = netloc
78
- return urlunsplit(urlp._replace(**repl)).rstrip("/")
95
+ return urlunparse(urlp._replace(**repl)).rstrip("/")
96
+
97
+
98
+ def ensure_ascii_url(url: str, /) -> str:
99
+ if url.isascii():
100
+ return url
101
+ return quote(url, safe=punctuation)
102
+
103
+
104
+ def urlencode(
105
+ payload: string | Mapping[Any, Any] | Iterable[tuple[Any, Any]],
106
+ /,
107
+ encoding: str = "utf-8",
108
+ errors: str = "strict",
109
+ ) -> str:
110
+ if isinstance(payload, str):
111
+ return payload
112
+ elif isinstance(payload, UserString):
113
+ return str(payload)
114
+ elif isinstance(payload, Buffer):
115
+ return str(payload, encoding, errors)
116
+ def encode_iter(payload: Iterable[tuple[Any, Any]], /) -> Iterator[str]:
117
+ for i, (k, v) in enumerate(payload):
118
+ if i:
119
+ yield "&"
120
+ if isinstance(k, Buffer):
121
+ k = str(k, encoding, errors)
122
+ else:
123
+ k = str(k)
124
+ yield k.translate(QUERY_KEY_TRANSTAB)
125
+ yield "="
126
+ if v is True:
127
+ yield "true"
128
+ elif v is False:
129
+ yield "false"
130
+ elif v is None:
131
+ yield "null"
132
+ elif isinstance(v, (str, UserString)):
133
+ pass
134
+ elif isinstance(v, Buffer):
135
+ v = str(v, encoding, errors)
136
+ elif isinstance(v, (Mapping, Iterable)):
137
+ v = json_dumps(v, default=json_default).decode("utf-8")
138
+ else:
139
+ v = str(v)
140
+ yield v.replace("&", "%26")
141
+ return "".join(encode_iter(iter_items(payload)))
79
142
 
80
143
 
81
144
  def cookies_str_to_dict(
@@ -103,46 +166,6 @@ def headers_str_to_dict_by_lines(headers: str, /, ) -> dict[str, str]:
103
166
  return dict(batched(lines, 2)) # type: ignore
104
167
 
105
168
 
106
- def ensure_bytes(
107
- o,
108
- /,
109
- encoding: str = "utf-8",
110
- errors: str = "strict",
111
- ) -> bytes:
112
- if isinstance(o, bytes):
113
- return o
114
- elif isinstance(o, memoryview):
115
- return o.tobytes()
116
- elif isinstance(o, Buffer):
117
- return bytes(o)
118
- elif isinstance(o, int):
119
- return int_to_bytes(o)
120
- elif isinstance(o, (str, UserString)):
121
- return o.encode(encoding, errors)
122
- try:
123
- return bytes(o)
124
- except TypeError:
125
- return bytes(str(o), encoding, errors)
126
-
127
-
128
- def ensure_buffer(
129
- o,
130
- /,
131
- encoding: str = "utf-8",
132
- errors: str = "strict",
133
- ) -> Buffer:
134
- if isinstance(o, Buffer):
135
- return o
136
- elif isinstance(o, int):
137
- return int_to_bytes(o)
138
- elif isinstance(o, (str, UserString)):
139
- return o.encode(encoding, errors)
140
- try:
141
- return bytes(o)
142
- except TypeError:
143
- return bytes(str(o), encoding, errors)
144
-
145
-
146
169
  def encode_multipart_data(
147
170
  data: None | Mapping[Buffer | str, Any] = None,
148
171
  files: None | Mapping[Buffer | str, Any] = None,
@@ -330,3 +353,97 @@ def encode_multipart_data_async(
330
353
 
331
354
  return {"content-type": "multipart/form-data; boundary="+boundary}, encode_iter()
332
355
 
356
+
357
+ def json_default(o, /):
358
+ if isinstance(o, Mapping):
359
+ return dict(o)
360
+ elif isinstance(o, Buffer):
361
+ return ensure_str(o)
362
+ elif isinstance(o, UserString):
363
+ return str(o)
364
+ elif isinstance(o, Integral):
365
+ return int(o)
366
+ elif isinstance(o, (Real, Fraction, Decimal)):
367
+ try:
368
+ return float(o)
369
+ except Exception:
370
+ return str(o)
371
+ elif isinstance(o, (Iterator, Sequence)):
372
+ return list(o)
373
+ else:
374
+ return str(o)
375
+
376
+
377
+ def normalize_request_args(
378
+ method: string,
379
+ url: string | SupportsGeturl | URL,
380
+ params: Any = None,
381
+ data: Any = None,
382
+ json: Any = None,
383
+ headers: None | Mapping[string, Any] | Iterable[tuple[string, Any]] = None,
384
+ ensure_ascii: bool = False,
385
+ ) -> RequestArgs:
386
+ method = ensure_str(method).upper()
387
+ if isinstance(url, SupportsGeturl):
388
+ url = url.geturl()
389
+ elif isinstance(url, URL):
390
+ url = str(url)
391
+ url = complete_url(ensure_str(url))
392
+ if params and (params := urlencode(params)):
393
+ urlp = urlparse(url)
394
+ if query := urlp.query:
395
+ params = query + "&" + params
396
+ url = urlunparse(urlp._replace(query=params))
397
+ if ensure_ascii:
398
+ url = ensure_ascii_url(url)
399
+ headers_ = dict_map(
400
+ headers or (),
401
+ key=lambda k: ensure_str(k).lower(),
402
+ value=ensure_str,
403
+ )
404
+ content_type = headers_.get("content-type", "")
405
+ charset = get_charset(content_type)
406
+ mimetype = get_mimetype(charset).lower()
407
+ if data is not None:
408
+ if isinstance(data, Buffer):
409
+ pass
410
+ elif isinstance(data, (str, UserString)):
411
+ data = data.encode(charset)
412
+ elif isinstance(data, AsyncIterable):
413
+ data = async_map(ensure_buffer, data)
414
+ elif isinstance(data, Iterator):
415
+ data = map(ensure_buffer, data)
416
+ elif mimetype == "application/json":
417
+ if charset == "utf-8":
418
+ data = json_dumps(data, default=json_default)
419
+ else:
420
+ from json import dumps
421
+ data = dumps(data, default=json_default).encode(charset)
422
+ elif isinstance(data, (Mapping, Sequence)):
423
+ if data:
424
+ data = urlencode(data, charset).encode(charset)
425
+ if mimetype != "application/x-www-form-urlencoded":
426
+ headers_["content-type"] = "application/x-www-form-urlencoded"
427
+ else:
428
+ data = str(data).encode(charset)
429
+ elif json is not None:
430
+ if isinstance(json, Buffer):
431
+ data = json
432
+ elif isinstance(data, AsyncIterable):
433
+ data = async_map(ensure_buffer, data)
434
+ if charset == "utf-8":
435
+ data = json_dumps(data, default=json_default)
436
+ else:
437
+ from json import dumps
438
+ data = dumps(data, default=json_default).encode(charset)
439
+ if mimetype != "application/json":
440
+ headers_["content-type"] = "application/json; charset=" + charset
441
+ elif mimetype == "application/json":
442
+ data = b"null"
443
+ return {
444
+ "url": url,
445
+ "method": method,
446
+ "data": data,
447
+ "headers": headers_
448
+ }
449
+
@@ -1,12 +1,12 @@
1
1
  [tool.poetry]
2
2
  name = "python-http_request"
3
- version = "0.0.8"
3
+ version = "0.0.9"
4
4
  description = "Python http response utils."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
7
7
  readme = "readme.md"
8
- homepage = "https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-http_request"
9
- repository = "https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-http_request"
8
+ homepage = "https://github.com/ChenyangGao/python-modules/tree/main/python-http_request"
9
+ repository = "https://github.com/ChenyangGao/python-modules/tree/main/python-http_request"
10
10
  keywords = ["http", "request"]
11
11
  classifiers = [
12
12
  "License :: OSI Approved :: MIT License",
@@ -27,11 +27,13 @@ include = [
27
27
 
28
28
  [tool.poetry.dependencies]
29
29
  python = "^3.12"
30
+ http_response = ">=0.0.4"
31
+ orjson = "*"
30
32
  python-asynctools = ">=0.1.3"
31
33
  python-dicttools = ">=0.0.1"
34
+ python-ensure = ">=0.0.1"
32
35
  python-filewrap = ">=0.2.8"
33
36
  python-texttools = ">=0.0.4"
34
- integer_tool = ">=0.0.5"
35
37
 
36
38
  [build-system]
37
39
  requires = ["poetry-core"]