python-http_request 0.0.5.3__tar.gz → 0.0.7__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,30 +1,29 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-http_request
3
- Version: 0.0.5.3
3
+ Version: 0.0.7
4
4
  Summary: Python http response utils.
5
5
  Home-page: https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-http_request
6
6
  License: MIT
7
7
  Keywords: http,request
8
8
  Author: ChenyangGao
9
9
  Author-email: wosiwujm@gmail.com
10
- Requires-Python: >=3.10,<4.0
10
+ Requires-Python: >=3.12,<4.0
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
20
19
  Classifier: Programming Language :: Python :: 3 :: Only
21
20
  Classifier: Topic :: Software Development
22
21
  Classifier: Topic :: Software Development :: Libraries
23
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Requires-Dist: integer_tool
25
- Requires-Dist: python-asynctools
26
- Requires-Dist: python-filewrap (>=0.1)
27
- Requires-Dist: python-texttools
23
+ Requires-Dist: integer_tool (>=0.0.2)
24
+ Requires-Dist: python-asynctools (>=0.0.10)
25
+ Requires-Dist: python-filewrap (>=0.2.6)
26
+ Requires-Dist: python-texttools (>=0.0.3)
28
27
  Project-URL: Repository, https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-http_request
29
28
  Description-Content-Type: text/markdown
