mm-std 0.3.30__py3-none-any.whl → 0.4.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_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 Err, Ok, Result
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: ignore[attr-defined]
23
- if isinstance(res, Ok):
24
- return res.unwrap() # type: ignore[no-any-return]
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.err == "validator_error":
26
+ if res.error == "validator_error" and res.extra:
27
27
  print_plain("config validation errors")
28
- for e in res.data["errors"]:
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.err}")
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 Ok(cls(**data))
47
- except ValidationError as err:
48
- return Err("validator_error", data={"errors": err.errors()})
49
- except Exception as err:
50
- return Err(err)
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 BaseModel
8
+ from pydantic import GetCoreSchemaHandler
9
+ from pydantic_core import CoreSchema, core_schema
7
10
 
8
- from mm_std.data_result import DataResult
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(BaseModel):
20
- status_code: int | None = None
21
- error: HttpError | None = None
22
- error_message: str | None = None
23
- body: str | None = None
24
- headers: dict[str, str] | None = 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,53 @@ 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 to_data_result_err[T](self, error: str | None = None) -> DataResult[T]:
44
- return DataResult.err(error or self.error or "error", self.model_dump())
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
+ def __repr__(self) -> str:
76
+ parts: list[str] = []
77
+ if self.status_code is not None:
78
+ parts.append(f"status_code={self.status_code!r}")
79
+ if self.error is not None:
80
+ parts.append(f"error={self.error!r}")
81
+ if self.error_message is not None:
82
+ parts.append(f"error_message={self.error_message!r}")
83
+ if self.body is not None:
84
+ parts.append(f"body={self.body!r}")
85
+ if self.headers is not None:
86
+ parts.append(f"headers={self.headers!r}")
87
+ return f"HttpResponse({', '.join(parts)})"
88
+
89
+ @classmethod
90
+ def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
91
+ return core_schema.no_info_after_validator_function(
92
+ cls._validate,
93
+ core_schema.any_schema(),
94
+ serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.to_dict()),
95
+ )
45
96
 
46
- def to_data_result_ok[T](self, result: T) -> DataResult[T]:
47
- return DataResult.ok(result, self.model_dump())
97
+ @classmethod
98
+ def _validate(cls, value: object) -> HttpResponse:
99
+ if isinstance(value, cls):
100
+ return value
101
+ if isinstance(value, dict):
102
+ return cls(
103
+ status_code=value.get("status_code"),
104
+ error=HttpError(value["error"]) if value.get("error") else None,
105
+ error_message=value.get("error_message"),
106
+ body=value.get("body"),
107
+ headers=value.get("headers"),
108
+ )
109
+ 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 Err, Ok
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, Ok):
18
- return {"ok": o.ok}
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 time
4
- from collections.abc import Callable
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 pydantic_core import core_schema
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
- class Ok[T]:
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
- def unwrap_or_raise(self, _e: object) -> T:
61
- return self.ok
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
- def map[U](self, op: Callable[[T], U]) -> Ok[U]:
64
- return Ok(op(self.ok), data=self.data)
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 map_or[U](self, _default: object, op: Callable[[T], U]) -> U:
67
- return op(self.ok)
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 map_or_else[U](self, _err_op: object, ok_op: Callable[[T], U]) -> U:
29
+ def is_ok(self) -> bool:
70
30
  """
71
- The contained result is `Ok`, so return original value mapped to
72
- a new value using the passed in `op` function.
31
+ Returns True if the result represents success.
73
32
  """
74
- return ok_op(self.ok)
33
+ return self.error is None
75
34
 
76
- def map_err(self, _op: object) -> Ok[T]:
35
+ def is_error(self) -> bool:
77
36
  """
78
- The contained result is `Ok`, so return `Ok` with the original value
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 and_then[U](self, op: Callable[[T], U | Result[U]]) -> Result[U]:
41
+ def is_exception(self) -> bool:
83
42
  """
84
- The contained result is `Ok`, so return the result of `op` with the
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
- try:
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 ok_or_none(self) -> T | None:
103
- return self.ok
104
-
105
- @classmethod
106
- def __get_pydantic_core_schema__(cls, _source_type: object, _handler: object) -> core_schema.CoreSchema:
107
- return core_schema.model_schema(
108
- cls,
109
- core_schema.model_fields_schema(
110
- {
111
- "ok": core_schema.model_field(core_schema.any_schema()),
112
- "data": core_schema.model_field(core_schema.any_schema()),
113
- },
114
- ),
115
- )
116
-
117
-
118
- class Err:
119
- model_config: ClassVar[dict[str, object]] = {"arbitrary_types_allowed": True}
120
- __match_args__ = ("err",)
121
-
122
- def __init__(self, err: str | Exception, data: object = None) -> None:
123
- self.err = f"exception: {err}" if isinstance(err, Exception) else err
124
- self.data = data
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
- if self.data is None:
128
- return f"Err({self.err!r})"
129
- return f"Err({self.err!r}, data={self.data!r})"
130
-
131
- def __eq__(self, other: object) -> bool:
132
- return isinstance(other, Err) and self.err == other.err and self.data == other.data
133
-
134
- def __ne__(self, other: object) -> bool:
135
- return not (self == other)
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((False, self.err, self.data))
139
-
140
- def is_ok(self) -> Literal[False]:
141
- return False
142
-
143
- def is_err(self) -> Literal[True]:
144
- return True
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 ok_or_err[T](self) -> T | str:
227
- return self.err
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
- def ok_or_none[T](self) -> T | None:
230
- return None
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: object, _handler: object) -> core_schema.CoreSchema:
234
- return core_schema.model_schema(
235
- cls,
236
- core_schema.model_fields_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
- T = TypeVar("T")
246
- Result = Union[Ok[T], Err] # noqa: UP007
247
-
248
-
249
- class UnwrapError(Exception):
250
- _result: Result[Any]
251
-
252
- def __init__(self, result: Result[Any], message: str) -> None:
253
- self._result = result
254
- super().__init__(message)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.3.30
3
+ Version: 0.4.0
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: aiohttp-socks~=0.10.1
6
6
  Requires-Dist: aiohttp~=3.11.16
@@ -1,20 +1,18 @@
1
- mm_std/__init__.py,sha256=ePkjykjImp2Wu10lFzK3Fl1HpGqqXFb_HZK_BSiU3MM,3316
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=4ox4D2CgGR76bvZ2n2vGQOYUDagFnlKEDb87to5zpxE,1871
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/http_.py,sha256=5_V5Hx1M07xwzf0VVqAsxSFJkvtaAhj9_sFQP_HliQw,8215
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=2cLa0Lb1W7IY1Kxn-r-9GyqDz3colabp00dcvr7AKbU,7917
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/response.py,sha256=WNQWd1HC134WvjEOSvikHCmujbsvs2332VzUPp9DnyM,1377
32
- mm_std-0.3.30.dist-info/METADATA,sha256=wv2Gs6_yVnt-f58T01_Dt6-cP8btIij1MFm91G04xYM,447
33
- mm_std-0.3.30.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
- mm_std-0.3.30.dist-info/RECORD,,
29
+ mm_std/http/http_request_sync.py,sha256=aawVZfopzMI0alS3lkJQmVOVxH51rmtvsOK_inUlJOs,1537
30
+ mm_std/http/response.py,sha256=A5PXdFaP1YWygrt42WICTBLXLhShu37OkJmkfaIXtZQ,3621
31
+ mm_std-0.4.0.dist-info/METADATA,sha256=o6yleMNzi58oJWMULKE2CmnOT3qOccxfg7Lm58uoEeY,446
32
+ mm_std-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ mm_std-0.4.0.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