mm-std 0.4.17__py3-none-any.whl → 0.5.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.
@@ -1,126 +0,0 @@
1
- import aiohttp
2
- from aiohttp import ClientHttpProxyError, InvalidUrlClientError
3
- from aiohttp.typedefs import LooseCookies, Query
4
- from aiohttp_socks import ProxyConnectionError, ProxyConnector
5
- from multidict import CIMultiDictProxy
6
-
7
- from mm_std.http.http_response import HttpError, HttpResponse
8
-
9
-
10
- async def http_request(
11
- url: str,
12
- *,
13
- method: str = "GET",
14
- params: Query | None = None,
15
- data: dict[str, object] | None = None,
16
- json: dict[str, object] | None = None,
17
- headers: dict[str, str] | None = None,
18
- cookies: LooseCookies | None = None,
19
- user_agent: str | None = None,
20
- proxy: str | None = None,
21
- timeout: float | None = 10.0,
22
- ) -> HttpResponse:
23
- """
24
- Send an HTTP request and return the response.
25
- """
26
- timeout_ = aiohttp.ClientTimeout(total=timeout) if timeout else None
27
- if user_agent:
28
- if not headers:
29
- headers = {}
30
- headers["user-agent"] = user_agent
31
-
32
- try:
33
- if proxy and proxy.startswith("socks"):
34
- return await _request_with_socks_proxy(
35
- url,
36
- method=method,
37
- params=params,
38
- data=data,
39
- json=json,
40
- headers=headers,
41
- cookies=cookies,
42
- proxy=proxy,
43
- timeout=timeout_,
44
- )
45
- return await _request_with_http_or_none_proxy(
46
- url,
47
- method=method,
48
- params=params,
49
- data=data,
50
- json=json,
51
- headers=headers,
52
- cookies=cookies,
53
- proxy=proxy,
54
- timeout=timeout_,
55
- )
56
- except TimeoutError as err:
57
- return HttpResponse(error=HttpError.TIMEOUT, error_message=str(err))
58
- except (aiohttp.ClientProxyConnectionError, ProxyConnectionError, ClientHttpProxyError) as err:
59
- return HttpResponse(error=HttpError.PROXY, error_message=str(err))
60
- except InvalidUrlClientError as e:
61
- return HttpResponse(error=HttpError.INVALID_URL, error_message=str(e))
62
- except Exception as err:
63
- return HttpResponse(error=HttpError.ERROR, error_message=str(err))
64
-
65
-
66
- async def _request_with_http_or_none_proxy(
67
- url: str,
68
- *,
69
- method: str = "GET",
70
- params: Query | None = None,
71
- data: dict[str, object] | None = None,
72
- json: dict[str, object] | None = None,
73
- headers: dict[str, str] | None = None,
74
- cookies: LooseCookies | None = None,
75
- proxy: str | None = None,
76
- timeout: aiohttp.ClientTimeout | None,
77
- ) -> HttpResponse:
78
- async with aiohttp.request(
79
- method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, proxy=proxy, timeout=timeout
80
- ) as res:
81
- return HttpResponse(
82
- status_code=res.status,
83
- error=None,
84
- error_message=None,
85
- body=(await res.read()).decode(),
86
- headers=headers_dict(res.headers),
87
- )
88
-
89
-
90
- async def _request_with_socks_proxy(
91
- url: str,
92
- *,
93
- method: str = "GET",
94
- proxy: str,
95
- params: Query | None = None,
96
- data: dict[str, object] | None = None,
97
- json: dict[str, object] | None = None,
98
- headers: dict[str, str] | None = None,
99
- cookies: LooseCookies | None = None,
100
- timeout: aiohttp.ClientTimeout | None,
101
- ) -> HttpResponse:
102
- connector = ProxyConnector.from_url(proxy)
103
- async with (
104
- aiohttp.ClientSession(connector=connector) as session,
105
- session.request(
106
- method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, timeout=timeout
107
- ) as res,
108
- ):
109
- return HttpResponse(
110
- status_code=res.status,
111
- error=None,
112
- error_message=None,
113
- body=(await res.read()).decode(),
114
- headers=headers_dict(res.headers),
115
- )
116
-
117
-
118
- def headers_dict(headers: CIMultiDictProxy[str]) -> dict[str, str]:
119
- result: dict[str, str] = {}
120
- for key in headers:
121
- values = headers.getall(key)
122
- if len(values) == 1:
123
- result[key] = values[0]
124
- else:
125
- result[key] = ", ".join(values)
126
- return result
@@ -1,63 +0,0 @@
1
- from typing import Any
2
-
3
- import requests
4
- from requests.exceptions import InvalidSchema, MissingSchema, ProxyError
5
-
6
- from mm_std.http.http_response import HttpError, HttpResponse
7
-
8
-
9
- def http_request_sync(
10
- url: str,
11
- *,
12
- method: str = "GET",
13
- params: dict[str, Any] | None = None,
14
- data: dict[str, Any] | None = None,
15
- json: dict[str, Any] | None = None,
16
- headers: dict[str, Any] | None = None,
17
- cookies: dict[str, Any] | None = None,
18
- user_agent: str | None = None,
19
- proxy: str | None = None,
20
- timeout: float | None = 10.0,
21
- ) -> HttpResponse:
22
- """
23
- Send a synchronous HTTP request and return the response.
24
- """
25
- if user_agent:
26
- if headers is None:
27
- headers = {}
28
- headers["User-Agent"] = user_agent
29
-
30
- proxies: dict[str, str] | None = None
31
- if proxy:
32
- proxies = {
33
- "http": proxy,
34
- "https": proxy,
35
- }
36
-
37
- try:
38
- res = requests.request(
39
- method=method,
40
- url=url,
41
- params=params,
42
- data=data,
43
- json=json,
44
- headers=headers,
45
- cookies=cookies,
46
- timeout=timeout,
47
- proxies=proxies,
48
- )
49
- return HttpResponse(
50
- status_code=res.status_code,
51
- error=None,
52
- error_message=None,
53
- body=res.text,
54
- headers=dict(res.headers),
55
- )
56
- except requests.Timeout as e:
57
- return HttpResponse(error=HttpError.TIMEOUT, error_message=str(e))
58
- except ProxyError as e:
59
- return HttpResponse(error=HttpError.PROXY, error_message=str(e))
60
- except (InvalidSchema, MissingSchema) as e:
61
- return HttpResponse(error=HttpError.INVALID_URL, error_message=str(e))
62
- except Exception as e:
63
- return HttpResponse(error=HttpError.ERROR, error_message=str(e))
@@ -1,119 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import json
5
- from typing import Any
6
-
7
- import pydash
8
- from pydantic import GetCoreSchemaHandler
9
- from pydantic_core import CoreSchema, core_schema
10
-
11
- from mm_std.result import Result
12
-
13
-
14
- @enum.unique
15
- class HttpError(str, enum.Enum):
16
- TIMEOUT = "timeout"
17
- PROXY = "proxy"
18
- INVALID_URL = "invalid_url"
19
- CONNECTION = "connection"
20
- ERROR = "error"
21
-
22
-
23
- class HttpResponse:
24
- status_code: int | None
25
- error: HttpError | None
26
- error_message: str | None
27
- body: str | None
28
- headers: dict[str, str] | None
29
-
30
- def __init__(
31
- self,
32
- status_code: int | None = None,
33
- error: HttpError | None = None,
34
- error_message: str | None = None,
35
- body: str | None = None,
36
- headers: dict[str, str] | None = None,
37
- ) -> None:
38
- self.status_code = status_code
39
- self.error = error
40
- self.error_message = error_message
41
- self.body = body
42
- self.headers = headers
43
-
44
- def parse_json_body(self, path: str | None = None, none_on_error: bool = False) -> Any: # noqa: ANN401
45
- if self.body is None:
46
- if none_on_error:
47
- return None
48
- raise ValueError("Body is None")
49
-
50
- try:
51
- res = json.loads(self.body)
52
- return pydash.get(res, path, None) if path else res
53
- except json.JSONDecodeError:
54
- if none_on_error:
55
- return None
56
- raise
57
-
58
- def is_err(self) -> bool:
59
- return self.error is not None or (self.status_code is not None and self.status_code >= 400)
60
-
61
- def to_err[T](self, error: str | Exception | tuple[str, Exception] | None = None) -> Result[T]:
62
- return Result.err(error or self.error or "error", extra=self.to_dict())
63
-
64
- def to_ok[T](self, value: T) -> Result[T]:
65
- return Result.ok(value, extra=self.to_dict())
66
-
67
- def to_dict(self) -> dict[str, Any]:
68
- return {
69
- "status_code": self.status_code,
70
- "error": self.error.value if self.error else None,
71
- "error_message": self.error_message,
72
- "body": self.body,
73
- "headers": self.headers,
74
- }
75
-
76
- @property
77
- def content_type(self) -> str | None:
78
- if self.headers is None:
79
- return None
80
- for key in self.headers:
81
- if key.lower() == "content-type":
82
- return self.headers[key]
83
- return None
84
-
85
- def __repr__(self) -> str:
86
- parts: list[str] = []
87
- if self.status_code is not None:
88
- parts.append(f"status_code={self.status_code!r}")
89
- if self.error is not None:
90
- parts.append(f"error={self.error!r}")
91
- if self.error_message is not None:
92
- parts.append(f"error_message={self.error_message!r}")
93
- if self.body is not None:
94
- parts.append(f"body={self.body!r}")
95
- if self.headers is not None:
96
- parts.append(f"headers={self.headers!r}")
97
- return f"HttpResponse({', '.join(parts)})"
98
-
99
- @classmethod
100
- def __get_pydantic_core_schema__(cls, _source_type: type[Any], _handler: GetCoreSchemaHandler) -> CoreSchema:
101
- return core_schema.no_info_after_validator_function(
102
- cls._validate,
103
- core_schema.any_schema(),
104
- serialization=core_schema.plain_serializer_function_ser_schema(lambda x: x.to_dict()),
105
- )
106
-
107
- @classmethod
108
- def _validate(cls, value: object) -> HttpResponse:
109
- if isinstance(value, cls):
110
- return value
111
- if isinstance(value, dict):
112
- return cls(
113
- status_code=value.get("status_code"),
114
- error=HttpError(value["error"]) if value.get("error") else None,
115
- error_message=value.get("error_message"),
116
- body=value.get("body"),
117
- headers=value.get("headers"),
118
- )
119
- raise TypeError(f"Invalid value for HttpResponse: {value}")
mm_std/json_.py DELETED
@@ -1,38 +0,0 @@
1
- import json
2
- from collections.abc import Callable
3
- from dataclasses import asdict, is_dataclass
4
- from datetime import date, datetime
5
- from decimal import Decimal
6
- from enum import Enum
7
- from json import JSONEncoder
8
- from pathlib import Path
9
-
10
- from pydantic import BaseModel
11
-
12
- from mm_std.result import Result
13
-
14
-
15
- class CustomJSONEncoder(JSONEncoder):
16
- def default(self, o: object) -> object:
17
- if isinstance(o, Result):
18
- return o.to_dict()
19
- if isinstance(o, Decimal):
20
- return str(o)
21
- if isinstance(o, Path):
22
- return str(o)
23
- if isinstance(o, datetime | date):
24
- return o.isoformat()
25
- if is_dataclass(o) and not isinstance(o, type):
26
- return asdict(o)
27
- if isinstance(o, Enum):
28
- return o.value
29
- if isinstance(o, BaseModel):
30
- return o.model_dump()
31
- if isinstance(o, Exception):
32
- return str(o)
33
-
34
- return super().default(o)
35
-
36
-
37
- def json_dumps(data: object, default_serializer: Callable[[object], str] | None = str) -> str:
38
- return json.dumps(data, cls=CustomJSONEncoder, default=default_serializer)
mm_std/log.py DELETED
@@ -1,85 +0,0 @@
1
- import logging
2
- from logging.handlers import RotatingFileHandler
3
- from pathlib import Path
4
-
5
- from rich.logging import RichHandler
6
-
7
-
8
- class ExtraFormatter(logging.Formatter):
9
- def format(self, record: logging.LogRecord) -> str:
10
- base = super().format(record)
11
- extras = {
12
- key: value
13
- for key, value in record.__dict__.items()
14
- if key not in logging.LogRecord.__dict__
15
- and key
16
- not in (
17
- "name",
18
- "msg",
19
- "args",
20
- "levelname",
21
- "levelno",
22
- "pathname",
23
- "filename",
24
- "module",
25
- "exc_info",
26
- "exc_text",
27
- "stack_info",
28
- "lineno",
29
- "funcName",
30
- "created",
31
- "msecs",
32
- "relativeCreated",
33
- "thread",
34
- "threadName",
35
- "processName",
36
- "process",
37
- "message",
38
- "taskName",
39
- "asctime",
40
- )
41
- }
42
- if extras:
43
- extras_str = " | " + " ".join(f"{k}={v}" for k, v in extras.items())
44
- return base + extras_str
45
- return base
46
-
47
-
48
- def configure_logging(developer_console: bool = False, console_level: int = logging.INFO) -> None:
49
- """
50
- Configure the root logger with a custom formatter that includes extra fields.
51
- """
52
- logger = logging.getLogger()
53
- logger.setLevel(console_level)
54
- logger.handlers.clear()
55
-
56
- console_handler: logging.Handler
57
-
58
- if developer_console:
59
- console_handler = RichHandler(rich_tracebacks=True, show_time=True, show_level=True, show_path=False)
60
- formatter = ExtraFormatter("{message}", style="{")
61
- else:
62
- console_handler = logging.StreamHandler()
63
- formatter = ExtraFormatter("{asctime} - {name} - {levelname} - {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{")
64
- console_handler.setFormatter(formatter)
65
-
66
- logger.addHandler(console_handler)
67
-
68
-
69
- def init_logger(name: str, file_path: str | None = None, file_mkdir: bool = True, level: int = logging.DEBUG) -> logging.Logger:
70
- log = logging.getLogger(name)
71
- log.setLevel(level)
72
- log.propagate = False
73
- fmt = logging.Formatter(fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
74
- console_handler = logging.StreamHandler()
75
- console_handler.setLevel(logging.DEBUG)
76
- console_handler.setFormatter(fmt)
77
- log.addHandler(console_handler)
78
- if file_path:
79
- if file_mkdir:
80
- Path(file_path).parent.mkdir(exist_ok=True)
81
- file_handler = RotatingFileHandler(file_path, maxBytes=10 * 1024 * 1024, backupCount=1)
82
- file_handler.setLevel(logging.INFO)
83
- file_handler.setFormatter(fmt)
84
- log.addHandler(file_handler)
85
- return log
mm_std/net.py DELETED
@@ -1,22 +0,0 @@
1
- import socket
2
- import time
3
- from typing import cast
4
-
5
-
6
- def check_port(ip: str, port: int, attempts: int = 3, sleep_seconds: float = 1, timeout: float = 1) -> bool:
7
- for _ in range(attempts):
8
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
9
- sock.settimeout(timeout)
10
- res = sock.connect_ex((ip, port)) == 0
11
- if res:
12
- return True
13
- time.sleep(sleep_seconds)
14
- return False
15
-
16
-
17
- def get_free_local_port() -> int:
18
- sock = socket.socket()
19
- sock.bind(("", 0))
20
- port = sock.getsockname()[1]
21
- sock.close()
22
- return cast(int, port)
mm_std/print_.py DELETED
@@ -1,58 +0,0 @@
1
- import sys
2
- from collections.abc import Callable
3
- from enum import Enum, unique
4
- from typing import Any, NoReturn
5
-
6
- import rich
7
- from rich.console import Console
8
- from rich.syntax import Syntax
9
- from rich.table import Table
10
-
11
- from mm_std.json_ import json_dumps
12
-
13
-
14
- @unique
15
- class PrintFormat(str, Enum):
16
- PLAIN = "plain"
17
- TABLE = "table"
18
- JSON = "json"
19
-
20
-
21
- def fatal(message: str, code: int = 1) -> NoReturn:
22
- print(message, file=sys.stderr) # noqa: T201
23
- sys.exit(code)
24
-
25
-
26
- def print_console(*messages: object, print_json: bool = False, default: Callable[[object], str] | None = str) -> None:
27
- if len(messages) == 1:
28
- message = messages[0]
29
- if isinstance(message, str):
30
- print(message) # noqa: T201
31
- elif print_json:
32
- rich.print_json(json_dumps(message, default_serializer=default))
33
- else:
34
- rich.print(message)
35
- else:
36
- rich.print(messages)
37
-
38
-
39
- def print_plain(messages: object) -> None:
40
- print(messages) # noqa: T201
41
-
42
-
43
- def print_json(data: object, default_serializer: Callable[[object], str] | None = None) -> None:
44
- rich.print_json(json_dumps(data, default_serializer=default_serializer))
45
-
46
-
47
- def print_table(title: str, columns: list[str], rows: list[list[Any]]) -> None:
48
- table = Table(*columns, title=title)
49
- for row in rows:
50
- table.add_row(*(str(cell) for cell in row))
51
- console = Console()
52
- console.print(table)
53
-
54
-
55
- def pretty_print_toml(data: str, line_numbers: bool = False, theme: str = "monokai") -> None:
56
- console = Console()
57
- syntax = Syntax(data, "toml", theme=theme, line_numbers=line_numbers)
58
- console.print(syntax)
mm_std/random_.py DELETED
@@ -1,35 +0,0 @@
1
- import random
2
- from collections.abc import Sequence
3
- from decimal import Decimal
4
-
5
-
6
- def random_choice[T](source: Sequence[T] | T | None) -> T | None:
7
- """Deprecated, don't use it"""
8
- if source is None:
9
- return None
10
- if isinstance(source, str):
11
- return source # type: ignore[return-value]
12
- if isinstance(source, Sequence):
13
- if source:
14
- return random.choice(source) # type:ignore[no-any-return]
15
- return None
16
- return source
17
-
18
-
19
- def random_str_choice(source: Sequence[str] | str | None) -> str | None:
20
- if source is None:
21
- return None
22
- if isinstance(source, str):
23
- return source
24
- if isinstance(source, Sequence):
25
- if source:
26
- return random.choice(source)
27
- return None
28
- return source
29
-
30
-
31
- def random_decimal(from_: Decimal, to: Decimal) -> Decimal:
32
- from_ndigits = abs(from_.as_tuple().exponent) # type:ignore[arg-type]
33
- to_ndigits = abs(to.as_tuple().exponent) # type:ignore[arg-type]
34
- ndigits = max(from_ndigits, to_ndigits)
35
- return Decimal(str(round(random.uniform(float(from_), float(to)), ndigits)))