✨(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:
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator, Callable
|
from typing import AsyncGenerator, Callable
|
||||||
|
|
||||||
@ -7,6 +8,8 @@ from thon.models.options import ThonOptions
|
|||||||
from thon.process import ProcessThon
|
from thon.process import ProcessThon
|
||||||
from thon.utils import move
|
from thon.utils import move
|
||||||
|
|
||||||
|
logger = logging.getLogger("thon")
|
||||||
|
|
||||||
|
|
||||||
class Thon:
|
class Thon:
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -23,10 +26,16 @@ class Thon:
|
|||||||
|
|
||||||
async def __aiter__(self) -> AsyncGenerator[ThonSession, None]:
|
async def __aiter__(self) -> AsyncGenerator[ThonSession, None]:
|
||||||
async for session in self.__converter:
|
async for session in self.__converter:
|
||||||
|
error_msg = None
|
||||||
async with ProcessThon(session, self.__options) as thon:
|
async with ProcessThon(session, self.__options) as thon:
|
||||||
if isinstance(thon, str):
|
if isinstance(thon, str):
|
||||||
if "Banned" in thon and self.__move_banned_folder:
|
error_msg = thon
|
||||||
print(
|
else:
|
||||||
|
yield ThonSession(session, thon)
|
||||||
|
|
||||||
|
if error_msg:
|
||||||
|
if "Banned" in error_msg and self.__move_banned_folder:
|
||||||
|
logger.info(
|
||||||
f"{session.item.name} | Аккаунт забанен, перемещение файлов..."
|
f"{session.item.name} | Аккаунт забанен, перемещение файлов..."
|
||||||
)
|
)
|
||||||
# Перемещаем файлы сессии с обработкой ошибок
|
# Перемещаем файлы сессии с обработкой ошибок
|
||||||
@ -34,15 +43,14 @@ class Thon:
|
|||||||
success_json = await move(session.json_file, "banned")
|
success_json = await move(session.json_file, "banned")
|
||||||
|
|
||||||
if success_session and success_json:
|
if success_session and success_json:
|
||||||
print(
|
logger.info(
|
||||||
f"{session.item.name} | Файлы успешно перемещены / Files moved successfully"
|
f"{session.item.name} | Файлы успешно перемещены / Files moved successfully"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.error(
|
||||||
f"{session.item.name} | Не удалось полностью переместить файлы / Failed to move all files"
|
f"{session.item.name} | Не удалось полностью переместить файлы / Failed to move all files"
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.__callback_error is not None:
|
if self.__callback_error is not None:
|
||||||
self.__callback_error()
|
self.__callback_error()
|
||||||
continue
|
continue
|
||||||
continue
|
|
||||||
yield ThonSession(session, thon)
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ from thon.proxy import ProxyParser
|
|||||||
from thon.session import ThonSession
|
from thon.session import ThonSession
|
||||||
from thon.utils import json_write
|
from thon.utils import json_write
|
||||||
|
|
||||||
|
logger = logging.getLogger("thon")
|
||||||
|
|
||||||
|
|
||||||
class Converter:
|
class Converter:
|
||||||
def __init__(self, sessions_folder: Path = Path("sessions"), proxy: str = ""):
|
def __init__(self, sessions_folder: Path = Path("sessions"), proxy: str = ""):
|
||||||
@ -36,6 +39,7 @@ class Converter:
|
|||||||
Конвертация сессии в json (string_session)
|
Конвертация сессии в json (string_session)
|
||||||
Возвращает объект Session
|
Возвращает объект Session
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"Converting session: {session.item.name}")
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
client = TelegramClient(str(session.item), self.__api_id, self.__api_hash)
|
client = TelegramClient(str(session.item), self.__api_id, self.__api_hash)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import contextlib
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Self
|
from typing import Self # ty:ignore[unresolved-import]
|
||||||
|
|
||||||
from telethon import TelegramClient
|
from telethon import TelegramClient
|
||||||
from telethon.tl.functions.account import UpdateStatusRequest
|
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.models.session import Session
|
||||||
from thon.utils import extract_verification_code
|
from thon.utils import extract_verification_code
|
||||||
|
|
||||||
|
logger = logging.getLogger("thon")
|
||||||
|
|
||||||
|
|
||||||
API_PACKS = {
|
API_PACKS = {
|
||||||
4: "android",
|
4: "android",
|
||||||
5: "android",
|
5: "android",
|
||||||
@ -46,7 +49,6 @@ class ProcessThon(Data):
|
|||||||
self.__item = session.item
|
self.__item = session.item
|
||||||
self.__retries = options.retries
|
self.__retries = options.retries
|
||||||
self.__timeout = options.timeout
|
self.__timeout = options.timeout
|
||||||
self._logger = logging.getLogger("thon")
|
|
||||||
self._async_check_timeout = options.check_timeout
|
self._async_check_timeout = options.check_timeout
|
||||||
super().__init__(session.json_data)
|
super().__init__(session.json_data)
|
||||||
self.__client = self.__get_client()
|
self.__client = self.__get_client()
|
||||||
@ -59,7 +61,7 @@ class ProcessThon(Data):
|
|||||||
def __get_client(self) -> TelegramClient:
|
def __get_client(self) -> TelegramClient:
|
||||||
__session = str(self.__item) if self.__item else self.string_session
|
__session = str(self.__item) if self.__item else self.string_session
|
||||||
proxy = self.proxy
|
proxy = self.proxy
|
||||||
self._logger.info(f"{self.session_file} | {proxy}")
|
logger.debug(f"{self.session_file} | {proxy}")
|
||||||
client = TelegramClient(
|
client = TelegramClient(
|
||||||
session=__session,
|
session=__session,
|
||||||
api_id=self.app_id,
|
api_id=self.app_id,
|
||||||
@ -125,7 +127,7 @@ class ProcessThon(Data):
|
|||||||
return f"{self.session_file} | Забанен / Banned"
|
return f"{self.session_file} | Забанен / Banned"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
self._logger.exception(e)
|
logger.exception(e)
|
||||||
return f"{self.session_file} | Ошибка авторизации / AuthorizationError: {e}"
|
return f"{self.session_file} | Ошибка авторизации / AuthorizationError: {e}"
|
||||||
|
|
||||||
async def check(self) -> str:
|
async def check(self) -> str:
|
||||||
@ -184,7 +186,7 @@ class ProcessThon(Data):
|
|||||||
|
|
||||||
async def __aenter__(self) -> Self | str:
|
async def __aenter__(self) -> Self | str:
|
||||||
r = await self.check()
|
r = await self.check()
|
||||||
self._logger.info(f"{self.session_file} | {r}")
|
logger.info(f"{self.session_file} | {r}")
|
||||||
if r != "OK":
|
if r != "OK":
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
return r
|
return r
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
|
|
||||||
|
logger = logging.getLogger("thon")
|
||||||
|
|
||||||
|
|
||||||
async def json_read(path: Path) -> dict:
|
async def json_read(path: Path) -> dict:
|
||||||
try:
|
try:
|
||||||
@ -28,7 +31,9 @@ def extract_verification_code(text: str, regexp: str) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
async def move(
|
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:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Перемещает файл с retry-логикой, если файл занят.
|
Перемещает файл с retry-логикой, если файл занят.
|
||||||
@ -46,39 +51,31 @@ async def move(
|
|||||||
try:
|
try:
|
||||||
# Проверяем, что файл существует и доступен для чтения
|
# Проверяем, что файл существует и доступен для чтения
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
print(f"{path.name} | Файл не найден / File not found")
|
logger.error(f"{path.name} | Файл не найден / File not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
print(f"{path.name} | Это не файл / Not a file")
|
logger.error(f"{path.name} | Это не файл / Not a file")
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
except OSError:
|
except OSError:
|
||||||
if attempt < max_retries - 1:
|
if attempt < max_retries - 1:
|
||||||
# Ждем перед следующей попыткой (с экспоненциальной задержкой)
|
# Ждем перед следующей попыткой (с экспоненциальной задержкой)
|
||||||
wait_time = 0.5 * (2**attempt) # 0.5s, 1s, 2s, 4s...
|
wait_time = 0.5 * (2**attempt) # 0.5s, 1s, 2s, 4s...
|
||||||
print(
|
logger.warning(
|
||||||
f"{path.name} | Попытка {attempt + 1}/{max_retries}: файл занят, ждем {wait_time}s..."
|
f"{path.name} | Попытка {attempt + 1}/{max_retries}: файл занят, ждем {wait_time}s..."
|
||||||
)
|
)
|
||||||
await asyncio.sleep(wait_time)
|
await asyncio.sleep(wait_time)
|
||||||
else:
|
else:
|
||||||
# Последняя попытка неудачна - попробуем копию
|
# Последняя попытка неудачна
|
||||||
print(
|
logger.error(
|
||||||
f"{path.name} | Ошибка перемещения / Failed to move (final attempt), trying copy..."
|
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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user