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 ADDED
@@ -0,0 +1,5 @@
1
+ from .http_request import http_request
2
+ from .http_request_sync import http_request_sync
3
+ from .response import HttpError, HttpResponse
4
+
5
+ __all__ = ["HttpError", "HttpResponse", "http_request", "http_request_sync"]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any