mm-std 0.3.30__py3-none-any.whl → 0.4.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_std/__init__.py +1 -14
- mm_std/config.py +12 -12
- mm_std/http/http_request_sync.py +58 -0
- mm_std/http/response.py +83 -12
- mm_std/json_.py +3 -5
- mm_std/result.py +168 -252
- {mm_std-0.3.30.dist-info → mm_std-0.4.1.dist-info}/METADATA +1 -1
- {mm_std-0.3.30.dist-info → mm_std-0.4.1.dist-info}/RECORD +9 -10
- mm_std/data_result.py +0 -249
- mm_std/http_.py +0 -257
- {mm_std-0.3.30.dist-info → mm_std-0.4.1.dist-info}/WHEEL +0 -0
mm_std/__init__.py
CHANGED
@@ -13,7 +13,6 @@ 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
|
17
16
|
from .date import parse_date as parse_date
|
18
17
|
from .date import utc_delta as utc_delta
|
19
18
|
from .date import utc_now as utc_now
|
@@ -21,16 +20,9 @@ from .date import utc_random as utc_random
|
|
21
20
|
from .dict import replace_empty_dict_values as replace_empty_dict_values
|
22
21
|
from .env import get_dotenv as get_dotenv
|
23
22
|
from .http.http_request import http_request as http_request
|
23
|
+
from .http.http_request_sync import http_request_sync as http_request_sync
|
24
24
|
from .http.response import HttpError as HttpError
|
25
25
|
from .http.response import HttpResponse as HttpResponse
|
26
|
-
from .http_ import CHROME_USER_AGENT as CHROME_USER_AGENT
|
27
|
-
from .http_ import FIREFOX_USER_AGENT as FIREFOX_USER_AGENT
|
28
|
-
from .http_ import HResponse as HResponse
|
29
|
-
from .http_ import add_query_params_to_url as add_query_params_to_url
|
30
|
-
from .http_ import hr as hr
|
31
|
-
from .http_ import hra as hra
|
32
|
-
from .http_ import hrequest as hrequest
|
33
|
-
from .http_ import hrequest_async as hrequest_async
|
34
26
|
from .json_ import CustomJSONEncoder as CustomJSONEncoder
|
35
27
|
from .json_ import json_dumps as json_dumps
|
36
28
|
from .log import configure_logging as configure_logging
|
@@ -47,12 +39,7 @@ from .print_ import print_table as print_table
|
|
47
39
|
from .random_ import random_choice as random_choice
|
48
40
|
from .random_ import random_decimal as random_decimal
|
49
41
|
from .random_ import random_str_choice as random_str_choice
|
50
|
-
from .result import Err as Err
|
51
|
-
from .result import Ok as Ok
|
52
42
|
from .result import Result as Result
|
53
|
-
from .result import err as err
|
54
|
-
from .result import ok as ok
|
55
|
-
from .result import try_ok as try_ok
|
56
43
|
from .str import number_with_separator as number_with_separator
|
57
44
|
from .str import str_contains_any as str_contains_any
|
58
45
|
from .str import str_ends_with_any as str_ends_with_any
|
mm_std/config.py
CHANGED
@@ -6,7 +6,7 @@ from typing import NoReturn, Self
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, ValidationError
|
7
7
|
|
8
8
|
from .print_ import print_json, print_plain
|
9
|
-
from .result import
|
9
|
+
from .result import Result
|
10
10
|
from .zip import read_text_from_zip_archive
|
11
11
|
|
12
12
|
|
@@ -19,18 +19,18 @@ class BaseConfig(BaseModel):
|
|
19
19
|
|
20
20
|
@classmethod
|
21
21
|
def read_toml_config_or_exit[T](cls: type[T], config_path: Path, zip_password: str = "") -> T: # noqa: PYI019 # nosec
|
22
|
-
res = cls.read_toml_config(config_path, zip_password) # type:
|
23
|
-
if
|
24
|
-
return res.unwrap()
|
22
|
+
res: Result[T] = cls.read_toml_config(config_path, zip_password) # type:ignore[attr-defined]
|
23
|
+
if res.is_ok():
|
24
|
+
return res.unwrap()
|
25
25
|
|
26
|
-
if res.
|
26
|
+
if res.error == "validator_error" and res.extra:
|
27
27
|
print_plain("config validation errors")
|
28
|
-
for e in res.
|
28
|
+
for e in res.extra["errors"]:
|
29
29
|
loc = e["loc"]
|
30
30
|
field = ".".join(str(lo) for lo in loc) if len(loc) > 0 else ""
|
31
31
|
print_plain(f"{field} {e['msg']}")
|
32
32
|
else:
|
33
|
-
print_plain(f"can't parse config file: {res.
|
33
|
+
print_plain(f"can't parse config file: {res.error}")
|
34
34
|
|
35
35
|
sys.exit(1)
|
36
36
|
|
@@ -43,8 +43,8 @@ class BaseConfig(BaseModel):
|
|
43
43
|
else:
|
44
44
|
with config_path.open("rb") as f:
|
45
45
|
data = tomllib.load(f)
|
46
|
-
return
|
47
|
-
except ValidationError as
|
48
|
-
return
|
49
|
-
except Exception as
|
50
|
-
return
|
46
|
+
return Result.success(cls(**data))
|
47
|
+
except ValidationError as e:
|
48
|
+
return Result.failure_with_exception(e, error="validator_error", extra={"errors": e.errors()})
|
49
|
+
except Exception as e:
|
50
|
+
return Result.failure_with_exception(e)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import requests
|
4
|
+
|
5
|
+
from mm_std.http.response import HttpError, HttpResponse
|
6
|
+
|
7
|
+
|
8
|
+
def http_request_sync(
|
9
|
+
url: str,
|
10
|
+
*,
|
11
|
+
method: str = "GET",
|
12
|
+
params: dict[str, Any] | None = None,
|
13
|
+
data: dict[str, Any] | None = None,
|
14
|
+
json: dict[str, Any] | None = None,
|
15
|
+
headers: dict[str, Any] | None = None,
|
16
|
+
cookies: dict[str, Any] | None = None,
|
17
|
+
user_agent: str | None = None,
|
18
|
+
proxy: str | None = None,
|
19
|
+
timeout: float | None = 10.0,
|
20
|
+
) -> HttpResponse:
|
21
|
+
"""
|
22
|
+
Send a synchronous HTTP request and return the response.
|
23
|
+
"""
|
24
|
+
if user_agent:
|
25
|
+
if headers is None:
|
26
|
+
headers = {}
|
27
|
+
headers["User-Agent"] = user_agent
|
28
|
+
|
29
|
+
proxies: dict[str, str] | None = None
|
30
|
+
if proxy:
|
31
|
+
proxies = {
|
32
|
+
"http": proxy,
|
33
|
+
"https": proxy,
|
34
|
+
}
|
35
|
+
|
36
|
+
try:
|
37
|
+
res = requests.request(
|
38
|
+
method=method,
|
39
|
+
url=url,
|
40
|
+
params=params,
|
41
|
+
data=data,
|
42
|
+
json=json,
|
43
|
+
headers=headers,
|
44
|
+
cookies=cookies,
|
45
|
+
timeout=timeout,
|
46
|
+
proxies=proxies,
|
47
|
+
)
|
48
|
+
return HttpResponse(
|
49
|
+
status_code=res.status_code,
|
50
|
+
error=None,
|
51
|
+
error_message=None,
|
52
|
+
body=res.text,
|
53
|
+
headers=dict(res.headers),
|
54
|
+
)
|
55
|
+
except requests.Timeout 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))
|
mm_std/http/response.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import enum
|
2
4
|
import json
|
3
5
|
from typing import Any
|
4
6
|
|
5
7
|
import pydash
|
6
|
-
from pydantic import
|
8
|
+
from pydantic import GetCoreSchemaHandler
|
9
|
+
from pydantic_core import CoreSchema, core_schema
|
7
10
|
|
8
|
-
from mm_std.
|
11
|
+
from mm_std.result import Result
|
9
12
|
|
10
13
|
|
11
14
|
@enum.unique
|
@@ -16,12 +19,26 @@ class HttpError(str, enum.Enum):
|
|
16
19
|
ERROR = "error"
|
17
20
|
|
18
21
|
|
19
|
-
class HttpResponse
|
20
|
-
status_code: int | None
|
21
|
-
error: HttpError | None
|
22
|
-
error_message: str | None
|
23
|
-
body: str | None
|
24
|
-
headers: dict[str, str] | None
|
22
|
+
class HttpResponse:
|
23
|
+
status_code: int | None
|
24
|
+
error: HttpError | None
|
25
|
+
error_message: str | None
|
26
|
+
body: str | None
|
27
|
+
headers: dict[str, str] | None
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
status_code: int | None = None,
|
32
|
+
error: HttpError | None = None,
|
33
|
+
error_message: str | None = None,
|
34
|
+
body: str | None = None,
|
35
|
+
headers: dict[str, str] | None = None,
|
36
|
+
) -> None:
|
37
|
+
self.status_code = status_code
|
38
|
+
self.error = error
|
39
|
+
self.error_message = error_message
|
40
|
+
self.body = body
|
41
|
+
self.headers = headers
|
25
42
|
|
26
43
|
def parse_json_body(self, path: str | None = None, none_on_error: bool = False) -> Any: # noqa: ANN401
|
27
44
|
if self.body is None:
|
@@ -40,8 +57,62 @@ class HttpResponse(BaseModel):
|
|
40
57
|
def is_error(self) -> bool:
|
41
58
|
return self.error is not None or (self.status_code is not None and self.status_code >= 400)
|
42
59
|
|
43
|
-
def
|
44
|
-
return
|
60
|
+
def to_result_err[T](self, error: str | None = None) -> Result[T]:
|
61
|
+
return Result.failure(error or self.error or "error", extra=self.to_dict())
|
62
|
+
|
63
|
+
def to_result_ok[T](self, result: T) -> Result[T]:
|
64
|
+
return Result.success(result, extra=self.to_dict())
|
65
|
+
|
66
|
+
def to_dict(self) -> dict[str, Any]:
|
67
|
+
return {
|
68
|
+
"status_code": self.status_code,
|
69
|
+
"error": self.error.value if self.error else None,
|
70
|
+
"error_message": self.error_message,
|
71
|
+
"body": self.body,
|
72
|
+
"headers": self.headers,
|
73
|
+
}
|
74
|
+
|
75
|
+
@property
|
76
|
+
def content_type(self) -> str | None:
|
77
|
+
if self.headers is None:
|
78
|
+
return None
|
79
|
+
for key in self.headers:
|
80
|
+
if key.lower() == "content-type":
|
81
|
+
return self.headers[key]
|
82
|
+
return None
|
83
|
+
|
84
|
+
def __repr__(self) -> str:
|
85
|
+
parts: list[str] = []
|
86
|
+
if self.status_code is not None:
|
87
|
+
parts.append(f"status_code={self.status_code!r}")
|
88
|
+
if self.error is not None:
|
89
|
+
parts.append(f"error={self.error!r}")
|
90
|
+
if self.error_message is not None:
|
91
|
+
parts.append(f"error_message={self.error_message!r}")
|
92
|
+
if self.body is not None:
|
93
|
+
parts.append(f"body={self.body!r}")
|
94
|
+
if self.headers is not None:
|
95
|
+
parts.append(f"headers={self.headers!r}")
|
96
|
+
return f"HttpResponse({', '.join(parts)})"
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
|
100
|
+
return core_schema.no_info_after_validator_function(
|
101
|
+
cls._validate,
|
102
|
+
core_schema.any_schema(),
|
103
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.to_dict()),
|
104
|
+
)
|
45
105
|
|
46
|
-
|
47
|
-
|
106
|
+
@classmethod
|
107
|
+
def _validate(cls, value: object) -> HttpResponse:
|
108
|
+
if isinstance(value, cls):
|
109
|
+
return value
|
110
|
+
if isinstance(value, dict):
|
111
|
+
return cls(
|
112
|
+
status_code=value.get("status_code"),
|
113
|
+
error=HttpError(value["error"]) if value.get("error") else None,
|
114
|
+
error_message=value.get("error_message"),
|
115
|
+
body=value.get("body"),
|
116
|
+
headers=value.get("headers"),
|
117
|
+
)
|
118
|
+
raise TypeError(f"Invalid value for HttpResponse: {value}")
|
mm_std/json_.py
CHANGED
@@ -9,15 +9,13 @@ from pathlib import Path
|
|
9
9
|
|
10
10
|
from pydantic import BaseModel
|
11
11
|
|
12
|
-
from mm_std.result import
|
12
|
+
from mm_std.result import Result
|
13
13
|
|
14
14
|
|
15
15
|
class CustomJSONEncoder(JSONEncoder):
|
16
16
|
def default(self, o: object) -> object:
|
17
|
-
if isinstance(o,
|
18
|
-
return
|
19
|
-
if isinstance(o, Err):
|
20
|
-
return {"err": o.err}
|
17
|
+
if isinstance(o, Result):
|
18
|
+
return o.to_dict()
|
21
19
|
if isinstance(o, Decimal):
|
22
20
|
return str(o)
|
23
21
|
if isinstance(o, Path):
|
mm_std/result.py
CHANGED
@@ -1,281 +1,197 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
from
|
5
|
-
from typing import Any, ClassVar, Literal, NoReturn, TypeGuard, TypeVar, Union
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
|
+
from typing import Any, TypeVar, cast
|
6
5
|
|
7
|
-
from
|
6
|
+
from pydantic import GetCoreSchemaHandler
|
7
|
+
from pydantic_core import CoreSchema, core_schema
|
8
8
|
|
9
|
+
T = TypeVar("T")
|
10
|
+
U = TypeVar("U")
|
9
11
|
|
10
|
-
|
11
|
-
model_config: ClassVar[dict[str, object]] = {"arbitrary_types_allowed": True}
|
12
|
-
__match_args__ = ("ok",)
|
13
|
-
|
14
|
-
def __init__(self, ok: T, data: object = None) -> None:
|
15
|
-
self.ok = ok
|
16
|
-
self.data = data
|
17
|
-
|
18
|
-
def __repr__(self) -> str:
|
19
|
-
if self.data is None:
|
20
|
-
return f"Ok({self.ok!r})"
|
21
|
-
return f"Ok({self.ok!r}, data={self.data!r})"
|
22
|
-
|
23
|
-
def __eq__(self, other: object) -> bool:
|
24
|
-
return isinstance(other, Ok) and self.ok == other.ok and self.data == other.data
|
25
|
-
|
26
|
-
def __ne__(self, other: object) -> bool:
|
27
|
-
return not (self == other)
|
28
|
-
|
29
|
-
def __hash__(self) -> int:
|
30
|
-
return hash((True, self.ok, self.data))
|
31
|
-
|
32
|
-
def is_ok(self) -> Literal[True]:
|
33
|
-
return True
|
34
|
-
|
35
|
-
def is_err(self) -> Literal[False]:
|
36
|
-
return False
|
37
|
-
|
38
|
-
@property
|
39
|
-
def err(self) -> None:
|
40
|
-
return None
|
41
|
-
|
42
|
-
def expect(self, _message: str) -> T:
|
43
|
-
return self.ok
|
44
|
-
|
45
|
-
def expect_err(self, message: str) -> NoReturn:
|
46
|
-
raise UnwrapError(self, message)
|
47
|
-
|
48
|
-
def unwrap(self) -> T:
|
49
|
-
return self.ok
|
50
|
-
|
51
|
-
def unwrap_err(self) -> NoReturn:
|
52
|
-
raise UnwrapError(self, "Called `Result.unwrap_err()` on an `Ok` value")
|
53
|
-
|
54
|
-
def unwrap_or[U](self, _default: U) -> T:
|
55
|
-
return self.ok
|
12
|
+
type Extra = dict[str, Any] | None
|
56
13
|
|
57
|
-
def unwrap_or_else(self, _op: object) -> T:
|
58
|
-
return self.ok
|
59
14
|
|
60
|
-
|
61
|
-
|
15
|
+
class Result[T]:
|
16
|
+
"""
|
17
|
+
A container representing either a successful result or an error.
|
18
|
+
Use `Result.success()` or `Result.failure()` to create instances.
|
19
|
+
"""
|
62
20
|
|
63
|
-
|
64
|
-
|
21
|
+
ok: T | None # Success value, if any
|
22
|
+
error: str | None # Error message, if any
|
23
|
+
exception: Exception | None # Exception, if any. It's optional.
|
24
|
+
extra: Extra # Optional extra metadata
|
65
25
|
|
66
|
-
def
|
67
|
-
|
26
|
+
def __init__(self) -> None:
|
27
|
+
raise RuntimeError("Result is not intended to be instantiated directly. Use the static methods instead.")
|
68
28
|
|
69
|
-
def
|
29
|
+
def is_ok(self) -> bool:
|
70
30
|
"""
|
71
|
-
|
72
|
-
a new value using the passed in `op` function.
|
31
|
+
Returns True if the result represents success.
|
73
32
|
"""
|
74
|
-
return
|
33
|
+
return self.error is None
|
75
34
|
|
76
|
-
def
|
35
|
+
def is_error(self) -> bool:
|
77
36
|
"""
|
78
|
-
|
37
|
+
Returns True if the result represents an error.
|
79
38
|
"""
|
80
|
-
return self
|
39
|
+
return self.error is not None
|
81
40
|
|
82
|
-
def
|
41
|
+
def is_exception(self) -> bool:
|
83
42
|
"""
|
84
|
-
|
85
|
-
original value passed in. If return of `op` function is not Result, it will be a Ok value.
|
43
|
+
Returns True if an exception is attached to the result.
|
86
44
|
"""
|
87
|
-
|
88
|
-
res = op(self.ok)
|
89
|
-
if not isinstance(res, Ok | Err):
|
90
|
-
res = Ok(res)
|
91
|
-
except Exception as e:
|
92
|
-
res = Err(e)
|
93
|
-
res.data = self.data
|
94
|
-
return res
|
95
|
-
|
96
|
-
def or_else(self, _op: object) -> Ok[T]:
|
97
|
-
return self
|
98
|
-
|
99
|
-
def ok_or_err(self) -> T | str:
|
100
|
-
return self.ok
|
45
|
+
return self.exception is not None
|
101
46
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
self.
|
47
|
+
def unwrap(self) -> T:
|
48
|
+
"""
|
49
|
+
Returns the success value.
|
50
|
+
Raises RuntimeError if the result is an error.
|
51
|
+
"""
|
52
|
+
if not self.is_ok():
|
53
|
+
raise RuntimeError(f"Called unwrap() on a failure value: {self.error}")
|
54
|
+
return cast(T, self.ok)
|
55
|
+
|
56
|
+
def unwrap_or(self, default: T) -> T:
|
57
|
+
"""
|
58
|
+
Returns the success value if available, otherwise returns the given default.
|
59
|
+
"""
|
60
|
+
if not self.is_ok():
|
61
|
+
return default
|
62
|
+
return cast(T, self.ok)
|
63
|
+
|
64
|
+
def unwrap_error(self) -> str:
|
65
|
+
"""
|
66
|
+
Returns the error message.
|
67
|
+
Raises RuntimeError if the result is a success.
|
68
|
+
"""
|
69
|
+
if self.is_ok():
|
70
|
+
raise RuntimeError("Called unwrap_error() on a success value")
|
71
|
+
return cast(str, self.error)
|
72
|
+
|
73
|
+
def unwrap_exception(self) -> Exception:
|
74
|
+
"""
|
75
|
+
Returns the attached exception if present.
|
76
|
+
Raises RuntimeError if the result has no exception attached.
|
77
|
+
"""
|
78
|
+
if self.exception is not None:
|
79
|
+
return self.exception
|
80
|
+
raise RuntimeError("No exception provided")
|
81
|
+
|
82
|
+
def to_dict(self) -> dict[str, object]:
|
83
|
+
"""
|
84
|
+
Returns a dictionary representation of the result.
|
85
|
+
Note: the exception is converted to a string if present.
|
86
|
+
"""
|
87
|
+
return {
|
88
|
+
"ok": self.ok,
|
89
|
+
"error": self.error,
|
90
|
+
"exception": str(self.exception) if self.exception else None,
|
91
|
+
"extra": self.extra,
|
92
|
+
}
|
93
|
+
|
94
|
+
def map(self, fn: Callable[[T], U]) -> Result[U]:
|
95
|
+
if self.is_ok():
|
96
|
+
try:
|
97
|
+
new_value = fn(cast(T, self.ok))
|
98
|
+
return Result.success(new_value, extra=self.extra)
|
99
|
+
except Exception as e:
|
100
|
+
return Result.failure_with_exception(e, error="map_exception", extra=self.extra)
|
101
|
+
return cast(Result[U], self)
|
102
|
+
|
103
|
+
async def map_async(self, fn: Callable[[T], Awaitable[U]]) -> Result[U]:
|
104
|
+
if self.is_ok():
|
105
|
+
try:
|
106
|
+
new_value = await fn(cast(T, self.ok))
|
107
|
+
return Result.success(new_value, extra=self.extra)
|
108
|
+
except Exception as e:
|
109
|
+
return Result.failure_with_exception(e, error="map_exception", extra=self.extra)
|
110
|
+
return cast(Result[U], self)
|
111
|
+
|
112
|
+
def and_then(self, fn: Callable[[T], Result[U]]) -> Result[U]:
|
113
|
+
if self.is_ok():
|
114
|
+
try:
|
115
|
+
return fn(cast(T, self.ok))
|
116
|
+
except Exception as e:
|
117
|
+
return Result.failure_with_exception(e, error="and_then_exception", extra=self.extra)
|
118
|
+
return cast(Result[U], self)
|
119
|
+
|
120
|
+
async def and_then_async(self, fn: Callable[[T], Awaitable[Result[U]]]) -> Result[U]:
|
121
|
+
if self.is_ok():
|
122
|
+
try:
|
123
|
+
return await fn(cast(T, self.ok))
|
124
|
+
except Exception as e:
|
125
|
+
return Result.failure_with_exception(e, error="and_then_exception", extra=self.extra)
|
126
|
+
return cast(Result[U], self)
|
125
127
|
|
126
128
|
def __repr__(self) -> str:
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
129
|
+
parts: list[str] = []
|
130
|
+
if self.ok is not None:
|
131
|
+
parts.append(f"ok={self.ok!r}")
|
132
|
+
if self.error is not None:
|
133
|
+
parts.append(f"error={self.error!r}")
|
134
|
+
if self.exception is not None:
|
135
|
+
parts.append(f"exception={self.exception!r}")
|
136
|
+
if self.extra is not None:
|
137
|
+
parts.append(f"extra={self.extra!r}")
|
138
|
+
return f"Result({', '.join(parts)})"
|
136
139
|
|
137
140
|
def __hash__(self) -> int:
|
138
|
-
return hash(
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
@property
|
147
|
-
def ok(self) -> None:
|
148
|
-
"""
|
149
|
-
Return `None`.
|
150
|
-
"""
|
151
|
-
return None
|
152
|
-
|
153
|
-
def expect(self, message: str) -> NoReturn:
|
154
|
-
"""
|
155
|
-
Raises an `UnwrapError`.
|
156
|
-
"""
|
157
|
-
exc = UnwrapError(self, f"{message}: {self.err!r}")
|
158
|
-
if isinstance(self.err, BaseException):
|
159
|
-
raise exc from self.err
|
160
|
-
raise exc
|
161
|
-
|
162
|
-
def expect_err(self, _message: str) -> str:
|
163
|
-
"""
|
164
|
-
Return the inner value
|
165
|
-
"""
|
166
|
-
return self.err
|
167
|
-
|
168
|
-
def unwrap(self) -> NoReturn:
|
169
|
-
"""
|
170
|
-
Raises an `UnwrapError`.
|
171
|
-
"""
|
172
|
-
exc = UnwrapError(self, f"Called `Result.unwrap()` on an `Err` value: {self.err!r}")
|
173
|
-
if isinstance(self.err, BaseException):
|
174
|
-
raise exc from self.err
|
175
|
-
raise exc
|
176
|
-
|
177
|
-
def unwrap_err(self) -> str:
|
178
|
-
"""
|
179
|
-
Return the inner value
|
180
|
-
"""
|
181
|
-
return self.err
|
182
|
-
|
183
|
-
def unwrap_or[U](self, default: U) -> U:
|
184
|
-
"""
|
185
|
-
Return `default`.
|
186
|
-
"""
|
187
|
-
return default
|
188
|
-
|
189
|
-
def unwrap_or_else[T](self, op: Callable[[str], T]) -> T:
|
190
|
-
"""
|
191
|
-
The contained result is ``Err``, so return the result of applying
|
192
|
-
``op`` to the error value.
|
193
|
-
"""
|
194
|
-
return op(self.err)
|
195
|
-
|
196
|
-
def unwrap_or_raise[TBE: BaseException](self, e: type[TBE]) -> NoReturn:
|
197
|
-
"""
|
198
|
-
The contained result is ``Err``, so raise the exception with the value.
|
199
|
-
"""
|
200
|
-
raise e(self.err)
|
201
|
-
|
202
|
-
def map(self, _op: object) -> Err:
|
203
|
-
"""
|
204
|
-
Return `Err` with the same value
|
205
|
-
"""
|
206
|
-
return self
|
207
|
-
|
208
|
-
def map_or[U](self, default: U, _op: object) -> U:
|
209
|
-
"""
|
210
|
-
Return the default value
|
211
|
-
"""
|
212
|
-
return default
|
213
|
-
|
214
|
-
def map_or_else[U](self, err_op: Callable[[str], U], _ok_op: object) -> U:
|
215
|
-
"""
|
216
|
-
Return the result of the default operation
|
217
|
-
"""
|
218
|
-
return err_op(self.err)
|
219
|
-
|
220
|
-
def and_then(self, _op: object) -> Err:
|
221
|
-
"""
|
222
|
-
The contained result is `Err`, so return `Err` with the original value
|
223
|
-
"""
|
224
|
-
return self
|
141
|
+
return hash(
|
142
|
+
(
|
143
|
+
self.ok,
|
144
|
+
self.error,
|
145
|
+
self.exception,
|
146
|
+
frozenset(self.extra.items()) if self.extra else None,
|
147
|
+
)
|
148
|
+
)
|
225
149
|
|
226
|
-
def
|
227
|
-
|
150
|
+
def __eq__(self, other: object) -> bool:
|
151
|
+
if not isinstance(other, Result):
|
152
|
+
return False
|
153
|
+
return (
|
154
|
+
self.ok == other.ok and self.error == other.error and self.exception == other.exception and self.extra == other.extra
|
155
|
+
)
|
228
156
|
|
229
|
-
|
230
|
-
|
157
|
+
@classmethod
|
158
|
+
def _create(cls, ok: T | None, error: str | None, exception: Exception | None, extra: Extra) -> Result[T]:
|
159
|
+
obj = object.__new__(cls)
|
160
|
+
obj.ok = ok
|
161
|
+
obj.error = error
|
162
|
+
obj.exception = exception
|
163
|
+
obj.extra = extra
|
164
|
+
return obj
|
165
|
+
|
166
|
+
@staticmethod
|
167
|
+
def success(ok: T, extra: Extra = None) -> Result[T]:
|
168
|
+
return Result._create(ok=ok, error=None, exception=None, extra=extra)
|
169
|
+
|
170
|
+
@staticmethod
|
171
|
+
def failure(error: str, extra: Extra = None) -> Result[T]:
|
172
|
+
return Result._create(ok=None, error=error, exception=None, extra=extra)
|
173
|
+
|
174
|
+
@staticmethod
|
175
|
+
def failure_with_exception(exception: Exception, *, error: str = "exception", extra: Extra = None) -> Result[T]:
|
176
|
+
return Result._create(ok=None, error=error, exception=exception, extra=extra)
|
231
177
|
|
232
178
|
@classmethod
|
233
|
-
def __get_pydantic_core_schema__(cls, _source_type:
|
234
|
-
return core_schema.
|
235
|
-
cls,
|
236
|
-
core_schema.
|
237
|
-
|
238
|
-
"err": core_schema.model_field(core_schema.any_schema()),
|
239
|
-
"data": core_schema.model_field(core_schema.any_schema()),
|
240
|
-
},
|
241
|
-
),
|
179
|
+
def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
|
180
|
+
return core_schema.no_info_after_validator_function(
|
181
|
+
cls._validate,
|
182
|
+
core_schema.any_schema(),
|
183
|
+
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.to_dict()),
|
242
184
|
)
|
243
185
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
@property
|
257
|
-
def result(self) -> Result[Any]:
|
258
|
-
return self._result
|
259
|
-
|
260
|
-
|
261
|
-
def ok(result: Result[T]) -> TypeGuard[Ok[T]]:
|
262
|
-
"""Used for type narrowing from `Result` to `Ok`."""
|
263
|
-
return isinstance(result, Ok)
|
264
|
-
|
265
|
-
|
266
|
-
def err(result: Result[T]) -> TypeGuard[Err]:
|
267
|
-
"""Used for type narrowing from `Result` to `Err`."""
|
268
|
-
return isinstance(result, Err)
|
269
|
-
|
270
|
-
|
271
|
-
def try_ok[T](fn: Callable[..., Result[T]], *, args: tuple[object], attempts: int, delay: float = 0) -> Result[T]:
|
272
|
-
if attempts <= 0:
|
273
|
-
raise ValueError("attempts must be more than zero")
|
274
|
-
res: Result[T] = Err("not started")
|
275
|
-
for _ in range(attempts):
|
276
|
-
res = fn(*args)
|
277
|
-
if res.is_ok():
|
278
|
-
return res
|
279
|
-
if delay:
|
280
|
-
time.sleep(delay)
|
281
|
-
return res
|
186
|
+
@classmethod
|
187
|
+
def _validate(cls, value: object) -> Result[Any]:
|
188
|
+
if isinstance(value, cls):
|
189
|
+
return value
|
190
|
+
if isinstance(value, dict):
|
191
|
+
return cls._create(
|
192
|
+
ok=value.get("ok"),
|
193
|
+
error=value.get("error"),
|
194
|
+
exception=value.get("exception"),
|
195
|
+
extra=value.get("extra"),
|
196
|
+
)
|
197
|
+
raise TypeError(f"Invalid value for Result: {value}")
|
@@ -1,20 +1,18 @@
|
|
1
|
-
mm_std/__init__.py,sha256=
|
1
|
+
mm_std/__init__.py,sha256=Kl-7TT_hd-xmwDJJ0E5AO2Qjpj5H8H7A93DgDwv_r-0,2804
|
2
2
|
mm_std/command.py,sha256=ze286wjUjg0QSTgIu-2WZks53_Vclg69UaYYgPpQvCU,1283
|
3
|
-
mm_std/config.py,sha256=
|
3
|
+
mm_std/config.py,sha256=VCrvTIjq21uDJngPANVClq5CO9xjmeDJCjRlZgn-fBs,1918
|
4
4
|
mm_std/crypto.py,sha256=jdk0_TCmeU0pPXMyz9xH6kQHSjjZ9GcGClBwQps5vBo,340
|
5
|
-
mm_std/data_result.py,sha256=aUDhnxp5EagYFkCiOGKYReQshDrK_3zXfGYla5mJ8Yk,8739
|
6
5
|
mm_std/date.py,sha256=976eEkSONuNqHQBgSRu8hrtH23tJqztbmHFHLdbP2TY,1879
|
7
6
|
mm_std/dict.py,sha256=6GkhJPXD0LiJDxPcYe6jPdEDw-MN7P7mKu6U5XxwYDk,675
|
8
7
|
mm_std/env.py,sha256=5zaR9VeIfObN-4yfgxoFeU5IM1GDeZZj9SuYf7t9sOA,125
|
9
8
|
mm_std/fs.py,sha256=RwarNRJq3tIMG6LVX_g03hasfYpjYFh_O27oVDt5IPQ,291
|
10
|
-
mm_std/
|
11
|
-
mm_std/json_.py,sha256=Naa6mBE4D0yiQGkPNRrFvndnUH3R7ovw3FeaejWV60o,1196
|
9
|
+
mm_std/json_.py,sha256=YVvROb5egcF1aQ2fXzyWG8Yw0JvYwpNBwtcBzsOADPo,1133
|
12
10
|
mm_std/log.py,sha256=0TkTsAlUTt00gjgukvsvnZRIAGELq0MI6Lv8mKP-Wz4,2887
|
13
11
|
mm_std/net.py,sha256=qdRCBIDneip6FaPNe5mx31UtYVmzqam_AoUF7ydEyjA,590
|
14
12
|
mm_std/print_.py,sha256=zB7sVbSSF8RffMxvnOdbKCXjCKtKzKV3R68pBri4NkQ,1638
|
15
13
|
mm_std/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
14
|
mm_std/random_.py,sha256=OuUX4VJeSd13NZBya4qrGpR2TfN7_87tfebOY6DBUnI,1113
|
17
|
-
mm_std/result.py,sha256=
|
15
|
+
mm_std/result.py,sha256=gUmseH3fkapLIcTDkaeilPkubO5nHXWUCc5PmWh3EiY,6874
|
18
16
|
mm_std/str.py,sha256=BEjJ1p5O4-uSYK0h-enasSSDdwzkBbiwdQ4_dsrlEE8,3257
|
19
17
|
mm_std/toml.py,sha256=CNznWKR0bpOxS6e3VB5LGS-Oa9lW-wterkcPUFtPcls,610
|
20
18
|
mm_std/types_.py,sha256=9FGd2q47a8M9QQgsWJR1Kq34jLxBAkYSoJuwih4PPqg,257
|
@@ -28,7 +26,8 @@ mm_std/concurrency/sync_scheduler.py,sha256=j4tBL_cBI1spr0cZplTA7N2CoYsznuORMeRN
|
|
28
26
|
mm_std/concurrency/sync_task_runner.py,sha256=s5JPlLYLGQGHIxy4oDS-PN7O9gcy-yPZFoNm8RQwzcw,1780
|
29
27
|
mm_std/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
28
|
mm_std/http/http_request.py,sha256=h74_ZjACwdbeINjzhuEwNUpvnaHZvyGl7TExjlBwMGg,3704
|
31
|
-
mm_std/http/
|
32
|
-
mm_std
|
33
|
-
mm_std-0.
|
34
|
-
mm_std-0.
|
29
|
+
mm_std/http/http_request_sync.py,sha256=aawVZfopzMI0alS3lkJQmVOVxH51rmtvsOK_inUlJOs,1537
|
30
|
+
mm_std/http/response.py,sha256=wRFeBgsmShwsC3goSpKq7VG-Pcr9FGsPwKn4vd_VWqU,3875
|
31
|
+
mm_std-0.4.1.dist-info/METADATA,sha256=9ZG4BGhh3YvzMNgnOT_hgUUhJfWUJ_iYuIjI1Up_R_A,446
|
32
|
+
mm_std-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
33
|
+
mm_std-0.4.1.dist-info/RECORD,,
|
mm_std/data_result.py
DELETED
@@ -1,249 +0,0 @@
|
|
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
|
-
type Data = dict[str, object] | None
|
14
|
-
|
15
|
-
|
16
|
-
class DataResult(Generic[T]):
|
17
|
-
"""
|
18
|
-
A result wrapper that encapsulates either a successful result (`ok`) or an error message (`err`).
|
19
|
-
Optionally carries `data` field regardless of success or failure.
|
20
|
-
"""
|
21
|
-
|
22
|
-
_ok: T | None
|
23
|
-
_err: str | None
|
24
|
-
data: Data | None
|
25
|
-
|
26
|
-
def __init__(self) -> None:
|
27
|
-
raise RuntimeError("DataResult is not intended to be instantiated directly. Use the static methods instead.")
|
28
|
-
|
29
|
-
def is_ok(self) -> bool:
|
30
|
-
"""
|
31
|
-
Returns True if the result represents a success.
|
32
|
-
"""
|
33
|
-
return self._err is None
|
34
|
-
|
35
|
-
def is_err(self) -> bool:
|
36
|
-
"""
|
37
|
-
Returns True if the result represents an error.
|
38
|
-
"""
|
39
|
-
return self._err is not None
|
40
|
-
|
41
|
-
def unwrap(self) -> T:
|
42
|
-
"""
|
43
|
-
Returns the successful value or raises an exception if this is an error result.
|
44
|
-
"""
|
45
|
-
if self.is_err():
|
46
|
-
raise RuntimeError(f"Called `unwrap()` on an `Err` value: {self.err!r}")
|
47
|
-
return cast(T, self._ok)
|
48
|
-
|
49
|
-
def unwrap_ok_or(self, default: T) -> T:
|
50
|
-
"""
|
51
|
-
Returns the contained success value if this is a success result,
|
52
|
-
or returns the provided default value if this is an error result.
|
53
|
-
|
54
|
-
Args:
|
55
|
-
default: The value to return if this is an error result.
|
56
|
-
|
57
|
-
Returns:
|
58
|
-
The success value or the default value.
|
59
|
-
"""
|
60
|
-
if self.is_ok():
|
61
|
-
return cast(T, self._ok)
|
62
|
-
return default
|
63
|
-
|
64
|
-
def unwrap_err(self) -> str:
|
65
|
-
"""
|
66
|
-
Returns the error message or raises an exception if this is a success result.
|
67
|
-
"""
|
68
|
-
if self.is_ok():
|
69
|
-
raise RuntimeError(f"Called `unwrap_err()` on an `Ok` value: {self.ok!r}")
|
70
|
-
return cast(str, self._err)
|
71
|
-
|
72
|
-
def dict(self) -> dict[str, object]:
|
73
|
-
"""
|
74
|
-
Returns a dictionary representation of the result.
|
75
|
-
"""
|
76
|
-
return {"ok": self._ok, "err": self._err, "data": self.data}
|
77
|
-
|
78
|
-
def map(self, fn: Callable[[T], U]) -> DataResult[U]:
|
79
|
-
"""
|
80
|
-
Transforms the success value using the provided function if this is a success result.
|
81
|
-
If this is an error result, returns a new error result with the same error message.
|
82
|
-
If the function raises an exception, returns a new error result with the exception message.
|
83
|
-
|
84
|
-
Args:
|
85
|
-
fn: A function that transforms the success value from type T to type U.
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
A new DataResult with the transformed success value or an error.
|
89
|
-
"""
|
90
|
-
if self.is_err():
|
91
|
-
return DataResult[U].err(self.unwrap_err(), self.data)
|
92
|
-
|
93
|
-
try:
|
94
|
-
mapped_ok = fn(self.unwrap())
|
95
|
-
return DataResult[U].ok(mapped_ok, self.data)
|
96
|
-
except Exception as e:
|
97
|
-
return DataResult[U].exception(e, data={"original_data": self.data, "original_ok": self.ok})
|
98
|
-
|
99
|
-
async def map_async(self, fn: Callable[[T], Awaitable[U]]) -> DataResult[U]:
|
100
|
-
"""
|
101
|
-
Asynchronously transforms the success value using the provided async function if this is a success result.
|
102
|
-
If this is an error result, returns a new error result with the same error message.
|
103
|
-
If the function raises an exception, returns a new error result with the exception message.
|
104
|
-
|
105
|
-
Args:
|
106
|
-
fn: An async function that transforms the success value from type T to type U.
|
107
|
-
|
108
|
-
Returns:
|
109
|
-
A new DataResult with the transformed success value or an error.
|
110
|
-
"""
|
111
|
-
if self.is_err():
|
112
|
-
return DataResult[U].err(self.unwrap_err(), self.data)
|
113
|
-
|
114
|
-
try:
|
115
|
-
mapped_ok = await fn(self.unwrap())
|
116
|
-
return DataResult[U].ok(mapped_ok, self.data)
|
117
|
-
except Exception as e:
|
118
|
-
return DataResult[U].exception(e, data={"original_data": self.data, "original_ok": self.ok})
|
119
|
-
|
120
|
-
def and_then(self, fn: Callable[[T], DataResult[U]]) -> DataResult[U]:
|
121
|
-
"""
|
122
|
-
Applies the function to the success value if this is a success result.
|
123
|
-
If this is an error result, returns a new error result with the same error message.
|
124
|
-
|
125
|
-
Unlike map, the function must return a DataResult.
|
126
|
-
|
127
|
-
Args:
|
128
|
-
fn: A function that takes the success value and returns a new DataResult.
|
129
|
-
|
130
|
-
Returns:
|
131
|
-
The result of the function application or the original error.
|
132
|
-
"""
|
133
|
-
if self.is_err():
|
134
|
-
return DataResult[U].err(self.unwrap_err(), self.data)
|
135
|
-
|
136
|
-
try:
|
137
|
-
return fn(self.unwrap())
|
138
|
-
except Exception as e:
|
139
|
-
return DataResult[U].exception(e, data={"original_data": self.data, "original_ok": self.ok})
|
140
|
-
|
141
|
-
async def and_then_async(self, fn: Callable[[T], Awaitable[DataResult[U]]]) -> DataResult[U]:
|
142
|
-
"""
|
143
|
-
Asynchronously applies the function to the success value if this is a success result.
|
144
|
-
If this is an error result, returns a new error result with the same error message.
|
145
|
-
|
146
|
-
Unlike map_async, the function must return a DataResult.
|
147
|
-
|
148
|
-
Args:
|
149
|
-
fn: An async function that takes the success value and returns a new DataResult.
|
150
|
-
|
151
|
-
Returns:
|
152
|
-
The result of the function application or the original error.
|
153
|
-
"""
|
154
|
-
if self.is_err():
|
155
|
-
return DataResult[U].err(self.unwrap_err(), self.data)
|
156
|
-
|
157
|
-
try:
|
158
|
-
return await fn(self.unwrap())
|
159
|
-
except Exception as e:
|
160
|
-
return DataResult[U].exception(e, {"original_data": self.data, "original_ok": self.ok})
|
161
|
-
|
162
|
-
def __repr__(self) -> str:
|
163
|
-
"""
|
164
|
-
Returns the debug representation of the result.
|
165
|
-
"""
|
166
|
-
result = f"DataResult(ok={self._ok!r}" if self.is_ok() else f"DataResult(err={self._err!r}"
|
167
|
-
if self.data is not None:
|
168
|
-
result += f", data={self.data!r}"
|
169
|
-
return result + ")"
|
170
|
-
|
171
|
-
def __hash__(self) -> int:
|
172
|
-
"""
|
173
|
-
Enables hashing for use in sets and dict keys.
|
174
|
-
"""
|
175
|
-
return hash((self.ok, self.err, self.data))
|
176
|
-
|
177
|
-
def __eq__(self, other: object) -> bool:
|
178
|
-
"""
|
179
|
-
Compares two DataResult instances by value.
|
180
|
-
"""
|
181
|
-
if not isinstance(other, DataResult):
|
182
|
-
return NotImplemented
|
183
|
-
return self._ok == other._ok and self._err == other._err and self.data == other.data
|
184
|
-
|
185
|
-
@classmethod
|
186
|
-
def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
|
187
|
-
"""
|
188
|
-
Custom Pydantic v2 integration method for schema generation and validation.
|
189
|
-
"""
|
190
|
-
return core_schema.no_info_after_validator_function(
|
191
|
-
cls._validate,
|
192
|
-
core_schema.any_schema(),
|
193
|
-
serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.dict()),
|
194
|
-
)
|
195
|
-
|
196
|
-
@classmethod
|
197
|
-
def _validate(cls, v: object) -> DataResult[T]:
|
198
|
-
"""
|
199
|
-
Internal validation logic for Pydantic.
|
200
|
-
Accepts either an instance of DataResult or a dict-like input.
|
201
|
-
"""
|
202
|
-
if isinstance(v, cls):
|
203
|
-
return v
|
204
|
-
if isinstance(v, dict):
|
205
|
-
return cls._create(
|
206
|
-
ok=v.get("ok"),
|
207
|
-
err=v.get("err"),
|
208
|
-
data=v.get("data"),
|
209
|
-
)
|
210
|
-
raise TypeError(f"Cannot parse value as {cls.__name__}: {v}")
|
211
|
-
|
212
|
-
@classmethod
|
213
|
-
def _create(cls, ok: T | None, err: str | None, data: Data) -> DataResult[T]:
|
214
|
-
"""
|
215
|
-
Internal method to create a DataResult instance.
|
216
|
-
"""
|
217
|
-
obj = object.__new__(cls)
|
218
|
-
obj._ok = ok # noqa: SLF001
|
219
|
-
obj._err = err # noqa: SLF001
|
220
|
-
obj.data = data
|
221
|
-
return obj
|
222
|
-
|
223
|
-
@staticmethod
|
224
|
-
def ok(value: T, data: Data = None) -> DataResult[T]:
|
225
|
-
"""
|
226
|
-
Static method to create a successful DataResult.
|
227
|
-
"""
|
228
|
-
return DataResult._create(ok=value, err=None, data=data)
|
229
|
-
|
230
|
-
@staticmethod
|
231
|
-
def err(error: str, data: Data = None) -> DataResult[T]:
|
232
|
-
"""
|
233
|
-
Static method to create an error DataResult.
|
234
|
-
"""
|
235
|
-
return DataResult._create(ok=None, err=error, data=data)
|
236
|
-
|
237
|
-
@staticmethod
|
238
|
-
def exception(err: Exception, data: Data = None) -> DataResult[T]:
|
239
|
-
"""
|
240
|
-
Static method to create an error DataResult from an exception.
|
241
|
-
"""
|
242
|
-
if data is None:
|
243
|
-
data = {}
|
244
|
-
key = "exception_message"
|
245
|
-
while key in data:
|
246
|
-
key += "_"
|
247
|
-
data[key] = str(err)
|
248
|
-
|
249
|
-
return DataResult._create(ok=None, err="exception", data=data)
|
mm_std/http_.py
DELETED
@@ -1,257 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from dataclasses import asdict, dataclass, field
|
3
|
-
from typing import Any, cast
|
4
|
-
from urllib.parse import urlencode, urlparse
|
5
|
-
|
6
|
-
import aiohttp
|
7
|
-
import pydash
|
8
|
-
import requests
|
9
|
-
from aiohttp_socks import ProxyConnector
|
10
|
-
from multidict import CIMultiDictProxy
|
11
|
-
from requests.auth import AuthBase
|
12
|
-
|
13
|
-
from mm_std.result import Err, Ok, Result
|
14
|
-
|
15
|
-
FIREFOX_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:134.0) Gecko/20100101 Firefox/134.0"
|
16
|
-
SAFARI_USER_AGENT = (
|
17
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15"
|
18
|
-
)
|
19
|
-
CHROME_USER_AGENT = (
|
20
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
21
|
-
)
|
22
|
-
|
23
|
-
|
24
|
-
@dataclass
|
25
|
-
class HResponse:
|
26
|
-
code: int = 0
|
27
|
-
error: str | None = None
|
28
|
-
body: str = ""
|
29
|
-
headers: dict[str, str] = field(default_factory=dict)
|
30
|
-
|
31
|
-
_json_data: Any = None
|
32
|
-
_json_parsed = False
|
33
|
-
_json_parsed_error = False
|
34
|
-
|
35
|
-
def _parse_json(self) -> None:
|
36
|
-
try:
|
37
|
-
self._json_data = None
|
38
|
-
self._json_data = json.loads(self.body)
|
39
|
-
self._json_parsed_error = False
|
40
|
-
except json.JSONDecodeError:
|
41
|
-
self._json_parsed_error = True
|
42
|
-
self._json_parsed = True
|
43
|
-
|
44
|
-
@property
|
45
|
-
def json(self) -> Any: # noqa: ANN401
|
46
|
-
if not self._json_parsed:
|
47
|
-
self._parse_json()
|
48
|
-
return self._json_data
|
49
|
-
|
50
|
-
@property
|
51
|
-
def json_parse_error(self) -> bool:
|
52
|
-
if not self._json_parsed:
|
53
|
-
self._parse_json()
|
54
|
-
return self._json_parsed_error
|
55
|
-
|
56
|
-
@property
|
57
|
-
def content_type(self) -> str | None:
|
58
|
-
for key in self.headers:
|
59
|
-
if key.lower() == "content-type":
|
60
|
-
return self.headers[key]
|
61
|
-
return None
|
62
|
-
|
63
|
-
def to_err_result[T](self, error: str | None = None) -> Err:
|
64
|
-
return Err(error or self.error or "error", data=asdict(self))
|
65
|
-
|
66
|
-
def to_ok_result[T](self, result: T) -> Result[T]:
|
67
|
-
return Ok(result, data=asdict(self))
|
68
|
-
|
69
|
-
def is_error(self) -> bool:
|
70
|
-
return self.error is not None
|
71
|
-
|
72
|
-
def is_timeout_error(self) -> bool:
|
73
|
-
return self.error == "timeout"
|
74
|
-
|
75
|
-
def is_proxy_error(self) -> bool:
|
76
|
-
return self.error == "proxy"
|
77
|
-
|
78
|
-
def is_connection_error(self) -> bool:
|
79
|
-
return self.error is not None and self.error.startswith("connection:")
|
80
|
-
|
81
|
-
def is_dns_error(self) -> bool:
|
82
|
-
return self.error is not None and self.error.startswith("dns:")
|
83
|
-
|
84
|
-
def to_dict(self) -> dict[str, Any]:
|
85
|
-
return pydash.omit(asdict(self), "_json_data")
|
86
|
-
|
87
|
-
|
88
|
-
def hrequest(
|
89
|
-
url: str,
|
90
|
-
*,
|
91
|
-
method: str = "GET",
|
92
|
-
proxy: str | None = None,
|
93
|
-
params: dict[str, Any] | None = None,
|
94
|
-
headers: dict[str, Any] | None = None,
|
95
|
-
cookies: dict[str, Any] | None = None,
|
96
|
-
timeout: float = 10,
|
97
|
-
user_agent: str | None = None,
|
98
|
-
json_params: bool = True,
|
99
|
-
auth: AuthBase | tuple[str, str] | None = None,
|
100
|
-
verify: bool = True,
|
101
|
-
) -> HResponse:
|
102
|
-
query_params: dict[str, Any] | None = None
|
103
|
-
data: dict[str, Any] | None = None
|
104
|
-
json_: dict[str, Any] | None = None
|
105
|
-
method = method.upper()
|
106
|
-
if not headers:
|
107
|
-
headers = {}
|
108
|
-
if user_agent:
|
109
|
-
headers["user-agent"] = user_agent
|
110
|
-
if method == "GET":
|
111
|
-
query_params = params
|
112
|
-
elif json_params:
|
113
|
-
json_ = params
|
114
|
-
else:
|
115
|
-
data = params
|
116
|
-
|
117
|
-
proxies = None
|
118
|
-
if proxy:
|
119
|
-
proxies = {
|
120
|
-
"http": proxy,
|
121
|
-
"https": proxy,
|
122
|
-
}
|
123
|
-
|
124
|
-
try:
|
125
|
-
r = requests.request(
|
126
|
-
method,
|
127
|
-
url,
|
128
|
-
proxies=proxies,
|
129
|
-
timeout=timeout,
|
130
|
-
cookies=cookies,
|
131
|
-
auth=auth,
|
132
|
-
verify=verify,
|
133
|
-
headers=headers,
|
134
|
-
params=query_params,
|
135
|
-
json=json_,
|
136
|
-
data=data,
|
137
|
-
)
|
138
|
-
return HResponse(code=r.status_code, body=r.text, headers=dict(r.headers))
|
139
|
-
except requests.exceptions.Timeout:
|
140
|
-
return HResponse(error="timeout")
|
141
|
-
except requests.exceptions.ProxyError:
|
142
|
-
return HResponse(error="proxy")
|
143
|
-
except requests.exceptions.RequestException as err:
|
144
|
-
return HResponse(error=f"connection: {err}")
|
145
|
-
except Exception as err:
|
146
|
-
return HResponse(error=f"exception: {err}")
|
147
|
-
|
148
|
-
|
149
|
-
async def hrequest_async(
|
150
|
-
url: str,
|
151
|
-
*,
|
152
|
-
method: str = "GET",
|
153
|
-
proxy: str | None = None,
|
154
|
-
params: dict[str, Any] | None = None,
|
155
|
-
headers: dict[str, Any] | None = None,
|
156
|
-
cookies: dict[str, Any] | None = None,
|
157
|
-
timeout: float = 10,
|
158
|
-
user_agent: str | None = None,
|
159
|
-
json_params: bool = True,
|
160
|
-
auth: tuple[str, str] | None = None,
|
161
|
-
) -> HResponse:
|
162
|
-
query_params: dict[str, Any] | None = None
|
163
|
-
data: dict[str, Any] | None = None
|
164
|
-
json_: dict[str, Any] | None = None
|
165
|
-
method = method.upper()
|
166
|
-
|
167
|
-
if not headers:
|
168
|
-
headers = {}
|
169
|
-
if user_agent:
|
170
|
-
headers["user-agent"] = user_agent
|
171
|
-
if method == "GET":
|
172
|
-
query_params = params
|
173
|
-
elif json_params:
|
174
|
-
json_ = params
|
175
|
-
else:
|
176
|
-
data = params
|
177
|
-
|
178
|
-
try:
|
179
|
-
request_kwargs: dict[str, Any] = {"headers": headers}
|
180
|
-
if query_params:
|
181
|
-
request_kwargs["params"] = query_params
|
182
|
-
if json_:
|
183
|
-
request_kwargs["json"] = json_
|
184
|
-
if data:
|
185
|
-
request_kwargs["data"] = data
|
186
|
-
if cookies:
|
187
|
-
request_kwargs["cookies"] = cookies
|
188
|
-
if auth and isinstance(auth, tuple) and len(auth) == 2:
|
189
|
-
request_kwargs["auth"] = aiohttp.BasicAuth(auth[0], auth[1])
|
190
|
-
|
191
|
-
if proxy and proxy.startswith("socks"):
|
192
|
-
return await _aiohttp_socks5(url, method, proxy, request_kwargs, timeout)
|
193
|
-
return await _aiohttp(url, method, request_kwargs, timeout=timeout, proxy=proxy)
|
194
|
-
|
195
|
-
except TimeoutError:
|
196
|
-
return HResponse(error="timeout")
|
197
|
-
except (aiohttp.ClientProxyConnectionError, aiohttp.ClientHttpProxyError, aiohttp.ClientConnectorError) as err:
|
198
|
-
if is_proxy_error(str(err), proxy):
|
199
|
-
return HResponse(error="proxy")
|
200
|
-
return HResponse(error=f"connection: {err}")
|
201
|
-
except aiohttp.ClientError as err:
|
202
|
-
return HResponse(error=f"error: {err}")
|
203
|
-
except Exception as err:
|
204
|
-
return HResponse(error=f"exception: {err}")
|
205
|
-
|
206
|
-
|
207
|
-
def is_proxy_error(error_message: str, proxy: str | None) -> bool:
|
208
|
-
if not proxy:
|
209
|
-
return False
|
210
|
-
error_message = error_message.lower()
|
211
|
-
if "proxy" in error_message:
|
212
|
-
return True
|
213
|
-
return bool("cannot connect to" in error_message and cast(str, urlparse(proxy).hostname) in error_message)
|
214
|
-
|
215
|
-
|
216
|
-
async def _aiohttp(
|
217
|
-
url: str, method: str, request_kwargs: dict[str, object], timeout: float | None = None, proxy: str | None = None
|
218
|
-
) -> HResponse:
|
219
|
-
if proxy:
|
220
|
-
request_kwargs["proxy"] = proxy
|
221
|
-
client_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
222
|
-
async with aiohttp.ClientSession(timeout=client_timeout) as session, session.request(method, url, **request_kwargs) as res: # type: ignore[arg-type]
|
223
|
-
return HResponse(code=res.status, body=await res.text(), headers=headers_dict(res.headers))
|
224
|
-
|
225
|
-
|
226
|
-
async def _aiohttp_socks5(
|
227
|
-
url: str, method: str, proxy: str, request_kwargs: dict[str, object], timeout: float | None = None
|
228
|
-
) -> HResponse:
|
229
|
-
connector = ProxyConnector.from_url(proxy)
|
230
|
-
client_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None
|
231
|
-
async with (
|
232
|
-
aiohttp.ClientSession(connector=connector, timeout=client_timeout) as session,
|
233
|
-
session.request(method, url, **request_kwargs) as res, # type: ignore[arg-type]
|
234
|
-
):
|
235
|
-
return HResponse(code=res.status, body=await res.text(), headers=headers_dict(res.headers))
|
236
|
-
|
237
|
-
|
238
|
-
def add_query_params_to_url(url: str, params: dict[str, object]) -> str:
|
239
|
-
query_params = urlencode({k: v for k, v in params.items() if v is not None})
|
240
|
-
if query_params:
|
241
|
-
url += f"?{query_params}"
|
242
|
-
return url
|
243
|
-
|
244
|
-
|
245
|
-
hr = hrequest
|
246
|
-
hra = hrequest_async
|
247
|
-
|
248
|
-
|
249
|
-
def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
|
250
|
-
result: dict[str, str] = {}
|
251
|
-
for key in headers:
|
252
|
-
values = headers.getall(key)
|
253
|
-
if len(values) == 1:
|
254
|
-
result[key] = values[0]
|
255
|
-
else:
|
256
|
-
result[key] = ", ".join(values)
|
257
|
-
return result
|
File without changes
|