python-http_request 0.0.8__tar.gz → 0.0.10__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.10
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,71 +2,100 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
- __version__ = (0, 0, 8)
5
+ __version__ = (0, 0, 10)
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
 
43
- def url_origin(url: str, /, default_port: int = 0) -> str:
60
+ def url_origin(
61
+ url: str,
62
+ /,
63
+ default_port: int = 0,
64
+ ) -> str:
44
65
  if url.startswith("/"):
45
66
  url = "http://localhost" + url
46
67
  elif url.startswith("//"):
47
68
  url = "http:" + url
48
69
  elif url.startswith("://"):
49
70
  url = "http" + url
50
- elif not CRE_URL_SCHEME_match(url):
51
- url = "http://" + url
52
- urlp = urlsplit(url)
71
+ urlp = urlparse(url)
53
72
  scheme, netloc = urlp.scheme or "http", urlp.netloc or "localhost"
54
73
  if default_port and not urlp.port:
55
74
  netloc = netloc.removesuffix(":") + f":{default_port}"
56
75
  return f"{scheme}://{netloc}"
57
76
 
58
77
 
59
- def complete_url(url: str, /, default_port: int = 0) -> str:
78
+ def complete_url(
79
+ url: str,
80
+ /,
81
+ default_port: int = 0,
82
+ clean: bool = False,
83
+ ) -> str:
60
84
  if url.startswith("/"):
61
85
  url = "http://localhost" + url
62
86
  elif url.startswith("//"):
63
87
  url = "http:" + url
64
88
  elif url.startswith("://"):
65
89
  url = "http" + url
66
- elif not CRE_URL_SCHEME_match(url):
67
- url = "http://" + url
68
- urlp = urlsplit(url)
69
- repl = {"query": "", "fragment": ""}
90
+ if not (clean or default_port):
91
+ if not CRE_URL_SCHEME_match(url):
92
+ url = "http://" + url
93
+ return url
94
+ urlp = urlparse(url)
95
+ if clean:
96
+ repl = {"params": "", "query": "", "fragment": ""}
97
+ else:
98
+ repl = {}
70
99
  if not urlp.scheme:
71
100
  repl["scheme"] = "http"
72
101
  netloc = urlp.netloc
@@ -74,8 +103,57 @@ def complete_url(url: str, /, default_port: int = 0) -> str:
74
103
  netloc = "localhost"
75
104
  if default_port and not urlp.port:
76
105
  netloc = netloc.removesuffix(":") + f":{default_port}"
77
- repl["netloc"] = netloc
78
- return urlunsplit(urlp._replace(**repl)).rstrip("/")
106
+ if netloc != urlp.netloc:
107
+ repl["netloc"] = netloc
108
+ if not repl:
109
+ return url
110
+ return urlunparse(urlp._replace(**repl)).rstrip("/")
111
+
112
+
113
+ def ensure_ascii_url(url: str, /) -> str:
114
+ if url.isascii():
115
+ return url
116
+ return quote(url, safe=punctuation)
117
+
118
+
119
+ def urlencode(
120
+ payload: string | Mapping[Any, Any] | Iterable[tuple[Any, Any]],
121
+ /,
122
+ encoding: str = "utf-8",
123
+ errors: str = "strict",
124
+ ) -> str:
125
+ if isinstance(payload, str):
126
+ return payload
127
+ elif isinstance(payload, UserString):
128
+ return str(payload)
129
+ elif isinstance(payload, Buffer):
130
+ return str(payload, encoding, errors)
131
+ def encode_iter(payload: Iterable[tuple[Any, Any]], /) -> Iterator[str]:
132
+ for i, (k, v) in enumerate(payload):
133
+ if i:
134
+ yield "&"
135
+ if isinstance(k, Buffer):
136
+ k = str(k, encoding, errors)
137
+ else:
138
+ k = str(k)
139
+ yield k.translate(QUERY_KEY_TRANSTAB)
140
+ yield "="
141
+ if v is True:
142
+ yield "true"
143
+ elif v is False:
144
+ yield "false"
145
+ elif v is None:
146
+ yield "null"
147
+ elif isinstance(v, (str, UserString)):
148
+ pass
149
+ elif isinstance(v, Buffer):
150
+ v = str(v, encoding, errors)
151
+ elif isinstance(v, (Mapping, Iterable)):
152
+ v = json_dumps(v, default=json_default).decode("utf-8")
153
+ else:
154
+ v = str(v)
155
+ yield v.replace("&", "%26")
156
+ return "".join(encode_iter(iter_items(payload)))
79
157
 
