mm-http 0.0.1__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_http/__init__.py +5 -0
- mm_http/http_request.py +128 -0
- mm_http/http_request_sync.py +63 -0
- mm_http/py.typed +0 -0
- mm_http/response.py +106 -0
- mm_http-0.0.1.dist-info/METADATA +10 -0
- mm_http-0.0.1.dist-info/RECORD +8 -0
- mm_http-0.0.1.dist-info/WHEEL +4 -0
mm_http/__init__.py
ADDED
mm_http/http_request.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
from aiohttp import ClientHttpProxyError, InvalidUrlClientError
|
|
5
|
+
from aiohttp.typedefs import LooseCookies
|
|
6
|
+
from aiohttp_socks import ProxyConnectionError, ProxyConnector
|
|
7
|
+
from multidict import CIMultiDictProxy
|
|
8
|
+
|
|
9
|
+
from .response import HttpError, HttpResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def http_request(
|
|
13
|
+
url: str,
|
|
14
|
+
*,
|
|
15
|
+
method: str = "GET",
|
|
16
|
+
params: dict[str, Any] | None = None,
|
|
17
|
+
data: dict[str, object] | None = None,
|
|
18
|
+
json: dict[str, object] | None = None,
|
|
19
|
+
headers: dict[str, str] | None = None,
|
|
20
|
+
cookies: LooseCookies | None = None,
|
|
21
|
+
user_agent: str | None = None,
|
|
22
|
+
proxy: str | None = None,
|
|
23
|
+
timeout: float | None = 10.0,
|
|
24
|
+
) -> HttpResponse:
|
|
25
|
+
"""
|
|
26
|
+
Send an HTTP request and return the response.
|
|
27
|
+
"""
|
|
28
|
+
timeout_ = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
|
29
|
+
if user_agent:
|
|
30
|
+
if not headers:
|
|
31
|
+
headers = {}
|
|
32
|
+
headers["User-Agent"] = user_agent
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if proxy and proxy.startswith("socks"):
|
|
36
|
+
return await _request_with_socks_proxy(
|
|
37
|
+
url,
|
|
38
|
+
method=method,
|
|
39
|
+
params=params,
|
|
40
|
+
data=data,
|
|
41
|
+
json=json,
|
|
42
|
+
headers=headers,
|
|
43
|
+
cookies=cookies,
|
|
44
|
+
proxy=proxy,
|
|
45
|
+
timeout=timeout_,
|
|
46
|
+
)
|
|
47
|
+
return await _request_with_http_or_none_proxy(
|
|
48
|
+
url,
|
|
49
|
+
method=method,
|
|
50
|
+
params=params,
|
|
51
|
+
data=data,
|
|
52
|
+
json=json,
|
|
53
|
+
headers=headers,
|
|
54
|
+
cookies=cookies,
|
|
55
|
+
proxy=proxy,
|
|
56
|
+
timeout=timeout_,
|
|
57
|
+
)
|
|
58
|
+
except TimeoutError as err:
|
|
59
|
+
return HttpResponse(error=HttpError.TIMEOUT, error_message=str(err))
|
|
60
|
+
except (aiohttp.ClientProxyConnectionError, ProxyConnectionError, ClientHttpProxyError) as err:
|
|
61
|
+
return HttpResponse(error=HttpError.PROXY, error_message=str(err))
|
|
62
|
+
except InvalidUrlClientError as e:
|
|
63
|
+
return HttpResponse(error=HttpError.INVALID_URL, error_message=str(e))
|
|
64
|
+
except Exception as err:
|
|
65
|
+
return HttpResponse(error=HttpError.ERROR, error_message=str(err))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _request_with_http_or_none_proxy(
|
|
69
|
+
url: str,
|
|
70
|
+
*,
|
|
71
|
+
method: str = "GET",
|
|
72
|
+
params: dict[str, Any] | None = None,
|
|
73
|
+
data: dict[str, object] | None = None,
|
|
74
|
+
json: dict[str, object] | None = None,
|
|
75
|
+
headers: dict[str, str] | None = None,
|
|
76
|
+
cookies: LooseCookies | None = None,
|
|
77
|
+
proxy: str | None = None,
|
|
78
|
+
timeout: aiohttp.ClientTimeout | None,
|
|
79
|
+
) -> HttpResponse:
|
|
80
|
+
async with aiohttp.request(
|
|
81
|
+
method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, proxy=proxy, timeout=timeout
|
|
82
|
+
) as res:
|
|
83
|
+
return HttpResponse(
|
|
84
|
+
status_code=res.status,
|
|
85
|
+
error=None,
|
|
86
|
+
error_message=None,
|
|
87
|
+
body=(await res.read()).decode(),
|
|
88
|
+
headers=headers_dict(res.headers),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _request_with_socks_proxy(
|
|
93
|
+
url: str,
|
|
94
|
+
*,
|
|
95
|
+
method: str = "GET",
|
|
96
|
+
proxy: str,
|
|
97
|
+
params: dict[str, Any] | None = None,
|
|
98
|
+
data: dict[str, object] | None = None,
|
|
99
|
+
json: dict[str, object] | None = None,
|
|
100
|
+
headers: dict[str, str] | None = None,
|
|
101
|
+
cookies: LooseCookies | None = None,
|
|
102
|
+
timeout: aiohttp.ClientTimeout | None,
|
|
103
|
+
) -> HttpResponse:
|
|
104
|
+
connector = ProxyConnector.from_url(proxy)
|
|
105
|
+
async with (
|
|
106
|
+
aiohttp.ClientSession(connector=connector) as session,
|
|
107
|
+
session.request(
|
|
108
|
+
method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, timeout=timeout
|
|
109
|
+
) as res,
|
|
110
|
+
):
|
|
111
|
+
return HttpResponse(
|
|
112
|
+
status_code=res.status,
|
|
113
|
+
error=None,
|
|
114
|
+
error_message=None,
|
|
115
|
+
body=(await res.read()).decode(),
|
|
116
|
+
headers=headers_dict(res.headers),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
|
|
121
|
+
result: dict[str, str] = {}
|
|
122
|
+
for key in headers:
|
|
123
|
+
values = headers.getall(key)
|
|
124
|
+
if len(values) == 1:
|
|
125
|
+
result[key] = values[0]
|
|
126
|
+
else:
|
|
127
|
+
result[key] = ", ".join(values)
|
|
128
|
+
return result
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from requests.exceptions import InvalidSchema, MissingSchema, ProxyError
|
|
5
|
+
|
|
6
|
+
from .response import HttpError, HttpResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def http_request_sync(
|
|
10
|
+
url: str,
|
|
11
|
+
*,
|
|
12
|
+
method: str = "GET",
|
|
13
|
+
params: dict[str, Any] | None = None,
|
|
14
|
+
data: dict[str, Any] | None = None,
|
|
15
|
+
json: dict[str, Any] | None = None,
|
|
16
|
+
headers: dict[str, Any] | None = None,
|
|
17
|
+
cookies: dict[str, Any] | None = None,
|
|
18
|
+
user_agent: str | None = None,
|
|
19
|
+
proxy: str | None = None,
|
|
20
|
+
timeout: float | None = 10.0,
|
|
21
|
+
) -> HttpResponse:
|
|
22
|
+
"""
|
|
23
|
+
Send a synchronous HTTP request and return the response.
|
|
24
|
+
"""
|
|
25
|
+
if user_agent:
|
|
26
|
+
if headers is None:
|
|
27
|
+
headers = {}
|
|
28
|
+
headers["User-Agent"] = user_agent
|
|
29
|
+
|
|
30
|
+
proxies: dict[str, str] | None = None
|
|
31
|
+
if proxy:
|
|
32
|
+
proxies = {
|
|
33
|
+
"http": proxy,
|
|
34
|
+
"https": proxy,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
res = requests.request(
|
|
39
|
+
method=method,
|
|
40
|
+
url=url,
|
|
41
|
+
params=params,
|
|
42
|
+
data=data,
|
|
43
|
+
json=json,
|
|
44
|
+
headers=headers,
|
|
45
|
+
cookies=cookies,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
proxies=proxies,
|
|
48
|
+
)
|
|
49
|
+
return HttpResponse(
|
|
50
|
+
status_code=res.status_code,
|
|
51
|
+
error=None,
|
|
52
|
+
error_message=None,
|
|
53
|
+
body=res.text,
|
|
54
|
+
headers=dict(res.headers),
|
|
55
|
+
)
|
|
56
|
+
except requests.Timeout as e:
|
|
57
|
+
return HttpResponse(error=HttpError.TIMEOUT, error_message=str(e))
|
|
58
|
+
except ProxyError as e:
|
|
59
|
+
return HttpResponse(error=HttpError.PROXY, error_message=str(e))
|
|
60
|
+
except (InvalidSchema, MissingSchema) as e:
|
|
61
|
+
return HttpResponse(error=HttpError.INVALID_URL, error_message=str(e))
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return HttpResponse(error=HttpError.ERROR, error_message=str(e))
|
mm_http/py.typed
ADDED
|
File without changes
|
mm_http/response.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pydash
|
|
9
|
+
from mm_result import Result
|
|
10
|
+
from pydantic import GetCoreSchemaHandler
|
|
11
|
+
from pydantic_core import CoreSchema, core_schema
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@enum.unique
|
|
15
|
+
class HttpError(str, enum.Enum):
|
|
16
|
+
TIMEOUT = "timeout"
|
|
17
|
+
PROXY = "proxy"
|
|
18
|
+
INVALID_URL = "invalid_url"
|
|
19
|
+
CONNECTION = "connection"
|
|
20
|
+
ERROR = "error"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class HttpResponse:
|
|
25
|
+
status_code: int | None = None
|
|
26
|
+
error: HttpError | None = None
|
|
27
|
+
error_message: str | None = None
|
|
28
|
+
body: str | None = None
|
|
29
|
+
headers: dict[str, str] | None = None
|
|
30
|
+
|
|
31
|
+
def parse_json_body(self, path: str | None = None, none_on_error: bool = False) -> Any: # noqa: ANN401
|
|
32
|
+
if self.body is None:
|
|
33
|
+
if none_on_error:
|
|
34
|
+
return None
|
|
35
|
+
raise ValueError("Body is None")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
res = json.loads(self.body)
|
|
39
|
+
return pydash.get(res, path, None) if path else res
|
|
40
|
+
except json.JSONDecodeError:
|
|
41
|
+
if none_on_error:
|
|
42
|
+
return None
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
def is_err(self) -> bool:
|
|
46
|
+
return self.error is not None or (self.status_code is not None and self.status_code >= 400)
|
|
47
|
+
|
|
48
|
+
def to_err[T](self, error: str | Exception | tuple[str, Exception] | None = None) -> Result[T]:
|
|
49
|
+
return Result.err(error or self.error or "error", extra=self.to_dict())
|
|
50
|
+
|
|
51
|
+
def to_ok[T](self, value: T) -> Result[T]:
|
|
52
|
+
return Result.ok(value, extra=self.to_dict())
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict[str, Any]:
|
|
55
|
+
return {
|
|
56
|
+
"status_code": self.status_code,
|
|
57
|
+
"error": self.error.value if self.error else None,
|
|
58
|
+
"error_message": self.error_message,
|
|
59
|
+
"body": self.body,
|
|
60
|
+
"headers": self.headers,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def content_type(self) -> str | None:
|
|
65
|
+
if self.headers is None:
|
|
66
|
+
return None
|
|
67
|
+
for key in self.headers:
|
|
68
|
+
if key.lower() == "content-type":
|
|
69
|
+
return self.headers[key]
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
parts: list[str] = []
|
|
74
|
+
if self.status_code is not None:
|
|
75
|
+
parts.append(f"status_code={self.status_code!r}")
|
|
76
|
+
if self.error is not None:
|
|
77
|
+
parts.append(f"error={self.error!r}")
|
|
78
|
+
if self.error_message is not None:
|
|
79
|
+
parts.append(f"error_message={self.error_message!r}")
|
|
80
|
+
if self.body is not None:
|
|
81
|
+
parts.append(f"body={self.body!r}")
|
|
82
|
+
if self.headers is not None:
|
|
83
|
+
parts.append(f"headers={self.headers!r}")
|
|
84
|
+
return f"HttpResponse({', '.join(parts)})"
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
88
|
+
return core_schema.no_info_after_validator_function(
|
|
89
|
+
cls._validate,
|
|
90
|
+
core_schema.any_schema(),
|
|
91
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.to_dict()),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _validate(cls, value: object) -> HttpResponse:
|
|
96
|
+
if isinstance(value, cls):
|
|
97
|
+
return value
|
|
98
|
+
if isinstance(value, dict):
|
|
99
|
+
return cls(
|
|
100
|
+
status_code=value.get("status_code"),
|
|
101
|
+
error=HttpError(value["error"]) if value.get("error") else None,
|
|
102
|
+
error_message=value.get("error_message"),
|
|
103
|
+
body=value.get("body"),
|
|
104
|
+
headers=value.get("headers"),
|
|
105
|
+
)
|
|
106
|
+
raise TypeError(f"Invalid value for HttpResponse: {value}")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mm-http
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Requires-Python: >=3.13
|
|
5
|
+
Requires-Dist: aiohttp-socks~=0.10.1
|
|
6
|
+
Requires-Dist: aiohttp~=3.12.2
|
|
7
|
+
Requires-Dist: mm-result>=0.0.2
|
|
8
|
+
Requires-Dist: pydantic>=2.11.5
|
|
9
|
+
Requires-Dist: pydash>=8.0.5
|
|
10
|
+
Requires-Dist: requests[socks]~=2.32.3
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mm_http/__init__.py,sha256=vW7ASPktUjm6S2Q7_biiA4F_PzG7gPF8gK3E7bBO4yQ,212
|
|
2
|
+
mm_http/http_request.py,sha256=3yapH-ADtSGqV7pMQj1cGWrGywDmRnTIE53AzM6ECtg,4116
|
|
3
|
+
mm_http/http_request_sync.py,sha256=lajd4Xt_wHKuRYyPtybO5CHHvquLzGPZEMHzKjN1wlM,1819
|
|
4
|
+
mm_http/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
mm_http/response.py,sha256=Ve0uE6BzQ4FPoaF-DbJ_9iCI2vqyDfbxGTMm1L3Bndc,3576
|
|
6
|
+
mm_http-0.0.1.dist-info/METADATA,sha256=CgOzxJf2MJaO6k2EC5JBtJNQsD888ievm2BNszyHKGg,275
|
|
7
|
+
mm_http-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
mm_http-0.0.1.dist-info/RECORD,,
|