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 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 HttpError, HttpResponse
3
+ from .response import HttpResponse, TransportError, TransportErrorDetail
4
4
 
5
- __all__ = ["HttpError", "HttpResponse", "http_request", "http_request_sync"]
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 ClientHttpProxyError, InvalidUrlClientError
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 HttpError, HttpResponse
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, object] | None = None,
18
- json: dict[str, object] | None = None,
19
- headers: dict[str, str] | None = None,
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(error=HttpError.TIMEOUT, error_message=str(err))
67
+ return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.TIMEOUT, message=str(err)))
60
68
  except (aiohttp.ClientProxyConnectionError, ProxyConnectionError, ClientHttpProxyError) as err:
61
- return HttpResponse(error=HttpError.PROXY, error_message=str(err))
69
+ return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.PROXY, message=str(err)))
62
70
  except InvalidUrlClientError as e:
63
- return HttpResponse(error=HttpError.INVALID_URL, error_message=str(e))
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(error=HttpError.ERROR, error_message=str(err))
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, object] | None = None,
74
- json: dict[str, object] | None = None,
75
- headers: dict[str, str] | None = None,
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
- error=None,
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, object] | None = None,
99
- json: dict[str, object] | None = None,
100
- headers: dict[str, str] | None = None,
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
- error=None,
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
 
@@ -1,9 +1,17 @@
1
1
  from typing import Any
2
2
 
3
3
  import requests
4
- from requests.exceptions import InvalidSchema, MissingSchema, ProxyError
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 HttpError, HttpResponse
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(error=HttpError.TIMEOUT, error_message=str(e))
63
+ return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.TIMEOUT, message=str(e)))
58
64
  except ProxyError as e:
59
- return HttpResponse(error=HttpError.PROXY, error_message=str(e))
65
+ return HttpResponse(transport_error=TransportErrorDetail(type=TransportError.PROXY, message=str(e)))
60
66
  except (InvalidSchema, MissingSchema) as e:
61
- return HttpResponse(error=HttpError.INVALID_URL, error_message=str(e))
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(error=HttpError.ERROR, error_message=str(e))
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 HttpError(str, enum.Enum):
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
- @dataclass
22
- class HttpResponse:
23
- """HTTP response with status, error, body, and headers."""
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
- def parse_json_body(self, path: str | None = None, none_on_error: bool = False) -> Any: # noqa: ANN401
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.error is not None or (self.status_code is not None and self.status_code >= 400)
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
- return Result.err(error or self.error or "error", extra=self.to_dict())
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.to_dict())
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
- if self.headers is None:
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.1.3
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.12.15
7
- Requires-Dist: mm-result~=0.1.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,,
@@ -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,,