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 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
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
@@ -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
- res = await _aiohttp_socks5(url, method, proxy, request_kwargs, timeout)
189
- else:
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
- return HResponse(error="proxy")
197
- except aiohttp.ClientConnectorError as err:
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
- @dataclass
208
- class AioHttpResponse:
209
- status: int
210
- body: str
211
- headers: dict[str, str]
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
- ) -> AioHttpResponse:
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 AioHttpResponse(status=res.status, headers=dict(res.headers), body=await res.text())
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
- ) -> AioHttpResponse:
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 AioHttpResponse(status=res.status, headers=dict(res.headers), body=await res.text())
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.3.24
3
+ Version: 0.3.26
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: aiohttp-socks~=0.10.1
6
6
  Requires-Dist: aiohttp~=3.11.16
@@ -1,4 +1,4 @@
1
- mm_std/__init__.py,sha256=bdkBTX7_u4NYtTRdVDHgZUozQlFRBx3TTiMK7Cenup0,3100
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=A9SUidkmTEFYWXD9y-rX4MrUXtaZFiy84Ul8floYBn8,7700
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-0.3.24.dist-info/METADATA,sha256=El91GJxSAEQMfJHtBogFAyAsXGcRJ3J9sfCPCNwIehA,415
29
- mm_std-0.3.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- mm_std-0.3.24.dist-info/RECORD,,
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,,