mm-http 0.1.3__py3-none-any.whl → 0.2.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 +2 -2
- mm_http/http_request.py +33 -21
- mm_http/http_request_sync.py +16 -8
- mm_http/response.py +57 -43
- {mm_http-0.1.3.dist-info → mm_http-0.2.0.dist-info}/METADATA +4 -3
- mm_http-0.2.0.dist-info/RECORD +8 -0
- mm_http-0.1.3.dist-info/RECORD +0 -8
- {mm_http-0.1.3.dist-info → mm_http-0.2.0.dist-info}/WHEEL +0 -0
mm_http/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .http_request import http_request
|
|
2
2
|
from .http_request_sync import http_request_sync
|
|
3
|
-
from .response import
|
|
3
|
+
from .response import HttpResponse, TransportError, TransportErrorDetail
|
|
4
4
|
|
|
5
|
-
__all__ = ["
|
|
5
|
+
__all__ = ["HttpResponse", "TransportError", "TransportErrorDetail", "http_request", "http_request_sync"]
|
mm_http/http_request.py
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
import aiohttp
|
|
4
|
-
from aiohttp import
|
|
4
|
+
from aiohttp import (
|
|
5
|
+
ClientConnectionError,
|
|
6
|
+
ClientConnectorError,
|
|
7
|
+
ClientHttpProxyError,
|
|
8
|
+
ClientSSLError,
|
|
9
|
+
InvalidUrlClientError,
|
|
10
|
+
ServerConnectionError,
|
|
11
|
+
ServerDisconnectedError,
|
|
12
|
+
)
|
|
5
13
|
from aiohttp.typedefs import LooseCookies
|
|
6
14
|
from aiohttp_socks import ProxyConnectionError, ProxyConnector
|
|
7
15
|
from multidict import CIMultiDictProxy
|
|
8
16
|
|
|
9
|
-
from .response import
|
|
17
|
+
from .response import HttpResponse, TransportError, TransportErrorDetail
|
|
10
18
|
|
|
11
19
|
|
|
12
20
|
async def http_request(
|
|
@@ -14,9 +22,9 @@ async def http_request(
|
|
|
14
22
|
*,
|
|
15
23
|
method: str = "GET",
|
|
16
24
|
params: dict[str, Any] | None = None,
|
|
17
|
-
data: dict[str,
|
|
18
|
-
json: dict[str,
|
|
19
|
-
headers: dict[str,
|
|
25
|
+
data: dict[str, Any] | None = None,
|
|
26
|
+
json: dict[str, Any] | None = None,
|
|
27
|
+
headers: dict[str, Any] | None = None,
|
|
20
28
|
cookies: LooseCookies | None = None,
|
|
21
29
|
user_agent: str | None = None,
|
|
22
30
|
proxy: str | None = None,
|
|
@@ -56,13 +64,21 @@ async def http_request(
|
|
|
56
64
|
timeout=timeout_,
|
|
57
65
|
)
|
|
58
66
|
except TimeoutError as err:
|
|
59
|
-
return HttpResponse(
|
|
67
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.TIMEOUT, message=str(err)))
|
|
60
68
|
except (aiohttp.ClientProxyConnectionError, ProxyConnectionError, ClientHttpProxyError) as err:
|
|
61
|
-
return HttpResponse(
|
|
69
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.PROXY, message=str(err)))
|
|
62
70
|
except InvalidUrlClientError as e:
|
|
63
|
-
return HttpResponse(
|
|
71
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.INVALID_URL, message=str(e)))
|
|
72
|
+
except (
|
|
73
|
+
ClientConnectorError,
|
|
74
|
+
ServerConnectionError,
|
|
75
|
+
ServerDisconnectedError,
|
|
76
|
+
ClientSSLError,
|
|
77
|
+
ClientConnectionError,
|
|
78
|
+
) as err:
|
|
79
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.CONNECTION, message=str(err)))
|
|
64
80
|
except Exception as err:
|
|
65
|
-
return HttpResponse(
|
|
81
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.ERROR, message=str(err)))
|
|
66
82
|
|
|
67
83
|
|
|
68
84
|
async def _request_with_http_or_none_proxy(
|
|
@@ -70,9 +86,9 @@ async def _request_with_http_or_none_proxy(
|
|
|
70
86
|
*,
|
|
71
87
|
method: str = "GET",
|
|
72
88
|
params: dict[str, Any] | None = None,
|
|
73
|
-
data: dict[str,
|
|
74
|
-
json: dict[str,
|
|
75
|
-
headers: dict[str,
|
|
89
|
+
data: dict[str, Any] | None = None,
|
|
90
|
+
json: dict[str, Any] | None = None,
|
|
91
|
+
headers: dict[str, Any] | None = None,
|
|
76
92
|
cookies: LooseCookies | None = None,
|
|
77
93
|
proxy: str | None = None,
|
|
78
94
|
timeout: aiohttp.ClientTimeout | None,
|
|
@@ -82,9 +98,7 @@ async def _request_with_http_or_none_proxy(
|
|
|
82
98
|
) as res:
|
|
83
99
|
return HttpResponse(
|
|
84
100
|
status_code=res.status,
|
|
85
|
-
|
|
86
|
-
error_message=None,
|
|
87
|
-
body=(await res.read()).decode(),
|
|
101
|
+
body=await res.text(),
|
|
88
102
|
headers=headers_dict(res.headers),
|
|
89
103
|
)
|
|
90
104
|
|
|
@@ -95,9 +109,9 @@ async def _request_with_socks_proxy(
|
|
|
95
109
|
method: str = "GET",
|
|
96
110
|
proxy: str,
|
|
97
111
|
params: dict[str, Any] | None = None,
|
|
98
|
-
data: dict[str,
|
|
99
|
-
json: dict[str,
|
|
100
|
-
headers: dict[str,
|
|
112
|
+
data: dict[str, Any] | None = None,
|
|
113
|
+
json: dict[str, Any] | None = None,
|
|
114
|
+
headers: dict[str, Any] | None = None,
|
|
101
115
|
cookies: LooseCookies | None = None,
|
|
102
116
|
timeout: aiohttp.ClientTimeout | None,
|
|
103
117
|
) -> HttpResponse:
|
|
@@ -110,9 +124,7 @@ async def _request_with_socks_proxy(
|
|
|
110
124
|
):
|
|
111
125
|
return HttpResponse(
|
|
112
126
|
status_code=res.status,
|
|
113
|
-
|
|
114
|
-
error_message=None,
|
|
115
|
-
body=(await res.read()).decode(),
|
|
127
|
+
body=await res.text(),
|
|
116
128
|
headers=headers_dict(res.headers),
|
|
117
129
|
)
|
|
118
130
|
|
mm_http/http_request_sync.py
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
|
-
from requests.exceptions import
|
|
4
|
+
from requests.exceptions import (
|
|
5
|
+
ConnectionError as RequestsConnectionError,
|
|
6
|
+
)
|
|
7
|
+
from requests.exceptions import (
|
|
8
|
+
InvalidSchema,
|
|
9
|
+
MissingSchema,
|
|
10
|
+
ProxyError,
|
|
11
|
+
SSLError,
|
|
12
|
+
)
|
|
5
13
|
|
|
6
|
-
from .response import
|
|
14
|
+
from .response import HttpResponse, TransportError, TransportErrorDetail
|
|
7
15
|
|
|
8
16
|
|
|
9
17
|
def http_request_sync(
|
|
@@ -48,16 +56,16 @@ def http_request_sync(
|
|
|
48
56
|
)
|
|
49
57
|
return HttpResponse(
|
|
50
58
|
status_code=res.status_code,
|
|
51
|
-
error=None,
|
|
52
|
-
error_message=None,
|
|
53
59
|
body=res.text,
|
|
54
60
|
headers=dict(res.headers),
|
|
55
61
|
)
|
|
56
62
|
except requests.Timeout as e:
|
|
57
|
-
return HttpResponse(
|
|
63
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.TIMEOUT, message=str(e)))
|
|
58
64
|
except ProxyError as e:
|
|
59
|
-
return HttpResponse(
|
|
65
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.PROXY, message=str(e)))
|
|
60
66
|
except (InvalidSchema, MissingSchema) as e:
|
|
61
|
-
return HttpResponse(
|
|
67
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.INVALID_URL, message=str(e)))
|
|
68
|
+
except (RequestsConnectionError, SSLError) as e:
|
|
69
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.CONNECTION, message=str(e)))
|
|
62
70
|
except Exception as e:
|
|
63
|
-
return HttpResponse(
|
|
71
|
+
return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.ERROR, message=str(e)))
|
mm_http/response.py
CHANGED
|
@@ -2,15 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
import json
|
|
5
|
-
from dataclasses import dataclass
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
7
|
import pydash
|
|
9
8
|
from mm_result import Result
|
|
9
|
+
from pydantic import BaseModel, model_validator
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@enum.unique
|
|
13
|
-
class
|
|
13
|
+
class TransportError(str, enum.Enum):
|
|
14
14
|
TIMEOUT = "timeout"
|
|
15
15
|
PROXY = "proxy"
|
|
16
16
|
INVALID_URL = "invalid_url"
|
|
@@ -18,17 +18,38 @@ class HttpError(str, enum.Enum):
|
|
|
18
18
|
ERROR = "error"
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
class TransportErrorDetail(BaseModel):
|
|
22
|
+
"""Transport error with type and message."""
|
|
23
|
+
|
|
24
|
+
type: TransportError
|
|
25
|
+
message: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HttpResponse(BaseModel):
|
|
29
|
+
"""HTTP response with status, body, headers, and optional transport error."""
|
|
24
30
|
|
|
25
31
|
status_code: int | None = None
|
|
26
|
-
error: HttpError | None = None
|
|
27
|
-
error_message: str | None = None
|
|
28
32
|
body: str | None = None
|
|
29
33
|
headers: dict[str, str] | None = None
|
|
34
|
+
transport_error: TransportErrorDetail | None = None
|
|
35
|
+
|
|
36
|
+
@model_validator(mode="after")
|
|
37
|
+
def validate_mutually_exclusive_states(self) -> HttpResponse:
|
|
38
|
+
"""Validate that response has either HTTP data or transport error, but not both."""
|
|
39
|
+
has_http_response = self.status_code is not None
|
|
40
|
+
has_transport_error = self.transport_error is not None
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
if has_http_response and has_transport_error:
|
|
43
|
+
msg = "Cannot have both HTTP response and transport error"
|
|
44
|
+
raise ValueError(msg)
|
|
45
|
+
|
|
46
|
+
if not has_http_response and not has_transport_error:
|
|
47
|
+
msg = "Must have either HTTP response or transport error"
|
|
48
|
+
raise ValueError(msg)
|
|
49
|
+
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def parse_json(self, path: str | None = None, none_on_error: bool = False) -> Any: # noqa: ANN401
|
|
32
53
|
"""Parse JSON body and optionally extract value by path."""
|
|
33
54
|
if self.body is None:
|
|
34
55
|
if none_on_error:
|
|
@@ -43,48 +64,41 @@ class HttpResponse:
|
|
|
43
64
|
return None
|
|
44
65
|
raise
|
|
45
66
|
|
|
67
|
+
def get_header(self, name: str) -> str | None:
|
|
68
|
+
"""Get header value (case-insensitive)."""
|
|
69
|
+
if self.headers is None:
|
|
70
|
+
return None
|
|
71
|
+
name_lower = name.lower()
|
|
72
|
+
for key, value in self.headers.items():
|
|
73
|
+
if key.lower() == name_lower:
|
|
74
|
+
return value
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def is_success(self) -> bool:
|
|
78
|
+
"""Check if response has 2xx status."""
|
|
79
|
+
return self.status_code is not None and 200 <= self.status_code < 300
|
|
80
|
+
|
|
46
81
|
def is_err(self) -> bool:
|
|
47
|
-
"""Check if response represents an error (has error or status >= 400)."""
|
|
48
|
-
return self.
|
|
82
|
+
"""Check if response represents an error (has transport error or status >= 400)."""
|
|
83
|
+
return self.transport_error is not None or (self.status_code is not None and self.status_code >= 400)
|
|
49
84
|
|
|
50
85
|
def to_result_err[T](self, error: str | Exception | tuple[str, Exception] | None = None) -> Result[T]:
|
|
51
|
-
"""Create error Result[T] from HttpResponse."""
|
|
52
|
-
|
|
86
|
+
"""Create error Result[T] from HttpResponse with meaningful error message."""
|
|
87
|
+
if error is not None:
|
|
88
|
+
result_error = error
|
|
89
|
+
elif self.transport_error is not None:
|
|
90
|
+
result_error = self.transport_error.type
|
|
91
|
+
elif self.status_code is not None:
|
|
92
|
+
result_error = f"HTTP {self.status_code}"
|
|
93
|
+
else:
|
|
94
|
+
result_error = "error"
|
|
95
|
+
return Result.err(result_error, extra=self.model_dump(mode="json"))
|
|
53
96
|
|
|
54
97
|
def to_result_ok[T](self, value: T) -> Result[T]:
|
|
55
98
|
"""Create success Result[T] from HttpResponse with given value."""
|
|
56
|
-
return Result.ok(value, extra=self.
|
|
57
|
-
|
|
58
|
-
def to_dict(self) -> dict[str, Any]:
|
|
59
|
-
"""Convert HttpResponse to dictionary."""
|
|
60
|
-
return {
|
|
61
|
-
"status_code": self.status_code,
|
|
62
|
-
"error": self.error.value if self.error else None,
|
|
63
|
-
"error_message": self.error_message,
|
|
64
|
-
"body": self.body,
|
|
65
|
-
"headers": self.headers,
|
|
66
|
-
}
|
|
99
|
+
return Result.ok(value, extra=self.model_dump(mode="json"))
|
|
67
100
|
|
|
68
101
|
@property
|
|
69
102
|
def content_type(self) -> str | None:
|
|
70
103
|
"""Get Content-Type header value (case-insensitive)."""
|
|
71
|
-
|
|
72
|
-
return None
|
|
73
|
-
for key in self.headers:
|
|
74
|
-
if key.lower() == "content-type":
|
|
75
|
-
return self.headers[key]
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
def __repr__(self) -> str:
|
|
79
|
-
parts: list[str] = []
|
|
80
|
-
if self.status_code is not None:
|
|
81
|
-
parts.append(f"status_code={self.status_code!r}")
|
|
82
|
-
if self.error is not None:
|
|
83
|
-
parts.append(f"error={self.error!r}")
|
|
84
|
-
if self.error_message is not None:
|
|
85
|
-
parts.append(f"error_message={self.error_message!r}")
|
|
86
|
-
if self.body is not None:
|
|
87
|
-
parts.append(f"body={self.body!r}")
|
|
88
|
-
if self.headers is not None:
|
|
89
|
-
parts.append(f"headers={self.headers!r}")
|
|
90
|
-
return f"HttpResponse({', '.join(parts)})"
|
|
104
|
+
return self.get_header("content-type")
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-http
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Requires-Python: >=3.13
|
|
5
5
|
Requires-Dist: aiohttp-socks~=0.10.1
|
|
6
|
-
Requires-Dist: aiohttp~=3.
|
|
7
|
-
Requires-Dist: mm-result~=0.1.
|
|
6
|
+
Requires-Dist: aiohttp~=3.13.0
|
|
7
|
+
Requires-Dist: mm-result~=0.1.2
|
|
8
|
+
Requires-Dist: pydantic~=2.12.0
|
|
8
9
|
Requires-Dist: pydash~=8.0.5
|
|
9
10
|
Requires-Dist: requests[socks]~=2.32.5
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mm_http/__init__.py,sha256=-a5cUQa-Gr6xKErvlyJNApIb1itlL48xfi_pg-GMM7Y,268
|
|
2
|
+
mm_http/http_request.py,sha256=3uVkKiB89jL5o64SY-hyQV4iBlwLL8Pt4UekwU2zYVI,4569
|
|
3
|
+
mm_http/http_request_sync.py,sha256=2YjWAvWiut2HPcLMuwpCVMwpR99bmTQA3jYEutISKJE,2216
|
|
4
|
+
mm_http/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
mm_http/response.py,sha256=hoqVqLT4yzWxHYtkEGg0eTmAV530gvaB20WERQgpZpg,3634
|
|
6
|
+
mm_http-0.2.0.dist-info/METADATA,sha256=ddTBM6-fSpviUfM7Xta54yb4xVdPmOJjaBZctfaeh7U,275
|
|
7
|
+
mm_http-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
mm_http-0.2.0.dist-info/RECORD,,
|
mm_http-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
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=CCnL9COZvzP0kyAy6sASRbo1VtmnGsSiTd0kgvrt-MI,3036
|
|
6
|
-
mm_http-0.1.3.dist-info/METADATA,sha256=01CkW2dV0gfZp_8Kl-IsJYc_2SwASD0wwDPIY9hSHc4,244
|
|
7
|
-
mm_http-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
mm_http-0.1.3.dist-info/RECORD,,
|
|
File without changes
|