(thon): add structured logging and improve error handling

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.
This commit is contained in:
2026-04-02 23:06:21 +03:00
parent 174b74f0ab
commit 38078b36b3
4 changed files with 56 additions and 45 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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