mm-std 0.3.24__py3-none-any.whl → 0.3.26__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.
- mm_std/__init__.py +3 -0
- mm_std/http/__init__.py +0 -0
- mm_std/http/http_request.py +121 -0
- mm_std/http/response.py +93 -0
- mm_std/http_.py +33 -20
- {mm_std-0.3.24.dist-info → mm_std-0.3.26.dist-info}/METADATA +1 -1
- {mm_std-0.3.24.dist-info → mm_std-0.3.26.dist-info}/RECORD +8 -5
- {mm_std-0.3.24.dist-info → mm_std-0.3.26.dist-info}/WHEEL +0 -0
mm_std/__init__.py
CHANGED
@@ -19,6 +19,9 @@ from .date import utc_now as utc_now
|
|
19
19
|
from .date import utc_random as utc_random
|
20
20
|
from .dict import replace_empty_dict_values as replace_empty_dict_values
|
21
21
|
from .env import get_dotenv as get_dotenv
|
22
|
+
from .http.http_request import http_request as http_request
|
23
|
+
from .http.response import HttpError as HttpError
|
24
|
+
from .http.response import HttpResponse as HttpResponse
|
22
25
|
from .http_ import CHROME_USER_AGENT as CHROME_USER_AGENT
|
23
26
|
from .http_ import FIREFOX_USER_AGENT as FIREFOX_USER_AGENT
|
24
27
|
from .http_ import HResponse as HResponse
|
mm_std/http/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import aiohttp
|
2
|
+
from aiohttp.typedefs import LooseCookies, Query
|
3
|
+
from aiohttp_socks import ProxyConnector
|
4
|
+
from multidict import CIMultiDictProxy
|
5
|
+
|
6
|
+
from mm_std.http.response import HttpError, HttpResponse
|
7
|
+
|
8
|
+
|
9
|
+
async def http_request(
|
10
|
+
url: str,
|
11
|
+
*,
|
12
|
+
method: str = "GET",
|
13
|
+
params: Query | None = None,
|
14
|
+
data: dict[str, object] | None = None,
|
15
|
+
json: dict[str, object] | None = None,
|
16
|
+
headers: dict[str, str] | None = None,
|
17
|
+
cookies: LooseCookies | None = None,
|
18
|
+
user_agent: str | None = None,
|
19
|
+
proxy: str | None = None,
|
20
|
+
timeout: float | None = 10.0,
|
21
|
+
) -> HttpResponse:
|
22
|
+
"""
|
23
|
+
Send an HTTP request and return the response.
|
24
|
+
"""
|
25
|
+
timeout_ = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
26
|
+
if user_agent:
|
27
|
+
if not headers:
|
28
|
+
headers = {}
|
29
|
+
headers["user-agent"] = user_agent
|
30
|
+
|
31
|
+
try:
|
32
|
+
if proxy and proxy.startswith("socks"):
|
33
|
+
return await _request_with_socks_proxy(
|
34
|
+
url,
|
35
|
+
method=method,
|
36
|
+
params=params,
|
37
|
+
data=data,
|
38
|
+
json=json,
|
39
|
+
headers=headers,
|
40
|
+
cookies=cookies,
|
41
|
+
proxy=proxy,
|
42
|
+
timeout=timeout_,
|
43
|
+
)
|
44
|
+
return await _request_with_http_or_none_proxy(
|
45
|
+
url,
|
46
|
+
method=method,
|
47
|
+
params=params,
|
48
|
+
data=data,
|
49
|
+
json=json,
|
50
|
+
headers=headers,
|
51
|
+
cookies=cookies,
|
52
|
+
proxy=proxy,
|
53
|
+
timeout=timeout_,
|
54
|
+
)
|
55
|
+
except TimeoutError as err:
|
56
|
+
return HttpResponse(error=HttpError.TIMEOUT, error_message=str(err))
|
57
|
+
except Exception as err:
|
58
|
+
return HttpResponse(error=HttpError.ERROR, error_message=str(err))
|
59
|
+
|
60
|
+
|
61
|
+
async def _request_with_http_or_none_proxy(
|
62
|
+
url: str,
|
63
|
+
*,
|
64
|
+
method: str = "GET",
|
65
|
+
params: Query | None = None,
|
66
|
+
data: dict[str, object] | None = None,
|
67
|
+
json: dict[str, object] | None = None,
|
68
|
+
headers: dict[str, str] | None = None,
|
69
|
+
cookies: LooseCookies | None = None,
|
70
|
+
proxy: str | None = None,
|
71
|
+
timeout: aiohttp.ClientTimeout | None,
|
72
|
+
) -> HttpResponse:
|
73
|
+
async with aiohttp.request(
|
74
|
+
method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, proxy=proxy, timeout=timeout
|
75
|
+
) as res:
|
76
|
+
return HttpResponse(
|
77
|
+
status=res.status,
|
78
|
+
error=None,
|
79
|
+
error_message=None,
|
80
|
+
body=(await res.read()).decode(),
|
81
|
+
headers=headers_dict(res.headers),
|
82
|
+
)
|
83
|
+
|
84
|
+
|
85
|
+
async def _request_with_socks_proxy(
|
86
|
+
url: str,
|
87
|
+
*,
|
88
|
+
method: str = "GET",
|
89
|
+
proxy: str,
|
90
|
+
params: Query | None = None,
|
91
|
+
data: dict[str, object] | None = None,
|
92
|
+
json: dict[str, object] | None = None,
|
93
|
+
headers: dict[str, str] | None = None,
|
94
|
+
cookies: LooseCookies | None = None,
|
95
|
+
timeout: aiohttp.ClientTimeout | None,
|
96
|
+
) -> HttpResponse:
|
97
|
+
connector = ProxyConnector.from_url(proxy)
|
98
|
+
async with (
|
99
|
+
aiohttp.ClientSession(connector=connector) as session,
|
100
|
+
session.request(
|
101
|
+
method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, timeout=timeout
|
102
|
+
) as res,
|
103
|
+
):
|
104
|
+
return HttpResponse(
|
105
|
+
status=res.status,
|
106
|
+
error=None,
|
107
|
+
error_message=None,
|
108
|
+
body=(await res.read()).decode(),
|
109
|
+
headers=headers_dict(res.headers),
|
110
|
+
)
|
111
|
+
|
112
|
+
|
113
|
+
def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
|
114
|
+
result: dict[str, str] = {}
|
115
|
+
for key in headers:
|
116
|
+
values = headers.getall(key)
|
117
|
+
if len(values) == 1:
|
118
|
+
result[key] = values[0]
|
119
|
+
else:
|
120
|
+
result[key] = ", ".join(values)
|
121
|
+
return result
|
mm_std/http/response.py
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
import enum
|
2
|
+
import json
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import pydash
|
6
|
+
from pydantic import GetCoreSchemaHandler
|
7
|
+
from pydantic_core import CoreSchema, core_schema
|
8
|
+
|
9
|
+
|
10
|
+
@enum.unique
|
11
|
+
class HttpError(str, enum.Enum):
|
12
|
+
TIMEOUT = "timeout"
|
13
|
+
PROXY = "proxy"
|
14
|
+
CONNECTION = "connection"
|
15
|
+
ERROR = "error"
|
16
|
+
|
17
|
+
|
18
|
+
class HttpResponse:
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
status: int | None = None,
|
22
|
+
error: HttpError | None = None,
|
23
|
+
error_message: str | None = None,
|
24
|
+
body: str | None = None,
|
25
|
+
headers: dict[str, str] | None = None,
|
26
|
+
) -> None:
|
27
|
+
self.status = status
|
28
|
+
self.error = error
|
29
|
+
self.error_message = error_message
|
30
|
+
self.body = body
|
31
|
+
self.headers = headers
|
32
|
+
|
33
|
+
self._json_data: Any = None
|
34
|
+
self._json_parsed = False
|
35
|
+
self._json_parsed_error = False
|
36
|
+
|
37
|
+
def _parse_json(self) -> None:
|
38
|
+
if self.body is None:
|
39
|
+
self._json_parsed_error = True
|
40
|
+
return
|
41
|
+
try:
|
42
|
+
self._json_data = None
|
43
|
+
self._json_data = json.loads(self.body)
|
44
|
+
self._json_parsed_error = False
|
45
|
+
except json.JSONDecodeError:
|
46
|
+
self._json_parsed_error = True
|
47
|
+
self._json_parsed = True
|
48
|
+
|
49
|
+
def json(self, path: str | None = None) -> Any: # noqa: ANN401
|
50
|
+
if not self._json_parsed:
|
51
|
+
self._parse_json()
|
52
|
+
if path:
|
53
|
+
return pydash.get(self._json_data, path, None)
|
54
|
+
return self._json_data
|
55
|
+
|
56
|
+
def dict(self) -> dict[str, object]:
|
57
|
+
return {
|
58
|
+
"status": self.status,
|
59
|
+
"error": self.error,
|
60
|
+
"error_message": self.error_message,
|
61
|
+
"body": self.body,
|
62
|
+
"headers": self.headers,
|
63
|
+
}
|
64
|
+
|
65
|
+
def is_json_parse_error(self) -> bool:
|
66
|
+
if not self._json_parsed:
|
67
|
+
self._parse_json()
|
68
|
+
return self._json_parsed_error
|
69
|
+
|
70
|
+
def __repr__(self) -> str:
|
71
|
+
return f"HttpResponse(status={self.status}, error={self.error}, error_message={self.error_message}, body={self.body}, headers={self.headers})" # noqa: E501
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def __get_pydantic_core_schema__(cls, source_type: type[Any], handler: GetCoreSchemaHandler) -> CoreSchema:
|
75
|
+
return core_schema.no_info_after_validator_function(
|
76
|
+
cls._validate,
|
77
|
+
core_schema.any_schema(),
|
78
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.dict()),
|
79
|
+
)
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
def _validate(cls, v: object) -> "HttpResponse":
|
83
|
+
if isinstance(v, cls):
|
84
|
+
return v
|
85
|
+
if isinstance(v, dict):
|
86
|
+
return cls(
|
87
|
+
status=v.get("status"),
|
88
|
+
error=HttpError(v["error"]) if v.get("error") else None,
|
89
|
+
error_message=v.get("error_message"),
|
90
|
+
body=v.get("body"),
|
91
|
+
headers=v.get("headers"),
|
92
|
+
)
|
93
|
+
raise TypeError(f"Cannot parse value as {cls.__name__}: {v}")
|
mm_std/http_.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import json
|
2
2
|
from dataclasses import asdict, dataclass, field
|
3
|
-
from typing import Any
|
4
|
-
from urllib.parse import urlencode
|
3
|
+
from typing import Any, cast
|
4
|
+
from urllib.parse import urlencode, urlparse
|
5
5
|
|
6
6
|
import aiohttp
|
7
7
|
import pydash
|
8
8
|
import requests
|
9
9
|
from aiohttp_socks import ProxyConnector
|
10
|
+
from multidict import CIMultiDictProxy
|
10
11
|
from requests.auth import AuthBase
|
11
12
|
|
12
13
|
from mm_std.result import Err, Ok, Result
|
@@ -77,6 +78,9 @@ class HResponse:
|
|
77
78
|
def is_connection_error(self) -> bool:
|
78
79
|
return self.error is not None and self.error.startswith("connection:")
|
79
80
|
|
81
|
+
def is_dns_error(self) -> bool:
|
82
|
+
return self.error is not None and self.error.startswith("dns:")
|
83
|
+
|
80
84
|
def to_dict(self) -> dict[str, Any]:
|
81
85
|
return pydash.omit(asdict(self), "_json_data")
|
82
86
|
|
@@ -185,52 +189,50 @@ async def hrequest_async(
|
|
185
189
|
request_kwargs["auth"] = aiohttp.BasicAuth(auth[0], auth[1])
|
186
190
|
|
187
191
|
if proxy and proxy.startswith("socks"):
|
188
|
-
|
189
|
-
|
190
|
-
res = await _aiohttp(url, method, request_kwargs, timeout=timeout, proxy=proxy)
|
192
|
+
return await _aiohttp_socks5(url, method, proxy, request_kwargs, timeout)
|
193
|
+
return await _aiohttp(url, method, request_kwargs, timeout=timeout, proxy=proxy)
|
191
194
|
|
192
|
-
return HResponse(code=res.status, body=res.body, headers=res.headers)
|
193
195
|
except TimeoutError:
|
194
196
|
return HResponse(error="timeout")
|
195
|
-
except (aiohttp.ClientProxyConnectionError, aiohttp.ClientHttpProxyError):
|
196
|
-
|
197
|
-
|
197
|
+
except (aiohttp.ClientProxyConnectionError, aiohttp.ClientHttpProxyError, aiohttp.ClientConnectorError) as err:
|
198
|
+
if is_proxy_error(str(err), proxy):
|
199
|
+
return HResponse(error="proxy")
|
198
200
|
return HResponse(error=f"connection: {err}")
|
199
201
|
except aiohttp.ClientError as err:
|
200
202
|
return HResponse(error=f"error: {err}")
|
201
203
|
except Exception as err:
|
202
|
-
if "couldn't connect to proxy" in str(err).lower():
|
203
|
-
return HResponse(error="proxy")
|
204
204
|
return HResponse(error=f"exception: {err}")
|
205
205
|
|
206
206
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
207
|
+
def is_proxy_error(error_message: str, proxy: str | None) -> bool:
|
208
|
+
if not proxy:
|
209
|
+
return False
|
210
|
+
error_message = error_message.lower()
|
211
|
+
if "proxy" in error_message:
|
212
|
+
return True
|
213
|
+
return bool("cannot connect to" in error_message and cast(str, urlparse(proxy).hostname) in error_message)
|
212
214
|
|
213
215
|
|
214
216
|
async def _aiohttp(
|
215
217
|
url: str, method: str, request_kwargs: dict[str, object], timeout: float | None = None, proxy: str | None = None
|
216
|
-
) ->
|
218
|
+
) -> HResponse:
|
217
219
|
if proxy:
|
218
220
|
request_kwargs["proxy"] = proxy
|
219
221
|
client_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
220
222
|
async with aiohttp.ClientSession(timeout=client_timeout) as session, session.request(method, url, **request_kwargs) as res: # type: ignore[arg-type]
|
221
|
-
return
|
223
|
+
return HResponse(code=res.status, body=await res.text(), headers=headers_dict(res.headers))
|
222
224
|
|
223
225
|
|
224
226
|
async def _aiohttp_socks5(
|
225
227
|
url: str, method: str, proxy: str, request_kwargs: dict[str, object], timeout: float | None = None
|
226
|
-
) ->
|
228
|
+
) -> HResponse:
|
227
229
|
connector = ProxyConnector.from_url(proxy)
|
228
230
|
client_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
229
231
|
async with (
|
230
232
|
aiohttp.ClientSession(connector=connector, timeout=client_timeout) as session,
|
231
233
|
session.request(method, url, **request_kwargs) as res, # type: ignore[arg-type]
|
232
234
|
):
|
233
|
-
return
|
235
|
+
return HResponse(code=res.status, body=await res.text(), headers=headers_dict(res.headers))
|
234
236
|
|
235
237
|
|
236
238
|
def add_query_params_to_url(url: str, params: dict[str, object]) -> str:
|
@@ -242,3 +244,14 @@ def add_query_params_to_url(url: str, params: dict[str, object]) -> str:
|
|
242
244
|
|
243
245
|
hr = hrequest
|
244
246
|
hra = hrequest_async
|
247
|
+
|
248
|
+
|
249
|
+
def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
|
250
|
+
result: dict[str, str] = {}
|
251
|
+
for key in headers:
|
252
|
+
values = headers.getall(key)
|
253
|
+
if len(values) == 1:
|
254
|
+
result[key] = values[0]
|
255
|
+
else:
|
256
|
+
result[key] = ", ".join(values)
|
257
|
+
return result
|
@@ -1,4 +1,4 @@
|
|
1
|
-
mm_std/__init__.py,sha256=
|
1
|
+
mm_std/__init__.py,sha256=Q0UguFT4b_LdKmxFSI4NQ47mwYRVRTjZ6fgYh8qZY4Q,3266
|
2
2
|
mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
|
3
3
|
mm_std/config.py,sha256=4ox4D2CgGR76bvZ2n2vGQOYUDagFnlKEDb87to5zpxE,1871
|
4
4
|
mm_std/crypto.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
|
@@ -6,7 +6,7 @@ mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
|
|
6
6
|
mm_std/dict.py,sha256=6GkhJPXD0LiJDxPcYe6jPdEDw-MN7P7mKu6U5XxwYDk,675
|
7
7
|
mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
|
8
8
|
mm_std/fs.py,sha256=RwarNRJq3tIMG6LVX_g03hasfYpjYFh_O27oVDt5IPQ,291
|
9
|
-
mm_std/http_.py,sha256=
|
9
|
+
mm_std/http_.py,sha256=5_V5Hx1M07xwzf0VVqAsxSFJkvtaAhj9_sFQP_HliQw,8215
|
10
10
|
mm_std/json_.py,sha256=Naa6mBE4D0yiQGkPNRrFvndnUH3R7ovw3FeaejWV60o,1196
|
11
11
|
mm_std/log.py,sha256=0TkTsAlUTt00gjgukvsvnZRIAGELq0MI6Lv8mKP-Wz4,2887
|
12
12
|
mm_std/net.py,sha256=qdRCBIDneip6FaPNe5mx31UtYVmzqam_AoUF7ydEyjA,590
|
@@ -25,6 +25,9 @@ mm_std/concurrency/async_task_runner.py,sha256=EN7tN2enkVYVgDbhSiAr-_W4o9m9wBXCv
|
|
25
25
|
mm_std/concurrency/sync_decorators.py,sha256=syCQBOmN7qPO55yzgJB2rbkh10CVww376hmyvs6e5tA,1080
|
26
26
|
mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN8rpR6gY,2407
|
27
27
|
mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
|
28
|
-
mm_std
|
29
|
-
mm_std
|
30
|
-
mm_std
|
28
|
+
mm_std/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
+
mm_std/http/http_request.py,sha256=mJak332gv9NvY0JLO3hNrBxroQGS1HlNBzd1yq4Dy24,3694
|
30
|
+
mm_std/http/response.py,sha256=vvv5COTjJula9t33mFyrruhrFC4dr_Uy0jDKj6t1JxM,2923
|
31
|
+
mm_std-0.3.26.dist-info/METADATA,sha256=GW6fYKKEoBHo1GlukZ2LtY3gII8feRqjIynazLiX16g,415
|
32
|
+
mm_std-0.3.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
33
|
+
mm_std-0.3.26.dist-info/RECORD,,
|
File without changes
|