From 2932ff8f48a4749c752066a8458284dde2123682 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:36:20 +0300 Subject: [PATCH] fix(logging): remove logging configuration --- docs/api/client-config.rst | 2 - docs/client.rst | 17 ++--- docs/faq.rst | 28 +------- docs/troubleshooting.rst | 14 ++-- src/pymax/__init__.py | 2 - src/pymax/api/chats/service.py | 52 +++++++++++---- src/pymax/api/models.py | 2 +- src/pymax/base.py | 1 - src/pymax/client.py | 6 +- src/pymax/client_web.py | 6 +- src/pymax/config.py | 6 +- src/pymax/logging.py | 115 --------------------------------- uv.lock | 50 -------------- 13 files changed, 60 insertions(+), 241 deletions(-) diff --git a/docs/api/client-config.rst b/docs/api/client-config.rst index 816b8f5..09fa439 100644 --- a/docs/api/client-config.rst +++ b/docs/api/client-config.rst @@ -7,5 +7,3 @@ Client Config .. autoclass:: ExtraConfig :members: - -.. autofunction:: configure_logging diff --git a/docs/client.rst b/docs/client.rst index afd151f..bb5c1d0 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -66,7 +66,6 @@ Client work_dir="cache", session_name="account.db", extra_config=ExtraConfig( - log_level="INFO", reconnect=True, reconnect_delay=3, ), @@ -85,7 +84,7 @@ Client Имя файла сессии внутри ``work_dir``. ``extra_config`` - Настройки соединения, логов, reconnect, token, device/user-agent и sync. + Настройки соединения, reconnect, token, device/user-agent и sync. Тип устройства --------------- @@ -259,17 +258,13 @@ Debug-логи Debug-логи показывают handshake, login, входящие события, API-ошибки, причины reconnect и детали upload. Начинайте диагностику с них. -.. code-block:: python - - from pymax import Client, ExtraConfig +PyMax не настраивает логирование самостоятельно. Включите debug-логи +стандартными средствами Python до создания клиента: - client = Client( - phone="+79990000000", - extra_config=ExtraConfig(log_level="DEBUG"), - ) +.. code-block:: python -Можно также вызвать ``configure_logging("DEBUG")`` до создания клиента, но -обычно достаточно ``ExtraConfig(log_level="DEBUG")``. + import logging + logging.basicConfig(level=logging.DEBUG) Группы методов клиента ---------------------- diff --git a/docs/faq.rst b/docs/faq.rst index 7739cbd..b9d2f9e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -14,8 +14,8 @@ FAQ Проверьте, что ``await client.start()`` действительно запущен, handler зарегистрирован до запуска клиента, аккаунт видит нужный чат, а соединение не -упало. Для диагностики включите ``log_level="DEBUG"`` и временно добавьте -``on_raw``. +упало. Для диагностики включите ``logging.basicConfig(level=logging.DEBUG)`` +и временно добавьте ``on_raw``. Почему handler не вызывается? ----------------------------- @@ -58,30 +58,6 @@ Max не всегда присылает полный объект сообще Если удалить файл сессии, PyMax создает новую сессию и снова запускает авторизацию. -Как включить debug-логи? ------------------------- - -.. code-block:: python - - from pymax import Client, ExtraConfig - - client = Client( - phone="+79990000000", - extra_config=ExtraConfig(log_level="DEBUG"), - ) - -Как отключить reconnect? ------------------------- - -.. code-block:: python - - from pymax import Client, ExtraConfig - - client = Client( - phone="+79990000000", - extra_config=ExtraConfig(reconnect=False), - ) - Что делать, если upload файла падает? ------------------------------------- diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 28d2e25..1bd9c0e 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -4,17 +4,13 @@ Troubleshooting Базовая диагностика ------------------- -Начинайте с debug-логов: +Начинайте с debug-логов. PyMax не настраивает логирование самостоятельно — +включите их до создания клиента: .. code-block:: python - from pymax import Client, ExtraConfig - - client = Client( - phone="+79990000000", - work_dir="cache", - extra_config=ExtraConfig(log_level="DEBUG"), - ) + import logging + logging.basicConfig(level=logging.DEBUG) Если событие не распознается, добавьте raw-handler: @@ -110,5 +106,5 @@ Reconnect консольный provider. В UI-приложениях передайте собственный async-provider. ``remove_2fa()`` не отключает пароль - Метод требует текущий пароль 2FA. Включите ``DEBUG``-логи и проверьте, что + Метод требует текущий пароль 2FA. Включите debug-логи и проверьте, что сервер принял проверку пароля и запрос отключения. diff --git a/src/pymax/__init__.py b/src/pymax/__init__.py index 90e2eb6..f51d699 100644 --- a/src/pymax/__init__.py +++ b/src/pymax/__init__.py @@ -18,7 +18,6 @@ from .dispatch import EventType, Router from .exceptions import ApiError, PyMaxError, UploadError from .files import File, Photo, Video -from .logging import configure_logging from .routers import ClientRouter, WebRouter from .types import Chat, Message, MessageDeleteEvent, Profile, User from .types.domain.sync import SyncOverrides, SyncState @@ -54,5 +53,4 @@ "WebClient", "WebRouter", "__version__", - "configure_logging", ) diff --git a/src/pymax/api/chats/service.py b/src/pymax/api/chats/service.py index 48fc54f..aedf751 100644 --- a/src/pymax/api/chats/service.py +++ b/src/pymax/api/chats/service.py @@ -73,7 +73,9 @@ def _remove_cached_chat(self, chat_id: int) -> None: if self.app.chats is None: return - self.app.chats = [chat for chat in self.app.chats if chat.id != chat_id] + self.app.chats = [ + chat for chat in self.app.chats if chat.id != chat_id + ] @staticmethod def _process_chat_join_link(link: str) -> str | None: @@ -151,7 +153,9 @@ async def invite_users_to_channel( user_ids: list[int], show_history: bool = True, ) -> Chat | None: - return await self.invite_users_to_group(chat_id, user_ids, show_history) + return await self.invite_users_to_group( + chat_id, user_ids, show_history + ) async def remove_users_from_group( self, @@ -195,7 +199,9 @@ async def change_group_settings( ), ) - response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload()) + response = await self.app.invoke( + Opcode.CHAT_UPDATE, frame.to_payload() + ) chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat) if chat: self._cache_chat(chat) @@ -212,7 +218,9 @@ async def change_group_profile( description=description, ) - response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload()) + response = await self.app.invoke( + Opcode.CHAT_UPDATE, frame.to_payload() + ) chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat) if chat: self._cache_chat(chat) @@ -244,7 +252,9 @@ async def resolve_group_by_link(self, link: str) -> Chat | None: async def rework_invite_link(self, chat_id: int) -> Chat: frame = ReworkInviteLinkPayload(chat_id=chat_id) - response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload()) + response = await self.app.invoke( + Opcode.CHAT_UPDATE, frame.to_payload() + ) chat = require_payload_item_model(response, ChatPayloadKey.CHAT, Chat) return self._cache_chat(chat) @@ -254,12 +264,18 @@ async def get_chats(self, chat_ids: list[int]) -> list[Chat]: for chat_id in chat_ids if (chat := self._get_cached_chat(chat_id)) is not None } - missed_chat_ids = [chat_id for chat_id in chat_ids if chat_id not in cached] + missed_chat_ids = [ + chat_id for chat_id in chat_ids if chat_id not in cached + ] if missed_chat_ids: frame = GetChatInfoPayload(chat_ids=missed_chat_ids) - response = await self.app.invoke(Opcode.CHAT_INFO, frame.to_payload()) - for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat): + response = await self.app.invoke( + Opcode.CHAT_INFO, frame.to_payload() + ) + for chat in parse_payload_list( + response, ChatPayloadKey.CHATS, Chat + ): chat = self._cache_chat(chat) cached[chat.id] = chat @@ -286,14 +302,20 @@ async def fetch_chats(self, marker: int | None = None) -> list[Chat]: chats = [ self._cache_chat(chat) - for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat) + for chat in parse_payload_list( + response, ChatPayloadKey.CHATS, Chat + ) ] return chats - async def get_join_requests(self, chat_id: int, count: int = 100) -> list[Member]: + async def get_join_requests( + self, chat_id: int, count: int = 100 + ) -> list[Member]: frame = FetchJoinRequests(chat_id=chat_id, count=count) - response = await self.app.invoke(Opcode.CHAT_MEMBERS, frame.to_payload()) + response = await self.app.invoke( + Opcode.CHAT_MEMBERS, frame.to_payload() + ) return bind_api_model( self.app, @@ -313,7 +335,9 @@ async def confirm_join_requests( operation=ChatMemberOperation.ADD, ) - response = await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()) + response = await self.app.invoke( + Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload() + ) chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat) if chat: @@ -345,7 +369,9 @@ async def decline_join_requests( operation=ChatMemberOperation.REMOVE, ) - response = await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()) + response = await self.app.invoke( + Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload() + ) chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat) if chat: diff --git a/src/pymax/api/models.py b/src/pymax/api/models.py index 0a358bb..46712cd 100644 --- a/src/pymax/api/models.py +++ b/src/pymax/api/models.py @@ -6,7 +6,7 @@ class CamelModel(BaseModel): model_config = ConfigDict( alias_generator=to_camel, populate_by_name=True, - arbitrary_types_allowed=True + arbitrary_types_allowed=True, ) def to_payload(self) -> dict: diff --git a/src/pymax/base.py b/src/pymax/base.py index af22960..ca49e6c 100644 --- a/src/pymax/base.py +++ b/src/pymax/base.py @@ -78,7 +78,6 @@ def _build_config( port=self.extra_config.port, use_ssl=self.extra_config.use_ssl, request_timeout=self.extra_config.request_timeout, - log_level=self.extra_config.log_level, telemetry=self.extra_config.telemetry, sync=self.extra_config.sync, store=self.extra_config.store, diff --git a/src/pymax/client.py b/src/pymax/client.py index c9bee67..0387b3f 100644 --- a/src/pymax/client.py +++ b/src/pymax/client.py @@ -9,7 +9,7 @@ ) from pymax.connection import ConnectionManager from pymax.connection.readers import TCPReader -from pymax.logging import configure_logging, get_logger +from pymax.logging import get_logger from pymax.protocol.tcp import TcpProtocol from pymax.protocol.tcp.framing import TcpPacketFramer from pymax.transport.tcp import TCPTransport @@ -54,9 +54,9 @@ def __init__( self.session_name = session_name self.work_dir = work_dir - configure_logging(self.extra_config.log_level) logger.debug( - "creating client phone_set=%s session=%s work_dir=%s proxy_set=%s reconnect=%s", + "creating client phone_set=%s session=%s " + "work_dir=%s proxy_set=%s reconnect=%s", bool(phone), self.session_name, self.work_dir, diff --git a/src/pymax/client_web.py b/src/pymax/client_web.py index 22c8c8d..445e543 100644 --- a/src/pymax/client_web.py +++ b/src/pymax/client_web.py @@ -5,7 +5,7 @@ from pymax.auth.qr import QrAuthFlow from pymax.connection import ConnectionManager from pymax.connection.readers import WSReader -from pymax.logging import configure_logging, get_logger +from pymax.logging import get_logger from pymax.protocol.ws import WsProtocol from pymax.transport.websocket import WebSocketTransport @@ -42,9 +42,9 @@ def __init__( self.session_name = session_name self.work_dir = work_dir - configure_logging(self.extra_config.log_level) logger.debug( - "creating web client session=%s work_dir=%s proxy_set=%s reconnect=%s", + "creating web client session=%s work_dir=%s " + "proxy_set=%s reconnect=%s", self.session_name, self.work_dir, bool(self.extra_config.proxy), diff --git a/src/pymax/config.py b/src/pymax/config.py index 27677cf..b125c03 100644 --- a/src/pymax/config.py +++ b/src/pymax/config.py @@ -100,7 +100,6 @@ class ClientConfig(BaseModel): protocol_version: int = 10 request_timeout: float = 30.0 - log_level: str = "INFO" telemetry: bool = False store: StoreProtocol | None = None @@ -117,7 +116,7 @@ def ensure_config(self) -> None: class ExtraConfig(BaseModel): """Дополнительные настройки ``Client`` и ``WebClient``. - Используйте ``ExtraConfig`` для token-логина, debug-логов, reconnect, + Используйте ``ExtraConfig`` для token-логина, reconnect, пользовательского device/user-agent и переопределения sync-state. Args: @@ -134,7 +133,6 @@ class ExtraConfig(BaseModel): user_agent: Полностью заданный user-agent payload. mt_instance_id: Instance ID устройства. request_timeout: Timeout API-запросов в секундах. - log_level: Уровень логов ``pymax``. telemetry: Отправлять telemetry-события Max. sync: Переопределения sync-маркеров для login. @@ -146,7 +144,6 @@ class ExtraConfig(BaseModel): client = Client( phone="+79990000000", extra_config=ExtraConfig( - log_level="DEBUG", reconnect=False, sync=SyncOverrides(chats_sync=-1), ), @@ -171,7 +168,6 @@ class ExtraConfig(BaseModel): mt_instance_id: str = Field(default_factory=lambda: str(uuid4())) request_timeout: float = 30.0 - log_level: str = "INFO" telemetry: bool = True store: StoreProtocol | None = None diff --git a/src/pymax/logging.py b/src/pymax/logging.py index ecba8c5..87fdc5f 100644 --- a/src/pymax/logging.py +++ b/src/pymax/logging.py @@ -1,102 +1,4 @@ import logging -import re -import sys -from typing import TextIO - -DATE_FORMAT = "%H:%M:%S" - -RESET = "\x1b[0m" -DIM = "\x1b[2m" -BOLD = "\x1b[1m" - -LEVEL_STYLES = { - logging.DEBUG: ("\x1b[90m", "DEBUG"), - logging.INFO: ("\x1b[36m", "INFO"), - logging.WARNING: ("\x1b[33m", "WARN"), - logging.ERROR: ("\x1b[31m", "ERROR"), - logging.CRITICAL: ("\x1b[1;37;41m", "CRIT"), -} - -LEVELS = { - "CRITICAL": logging.CRITICAL, - "FATAL": logging.FATAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "WARN": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, - "NOTSET": logging.NOTSET, -} - - -class PrettyFormatter(logging.Formatter): - def __init__(self, *, use_colors: bool = True) -> None: - super().__init__() - self.use_colors = use_colors - - def format(self, record: logging.LogRecord) -> str: - color, level = LEVEL_STYLES.get(record.levelno, ("", "???")) - - time = self.formatTime(record, DATE_FORMAT) - message = record.getMessage() - - line = f"{DIM}{time}{RESET} {color}{BOLD}{level}{RESET} {message}" - - if record.exc_info: - line += "\n" + self.formatException(record.exc_info) - - if not self.use_colors: - line = _strip_ansi(line) - - return line - - -def configure_logging( - level: int | str = logging.INFO, - *, - stream: TextIO | None = None, - use_colors: bool | None = None, -) -> None: - """Настраивает pretty-логи для logger-а ``pymax``. - - Обычно уровень логов задают через ``ExtraConfig(log_level="DEBUG")``. - Вызывайте эту функцию вручную, если хотите управлять stream или цветами. - - Args: - level: Уровень логирования: строка вроде ``"DEBUG"`` или число из - модуля ``logging``. - stream: Поток для вывода. По умолчанию ``sys.stderr``. - use_colors: Включить ANSI-цвета. Если ``None``, определяется по TTY. - - Returns: - ``None``. - - Example: - .. code-block:: python - - from pymax import configure_logging - - configure_logging("DEBUG", use_colors=False) - """ - stream = stream or sys.stderr - - if use_colors is None: - use_colors = hasattr(stream, "isatty") and stream.isatty() - - logger = logging.getLogger("pymax") - logger.handlers.clear() - logger.setLevel(_normalize_level(level)) - logger.propagate = False - - handler = logging.StreamHandler(stream) - handler.setLevel(_normalize_level(level)) - handler.setFormatter( - PrettyFormatter( - use_colors=use_colors, - ) - ) - - logger.addHandler(handler) def get_logger(name: str | None = None) -> logging.Logger: @@ -109,21 +11,4 @@ def get_logger(name: str | None = None) -> logging.Logger: return logging.getLogger(f"pymax.{name}") -def _normalize_level(level: int | str) -> int: - if isinstance(level, int): - return level - - value = LEVELS.get(level.upper()) - - if isinstance(value, int): - return value - - raise ValueError(f"Unknown log level: {level}") - - -def _strip_ansi(text: str) -> str: - - return re.sub(r"\x1b\[[0-9;]*m", "", text) - - logging.getLogger("pymax").addHandler(logging.NullHandler()) diff --git a/uv.lock b/uv.lock index 238d90f..ac4d351 100644 --- a/uv.lock +++ b/uv.lock @@ -255,22 +255,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] -[[package]] -name = "build" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, -] - [[package]] name = "certifi" version = "2026.4.22" @@ -1046,24 +1030,8 @@ dependencies = [ { name = "websockets" }, ] -[package.optional-dependencies] -docs = [ - { name = "furo" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "sphinx-copybutton" }, -] -test = [ - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-timeout" }, -] - [package.dev-dependencies] dev = [ - { name = "build" }, { name = "furo" }, { name = "pre-commit" }, { name = "pyright" }, @@ -1097,24 +1065,15 @@ requires-dist = [ { name = "aiofiles", specifier = ">=25.1.0" }, { name = "aiohttp", specifier = ">=3.13.5" }, { name = "aiosqlite", specifier = ">=0.22.1" }, - { name = "furo", marker = "extra == 'docs'", specifier = ">=2025.12.19" }, { name = "msgpack", specifier = ">=1.1.2" }, { name = "pydantic", specifier = ">=2.10.0" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, - { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24.0" }, - { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" }, - { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.1.0" }, { name = "python-socks", extras = ["asyncio"], specifier = ">=2.8.1" }, { name = "qrcode", specifier = ">=8.2" }, - { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.1.3" }, - { name = "sphinx-copybutton", marker = "extra == 'docs'", specifier = ">=0.5.2" }, { name = "websockets", specifier = ">=16.0" }, ] -provides-extras = ["docs", "test"] [package.metadata.requires-dev] dev = [ - { name = "build", specifier = ">=1.2.0" }, { name = "furo", specifier = ">=2025.12.19" }, { name = "pre-commit", specifier = ">=4.0.0" }, { name = "pyright", specifier = ">=1.1.390" }, @@ -1705,15 +1664,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, -] - [[package]] name = "pyright" version = "1.1.409"