80
158
 
81
159
  def cookies_str_to_dict(
@@ -103,46 +181,6 @@ def headers_str_to_dict_by_lines(headers: str, /, ) -> dict[str, str]:
103
181
  return dict(batched(lines, 2)) # type: ignore
104
182
 
105
183
 
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
184
  def encode_multipart_data(
147
185
  data: None | Mapping[Buffer | str, Any] = None,
148
186
  files: None | Mapping[Buffer | str, Any] = None,
@@ -330,3 +368,97 @@ def encode_multipart_data_async(
330
368
 
331
369
  return {"content-type": "multipart/form-data; boundary="+boundary}, encode_iter()
332
370
 
371
+
372
+ def json_default(o, /):
373
+ if isinstance(o, Mapping):
374
+ return dict(o)
375
+ elif isinstance(o, Buffer):
376
+ return ensure_str(o)
377
+ elif isinstance(o, UserString):
378
+ return str(o)
379
+ elif isinstance(o, Integral):
380
+ return int(o)
381
+ elif isinstance(o, (Real, Fraction, Decimal)):
382
+ try:
383
+ return float(o)
384
+ except Exception:
385
+ return str(o)
386
+ elif isinstance(o, (Iterator, Sequence)):
387
+ return list(o)
388
+ else:
389
+ return str(o)
390
+
391
+
392
+ def normalize_request_args(
393
+ method: string,
394
+ url: string | SupportsGeturl | URL,
395
+ params: Any = None,
396
+ data: Any = None,
397
+ json: Any = None,
398
+ headers: None | Mapping[string, Any] | Iterable[tuple[string, Any]] = None,
399
+ ensure_ascii: bool = False,
400
+ ) -> RequestArgs:
401
+ method = ensure_str(method).upper()
402
+ if isinstance(url, SupportsGeturl):
403
+ url = url.geturl()
404
+ elif isinstance(url, URL):
405
+ url = str(url)
406
+ url = complete_url(ensure_str(url))
407
+ if params and (params := urlencode(params)):
408
+ urlp = urlparse(url)
409
+ if query := urlp.query:
410
+ params = query + "&" + params
411
+ url = urlunparse(urlp._replace(query=params))
412
+ if ensure_ascii:
413
+ url = ensure_ascii_url(url)
414
+ headers_ = dict_map(
415
+ headers or (),
416
+ key=lambda k: ensure_str(k).lower(),
417
+ value=ensure_str,
418
+ )
419
+ content_type = headers_.get("content-type", "")
420
+ charset = get_charset(content_type)
421
+ mimetype = get_mimetype(charset).lower()
422
+ if data is not None:
423
+ if isinstance(data, Buffer):
424
+ pass
425
+ elif isinstance(data, (str, UserString)):
426
+ data = data.encode(charset)
427
+ elif isinstance(data, AsyncIterable):
428
+ data = async_map(ensure_buffer, data)
429
+ elif isinstance(data, Iterator):
430
+ data = map(ensure_buffer, data)
431
+ elif mimetype == "application/json":
432
+ if charset == "utf-8":
433
+ data = json_dumps(data, default=json_default)
434
+ else:
435
+ from json import dumps
436
+ data = dumps(data, default=json_default).encode(charset)
437
+ elif isinstance(data, (Mapping, Sequence)):
438
+ if data:
439
+ data = urlencode(data, charset).encode(charset)
440
+ if mimetype != "application/x-www-form-urlencoded":
441
+ headers_["content-type"] = "application/x-www-form-urlencoded"
442
+ else:
443
+ data = str(data).encode(charset)
444
+ elif json is not None:
445
+ if isinstance(json, Buffer):
446
+ data = json
447
+ elif isinstance(data, AsyncIterable):
448
+ data = async_map(ensure_buffer, data)
449
+ if charset == "utf-8":
450
+ data = json_dumps(data, default=json_default)
451
+ else:
452
+ from json import dumps
453
+ data = dumps(data, default=json_default).encode(charset)
454
+ if mimetype != "application/json":
455
+ headers_["content-type"] = "application/json; charset=" + charset
456
+ elif mimetype == "application/json":
457
+ data = b"null"
458
+ return {
459
+ "url": url,
460
+ "method": method,
461
+ "data": data,
462
+ "headers": headers_
463
+ }
464
+
@@ -1,12 +1,12 @@
1
1
  [tool.poetry]
2
2
  name = "python-http_request"
3
- version = "0.0.8"
3
+ version = "0.0.10"
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"]