diff --git a/requirements.txt b/requirements.txt index bb463a7..86a8225 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ python-telegram-bot -SQLAlchemy -alembic requests -pydantic +alembic +SQLAlchemy psycopg2-binary +pydantic +pydantic-settings \ No newline at end of file diff --git a/src/__main__.py b/src/__main__.py index 6d4b270..739be3b 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -26,6 +26,8 @@ tg_logger.propagate = False tg_logger.addHandler(tg_log_handler) +logging.getLogger("httpx").setLevel(logging.WARNING) + logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO, diff --git a/src/handlers.py b/src/handlers.py index 363bad9..5d0ba37 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -1,6 +1,7 @@ # Marakulin Andrey https://github.com/Annndruha # 2023 +import functools import logging import traceback from io import BytesIO @@ -22,7 +23,7 @@ settings = Settings() -engine = create_engine(url=settings.DB_DSN, pool_pre_ping=True, isolation_level="AUTOCOMMIT") +engine = create_engine(url=str(settings.DB_DSN), pool_pre_ping=True, isolation_level="AUTOCOMMIT") Session = sessionmaker(bind=engine) session = Session() @@ -33,13 +34,13 @@ def reconnect_session(): global session session.rollback() - engine = create_engine(url=settings.DB_DSN, pool_pre_ping=True, isolation_level="AUTOCOMMIT") + engine = create_engine(url=str(settings.DB_DSN), pool_pre_ping=True, isolation_level="AUTOCOMMIT") Session = sessionmaker(bind=engine) session = Session() session.rollback() -async def native_error_handler(update, context): +async def native_error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): pass @@ -60,36 +61,58 @@ async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): return wrapper +def log_name(update): + """ + Get from update user id, username and message id. + Created for short code in logging + :param update: + :return: String with + """ + if update.message is None: + ucq = update.callback_query + return f"[{ucq.from_user.id} {ucq.from_user.full_name}] [{ucq.message.id}]" + return f'[{update.message.from_user.id} {update.message.from_user.full_name}]' + + +def log_formatter(func): + @functools.wraps(func) + async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): + if update.callback_query is None: + logging.info(f'{log_name(update)} [{func.__name__}]: {repr(update.message.text)}') + else: + logging.info(f'{log_name(update)} [{func.__name__}] callback_data: {update.callback_query.data}') + await func(update, context) + + return wrapper + + @error_handler +@log_formatter async def handler_start(update: Update, context: ContextTypes.DEFAULT_TYPE): keyboard_base = [[InlineKeyboardButton(ans["about"], callback_data="to_about")]] text, reply_markup = __change_message_by_auth(update, ans["hello"], keyboard_base) await update.message.reply_text(text=text, reply_markup=reply_markup, disable_web_page_preview=True) - logging.info(f"[{update.message.from_user.id} {update.message.from_user.full_name}] call /start") @error_handler +@log_formatter async def handler_help(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text(ans["help"], disable_web_page_preview=True, parse_mode=ParseMode("HTML")) - logging.info(f"[{update.message.from_user.id} {update.message.from_user.full_name}] call /help") @error_handler +@log_formatter async def handler_auth(update: Update, context: ContextTypes.DEFAULT_TYPE): requisites = __auth(update) if requisites is None: await update.message.reply_text(ans["val_need"]) else: await update.message.reply_text(ans["val_info"].format(*requisites), parse_mode=ParseMode("HTML")) - logging.info(f"[{update.message.from_user.id} {update.message.from_user.full_name}] call /auth") @error_handler +@log_formatter async def handler_button_browser(update: Update, context: CallbackContext) -> None: - logging.info( - f"[{update.callback_query.from_user.id} {update.callback_query.from_user.full_name}]" - f"[{update.callback_query.message.id}] button pressed with callback_data={update.callback_query.data}" - ) if update.callback_query.data == "to_hello": keyboard_base = [[InlineKeyboardButton(ans["about"], callback_data="to_about")]] text, reply_markup = __change_message_by_auth(update, ans["hello"], keyboard_base) @@ -118,39 +141,33 @@ async def handler_button_browser(update: Update, context: CallbackContext) -> No @error_handler +@log_formatter async def handler_unknown_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text(ans["unknown_command"]) - logging.info( - f"[{update.message.from_user.id} {update.message.from_user.full_name}]" - f" unknown_command: {repr(update.message.text)}" - ) @error_handler +@log_formatter async def handler_print(update: Update, context: ContextTypes.DEFAULT_TYPE): requisites = __auth(update) if requisites is None: await context.bot.send_message(chat_id=update.message.chat.id, text=ans["doc_not_accepted"]) - logging.warning(f"[{update.message.from_user.id} {update.message.from_user.full_name}] try print with no auth") + logging.warning(f"{log_name(update)} try print with no auth") return try: filebytes, filename = await __get_attachments(update, context) - logging.info(f"[{update.message.from_user.id} {update.message.from_user.full_name}] get attachments OK") + logging.info(f"{log_name(update)} get attachments OK") except FileSizeError: await update.message.reply_text( text=ans["file_size_error"].format(update.message.document.file_name), reply_to_message_id=update.message.id, parse_mode=ParseMode("HTML"), ) - logging.warning( - f"[{update.message.from_user.id} {update.message.from_user.full_name}] get attachments" f"FileSizeError" - ) + logging.warning(f"{log_name(update)} get attachments" f"FileSizeError") return except TelegramError: await update.message.reply_text(text=ans["download_error"], reply_to_message_id=update.message.id) - logging.warning( - f"[{update.message.from_user.id} {update.message.from_user.full_name}] get attachments" f"download_error" - ) + logging.warning(f"{log_name(update)} get attachments" f"download_error") return r = requests.post( @@ -187,7 +204,7 @@ async def handler_print(update: Update, context: ContextTypes.DEFAULT_TYPE): disable_web_page_preview=True, parse_mode=ParseMode("HTML"), ) - logging.info(f"[{update.message.from_user.id} {update.message.from_user.full_name}] print success") + logging.info(f"{log_name(update)} print success") marketing.print_success( tg_id=update.message.chat.id, surname=requisites[1], @@ -201,24 +218,25 @@ async def handler_print(update: Update, context: ContextTypes.DEFAULT_TYPE): reply_to_message_id=update.message.id, parse_mode=ParseMode("HTML"), ) - logging.warning(f"[{update.message.from_user.id} {update.message.from_user.full_name}] print api 413 SizeErr") + logging.warning(f"{log_name(update)} print api 413 SizeErr") return await context.bot.send_message( chat_id=update.effective_user.id, text=ans["print_err"], parse_mode=ParseMode("HTML"), ) - logging.warning(f"[{update.message.from_user.id} {update.message.from_user.full_name}] print unknown error") + logging.warning(f"{log_name(update)} print unknown error") @error_handler +@log_formatter async def handler_mismatch_doctype(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text(ans["only_pdf"]) - logging.warning(f"[{update.message.from_user.id} {update.message.from_user.full_name}] mismatch_doctype") marketing.print_exc_format(tg_id=update.message.chat_id) @error_handler +@log_formatter async def handler_register(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text chat_id = update.message.chat.id @@ -226,10 +244,10 @@ async def handler_register(update: Update, context: ContextTypes.DEFAULT_TYPE): async def text_fail(): if session.query(TgUser).filter(TgUser.tg_id == chat_id).one_or_none() is None: await context.bot.send_message(chat_id=chat_id, text=ans["val_need"]) - logging.warning(f"[{update.message.from_user.id} {update.message.from_user.full_name}] val_need") + logging.warning(f"{log_name(update)} val_need") else: await context.bot.send_message(chat_id=chat_id, text=ans["val_update_fail"]) - logging.warning(f"[{update.message.from_user.id} {update.message.from_user.full_name}] val_update_fail") + logging.warning(f"{log_name(update)} val_update_fail") if text is None: await text_fail() @@ -253,28 +271,19 @@ async def text_fail(): session.commit() await context.bot.send_message(chat_id=chat_id, text=ans["val_pass"]) marketing.register(tg_id=chat_id, surname=surname, number=number) - logging.info( - f"[{update.message.from_user.id} {update.message.from_user.full_name}] register OK: " - f"{surname} {number}" - ) + logging.info(f"{log_name(update)} register OK: {surname} {number}") return True elif r.json() and data is not None: data.surname = surname data.number = number await context.bot.send_message(chat_id=chat_id, text=ans["val_update_pass"]) marketing.re_register(tg_id=chat_id, surname=surname, number=number) - logging.info( - f"[{update.message.from_user.id} {update.message.from_user.full_name}] register repeat OK: " - f"{surname} {number}" - ) + logging.info(f"{log_name(update)} register repeat OK: {surname} {number}") return True elif r.json() is False: await context.bot.send_message(chat_id=chat_id, text=ans["val_fail"]) marketing.register_exc_wrong(tg_id=chat_id, surname=surname, number=number) - logging.info( - f"[{update.message.from_user.id} {update.message.from_user.full_name}] register val_fail: " - f"{surname} {number}" - ) + logging.info(f"{log_name(update)} register val_fail: {surname} {number}") async def __print_settings_solver(update: Update, context: CallbackContext): diff --git a/src/settings.py b/src/settings.py index 972fea8..104b350 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,4 +1,5 @@ -from pydantic import AnyUrl, BaseSettings, PostgresDsn +from pydantic import ConfigDict, PostgresDsn +from pydantic_settings import BaseSettings class Settings(BaseSettings): @@ -6,13 +7,9 @@ class Settings(BaseSettings): BOT_TOKEN: str DB_DSN: PostgresDsn - MARKETING_URL: AnyUrl - PRINT_URL: AnyUrl - PRINT_URL_QR: AnyUrl + MARKETING_URL: str + PRINT_URL: str + PRINT_URL_QR: str MAX_PDF_SIZE_MB: float - class Config: - """Pydantic BaseSettings config""" - - case_sensitive = True - env_file = ".env" + model_config = ConfigDict(case_sensitive=True, env_file=".env", extra="allow")