30
29
 
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python3
2
+ # encoding: utf-8
3
+
4
+ __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
+ __version__ = (0, 0, 7)
6
+ __all__ = [
7
+ "SupportsGeturl", "url_origin", "complete_url", "cookies_str_to_dict", "headers_str_to_dict",
8
+ "encode_multipart_data", "encode_multipart_data_async",
9
+ ]
10
+
11
+ from collections.abc import AsyncIterable, AsyncIterator, Buffer, ItemsView, Iterable, Iterator, Mapping
12
+ from itertools import chain
13
+ from mimetypes import guess_type
14
+ from os import fsdecode
15
+ from os.path import basename
16
+ from re import compile as re_compile, Pattern
17
+ from typing import runtime_checkable, Any, Final, Protocol, TypeVar
18
+ from urllib.parse import quote, urlsplit, urlunsplit
19
+ from uuid import uuid4
20
+
21
+ from asynctools import ensure_aiter, async_chain
22
+ from filewrap import bio_chunk_iter, bio_chunk_async_iter, SupportsRead
23
+ from integer_tool import int_to_bytes
24
+ from texttools import text_to_dict
25
+
26
+
27
+ AnyStr = TypeVar("AnyStr", bytes, str, covariant=True)
28
+
29
+ CRE_URL_SCHEME: Final = re_compile(r"^(?i:[a-z][a-z0-9.+-]*)://")
30
+
31
+
32
+ @runtime_checkable
33
+ class SupportsGeturl(Protocol[AnyStr]):
34
+ def geturl(self) -> AnyStr: ...
35
+
36
+
37
+ def url_origin(url: str, /) -> str:
38
+ if url.startswith("://"):
39
+ url = "http" + url
40
+ elif CRE_URL_SCHEME.match(url) is None:
41
+ url = "http://" + url
42
+ urlp = urlsplit(url)
43
+ scheme, netloc = urlp.scheme, urlp.netloc
44
+ if not netloc:
45
+ netloc = "localhost"
46
+ return f"{scheme}://{netloc}"
47
+
48
+
49
+ def complete_url(url: str, /) -> str:
50
+ if url.startswith("://"):
51
+ url = "http" + url
52
+ elif CRE_URL_SCHEME.match(url) is None:
53
+ url = "http://" + url
54
+ urlp = urlsplit(url)
55
+ repl = {"query": "", "fragment": ""}
56
+ if not urlp.netloc:
57
+ repl["path"] = "localhost"
58
+ return urlunsplit(urlp._replace(**repl)).rstrip("/")
59
+
60
+
61
+ def cookies_str_to_dict(
62
+ cookies: str,
63
+ /,
64
+ kv_sep: str | Pattern[str] = re_compile(r"\s*=\s*"),
65
+ entry_sep: str | Pattern[str] = re_compile(r"\s*;\s*"),
66
+ ) -> dict[str, str]:
67
+ return text_to_dict(cookies.strip(), kv_sep, entry_sep)
68
+
69
+
70
+ def headers_str_to_dict(
71
+ headers: str,
72
+ /,
73
+ kv_sep: str | Pattern[str] = re_compile(r":\s+"),
74
+ entry_sep: str | Pattern[str] = re_compile("\n+"),
75
+ ) -> dict[str, str]:
76
+ return text_to_dict(headers.strip(), kv_sep, entry_sep)
77
+
78
+
79
+ def ensure_bytes(s, /) -> bytes:
80
+ if isinstance(s, bytes):
81
+ return s
82
+ elif isinstance(s, memoryview):
83
+ return s.tobytes()
84
+ elif isinstance(s, Buffer):
85
+ return bytes(s)
86
+ if isinstance(s, int):
87
+ return int_to_bytes(s)
88
+ elif isinstance(s, str):
89
+ return bytes(s, "utf-8")
90
+ try:
91
+ return bytes(s)
92
+ except TypeError:
93
+ return bytes(str(s), "utf-8")
94
+
95
+
96
+ def encode_multipart_data(
97
+ data: None | Mapping[str, Any] = None,
98
+ files: None | Mapping[str, Buffer | SupportsRead[Buffer] | Iterable[Buffer]] = None,
99
+ boundary: None | str = None,
100
+ file_suffix: str = "",
101
+ ) -> tuple[dict, Iterator[Buffer]]:
102
+ if not boundary:
103
+ boundary = uuid4().hex
104
+ suffix = bytes(file_suffix, "ascii")
105
+ if suffix and not suffix.startswith(b"."):
106
+ suffix = b"." + suffix
107
+ headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
108
+
109
+ def encode_data(data) -> Iterator[Buffer]:
110
+ if not data:
111
+ return
112
+ if isinstance(data, Mapping):
113
+ data = ItemsView(data)
114
+ for name, value in data:
115
+ yield boundary_line
116
+ yield b'Content-Disposition: form-data; name="%s"\r\n\r\n' % bytes(quote(name), "ascii")
117
+ yield ensure_bytes(value)
118
+ yield b"\r\n"
119
+
120
+ def encode_files(files) -> Iterator[Buffer]:
121
+ if not files:
122
+ return
123
+ if isinstance(files, Mapping):
124
+ files = ItemsView(files)
125
+ for name, file in files:
126
+ headers: dict[bytes, bytes] = {b"Content-Disposition": b'form-data; name="%s"' % quote(name).encode("ascii")}
127
+ filename: bytes | str = ""
128
+ if isinstance(file, (list, tuple)):
129
+ match file:
130
+ case [file]:
131
+ pass
132
+ case [file_name, file]:
133
+ pass
134
+ case [file_name, file, file_type]:
135
+ if file_type:
136
+ headers[b"Content-Type"] = ensure_bytes(file_type)
137
+ case [file_name, file, file_type, file_headers, *rest]:
138
+ if isinstance(file_headers, Mapping):
139
+ file_headers = ItemsView(file_headers)
140
+ for k, v in file_headers:
141
+ headers[ensure_bytes(k).title()] = ensure_bytes(v)
142
+ if file_type:
143
+ headers[b"Content-Type"] = ensure_bytes(file_type)
144
+ if isinstance(file, Buffer):
145
+ pass
146
+ elif isinstance(file, str):
147
+ file = file.encode("utf-8")
148
+ elif hasattr(file, "read"):
149
+ file = bio_chunk_iter(file)
150
+ if not filename:
151
+ path = getattr(file, "name", None)
152
+ if path:
153
+ filename = basename(path)
154
+ if b"Content-Type" not in headers:
155
+ headers[b"Content-Type"] = ensure_bytes(guess_type(fsdecode(filename))[0] or b"application/octet-stream")
156
+ if filename:
157
+ name = bytes(quote(filename), "ascii")
158
+ if not name.endswith(suffix):
159
+ name += suffix
160
+ headers[b"Content-Disposition"] += b'; filename="%s"' % name
161
+ else:
162
+ headers[b"Content-Disposition"] += b'; filename="%032x%s"' % (uuid4().int, suffix)
163
+ yield boundary_line
164
+ for entry in headers.items():
165
+ yield b"%s: %s\r\n" % entry
166
+ yield b"\r\n"
167
+ if isinstance(file, Buffer):
168
+ yield file
169
+ else:
170
+ yield from file
171
+ yield b"\r\n"
172
+
173
+ boundary_line = b"--%s\r\n" % boundary.encode("utf-8")
174
+ return headers, chain(encode_data(data), encode_files(files), (b'--%s--\r\n' % boundary.encode("ascii"),))
175
+
176
+
177
+ def encode_multipart_data_async(
178
+ data: None | Mapping[str, Any] = None,
179
+ files: None | Mapping[str, Buffer | SupportsRead[Buffer] | Iterable[Buffer] | AsyncIterable[Buffer]] = None,
180
+ boundary: None | str = None,
181
+ file_suffix: str = "",
182
+ ) -> tuple[dict, AsyncIterator[Buffer]]:
183
+ if not boundary:
184
+ boundary = uuid4().hex
185
+ suffix = bytes(file_suffix, "ascii")
186
+ if suffix and not suffix.startswith(b"."):
187
+ suffix = b"." + suffix
188
+ headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
189
+
190
+ async def encode_data(data) -> AsyncIterator[Buffer]:
191
+ if not data:
192
+ return
193
+ if isinstance(data, Mapping):
194
+ data = ItemsView(data)
195
+ for name, value in data:
196
+ yield boundary_line
197
+ yield b'Content-Disposition: form-data; name="%s"\r\n\r\n' % bytes(quote(name), "ascii")
198
+ yield ensure_bytes(value)
199
+ yield b"\r\n"
200
+
201
+ async def encode_files(files) -> AsyncIterator[Buffer]:
202
+ if not files:
203
+ return
204
+ if isinstance(files, Mapping):
205
+ files = ItemsView(files)
206
+ for name, file in files:
207
+ headers: dict[bytes, bytes] = {b"Content-Disposition": b'form-data; name="%s"' % quote(name).encode("ascii")}
208
+ filename: bytes | str = ""
209
+ if isinstance(file, (list, tuple)):
210
+ match file:
211
+ case [file]:
212
+ pass
213
+ case [file_name, file]:
214
+ pass
215
+ case [file_name, file, file_type]:
216
+ if file_type:
217
+ headers[b"Content-Type"] = ensure_bytes(file_type)
218
+ case [file_name, file, file_type, file_headers, *rest]:
219
+ if isinstance(file_headers, Mapping):
220
+ file_headers = ItemsView(file_headers)
221
+ for k, v in file_headers:
222
+ headers[ensure_bytes(k).title()] = ensure_bytes(v)
223
+ if file_type:
224
+ headers[b"Content-Type"] = ensure_bytes(file_type)
225
+ if isinstance(file, Buffer):
226
+ pass
227
+ elif isinstance(file, str):
228
+ file = file.encode("utf-8")
229
+ elif hasattr(file, "read"):
230
+ file = bio_chunk_async_iter(file)
231
+ if not filename:
232
+ path = getattr(file, "name", None)
233
+ if path:
234
+ filename = basename(path)
235
+ if b"Content-Type" not in headers:
236
+ headers[b"Content-Type"] = ensure_bytes(guess_type(fsdecode(filename))[0] or b"application/octet-stream")
237
+ else:
238
+ file = ensure_aiter(file)
239
+ if filename:
240
+ name = bytes(quote(filename), "ascii")
241
+ if not name.endswith(suffix):
242
+ name += suffix
243
+ headers[b"Content-Disposition"] += b'; filename="%s"' % name
244
+ else:
245
+ headers[b"Content-Disposition"] += b'; filename="%032x%s"' % (uuid4().int, suffix)
246
+ yield boundary_line
247
+ for entry in headers.items():
248
+ yield b"%s: %s\r\n" % entry
249
+ yield b"\r\n"
250
+ if isinstance(file, Buffer):
251
+ yield file
252
+ else:
253
+ async for chunk in file:
254
+ yield chunk
255
+ yield b"\r\n"
256
+
257
+ boundary_line = b"--%s\r\n" % boundary.encode("utf-8")
258
+ return headers, async_chain(encode_data(data), encode_files(files), (b'--%s--\r\n' % boundary.encode("ascii"),))
259
+
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-http_request"
3
- version = "0.0.5.3"
3
+ version = "0.0.7"
4
4
  description = "Python http response utils."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
