diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..a7216375 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + push: + branches: ['**'] + pull_request: + branches: ['**'] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - run: uv sync --group dev + + - name: Check formatting + run: uv run ruff format --check . + + - name: Check linting + run: uv run ruff check . diff --git a/.gitignore b/.gitignore index 75160994..6d0fec20 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,13 @@ docker-compose.override.yml # Разрешаем .gitignore чтобы он попал в репозиторий !.gitignore +# Разрешаем .github/ (workflows, pre-commit и т.д.) +!.github/ +!.github/** + +# Разрешаем Makefile +!Makefile + # Внутри разрешенных папок игнорируем служебные файлы app/__pycache__/ app/**/__pycache__/ diff --git a/Makefile b/Makefile index b2438a82..d523d779 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,5 @@ help: ## Показать список доступных команд @echo "" @echo "📘 Команды Makefile:" @echo "" - @grep -E '^[a-zA-Z0-9_-]+:.*?##' $(MAKEFILE_LIST) | \ - sed -E 's/:.*?## /| /' | \ - awk -F'|' '{printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' + @awk -F':.*## ' '/^[a-zA-Z0-9_-]+:.*## / {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @echo "" diff --git a/app/cabinet/dependencies.py b/app/cabinet/dependencies.py index 2bc626b2..aa718529 100644 --- a/app/cabinet/dependencies.py +++ b/app/cabinet/dependencies.py @@ -147,7 +147,7 @@ async def get_current_cabinet_user( ) except HTTPException: raise - except asyncio.TimeoutError: + except TimeoutError: logger.warning(f'Timeout checking channel subscription for user {user.telegram_id}') # Don't block user if check times out except Exception as e: diff --git a/app/cabinet/routes/auth.py b/app/cabinet/routes/auth.py index 4cbf9a73..63adf67e 100644 --- a/app/cabinet/routes/auth.py +++ b/app/cabinet/routes/auth.py @@ -3,16 +3,22 @@ import asyncio import hashlib import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings -from app.database.crud.user import create_user, create_user_by_email, get_user_by_id, get_user_by_referral_code, get_user_by_telegram_id -from app.services.referral_service import process_referral_registration +from app.database.crud.user import ( + create_user, + create_user_by_email, + get_user_by_id, + get_user_by_referral_code, + get_user_by_telegram_id, +) from app.database.models import CabinetRefreshToken, User +from app.services.referral_service import process_referral_registration from ..auth import ( create_access_token, @@ -161,18 +167,16 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) - traffic_used_gb = panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes > 0 else 0 # Extract squad UUIDs from active_internal_squads - connected_squads = [ - s.get('uuid', '') for s in (panel_user.active_internal_squads or []) if s.get('uuid') - ] + connected_squads = [s.get('uuid', '') for s in (panel_user.active_internal_squads or []) if s.get('uuid')] # Device limit from panel device_limit = panel_user.hwid_device_limit or 1 # Determine status - use timezone-aware datetime for comparison - current_time = datetime.now(timezone.utc) + current_time = datetime.now(UTC) # Make expire_at timezone-aware if it's naive if expire_at.tzinfo is None: - expire_at = expire_at.replace(tzinfo=timezone.utc) + expire_at = expire_at.replace(tzinfo=UTC) if panel_user.status.value == 'ACTIVE' and expire_at > current_time: sub_status = SubscriptionStatus.ACTIVE @@ -195,7 +199,9 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) - existing_sub.connected_squads = connected_squads existing_sub.device_limit = device_limit existing_sub.is_trial = False # Panel subscription is not trial - logger.info(f'Updated subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}') + logger.info( + f'Updated subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}' + ) else: # Create new subscription # Convert current_time to naive for database storage if needed @@ -216,7 +222,9 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) - device_limit=device_limit, ) db.add(new_sub) - logger.info(f'Created subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}') + logger.info( + f'Created subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}' + ) await db.commit() @@ -469,7 +477,9 @@ async def register_email_standalone( logger.warning(f'Self-referral attempt blocked: email={request.email}, code={request.referral_code}') referrer = None else: - logger.info(f'Found referrer for email registration: referrer_id={referrer.id}, code={request.referral_code}') + logger.info( + f'Found referrer for email registration: referrer_id={referrer.id}, code={request.referral_code}' + ) # Создать пользователя user = await create_user_by_email( diff --git a/app/cabinet/routes/subscription.py b/app/cabinet/routes/subscription.py index e2413bbb..87af3a6d 100644 --- a/app/cabinet/routes/subscription.py +++ b/app/cabinet/routes/subscription.py @@ -1642,9 +1642,9 @@ async def purchase_tariff( if not user.telegram_id and user.email and user.email_verified: try: # Determine if this is a new subscription or extension - was_new_subscription = subscription.start_date and ( - datetime.utcnow() - subscription.start_date - ).total_seconds() < 60 + was_new_subscription = ( + subscription.start_date and (datetime.utcnow() - subscription.start_date).total_seconds() < 60 + ) notification_type = ( NotificationType.SUBSCRIPTION_ACTIVATED if was_new_subscription diff --git a/app/cabinet/services/email_service.py b/app/cabinet/services/email_service.py index 62877e8d..a07d7416 100644 --- a/app/cabinet/services/email_service.py +++ b/app/cabinet/services/email_service.py @@ -41,9 +41,7 @@ class EmailService: if smtp.has_extn('auth'): smtp.login(self.user, self.password) else: - logger.debug( - f'SMTP server {self.host} does not support AUTH, skipping authentication' - ) + logger.debug(f'SMTP server {self.host} does not support AUTH, skipping authentication') return smtp diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index a530a828..e76acb63 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -1886,9 +1886,7 @@ async def get_disabled_daily_subscriptions_for_resume( result = await db.execute(query) subscriptions = result.scalars().all() - logger.info( - f"🔍 Найдено {len(subscriptions)} DISABLED суточных подписок для возобновления" - ) + logger.info(f'🔍 Найдено {len(subscriptions)} DISABLED суточных подписок для возобновления') return list(subscriptions) diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index 739fb0e5..f75e645b 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -6079,7 +6079,7 @@ async def migrate_cloudpayments_transaction_id_to_bigint() -> bool: try: table_exists = await check_table_exists('cloudpayments_payments') if not table_exists: - logger.info("ℹ️ Таблица cloudpayments_payments не существует, пропускаем миграцию") + logger.info('ℹ️ Таблица cloudpayments_payments не существует, пропускаем миграцию') return True db_type = await get_database_type() @@ -6087,51 +6087,53 @@ async def migrate_cloudpayments_transaction_id_to_bigint() -> bool: async with engine.begin() as conn: if db_type == 'postgresql': # Проверяем текущий тип колонки - result = await conn.execute(text(""" + result = await conn.execute( + text(""" SELECT data_type FROM information_schema.columns WHERE table_name = 'cloudpayments_payments' AND column_name = 'transaction_id_cp' - """)) + """) + ) row = result.fetchone() if row and row[0] == 'bigint': - logger.info("ℹ️ Колонка transaction_id_cp уже имеет тип BIGINT") + logger.info('ℹ️ Колонка transaction_id_cp уже имеет тип BIGINT') return True # Меняем тип на BIGINT - await conn.execute(text( - "ALTER TABLE cloudpayments_payments ALTER COLUMN transaction_id_cp TYPE BIGINT" - )) - logger.info("✅ Колонка transaction_id_cp изменена на BIGINT") + await conn.execute( + text('ALTER TABLE cloudpayments_payments ALTER COLUMN transaction_id_cp TYPE BIGINT') + ) + logger.info('✅ Колонка transaction_id_cp изменена на BIGINT') elif db_type == 'mysql': # Проверяем текущий тип колонки - result = await conn.execute(text(""" + result = await conn.execute( + text(""" SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'cloudpayments_payments' AND COLUMN_NAME = 'transaction_id_cp' - """)) + """) + ) row = result.fetchone() if row and row[0].lower() == 'bigint': - logger.info("ℹ️ Колонка transaction_id_cp уже имеет тип BIGINT") + logger.info('ℹ️ Колонка transaction_id_cp уже имеет тип BIGINT') return True - await conn.execute(text( - "ALTER TABLE cloudpayments_payments MODIFY transaction_id_cp BIGINT" - )) - logger.info("✅ Колонка transaction_id_cp изменена на BIGINT") + await conn.execute(text('ALTER TABLE cloudpayments_payments MODIFY transaction_id_cp BIGINT')) + logger.info('✅ Колонка transaction_id_cp изменена на BIGINT') elif db_type == 'sqlite': # SQLite не поддерживает ALTER COLUMN, но INTEGER в SQLite уже 64-bit - logger.info("ℹ️ SQLite использует 64-bit INTEGER по умолчанию, миграция не требуется") + logger.info('ℹ️ SQLite использует 64-bit INTEGER по умолчанию, миграция не требуется') return True except Exception as error: - logger.error(f"❌ Ошибка миграции transaction_id_cp на BIGINT: {error}") + logger.error(f'❌ Ошибка миграции transaction_id_cp на BIGINT: {error}') return False @@ -6766,12 +6768,12 @@ async def run_universal_migration(): else: logger.warning('⚠️ Проблемы с таблицами колеса удачи') - logger.info("=== МИГРАЦИЯ CLOUDPAYMENTS TRANSACTION_ID НА BIGINT ===") + logger.info('=== МИГРАЦИЯ CLOUDPAYMENTS TRANSACTION_ID НА BIGINT ===') cloudpayments_bigint_ready = await migrate_cloudpayments_transaction_id_to_bigint() if cloudpayments_bigint_ready: - logger.info("✅ Колонка transaction_id_cp в cloudpayments_payments обновлена до BIGINT") + logger.info('✅ Колонка transaction_id_cp в cloudpayments_payments обновлена до BIGINT') else: - logger.warning("⚠️ Проблемы с миграцией transaction_id_cp") + logger.warning('⚠️ Проблемы с миграцией transaction_id_cp') async with engine.begin() as conn: total_subs = await conn.execute(text('SELECT COUNT(*) FROM subscriptions')) diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py index 6766a5ef..4e7db3ab 100644 --- a/app/external/remnawave_api.py +++ b/app/external/remnawave_api.py @@ -341,7 +341,11 @@ class RemnaWaveAPI: connector = aiohttp.TCPConnector(**connector_kwargs) - session_kwargs = {'timeout': aiohttp.ClientTimeout(total=60, connect=10), 'headers': headers, 'connector': connector} + session_kwargs = { + 'timeout': aiohttp.ClientTimeout(total=60, connect=10), + 'headers': headers, + 'connector': connector, + } if cookies: session_kwargs['cookies'] = cookies diff --git a/app/services/notification_delivery_service.py b/app/services/notification_delivery_service.py index 08230c8d..30c8d8a9 100644 --- a/app/services/notification_delivery_service.py +++ b/app/services/notification_delivery_service.py @@ -202,7 +202,7 @@ class NotificationDeliveryService: ) return True - except asyncio.TimeoutError: + except TimeoutError: logger.warning( 'Timeout при отправке Telegram уведомления пользователю %s', user.telegram_id, diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index 75480cd7..4009fbf4 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -1174,10 +1174,7 @@ class RemnaWaveService: user.remnawave_uuid: user for user in bot_users if getattr(user, 'remnawave_uuid', None) } # Index users by email for email-only sync - bot_users_by_email = { - user.email.lower(): user for user in bot_users - if user.email and user.email_verified - } + bot_users_by_email = {user.email.lower(): user for user in bot_users if user.email and user.email_verified} # Also index email-only users by their remnawave_uuid for sync email_users_count = sum(1 for u in bot_users if u.telegram_id is None) if email_users_count > 0: @@ -1203,8 +1200,7 @@ class RemnaWaveService: # Email-only пользователи из панели (без telegram_id, но с email) panel_users_email_only = [ - user for user in panel_users - if user.get('telegramId') is None and user.get('email') + user for user in panel_users if user.get('telegramId') is None and user.get('email') ] if panel_users_email_only: logger.info(f'📧 Пользователей в панели с Email (без Telegram): {len(panel_users_email_only)}') @@ -1834,7 +1830,10 @@ class RemnaWaveService: telegram_id=user.telegram_id, email=user.email, description=settings.format_remnawave_user_description( - full_name=user.full_name, username=user.username, telegram_id=user.telegram_id, email=user.email + full_name=user.full_name, + username=user.username, + telegram_id=user.telegram_id, + email=user.email, ), active_internal_squads=sub.connected_squads, )