ci: add ruff lint workflow and fix formatting

This commit is contained in:
c0mrade
2026-01-25 19:31:00 +03:00
parent bd18e6c187
commit 1b212cc8d6
12 changed files with 96 additions and 53 deletions

27
.github/workflows/lint.yml vendored Normal file
View File

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

7
.gitignore vendored
View File

@@ -31,6 +31,13 @@ docker-compose.override.yml
# Разрешаем .gitignore чтобы он попал в репозиторий
!.gitignore
# Разрешаем .github/ (workflows, pre-commit и т.д.)
!.github/
!.github/**
# Разрешаем Makefile
!Makefile
# Внутри разрешенных папок игнорируем служебные файлы
app/__pycache__/
app/**/__pycache__/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -202,7 +202,7 @@ class NotificationDeliveryService:
)
return True
except asyncio.TimeoutError:
except TimeoutError:
logger.warning(
'Timeout при отправке Telegram уведомления пользователю %s',
user.telegram_id,

View File

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