@@ -13,7 +13,7 @@ classifiers = [
13
13
  "Development Status :: 5 - Production/Stable",
14
14
  "Programming Language :: Python",
15
15
  "Programming Language :: Python :: 3",
16
- "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.12",
17
17
  "Programming Language :: Python :: 3 :: Only",
18
18
  "Operating System :: OS Independent",
19
19
  "Intended Audience :: Developers",
@@ -26,11 +26,11 @@ include = [
26
26
  ]
27
27
 
28
28
  [tool.poetry.dependencies]
29
- python = "^3.10"
30
- python-asynctools = "*"
31
- python-filewrap = ">=0.1"
32
- python-texttools = "*"
33
- integer_tool = "*"
29
+ python = "^3.12"
30
+ python-asynctools = ">=0.0.10"
31
+ python-filewrap = ">=0.2.6"
32
+ python-texttools = ">=0.0.3"
33
+ integer_tool = ">=0.0.2"
34
34
 
35
35
  [build-system]
36
36
  requires = ["poetry-core"]
@@ -1,169 +0,0 @@
1
- #!/usr/bin/env python3
2
- # encoding: utf-8
3
-
4
- __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
- __version__ = (0, 0, 5)
6
- __all__ = [
7
- "SupportsGeturl", "url_origin", "complete_url", "cookies_str_to_dict", "headers_str_to_dict",
8
- "encode_multipart_data", "encode_multipart_data_async",
9
- ]
10
-
11
- from itertools import chain
12
- from collections.abc import AsyncIterable, AsyncIterator, ItemsView, Iterable, Iterator, Mapping
13
- from re import compile as re_compile, Pattern
14
- from typing import runtime_checkable, Any, Final, Protocol, TypeVar
15
- from urllib.parse import quote, urlsplit, urlunsplit
16
- from uuid import uuid4
17
-
18
- from asynctools import ensure_aiter, async_chain
19
- from filewrap import bio_chunk_iter, bio_chunk_async_iter, Buffer, SupportsRead
20
- from integer_tool import int_to_bytes
21
- from texttools import text_to_dict
22
-
23
-
24
- AnyStr = TypeVar("AnyStr", bytes, str, covariant=True)
25
-
26
- CRE_URL_SCHEME: Final = re_compile(r"^(?i:[a-z][a-z0-9.+-]*)://")
27
-
28
-
29
- @runtime_checkable
30
- class SupportsGeturl(Protocol[AnyStr]):
31
- def geturl(self) -> AnyStr: ...
32
-
33
-
34
- def url_origin(url: str, /) -> str:
35
- if url.startswith("://"):
36
- url = "http" + url
37
- elif CRE_URL_SCHEME.match(url) is None:
38
- url = "http://" + url
39
- urlp = urlsplit(url)
40
- scheme, netloc = urlp.scheme, urlp.netloc
41
- if not netloc:
42
- netloc = "localhost"
43
- return f"{scheme}://{netloc}"
44
-
45
-
46
- def complete_url(url: str, /) -> str:
47
- if url.startswith("://"):
48
- url = "http" + url
49
- elif CRE_URL_SCHEME.match(url) is None:
50
- url = "http://" + url
51
- urlp = urlsplit(url)
52
- repl = {"query": "", "fragment": ""}
53
- if not urlp.netloc:
54
- repl["path"] = "localhost"
55
- return urlunsplit(urlp._replace(**repl)).rstrip("/")
56
-
57
-
58
- def cookies_str_to_dict(
59
- cookies: str,
60
- /,
61
- kv_sep: str | Pattern[str] = re_compile(r"\s*=\s*"),
62
- entry_sep: str | Pattern[str] = re_compile(r"\s*;\s*"),
63
- ) -> dict[str, str]:
64
- return text_to_dict(cookies.strip(), kv_sep, entry_sep)
65
-
66
-
67
- def headers_str_to_dict(
68
- headers: str,
69
- /,
70
- kv_sep: str | Pattern[str] = re_compile(r":\s+"),
71
- entry_sep: str | Pattern[str] = re_compile("\n+"),
72
- ) -> dict[str, str]:
73
- return text_to_dict(headers.strip(), kv_sep, entry_sep)
74
-
75
-
76
- def ensure_bytes(s, /) -> Buffer:
77
- if isinstance(s, Buffer):
78
- return s
79
- if isinstance(s, int):
80
- return int_to_bytes(s)
81
- elif isinstance(s, str):
82
- return bytes(s, "utf-8")
83
- try:
84
- return bytes(s)
85
- except TypeError:
86
- return bytes(str(s), "utf-8")
87
-
88
-
89
- def encode_multipart_data(
90
- data: None | Mapping[str, Any] = None,
91
- files: None | Mapping[str, Buffer | SupportsRead[Buffer] | Iterable[Buffer]] = None,
92
- boundary: None | str = None,
93
- ) -> tuple[dict, Iterator[Buffer]]:
94
- if not boundary:
95
- boundary = uuid4().bytes.hex()
96
- headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
97
-
98
- def encode_data(data) -> Iterator[Buffer]:
99
- if not data:
100
- return
101
- if isinstance(data, Mapping):
102
- data = ItemsView(data)
103
- for name, value in data:
104
- yield boundary_line
105
- yield b'Content-Disposition: form-data; name="%s"\r\n\r\n' % bytes(quote(name), "ascii")
106
- yield ensure_bytes(value)
107
- yield b"\r\n"
108
-
109
- def encode_files(files) -> Iterator[Buffer]:
110
- if not files:
111
- return
112
- if isinstance(files, Mapping):
113
- files = ItemsView(files)
114
- for name, file in files:
115
- yield boundary_line
116
- yield b'Content-Disposition: form-data; name="%s"\r\nContent-Type: application/octet-stream\r\n\r\n' % bytes(quote(name), "ascii")
117
- if isinstance(file, Buffer):
118
- yield file
119
- elif hasattr(file, "read"):
120
- yield from bio_chunk_iter(file)
121
- else:
122
- yield from file
123
- yield b"\r\n"
124
-
125
- boundary_line = b"--%s\r\n" % boundary.encode("utf-8")
126
- return headers, chain(encode_data(data), encode_files(files), (b'--%s--\r\n' % boundary.encode("ascii"),))
127
-
128
-
129
- def encode_multipart_data_async(
130
- data: None | Mapping[str, Any] = None,
131
- files: None | Mapping[str, Buffer | SupportsRead[Buffer] | Iterable[Buffer] | AsyncIterable[Buffer]] = None,
132
- boundary: None | str = None,
133
- ) -> tuple[dict, AsyncIterator[Buffer]]:
134
- if not boundary:
135
- boundary = uuid4().bytes.hex()
136
- headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
137
-
138
- async def encode_data(data) -> AsyncIterator[Buffer]:
139
- if not data:
140
- return
141
- if isinstance(data, Mapping):
142
- data = ItemsView(data)
143
- for name, value in data:
144
- yield boundary_line
145
- yield b'Content-Disposition: form-data; name="%s"\r\n\r\n' % bytes(quote(name), "ascii")
146
- yield ensure_bytes(value)
147
- yield b"\r\n"
148
-
149
- async def encode_files(files) -> AsyncIterator[Buffer]:
150
- if not files:
151
- return
152
- if isinstance(files, Mapping):
153
- files = ItemsView(files)
154
- for name, file in files:
155
- yield boundary_line
156
- yield b'Content-Disposition: form-data; name="%s"\r\nContent-Type: application/octet-stream\r\n\r\n' % bytes(quote(name), "ascii")
157
- if isinstance(file, Buffer):
158
- yield file
159
- elif hasattr(file, "read"):
160
- async for b in bio_chunk_async_iter(file):
161
- yield b
162
- else:
163
- async for b in ensure_aiter(file):
164
- yield b
165
- yield b"\r\n"
166
-
167
- boundary_line = b"--%s\r\n" % boundary.encode("utf-8")
168
- return headers, async_chain(encode_data(data), encode_files(files), (b'--%s--\r\n' % boundary.encode("ascii"),))
169
-