mm-std 0.3.25__py3-none-any.whl → 0.3.27__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_std/__init__.py +4 -0
- mm_std/data_result.py +168 -0
- mm_std/http/__init__.py +0 -0
- mm_std/http/http_request.py +121 -0
- mm_std/http/response.py +93 -0
- {mm_std-0.3.25.dist-info → mm_std-0.3.27.dist-info}/METADATA +1 -1
- {mm_std-0.3.25.dist-info → mm_std-0.3.27.dist-info}/RECORD +8 -4
- {mm_std-0.3.25.dist-info → mm_std-0.3.27.dist-info}/WHEEL +0 -0
mm_std/__init__.py
CHANGED
@@ -13,12 +13,16 @@ from .config import BaseConfig as BaseConfig
|
|
13
13
|
from .crypto import fernet_decrypt as fernet_decrypt
|
14
14
|
from .crypto import fernet_encrypt as fernet_encrypt
|
15
15
|
from .crypto import fernet_generate_key as fernet_generate_key
|
16
|
+
from .data_result import DataResult as DataResult
|
16
17
|
from .date import parse_date as parse_date
|
17
18
|
from .date import utc_delta as utc_delta
|
18
19
|
from .date import utc_now as utc_now
|
19
20
|
from .date import utc_random as utc_random
|
20
21
|
from .dict import replace_empty_dict_values as replace_empty_dict_values
|
21
22
|
from .env import get_dotenv as get_dotenv
|
23
|
+
from .http.http_request import http_request as http_request
|
24
|
+
from .http.response import HttpError as HttpError
|
25
|
+
from .http.response import HttpResponse as HttpResponse
|
22
26
|
from .http_ import CHROME_USER_AGENT as CHROME_USER_AGENT
|
23
27
|
from .http_ import FIREFOX_USER_AGENT as FIREFOX_USER_AGENT
|
24
28
|
from .http_ import HResponse as HResponse
|
mm_std/data_result.py
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
|
+
from typing import Any, Generic, TypeVar, cast
|
5
|
+
|
6
|
+
from pydantic import GetCoreSchemaHandler
|
7
|
+
from pydantic_core import CoreSchema, core_schema
|
8
|
+
|
9
|
+
T = TypeVar("T")
|
10
|
+
U = TypeVar("U")
|
11
|
+
|
12
|
+
|
13
|
+
class DataResult(Generic[T]):
|
14
|
+
"""
|
15
|
+
A result wrapper that encapsulates either a successful result (`ok`) or an error message (`err`).
|
16
|
+
Optionally carries auxiliary `data` field regardless of success or failure.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
ok: T | None = None,
|
22
|
+
err: str | None = None,
|
23
|
+
data: object = None,
|
24
|
+
ok_is_none: bool = False, # Allow None as a valid success value
|
25
|
+
) -> None:
|
26
|
+
# Sanity check: at least one of ok or err must be provided, unless explicitly allowed via `ok_is_none`
|
27
|
+
if ok is None and err is None and not ok_is_none:
|
28
|
+
raise ValueError("Either ok or err must be set")
|
29
|
+
# You can't set both ok and err unless ok_is_none is True (used to explicitly accept None as success)
|
30
|
+
if (ok_is_none or ok is not None) and err is not None:
|
31
|
+
raise ValueError("Cannot set both ok and err")
|
32
|
+
|
33
|
+
self.ok = ok
|
34
|
+
self.err = err
|
35
|
+
self.data = data
|
36
|
+
|
37
|
+
def is_ok(self) -> bool:
|
38
|
+
"""
|
39
|
+
Returns True if the result represents a success.
|
40
|
+
"""
|
41
|
+
return self.err is None
|
42
|
+
|
43
|
+
def is_err(self) -> bool:
|
44
|
+
"""
|
45
|
+
Returns True if the result represents an error.
|
46
|
+
"""
|
47
|
+
return self.err is not None
|
48
|
+
|
49
|
+
def unwrap(self) -> T:
|
50
|
+
"""
|
51
|
+
Returns the successful value or raises an exception if this is an error result.
|
52
|
+
"""
|
53
|
+
if self.is_err():
|
54
|
+
raise RuntimeError(f"Called `unwrap()` on an `Err` value: {self.err!r}")
|
55
|
+
return cast(T, self.ok)
|
56
|
+
|
57
|
+
def unwrap_ok_or(self, default: T) -> T:
|
58
|
+
"""
|
59
|
+
Returns the contained success value if this is a success result,
|
60
|
+
or returns the provided default value if this is an error result.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
default: The value to return if this is an error result.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
The success value or the default value.
|
67
|
+
"""
|
68
|
+
if self.is_ok():
|
69
|
+
return cast(T, self.ok)
|
70
|
+
return default
|
71
|
+
|
72
|
+
def unwrap_err(self) -> str:
|
73
|
+
"""
|
74
|
+
Returns the error message or raises an exception if this is a success result.
|
75
|
+
"""
|
76
|
+
if self.is_ok():
|
77
|
+
raise RuntimeError(f"Called `unwrap_err()` on an `Ok` value: {self.ok!r}")
|
78
|
+
return cast(str, self.err)
|
79
|
+
|
80
|
+
def dict(self) -> dict[str, object]:
|
81
|
+
"""
|
82
|
+
Returns a dictionary representation of the result.
|
83
|
+
"""
|
84
|
+
return {"ok": self.ok, "err": self.err, "data": self.data}
|
85
|
+
|
86
|
+
def map(self, fn: Callable[[T], U]) -> DataResult[U]:
|
87
|
+
"""
|
88
|
+
Transforms the success value using the provided function if this is a success result.
|
89
|
+
If this is an error result, returns a new error result with the same error message.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
fn: A function that transforms the success value from type T to type U.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
A new DataResult with the transformed success value or the original error.
|
96
|
+
"""
|
97
|
+
if self.is_err():
|
98
|
+
return DataResult[U](err=self.err, data=self.data)
|
99
|
+
|
100
|
+
mapped_ok = fn(self.unwrap())
|
101
|
+
return DataResult[U](ok=mapped_ok, data=self.data)
|
102
|
+
|
103
|
+
async def map_async(self, fn: Callable[[T], Awaitable[U]]) -> DataResult[U]:
|
104
|
+
"""
|
105
|
+
Asynchronously transforms the success value using the provided async function if this is a success result.
|
106
|
+
If this is an error result, returns a new error result with the same error message.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
fn: An async function that transforms the success value from type T to type U.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
A new DataResult with the transformed success value or the original error.
|
113
|
+
"""
|
114
|
+
if self.is_err():
|
115
|
+
return DataResult[U](err=self.err, data=self.data)
|
116
|
+
|
117
|
+
mapped_ok = await fn(self.unwrap())
|
118
|
+
return DataResult[U](ok=mapped_ok, data=self.data)
|
119
|
+
|
120
|
+
def __repr__(self) -> str:
|
121
|
+
"""
|
122
|
+
Returns the debug representation of the result.
|
123
|
+
"""
|
124
|
+
result = f"DataResult(ok={self.ok!r}" if self.is_ok() else f"DataResult(err={self.err!r}"
|
125
|
+
if self.data is not None:
|
126
|
+
result += f", data={self.data!r}"
|
127
|
+
return result + ")"
|
128
|
+
|
129
|
+
def __hash__(self) -> int:
|
130
|
+
"""
|
131
|
+
Enables hashing for use in sets and dict keys.
|
132
|
+
"""
|
133
|
+
return hash((self.ok, self.err, self.data))
|
134
|
+
|
135
|
+
def __eq__(self, other: object) -> bool:
|
136
|
+
"""
|
137
|
+
Compares two DataResult instances by value.
|
138
|
+
"""
|
139
|
+
if not isinstance(other, DataResult):
|
140
|
+
return NotImplemented
|
141
|
+
return self.ok == other.ok and self.err == other.err and self.data == other.data
|
142
|
+
|
143
|
+
@classmethod
|
144
|
+
def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
|
145
|
+
"""
|
146
|
+
Custom Pydantic v2 integration method for schema generation and validation.
|
147
|
+
"""
|
148
|
+
return core_schema.no_info_after_validator_function(
|
149
|
+
cls._validate,
|
150
|
+
core_schema.any_schema(),
|
151
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.dict()),
|
152
|
+
)
|
153
|
+
|
154
|
+
@classmethod
|
155
|
+
def _validate(cls, v: object) -> DataResult[T]:
|
156
|
+
"""
|
157
|
+
Internal validation logic for Pydantic.
|
158
|
+
Accepts either an instance of DataResult or a dict-like input.
|
159
|
+
"""
|
160
|
+
if isinstance(v, cls):
|
161
|
+
return v
|
162
|
+
if isinstance(v, dict):
|
163
|
+
return cls(
|
164
|
+
ok=v.get("ok"),
|
165
|
+
err=v.get("err"),
|
166
|
+
data=v.get("data"),
|
167
|
+
)
|
168
|
+
raise TypeError(f"Cannot parse value as {cls.__name__}: {v}")
|
mm_std/http/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import aiohttp
|
2
|
+
from aiohttp.typedefs import LooseCookies, Query
|
3
|
+
from aiohttp_socks import ProxyConnector
|
4
|
+
from multidict import CIMultiDictProxy
|
5
|
+
|
6
|
+
from mm_std.http.response import HttpError, HttpResponse
|
7
|
+
|
8
|
+
|
9
|
+
async def http_request(
|
10
|
+
url: str,
|
11
|
+
*,
|
12
|
+
method: str = "GET",
|
13
|
+
params: Query | None = None,
|
14
|
+
data: dict[str, object] | None = None,
|
15
|
+
json: dict[str, object] | None = None,
|
16
|
+
headers: dict[str, str] | None = None,
|
17
|
+
cookies: LooseCookies | None = None,
|
18
|
+
user_agent: str | None = None,
|
19
|
+
proxy: str | None = None,
|
20
|
+
timeout: float | None = 10.0,
|
21
|
+
) -> HttpResponse:
|
22
|
+
"""
|
23
|
+
Send an HTTP request and return the response.
|
24
|
+
"""
|
25
|
+
timeout_ = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
26
|
+
if user_agent:
|
27
|
+
if not headers:
|
28
|
+
headers = {}
|
29
|
+
headers["user-agent"] = user_agent
|
30
|
+
|
31
|
+
try:
|
32
|
+
if proxy and proxy.startswith("socks"):
|
33
|
+
return await _request_with_socks_proxy(
|
34
|
+
url,
|
35
|
+
method=method,
|
36
|
+
params=params,
|
37
|
+
data=data,
|
38
|
+
json=json,
|
39
|
+
headers=headers,
|
40
|
+
cookies=cookies,
|
41
|
+
proxy=proxy,
|
42
|
+
timeout=timeout_,
|
43
|
+
)
|
44
|
+
return await _request_with_http_or_none_proxy(
|
45
|
+
url,
|
46
|
+
method=method,
|
47
|
+
params=params,
|
48
|
+
data=data,
|
49
|
+
json=json,
|
50
|
+
headers=headers,
|
51
|
+
cookies=cookies,
|
52
|
+
proxy=proxy,
|
53
|
+
timeout=timeout_,
|
54
|
+
)
|
55
|
+
except TimeoutError as err:
|
56
|
+
return HttpResponse(error=HttpError.TIMEOUT, error_message=str(err))
|
57
|
+
except Exception as err:
|
58
|
+
return HttpResponse(error=HttpError.ERROR, error_message=str(err))
|
59
|
+
|
60
|
+
|
61
|
+
async def _request_with_http_or_none_proxy(
|
62
|
+
url: str,
|
63
|
+
*,
|
64
|
+
method: str = "GET",
|
65
|
+
params: Query | None = None,
|
66
|
+
data: dict[str, object] | None = None,
|
67
|
+
json: dict[str, object] | None = None,
|
68
|
+
headers: dict[str, str] | None = None,
|
69
|
+
cookies: LooseCookies | None = None,
|
70
|
+
proxy: str | None = None,
|
71
|
+
timeout: aiohttp.ClientTimeout | None,
|
72
|
+
) -> HttpResponse:
|
73
|
+
async with aiohttp.request(
|
74
|
+
method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, proxy=proxy, timeout=timeout
|
75
|
+
) as res:
|
76
|
+
return HttpResponse(
|
77
|
+
status=res.status,
|
78
|
+
error=None,
|
79
|
+
error_message=None,
|
80
|
+
body=(await res.read()).decode(),
|
81
|
+
headers=headers_dict(res.headers),
|
82
|
+
)
|
83
|
+
|
84
|
+
|
85
|
+
async def _request_with_socks_proxy(
|
86
|
+
url: str,
|
87
|
+
*,
|
88
|
+
method: str = "GET",
|
89
|
+
proxy: str,
|
90
|
+
params: Query | None = None,
|
91
|
+
data: dict[str, object] | None = None,
|
92
|
+
json: dict[str, object] | None = None,
|
93
|
+
headers: dict[str, str] | None = None,
|
94
|
+
cookies: LooseCookies | None = None,
|
95
|
+
timeout: aiohttp.ClientTimeout | None,
|
96
|
+
) -> HttpResponse:
|
97
|
+
connector = ProxyConnector.from_url(proxy)
|
98
|
+
async with (
|
99
|
+
aiohttp.ClientSession(connector=connector) as session,
|
100
|
+
session.request(
|
101
|
+
method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, timeout=timeout
|
102
|
+
) as res,
|
103
|
+
):
|
104
|
+
return HttpResponse(
|
105
|
+
status=res.status,
|
106
|
+
error=None,
|
107
|
+
error_message=None,
|
108
|
+
body=(await res.read()).decode(),
|
109
|
+
headers=headers_dict(res.headers),
|
110
|
+
)
|
111
|
+
|
112
|
+
|
113
|
+
def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
|
114
|
+
result: dict[str, str] = {}
|
115
|
+
for key in headers:
|
116
|
+
values = headers.getall(key)
|
117
|
+
if len(values) == 1:
|
118
|
+
result[key] = values[0]
|
119
|
+
else:
|
120
|
+
result[key] = ", ".join(values)
|
121
|
+
return result
|
mm_std/http/response.py
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
import enum
|
2
|
+
import json
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import pydash
|
6
|
+
from pydantic import GetCoreSchemaHandler
|
7
|
+
from pydantic_core import CoreSchema, core_schema
|
8
|
+
|
9
|
+
|
10
|
+
@enum.unique
|
11
|
+
class HttpError(str, enum.Enum):
|
12
|
+
TIMEOUT = "timeout"
|
13
|
+
PROXY = "proxy"
|
14
|
+
CONNECTION = "connection"
|
15
|
+
ERROR = "error"
|
16
|
+
|
17
|
+
|
18
|
+
class HttpResponse:
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
status: int | None = None,
|
22
|
+
error: HttpError | None = None,
|
23
|
+
error_message: str | None = None,
|
24
|
+
body: str | None = None,
|
25
|
+
headers: dict[str, str] | None = None,
|
26
|
+
) -> None:
|
27
|
+
self.status = status
|
28
|
+
self.error = error
|
29
|
+
self.error_message = error_message
|
30
|
+
self.body = body
|
31
|
+
self.headers = headers
|
32
|
+
|
33
|
+
self._json_data: Any = None
|
34
|
+
self._json_parsed = False
|
35
|
+
self._json_parsed_error = False
|
36
|
+
|
37
|
+
def _parse_json(self) -> None:
|
38
|
+
if self.body is None:
|
39
|
+
self._json_parsed_error = True
|
40
|
+
return
|
41
|
+
try:
|
42
|
+
self._json_data = None
|
43
|
+
self._json_data = json.loads(self.body)
|
44
|
+
self._json_parsed_error = False
|
45
|
+
except json.JSONDecodeError:
|
46
|
+
self._json_parsed_error = True
|
47
|
+
self._json_parsed = True
|
48
|
+
|
49
|
+
def json(self, path: str | None = None) -> Any: # noqa: ANN401
|
50
|
+
if not self._json_parsed:
|
51
|
+
self._parse_json()
|
52
|
+
if path:
|
53
|
+
return pydash.get(self._json_data, path, None)
|
54
|
+
return self._json_data
|
55
|
+
|
56
|
+
def dict(self) -> dict[str, object]:
|
57
|
+
return {
|
58
|
+
"status": self.status,
|
59
|
+
"error": self.error,
|
60
|
+
"error_message": self.error_message,
|
61
|
+
"body": self.body,
|
62
|
+
"headers": self.headers,
|
63
|
+
}
|
64
|
+
|
65
|
+
def is_json_parse_error(self) -> bool:
|
66
|
+
if not self._json_parsed:
|
67
|
+
self._parse_json()
|
68
|
+
return self._json_parsed_error
|
69
|
+
|
70
|
+
def __repr__(self) -> str:
|
71
|
+
return f"HttpResponse(status={self.status}, error={self.error}, error_message={self.error_message}, body={self.body}, headers={self.headers})" # noqa: E501
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def __get_pydantic_core_schema__(cls, source_type: type[Any], handler: GetCoreSchemaHandler) -> CoreSchema:
|
75
|
+
return core_schema.no_info_after_validator_function(
|
76
|
+
cls._validate,
|
77
|
+
core_schema.any_schema(),
|
78
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.dict()),
|
79
|
+
)
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
def _validate(cls, v: object) -> "HttpResponse":
|
83
|
+
if isinstance(v, cls):
|
84
|
+
return v
|
85
|
+
if isinstance(v, dict):
|
86
|
+
return cls(
|
87
|
+
status=v.get("status"),
|
88
|
+
error=HttpError(v["error"]) if v.get("error") else None,
|
89
|
+
error_message=v.get("error_message"),
|
90
|
+
body=v.get("body"),
|
91
|
+
headers=v.get("headers"),
|
92
|
+
)
|
93
|
+
raise TypeError(f"Cannot parse value as {cls.__name__}: {v}")
|
@@ -1,7 +1,8 @@
|
|
1
|
-
mm_std/__init__.py,sha256=
|
1
|
+
mm_std/__init__.py,sha256=ePkjykjImp2Wu10lFzK3Fl1HpGqqXFb_HZK_BSiU3MM,3316
|
2
2
|
mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
|
3
3
|
mm_std/config.py,sha256=4ox4D2CgGR76bvZ2n2vGQOYUDagFnlKEDb87to5zpxE,1871
|
4
4
|
mm_std/crypto.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
|
5
|
+
mm_std/data_result.py,sha256=--rg0YPaJkszCmbt9TO-BthdGdYrCRARA8iII9LVocE,5871
|
5
6
|
mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
|
6
7
|
mm_std/dict.py,sha256=6GkhJPXD0LiJDxPcYe6jPdEDw-MN7P7mKu6U5XxwYDk,675
|
7
8
|
mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
|
@@ -25,6 +26,9 @@ mm_std/concurrency/async_task_runner.py,sha256=EN7tN2enkVYVgDbhSiAr-_W4o9m9wBXCv
|
|
25
26
|
mm_std/concurrency/sync_decorators.py,sha256=syCQBOmN7qPO55yzgJB2rbkh10CVww376hmyvs6e5tA,1080
|
26
27
|
mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN8rpR6gY,2407
|
27
28
|
mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
|
28
|
-
mm_std
|
29
|
-
mm_std
|
30
|
-
mm_std
|
29
|
+
mm_std/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
+
mm_std/http/http_request.py,sha256=mJak332gv9NvY0JLO3hNrBxroQGS1HlNBzd1yq4Dy24,3694
|
31
|
+
mm_std/http/response.py,sha256=vvv5COTjJula9t33mFyrruhrFC4dr_Uy0jDKj6t1JxM,2923
|
32
|
+
mm_std-0.3.27.dist-info/METADATA,sha256=H30ViZKdZolKRVDR9PxBk7OoQZWofyy-EqjrUpT2NkQ,415
|
33
|
+
mm_std-0.3.27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
34
|
+
mm_std-0.3.27.dist-info/RECORD,,
|
File without changes
|