From 38078b36b3c638cd46074dacd93814fc88ecb4c5 Mon Sep 17 00:00:00 2001 From: trojvn Date: Thu, 2 Apr 2026 23:06:21 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(thon):=20add=20structured=20logging?= =?UTF-8?q?=20and=20improve=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add logging infrastructure across the codebase with a consistent logger named "thon" and replace print statements with appropriate log levels (debug, info, warning, error). Improve error handling in the session processing pipeline by: - Adding proper error message tracking in Thon.__aiter__ - Using shutil.move instead of os.rename for better cross-platform compatibility - Enhancing the move utility function with better error handling and retry logic - Adding debug logging in Converter for session conversion tracking - Centralizing logging configuration in the main module This change improves debugging capabilities and provides better visibility into the application's operation while maintaining the same functionality. --- thon/__init__.py | 48 +++++++++++++++++++++++++++-------------------- thon/converter.py | 4 ++++ thon/process.py | 12 +++++++----- thon/utils.py | 37 +++++++++++++++++------------------- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/thon/__init__.py b/thon/__init__.py index f2c8453..204dca9 100644 --- a/thon/__init__.py +++ b/thon/__init__.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from typing import AsyncGenerator, Callable @@ -7,6 +8,8 @@ from thon.models.options import ThonOptions from thon.process import ProcessThon from thon.utils import move +logger = logging.getLogger("thon") + class Thon: def __init__( @@ -23,26 +26,31 @@ class Thon: async def __aiter__(self) -> AsyncGenerator[ThonSession, None]: async for session in self.__converter: + error_msg = None async with ProcessThon(session, self.__options) as thon: if isinstance(thon, str): - if "Banned" in thon and self.__move_banned_folder: - print( - f"{session.item.name} | Аккаунт забанен, перемещение файлов..." - ) - # Перемещаем файлы сессии с обработкой ошибок - success_session = await move(session.item, "banned") - success_json = await move(session.json_file, "banned") + error_msg = thon + else: + yield ThonSession(session, thon) - if success_session and success_json: - print( - f"{session.item.name} | Файлы успешно перемещены / Files moved successfully" - ) - else: - print( - f"{session.item.name} | Не удалось полностью переместить файлы / Failed to move all files" - ) - if self.__callback_error is not None: - self.__callback_error() - continue - continue - yield ThonSession(session, thon) + if error_msg: + if "Banned" in error_msg and self.__move_banned_folder: + logger.info( + f"{session.item.name} | Аккаунт забанен, перемещение файлов..." + ) + # Перемещаем файлы сессии с обработкой ошибок + success_session = await move(session.item, "banned") + success_json = await move(session.json_file, "banned") + + if success_session and success_json: + logger.info( + f"{session.item.name} | Файлы успешно перемещены / Files moved successfully" + ) + else: + logger.error( + f"{session.item.name} | Не удалось полностью переместить файлы / Failed to move all files" + ) + + if self.__callback_error is not None: + self.__callback_error() + continue diff --git a/thon/converter.py b/thon/converter.py index 1dd00f4..959e934 100644 --- a/thon/converter.py +++ b/thon/converter.py @@ -1,5 +1,6 @@ import asyncio import contextlib +import logging from pathlib import Path from typing import AsyncGenerator @@ -11,6 +12,8 @@ from thon.proxy import ProxyParser from thon.session import ThonSession from thon.utils import json_write +logger = logging.getLogger("thon") + class Converter: def __init__(self, sessions_folder: Path = Path("sessions"), proxy: str = ""): @@ -36,6 +39,7 @@ class Converter: Конвертация сессии в json (string_session) Возвращает объект Session """ + logger.debug(f"Converting session: {session.item.name}") loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) client = TelegramClient(str(session.item), self.__api_id, self.__api_hash) diff --git a/thon/process.py b/thon/process.py index 6a33a58..cf8b60e 100644 --- a/thon/process.py +++ b/thon/process.py @@ -3,7 +3,7 @@ import contextlib import logging from datetime import datetime, timedelta, timezone from random import randint -from typing import Self +from typing import Self # ty:ignore[unresolved-import] from telethon import TelegramClient from telethon.tl.functions.account import UpdateStatusRequest @@ -25,6 +25,9 @@ from thon.models.options import ThonOptions from thon.models.session import Session from thon.utils import extract_verification_code +logger = logging.getLogger("thon") + + API_PACKS = { 4: "android", 5: "android", @@ -46,7 +49,6 @@ class ProcessThon(Data): self.__item = session.item self.__retries = options.retries self.__timeout = options.timeout - self._logger = logging.getLogger("thon") self._async_check_timeout = options.check_timeout super().__init__(session.json_data) self.__client = self.__get_client() @@ -59,7 +61,7 @@ class ProcessThon(Data): def __get_client(self) -> TelegramClient: __session = str(self.__item) if self.__item else self.string_session proxy = self.proxy - self._logger.info(f"{self.session_file} | {proxy}") + logger.debug(f"{self.session_file} | {proxy}") client = TelegramClient( session=__session, api_id=self.app_id, @@ -125,7 +127,7 @@ class ProcessThon(Data): return f"{self.session_file} | Забанен / Banned" except Exception as e: await self.disconnect() - self._logger.exception(e) + logger.exception(e) return f"{self.session_file} | Ошибка авторизации / AuthorizationError: {e}" async def check(self) -> str: @@ -184,7 +186,7 @@ class ProcessThon(Data): async def __aenter__(self) -> Self | str: r = await self.check() - self._logger.info(f"{self.session_file} | {r}") + logger.info(f"{self.session_file} | {r}") if r != "OK": await self.disconnect() return r diff --git a/thon/utils.py b/thon/utils.py index b4e7dfc..ec9fc0d 100644 --- a/thon/utils.py +++ b/thon/utils.py @@ -1,11 +1,14 @@ import asyncio import json -import os +import logging import re +import shutil from pathlib import Path import aiofiles +logger = logging.getLogger("thon") + async def json_read(path: Path) -> dict: try: @@ -28,7 +31,9 @@ def extract_verification_code(text: str, regexp: str) -> str | None: async def move( - path: Path, move_folder_name: str = "banned", max_retries: int = 5 + path: Path, + move_folder_name: str = "banned", + max_retries: int = 5, ) -> bool: """ Перемещает файл с retry-логикой, если файл занят. @@ -46,40 +51,32 @@ async def move( try: # Проверяем, что файл существует и доступен для чтения if not path.exists(): - print(f"{path.name} | Файл не найден / File not found") + logger.error(f"{path.name} | Файл не найден / File not found") return False if not path.is_file(): - print(f"{path.name} | Это не файл / Not a file") + logger.error(f"{path.name} | Это не файл / Not a file") return False # Попытка переместить файл - await loop.run_in_executor(None, os.rename, path, _folder / path.name) + await loop.run_in_executor( + None, shutil.move, str(path), str(_folder / path.name) + ) return True except OSError: if attempt < max_retries - 1: # Ждем перед следующей попыткой (с экспоненциальной задержкой) wait_time = 0.5 * (2**attempt) # 0.5s, 1s, 2s, 4s... - print( + logger.warning( f"{path.name} | Попытка {attempt + 1}/{max_retries}: файл занят, ждем {wait_time}s..." ) await asyncio.sleep(wait_time) else: - # Последняя попытка неудачна - попробуем копию - print( - f"{path.name} | Ошибка перемещения / Failed to move (final attempt), trying copy..." + # Последняя попытка неудачна + logger.error( + f"{path.name} | Ошибка перемещения / Failed to move (final attempt)" ) - try: - await loop.run_in_executor( - None, os.replace, path, _folder / path.name - ) - print(f"{path.name} | Файл перемещен через копию / Moved via copy") - return True - except OSError as e: - print( - f"{path.name} | Не удалось ни переместить, ни скопировать / Failed to move or copy: {e}" - ) - return False + return False return False