mm-http 0.2.5__py3-none-any.whl → 0.3.0__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 +6 -4
- mm_http/request.py +47 -17
- mm_http/request_sync.py +21 -24
- mm_http/response.py +35 -15
- {mm_http-0.2.5.dist-info → mm_http-0.3.0.dist-info}/METADATA +4 -5
- mm_http-0.3.0.dist-info/RECORD +8 -0
- mm_http-0.2.5.dist-info/RECORD +0 -8
- {mm_http-0.2.5.dist-info → mm_http-0.3.0.dist-info}/WHEEL +0 -0
mm_http/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
from .request_sync import http_request_sync
|
|
3
|
-
from .response import HttpResponse, TransportError, TransportErrorDetail
|
|
1
|
+
"""HTTP client library with sync and async support."""
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
from .request import http_request as http_request
|
|
4
|
+
from .request_sync import http_request_sync as http_request_sync
|
|
5
|
+
from .response import HttpResponse as HttpResponse
|
|
6
|
+
from .response import TransportError as TransportError
|
|
7
|
+
from .response import TransportErrorType as TransportErrorType
|
mm_http/request.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Async HTTP request implementation using aiohttp."""
|
|
2
|
+
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
import aiohttp
|
|
@@ -10,11 +12,10 @@ from aiohttp import (
|
|
|
10
12
|
ServerConnectionError,
|
|
11
13
|
ServerDisconnectedError,
|
|
12
14
|
)
|
|
13
|
-
from aiohttp.typedefs import LooseCookies
|
|
14
15
|
from aiohttp_socks import ProxyConnectionError, ProxyConnector
|
|
15
16
|
from multidict import CIMultiDictProxy
|
|
16
17
|
|
|
17
|
-
from .response import HttpResponse,
|
|
18
|
+
from .response import HttpResponse, TransportErrorType
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
async def http_request(
|
|
@@ -25,14 +26,14 @@ async def http_request(
|
|
|
25
26
|
data: dict[str, Any] | None = None,
|
|
26
27
|
json: dict[str, Any] | None = None,
|
|
27
28
|
headers: dict[str, Any] | None = None,
|
|
28
|
-
cookies:
|
|
29
|
+
cookies: dict[str, str] | None = None,
|
|
29
30
|
user_agent: str | None = None,
|
|
30
31
|
proxy: str | None = None,
|
|
31
32
|
timeout: float | None = 10.0,
|
|
33
|
+
verify_ssl: bool = True,
|
|
34
|
+
follow_redirects: bool = True,
|
|
32
35
|
) -> HttpResponse:
|
|
33
|
-
"""
|
|
34
|
-
Send an HTTP request and return the response.
|
|
35
|
-
"""
|
|
36
|
+
"""Send an HTTP request and return the response."""
|
|
36
37
|
timeout_ = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
|
37
38
|
if user_agent:
|
|
38
39
|
if not headers:
|
|
@@ -51,6 +52,8 @@ async def http_request(
|
|
|
51
52
|
cookies=cookies,
|
|
52
53
|
proxy=proxy,
|
|
53
54
|
timeout=timeout_,
|
|
55
|
+
verify_ssl=verify_ssl,
|
|
56
|
+
follow_redirects=follow_redirects,
|
|
54
57
|
)
|
|
55
58
|
return await _request_with_http_or_none_proxy(
|
|
56
59
|
url,
|
|
@@ -62,13 +65,15 @@ async def http_request(
|
|
|
62
65
|
cookies=cookies,
|
|
63
66
|
proxy=proxy,
|
|
64
67
|
timeout=timeout_,
|
|
68
|
+
verify_ssl=verify_ssl,
|
|
69
|
+
follow_redirects=follow_redirects,
|
|
65
70
|
)
|
|
66
71
|
except TimeoutError as err:
|
|
67
|
-
return HttpResponse(
|
|
72
|
+
return HttpResponse.from_transport_error(TransportErrorType.TIMEOUT, str(err))
|
|
68
73
|
except (aiohttp.ClientProxyConnectionError, ProxyConnectionError, ClientHttpProxyError) as err:
|
|
69
|
-
return HttpResponse(
|
|
70
|
-
except InvalidUrlClientError as
|
|
71
|
-
return HttpResponse(
|
|
74
|
+
return HttpResponse.from_transport_error(TransportErrorType.PROXY, str(err))
|
|
75
|
+
except InvalidUrlClientError as err:
|
|
76
|
+
return HttpResponse.from_transport_error(TransportErrorType.INVALID_URL, str(err))
|
|
72
77
|
except (
|
|
73
78
|
ClientConnectorError,
|
|
74
79
|
ServerConnectionError,
|
|
@@ -76,9 +81,9 @@ async def http_request(
|
|
|
76
81
|
ClientSSLError,
|
|
77
82
|
ClientConnectionError,
|
|
78
83
|
) as err:
|
|
79
|
-
return HttpResponse(
|
|
84
|
+
return HttpResponse.from_transport_error(TransportErrorType.CONNECTION, str(err))
|
|
80
85
|
except Exception as err:
|
|
81
|
-
return HttpResponse(
|
|
86
|
+
return HttpResponse.from_transport_error(TransportErrorType.ERROR, str(err))
|
|
82
87
|
|
|
83
88
|
|
|
84
89
|
async def _request_with_http_or_none_proxy(
|
|
@@ -89,12 +94,25 @@ async def _request_with_http_or_none_proxy(
|
|
|
89
94
|
data: dict[str, Any] | None = None,
|
|
90
95
|
json: dict[str, Any] | None = None,
|
|
91
96
|
headers: dict[str, Any] | None = None,
|
|
92
|
-
cookies:
|
|
97
|
+
cookies: dict[str, str] | None = None,
|
|
93
98
|
proxy: str | None = None,
|
|
94
99
|
timeout: aiohttp.ClientTimeout | None,
|
|
100
|
+
verify_ssl: bool,
|
|
101
|
+
follow_redirects: bool,
|
|
95
102
|
) -> HttpResponse:
|
|
103
|
+
"""Execute request with HTTP proxy or no proxy."""
|
|
96
104
|
async with aiohttp.request(
|
|
97
|
-
method,
|
|
105
|
+
method,
|
|
106
|
+
url,
|
|
107
|
+
params=params,
|
|
108
|
+
data=data,
|
|
109
|
+
json=json,
|
|
110
|
+
headers=headers,
|
|
111
|
+
cookies=cookies,
|
|
112
|
+
proxy=proxy,
|
|
113
|
+
timeout=timeout,
|
|
114
|
+
ssl=verify_ssl,
|
|
115
|
+
allow_redirects=follow_redirects,
|
|
98
116
|
) as res:
|
|
99
117
|
return HttpResponse(
|
|
100
118
|
status_code=res.status,
|
|
@@ -112,14 +130,25 @@ async def _request_with_socks_proxy(
|
|
|
112
130
|
data: dict[str, Any] | None = None,
|
|
113
131
|
json: dict[str, Any] | None = None,
|
|
114
132
|
headers: dict[str, Any] | None = None,
|
|
115
|
-
cookies:
|
|
133
|
+
cookies: dict[str, str] | None = None,
|
|
116
134
|
timeout: aiohttp.ClientTimeout | None,
|
|
135
|
+
verify_ssl: bool,
|
|
136
|
+
follow_redirects: bool,
|
|
117
137
|
) -> HttpResponse:
|
|
118
|
-
|
|
138
|
+
"""Execute request through SOCKS proxy."""
|
|
139
|
+
connector = ProxyConnector.from_url(proxy, ssl=verify_ssl)
|
|
119
140
|
async with (
|
|
120
141
|
aiohttp.ClientSession(connector=connector) as session,
|
|
121
142
|
session.request(
|
|
122
|
-
method,
|
|
143
|
+
method,
|
|
144
|
+
url,
|
|
145
|
+
params=params,
|
|
146
|
+
data=data,
|
|
147
|
+
json=json,
|
|
148
|
+
headers=headers,
|
|
149
|
+
cookies=cookies,
|
|
150
|
+
timeout=timeout,
|
|
151
|
+
allow_redirects=follow_redirects,
|
|
123
152
|
) as res,
|
|
124
153
|
):
|
|
125
154
|
return HttpResponse(
|
|
@@ -130,6 +159,7 @@ async def _request_with_socks_proxy(
|
|
|
130
159
|
|
|
131
160
|
|
|
132
161
|
def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
|
|
162
|
+
"""Convert multidict headers to dict, joining duplicate keys with comma."""
|
|
133
163
|
result: dict[str, str] = {}
|
|
134
164
|
for key in headers:
|
|
135
165
|
values = headers.getall(key)
|
mm_http/request_sync.py
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
"""Sync HTTP request implementation using requests library."""
|
|
2
|
+
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
import requests
|
|
4
|
-
from requests.exceptions import
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
from requests.exceptions import (
|
|
8
|
-
InvalidSchema,
|
|
9
|
-
MissingSchema,
|
|
10
|
-
ProxyError,
|
|
11
|
-
SSLError,
|
|
12
|
-
)
|
|
6
|
+
from requests.exceptions import ConnectionError as RequestsConnectionError
|
|
7
|
+
from requests.exceptions import InvalidSchema, MissingSchema, ProxyError, SSLError
|
|
13
8
|
|
|
14
|
-
from .response import HttpResponse,
|
|
9
|
+
from .response import HttpResponse, TransportErrorType
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
def http_request_sync(
|
|
@@ -22,14 +17,14 @@ def http_request_sync(
|
|
|
22
17
|
data: dict[str, Any] | None = None,
|
|
23
18
|
json: dict[str, Any] | None = None,
|
|
24
19
|
headers: dict[str, Any] | None = None,
|
|
25
|
-
cookies: dict[str,
|
|
20
|
+
cookies: dict[str, str] | None = None,
|
|
26
21
|
user_agent: str | None = None,
|
|
27
22
|
proxy: str | None = None,
|
|
28
23
|
timeout: float | None = 10.0,
|
|
24
|
+
verify_ssl: bool = True,
|
|
25
|
+
follow_redirects: bool = True,
|
|
29
26
|
) -> HttpResponse:
|
|
30
|
-
"""
|
|
31
|
-
Send a synchronous HTTP request and return the response.
|
|
32
|
-
"""
|
|
27
|
+
"""Send a synchronous HTTP request and return the response."""
|
|
33
28
|
if user_agent:
|
|
34
29
|
if headers is None:
|
|
35
30
|
headers = {}
|
|
@@ -53,19 +48,21 @@ def http_request_sync(
|
|
|
53
48
|
cookies=cookies,
|
|
54
49
|
timeout=timeout,
|
|
55
50
|
proxies=proxies,
|
|
51
|
+
verify=verify_ssl,
|
|
52
|
+
allow_redirects=follow_redirects,
|
|
56
53
|
)
|
|
57
54
|
return HttpResponse(
|
|
58
55
|
status_code=res.status_code,
|
|
59
56
|
body=res.text,
|
|
60
57
|
headers=dict(res.headers),
|
|
61
58
|
)
|
|
62
|
-
except requests.Timeout as
|
|
63
|
-
return HttpResponse(
|
|
64
|
-
except ProxyError as
|
|
65
|
-
return HttpResponse(
|
|
66
|
-
except (InvalidSchema, MissingSchema) as
|
|
67
|
-
return HttpResponse(
|
|
68
|
-
except (RequestsConnectionError, SSLError) as
|
|
69
|
-
return HttpResponse(
|
|
70
|
-
except Exception as
|
|
71
|
-
return HttpResponse(
|
|
59
|
+
except requests.Timeout as err:
|
|
60
|
+
return HttpResponse.from_transport_error(TransportErrorType.TIMEOUT, str(err))
|
|
61
|
+
except ProxyError as err:
|
|
62
|
+
return HttpResponse.from_transport_error(TransportErrorType.PROXY, str(err))
|
|
63
|
+
except (InvalidSchema, MissingSchema) as err:
|
|
64
|
+
return HttpResponse.from_transport_error(TransportErrorType.INVALID_URL, str(err))
|
|
65
|
+
except (RequestsConnectionError, SSLError) as err:
|
|
66
|
+
return HttpResponse.from_transport_error(TransportErrorType.CONNECTION, str(err))
|
|
67
|
+
except Exception as err:
|
|
68
|
+
return HttpResponse.from_transport_error(TransportErrorType.ERROR, str(err))
|
mm_http/response.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""HTTP response models and error types."""
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
import json
|
|
@@ -10,7 +10,9 @@ from pydantic import BaseModel, model_validator
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@enum.unique
|
|
13
|
-
class
|
|
13
|
+
class TransportErrorType(str, enum.Enum):
|
|
14
|
+
"""Transport-level error types."""
|
|
15
|
+
|
|
14
16
|
TIMEOUT = "timeout"
|
|
15
17
|
PROXY = "proxy"
|
|
16
18
|
INVALID_URL = "invalid_url"
|
|
@@ -18,10 +20,10 @@ class TransportError(str, enum.Enum):
|
|
|
18
20
|
ERROR = "error"
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
class
|
|
23
|
+
class TransportError(BaseModel):
|
|
22
24
|
"""Transport error with type and message."""
|
|
23
25
|
|
|
24
|
-
type:
|
|
26
|
+
type: TransportErrorType
|
|
25
27
|
message: str
|
|
26
28
|
|
|
27
29
|
|
|
@@ -31,7 +33,7 @@ class HttpResponse(BaseModel):
|
|
|
31
33
|
status_code: int | None = None
|
|
32
34
|
body: str | None = None
|
|
33
35
|
headers: dict[str, str] | None = None
|
|
34
|
-
transport_error:
|
|
36
|
+
transport_error: TransportError | None = None
|
|
35
37
|
|
|
36
38
|
@model_validator(mode="after")
|
|
37
39
|
def validate_mutually_exclusive_states(self) -> HttpResponse:
|
|
@@ -49,20 +51,38 @@ class HttpResponse(BaseModel):
|
|
|
49
51
|
|
|
50
52
|
return self
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_transport_error(cls, error_type: TransportErrorType, message: str) -> HttpResponse:
|
|
56
|
+
"""Create HttpResponse from transport error."""
|
|
57
|
+
return cls(transport_error=TransportError(type=error_type, message=message))
|
|
58
|
+
|
|
59
|
+
def json_body(self, path: str | None = None) -> Result[Any]:
|
|
60
|
+
"""Parse body as JSON with explicit error handling."""
|
|
54
61
|
if self.body is None:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
return Result.err("body is None")
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(self.body)
|
|
65
|
+
except json.JSONDecodeError as e:
|
|
66
|
+
return Result.err(("JSON decode error", e))
|
|
67
|
+
|
|
68
|
+
if path:
|
|
69
|
+
if not pydash.has(data, path):
|
|
70
|
+
return Result.err(f"path not found: {path}")
|
|
71
|
+
return Result.ok(pydash.get(data, path))
|
|
72
|
+
return Result.ok(data)
|
|
58
73
|
|
|
74
|
+
def json_body_or_none(self, path: str | None = None) -> Any: # noqa: ANN401 - JSON returns dynamic types
|
|
75
|
+
"""Parse body as JSON. Returns None if body is None, JSON invalid, or path not found.
|
|
76
|
+
|
|
77
|
+
Warning: Do not use if None is a valid expected value — use json_body() instead.
|
|
78
|
+
"""
|
|
79
|
+
if self.body is None:
|
|
80
|
+
return None
|
|
59
81
|
try:
|
|
60
82
|
res = json.loads(self.body)
|
|
61
83
|
return pydash.get(res, path, None) if path else res
|
|
62
84
|
except json.JSONDecodeError:
|
|
63
|
-
|
|
64
|
-
return None
|
|
65
|
-
raise
|
|
85
|
+
return None
|
|
66
86
|
|
|
67
87
|
def get_header(self, name: str) -> str | None:
|
|
68
88
|
"""Get header value (case-insensitive)."""
|
|
@@ -101,11 +121,11 @@ class HttpResponse(BaseModel):
|
|
|
101
121
|
result_error = f"HTTP {self.status_code}"
|
|
102
122
|
else:
|
|
103
123
|
result_error = "error"
|
|
104
|
-
return Result.err(result_error,
|
|
124
|
+
return Result.err(result_error, context=self.model_dump(mode="json"))
|
|
105
125
|
|
|
106
126
|
def to_result_ok[T](self, value: T) -> Result[T]:
|
|
107
127
|
"""Create success Result[T] from HttpResponse with given value."""
|
|
108
|
-
return Result.ok(value,
|
|
128
|
+
return Result.ok(value, context=self.model_dump(mode="json"))
|
|
109
129
|
|
|
110
130
|
@property
|
|
111
131
|
def content_type(self) -> str | None:
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-http
|
|
3
|
-
Version: 0.
|
|
4
|
-
Requires-Python: >=3.
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Requires-Python: >=3.14
|
|
5
5
|
Requires-Dist: aiohttp-socks~=0.11.0
|
|
6
6
|
Requires-Dist: aiohttp~=3.13.3
|
|
7
|
-
Requires-Dist: mm-result~=0.
|
|
7
|
+
Requires-Dist: mm-result~=0.2.0
|
|
8
8
|
Requires-Dist: pydantic~=2.12.5
|
|
9
|
-
Requires-Dist: pydash~=8.0.
|
|
9
|
+
Requires-Dist: pydash~=8.0.6
|
|
10
10
|
Requires-Dist: requests[socks]~=2.32.5
|
|
11
|
-
Requires-Dist: urllib3~=2.6.3
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mm_http/__init__.py,sha256=UlnNFVFqi3qI07MbL7Cm3k3qaEmdUwv-H--6XvfLAEU,340
|
|
2
|
+
mm_http/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
mm_http/request.py,sha256=RjJ6QWwyQpIZ6jDhDufQ_R8AvDNvmbKb2VATUAYmVYA,5231
|
|
4
|
+
mm_http/request_sync.py,sha256=xQ0vgYJf4eSackna03GmV4h0iZvNPzWb535ts_oterE,2253
|
|
5
|
+
mm_http/response.py,sha256=dxGY6Tg669qhIJRM1bGawiJwOLmAPgD01tXgMKrYl24,4913
|
|
6
|
+
mm_http-0.3.0.dist-info/METADATA,sha256=OD3RTIe5K43mlKdiJDG8ta1zdLzhB4d8uDzUtr2XIE4,275
|
|
7
|
+
mm_http-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
mm_http-0.3.0.dist-info/RECORD,,
|
mm_http-0.2.5.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
mm_http/__init__.py,sha256=KQ5BhEyTpNXdV-gRYV8keh6_caee8jo8xuj292VWGTU,258
|
|
2
|
-
mm_http/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
mm_http/request.py,sha256=3uVkKiB89jL5o64SY-hyQV4iBlwLL8Pt4UekwU2zYVI,4569
|
|
4
|
-
mm_http/request_sync.py,sha256=2YjWAvWiut2HPcLMuwpCVMwpR99bmTQA3jYEutISKJE,2216
|
|
5
|
-
mm_http/response.py,sha256=WDRXNASFreDUGiUQk4QIelVP7qCPLWmNXsTd9uln-v8,4040
|
|
6
|
-
mm_http-0.2.5.dist-info/METADATA,sha256=tSRrBnFq5Ab__oX4_CkI8NqxmLLC3YBoIHj7R3L9P_A,305
|
|
7
|
-
mm_http-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
-
mm_http-0.2.5.dist-info/RECORD,,
|
|
File without changes
|