fix PR problems

This commit is contained in:
firewookie
2026-03-10 14:29:29 +05:00
parent dcfd54a7cb
commit fd3466b75c
50 changed files with 216 additions and 211 deletions
+4 -3
View File
@@ -85,6 +85,7 @@ async def update_partner_settings(
admin: User = Depends(require_permission('partners:settings')),
):
"""Update partner system settings."""
import asyncio
from pathlib import Path
# Update in-memory settings
@@ -104,8 +105,8 @@ async def update_partner_settings(
# Persist to .env file
try:
env_file = Path('.env')
if env_file.exists():
lines = env_file.read_text().splitlines()
if await asyncio.to_thread(env_file.exists):
lines = (await asyncio.to_thread(env_file.read_text)).splitlines()
updates: dict[str, str] = {}
if request.withdrawal_enabled is not None:
@@ -143,7 +144,7 @@ async def update_partner_settings(
if key not in updated_keys:
new_lines.append(f'{key}={value}')
env_file.write_text('\n'.join(new_lines) + '\n')
await asyncio.to_thread(env_file.write_text, '\n'.join(new_lines) + '\n')
logger.info('Updated partner settings in .env file', admin_id=admin.id)
except Exception as e:
logger.warning('Failed to update .env file', error=e)
+1 -1
View File
@@ -210,7 +210,7 @@ def _is_checkable(record: PendingPayment) -> bool:
if record.method == PaymentMethod.YOOKASSA:
return status_str in {'pending', 'waiting_for_capture'}
if record.method == PaymentMethod.CRYPTOBOT:
return status_str in {'active'}
return status_str == 'active'
if record.method == PaymentMethod.CLOUDPAYMENTS:
return status_str in {'pending', 'authorized'}
if record.method == PaymentMethod.FREEKASSA:
+1 -1
View File
@@ -303,7 +303,7 @@ async def create_new_tariff(
period_prices=period_prices_dict,
allowed_squads=request.allowed_squads,
server_traffic_limits=server_limits_dict,
promo_group_ids=request.promo_group_ids if request.promo_group_ids else None,
promo_group_ids=request.promo_group_ids or None,
# Произвольное количество дней
custom_days_enabled=request.custom_days_enabled,
price_per_day_kopeks=request.price_per_day_kopeks,
+4 -3
View File
@@ -246,6 +246,7 @@ async def update_ticket_settings(
db: AsyncSession = Depends(get_cabinet_db),
):
"""Update ticket system settings."""
import asyncio
from pathlib import Path
from app.services.support_settings_service import SupportSettingsService
@@ -280,8 +281,8 @@ async def update_ticket_settings(
# Try to persist to .env file
try:
env_file = Path('.env')
if env_file.exists():
lines = env_file.read_text().splitlines()
if await asyncio.to_thread(env_file.exists):
lines = (await asyncio.to_thread(env_file.read_text)).splitlines()
updates = {}
if request.sla_enabled is not None:
@@ -314,7 +315,7 @@ async def update_ticket_settings(
if key not in updated_keys:
new_lines.append(f'{key}={value}')
env_file.write_text('\n'.join(new_lines) + '\n')
await asyncio.to_thread(env_file.write_text, '\n'.join(new_lines) + '\n')
logger.info('Updated ticket settings in .env file')
except Exception as e:
logger.warning('Failed to update .env file', error=e)
+6 -6
View File
@@ -774,7 +774,7 @@ async def register_email(
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
custom_subject, custom_body = override or (None, None)
await asyncio.to_thread(
email_service.send_verification_email,
@@ -911,7 +911,7 @@ async def register_email_standalone(
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
custom_subject, custom_body = override or (None, None)
await asyncio.to_thread(
email_service.send_verification_email,
@@ -1049,7 +1049,7 @@ async def resend_verification(
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
custom_subject, custom_body = override or (None, None)
await asyncio.to_thread(
email_service.send_verification_email,
@@ -1356,7 +1356,7 @@ async def forgot_password(
context={'username': user.first_name or '', 'reset_url': full_url, 'expire_hours': str(expire_hours)},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
custom_subject, custom_body = override or (None, None)
await asyncio.to_thread(
email_service.send_password_reset_email,
@@ -1524,7 +1524,7 @@ async def request_email_change(
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
custom_subject, custom_body = override or (None, None)
try:
await asyncio.to_thread(
@@ -1579,7 +1579,7 @@ async def request_email_change(
},
db=db,
)
custom_subject, custom_body = override if override else (None, None)
custom_subject, custom_body = override or (None, None)
await asyncio.to_thread(
email_service.send_email_change_code,
+2 -2
View File
@@ -198,7 +198,7 @@ async def get_payment_methods(
'description': description,
}
)
options = formatted_options if formatted_options else None
options = formatted_options or None
methods.append(
PaymentMethodResponse(
@@ -885,7 +885,7 @@ def _is_checkable(record: PendingPayment) -> bool:
if record.method == PaymentMethod.YOOKASSA:
return status in {'pending', 'waiting_for_capture'}
if record.method == PaymentMethod.CRYPTOBOT:
return status in {'active'}
return status == 'active'
if record.method == PaymentMethod.CLOUDPAYMENTS:
return status in {'pending', 'authorized'}
if record.method == PaymentMethod.FREEKASSA:
+8 -7
View File
@@ -1,5 +1,6 @@
"""Branding routes for cabinet - logo, project name, and theme colors management."""
import asyncio
import json
import os
from pathlib import Path
@@ -401,7 +402,7 @@ async def get_logo():
"""
logo_path = get_logo_path()
if logo_path is None or not logo_path.exists():
if logo_path is None or not await asyncio.to_thread(logo_path.exists):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='No custom logo set')
# Determine media type from file extension
@@ -470,7 +471,7 @@ async def upload_logo(
)
# Ensure directory exists
ensure_branding_dir()
await asyncio.to_thread(ensure_branding_dir)
# Determine file extension from content type
ext_map = {
@@ -483,12 +484,12 @@ async def upload_logo(
extension = ext_map.get(file.content_type, '.png')
# Remove old logo files with any extension
for old_file in BRANDING_DIR.glob('logo.*'):
old_file.unlink()
for old_file in await asyncio.to_thread(lambda: list(BRANDING_DIR.glob('logo.*'))):
await asyncio.to_thread(old_file.unlink)
# Save new logo
logo_path = BRANDING_DIR / f'logo{extension}'
logo_path.write_bytes(content)
await asyncio.to_thread(logo_path.write_bytes, content)
# Mark that we have a custom logo
await set_setting_value(db, BRANDING_LOGO_KEY, 'custom')
@@ -517,8 +518,8 @@ async def delete_logo(
):
"""Delete custom logo and revert to letter. Admin only."""
# Remove logo files
for old_file in BRANDING_DIR.glob('logo.*'):
old_file.unlink()
for old_file in await asyncio.to_thread(lambda: list(BRANDING_DIR.glob('logo.*'))):
await asyncio.to_thread(old_file.unlink)
# Update setting
await set_setting_value(db, BRANDING_LOGO_KEY, 'default')
+23 -19
View File
@@ -260,13 +260,17 @@ async def create_gift_purchase(
detail='payment_method is required for gateway mode',
)
purchase_kwargs: dict = {
'gift_recipient_type': body.recipient_type,
'gift_recipient_value': body.recipient_value,
'gift_message': body.gift_message,
} if has_recipient else {
'gift_message': body.gift_message,
}
purchase_kwargs: dict = (
{
'gift_recipient_type': body.recipient_type,
'gift_recipient_value': body.recipient_value,
'gift_message': body.gift_message,
}
if has_recipient
else {
'gift_message': body.gift_message,
}
)
try:
purchase = await create_purchase(
@@ -345,13 +349,17 @@ async def create_gift_purchase(
)
# Create purchase record
balance_purchase_kwargs: dict = {
'gift_recipient_type': body.recipient_type,
'gift_recipient_value': body.recipient_value,
'gift_message': body.gift_message,
} if has_recipient else {
'gift_message': body.gift_message,
}
balance_purchase_kwargs: dict = (
{
'gift_recipient_type': body.recipient_type,
'gift_recipient_value': body.recipient_value,
'gift_message': body.gift_message,
}
if has_recipient
else {
'gift_message': body.gift_message,
}
)
try:
purchase = await create_purchase(
@@ -497,11 +505,7 @@ async def get_gift_purchase_status(
else:
token_filter = GuestPurchase.token.startswith(token)
result = await db.execute(
select(GuestPurchase)
.options(selectinload(GuestPurchase.tariff))
.where(token_filter)
)
result = await db.execute(select(GuestPurchase).options(selectinload(GuestPurchase.tariff)).where(token_filter))
purchase = result.scalars().first()
if purchase is None:
raise HTTPException(
+1 -1
View File
@@ -160,7 +160,7 @@ async def get_rules(
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get service rules - uses same function as bot."""
requested_lang = language.split('-')[0].lower()
requested_lang = language.split('-', maxsplit=1)[0].lower()
# Use the same function as bot to ensure consistent content
content = await get_current_rules_content(db, requested_lang)
+1 -1
View File
@@ -1469,7 +1469,7 @@ async def activate_trial(
duration_days=trial_duration,
traffic_limit_gb=trial_traffic_limit,
device_limit=trial_device_limit,
connected_squads=trial_squads if trial_squads else None,
connected_squads=trial_squads or None,
tariff_id=tariff_id_for_trial,
)
+4 -4
View File
@@ -1,13 +1,13 @@
"""Schemas for Admin Users management in cabinet."""
from datetime import datetime
from enum import Enum
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, Field
class UserStatusEnum(str, Enum):
class UserStatusEnum(StrEnum):
"""User status enum."""
ACTIVE = 'active'
@@ -15,7 +15,7 @@ class UserStatusEnum(str, Enum):
DELETED = 'deleted'
class SubscriptionStatusEnum(str, Enum):
class SubscriptionStatusEnum(StrEnum):
"""Subscription status enum."""
TRIAL = 'trial'
@@ -25,7 +25,7 @@ class SubscriptionStatusEnum(str, Enum):
PENDING = 'pending'
class SortByEnum(str, Enum):
class SortByEnum(StrEnum):
"""Sort options for users list."""
CREATED_AT = 'created_at'
+3 -3
View File
@@ -1,7 +1,7 @@
"""Схемы для колеса удачи (Fortune Wheel)."""
from datetime import datetime
from enum import Enum
from enum import StrEnum
from pydantic import BaseModel, Field
@@ -9,14 +9,14 @@ from pydantic import BaseModel, Field
# ==================== ENUMS ====================
class WheelPaymentType(str, Enum):
class WheelPaymentType(StrEnum):
"""Способы оплаты спина."""
TELEGRAM_STARS = 'telegram_stars'
SUBSCRIPTION_DAYS = 'subscription_days'
class WheelPrizeType(str, Enum):
class WheelPrizeType(StrEnum):
"""Типы призов."""
SUBSCRIPTION_DAYS = 'subscription_days'
+22 -22
View File
@@ -949,12 +949,12 @@ class Settings(BaseSettings):
def get_test_email(self) -> str | None:
"""Get test email for development/testing."""
email = (self.TEST_EMAIL or '').strip().lower()
return email if email else None
return email or None
def get_test_email_password(self) -> str | None:
"""Get test email password."""
password = (self.TEST_EMAIL_PASSWORD or '').strip()
return password if password else None
return password or None
def is_test_email(self, email: str) -> bool:
"""Check if email is the configured test email."""
@@ -1519,7 +1519,7 @@ class Settings(BaseSettings):
except (ValueError, IndexError):
continue
return packages if packages else self.get_traffic_packages()
return packages or self.get_traffic_packages()
def get_traffic_topup_price(self, gb: int | None) -> int:
"""Возвращает цену докупки для указанного количества ГБ."""
@@ -1609,7 +1609,7 @@ class Settings(BaseSettings):
def get_yookassa_display_name(self) -> str:
name = (self.YOOKASSA_DISPLAY_NAME or '').strip()
return name if name else 'YooKassa'
return name or 'YooKassa'
def is_nalogo_enabled(self) -> bool:
return self.NALOGO_ENABLED and self.NALOGO_INN is not None and self.NALOGO_PASSWORD is not None
@@ -1629,14 +1629,14 @@ class Settings(BaseSettings):
def get_cryptobot_display_name(self) -> str:
name = (self.CRYPTOBOT_DISPLAY_NAME or '').strip()
return name if name else 'CryptoBot'
return name or 'CryptoBot'
def is_heleket_enabled(self) -> bool:
return self.HELEKET_ENABLED and self.HELEKET_MERCHANT_ID is not None and self.HELEKET_API_KEY is not None
def get_heleket_display_name(self) -> str:
name = (self.HELEKET_DISPLAY_NAME or '').strip()
return name if name else 'Heleket Crypto'
return name or 'Heleket Crypto'
def is_mulenpay_enabled(self) -> bool:
return (
@@ -1674,7 +1674,7 @@ class Settings(BaseSettings):
def get_pal24_display_name(self) -> str:
name = (self.PAL24_DISPLAY_NAME or '').strip()
return name if name else 'PAL24'
return name or 'PAL24'
def is_platega_enabled(self) -> bool:
return self.PLATEGA_ENABLED and self.PLATEGA_MERCHANT_ID is not None and self.PLATEGA_SECRET is not None
@@ -1754,7 +1754,7 @@ class Settings(BaseSettings):
def get_wata_display_name(self) -> str:
name = (self.WATA_DISPLAY_NAME or '').strip()
return name if name else 'Wata'
return name or 'Wata'
def is_cloudpayments_enabled(self) -> bool:
return (
@@ -1765,7 +1765,7 @@ class Settings(BaseSettings):
def get_cloudpayments_display_name(self) -> str:
name = (self.CLOUDPAYMENTS_DISPLAY_NAME or '').strip()
return name if name else 'CloudPayments'
return name or 'CloudPayments'
def is_freekassa_enabled(self) -> bool:
return (
@@ -1778,7 +1778,7 @@ class Settings(BaseSettings):
def get_freekassa_display_name(self) -> str:
name = (self.FREEKASSA_DISPLAY_NAME or '').strip()
return name if name else 'Freekassa'
return name or 'Freekassa'
def get_freekassa_display_name_html(self) -> str:
return html.escape(self.get_freekassa_display_name())
@@ -1788,7 +1788,7 @@ class Settings(BaseSettings):
def get_freekassa_sbp_display_name(self) -> str:
name = (self.FREEKASSA_SBP_DISPLAY_NAME or '').strip()
return name if name else 'СБП (QR код)'
return name or 'СБП (QR код)'
def get_freekassa_sbp_display_name_html(self) -> str:
return html.escape(self.get_freekassa_sbp_display_name())
@@ -1798,7 +1798,7 @@ class Settings(BaseSettings):
def get_freekassa_card_display_name(self) -> str:
name = (self.FREEKASSA_CARD_DISPLAY_NAME or '').strip()
return name if name else 'Карта РФ'
return name or 'Карта РФ'
def get_freekassa_card_display_name_html(self) -> str:
return html.escape(self.get_freekassa_card_display_name())
@@ -1813,7 +1813,7 @@ class Settings(BaseSettings):
def get_kassa_ai_display_name(self) -> str:
name = (self.KASSA_AI_DISPLAY_NAME or '').strip()
return name if name else 'KassaAI'
return name or 'KassaAI'
def get_kassa_ai_display_name_html(self) -> str:
return html.escape(self.get_kassa_ai_display_name())
@@ -1823,7 +1823,7 @@ class Settings(BaseSettings):
def get_riopay_display_name(self) -> str:
name = (self.RIOPAY_DISPLAY_NAME or '').strip()
return name if name else 'RioPay'
return name or 'RioPay'
def get_riopay_display_name_html(self) -> str:
return html.escape(self.get_riopay_display_name())
@@ -1923,7 +1923,7 @@ class Settings(BaseSettings):
'windows': ((self.HAPP_DOWNLOAD_LINK_WINDOWS or '').strip() or (self.HAPP_DOWNLOAD_LINK_PC or '').strip()),
}
link = links.get(platform_key)
return link if link else None
return link or None
def is_maintenance_mode(self) -> bool:
return self.MAINTENANCE_MODE
@@ -2011,7 +2011,7 @@ class Settings(BaseSettings):
# т.к. в режиме classic цена складывается из серверов/трафика/устройств)
periods = sorted(allowed_periods)
return periods if periods else [30, 90, 180]
return periods or [30, 90, 180]
def get_available_renewal_periods(self) -> list[int]:
"""
@@ -2036,7 +2036,7 @@ class Settings(BaseSettings):
# Возвращаем только разрешённые периоды (без фильтрации по цене)
periods = sorted(allowed_periods)
return periods if periods else [30, 90, 180]
return periods or [30, 90, 180]
def get_configured_subscription_periods(self) -> list[int]:
"""
@@ -2101,7 +2101,7 @@ class Settings(BaseSettings):
def get_telegram_stars_display_name(self) -> str:
name = (self.TELEGRAM_STARS_DISPLAY_NAME or '').strip()
return name if name else 'Telegram Stars'
return name or 'Telegram Stars'
def stars_to_rubles(self, stars: int) -> float:
return stars * self.get_stars_rate()
@@ -2138,7 +2138,7 @@ class Settings(BaseSettings):
def get_backup_archive_password(self) -> str | None:
password = (self.BACKUP_ARCHIVE_PASSWORD or '').strip()
return password if password else None
return password or None
# === Log Rotation Methods ===
@@ -2219,7 +2219,7 @@ class Settings(BaseSettings):
except ValueError:
continue
return packages if packages else self._get_fallback_traffic_packages()
return packages or self._get_fallback_traffic_packages()
except Exception as e:
logger.warning('ERROR PARSING CONFIG', error=e)
@@ -2352,13 +2352,13 @@ class Settings(BaseSettings):
if contact.startswith(('t.me/', 'telegram.me/', 'telegram.dog/')):
url = self.get_support_contact_url()
return url if url else contact
return url or contact
contact_without_prefix = contact.lstrip('@')
if '.' in contact_without_prefix:
url = self.get_support_contact_url()
return url if url else contact
return url or contact
if re.fullmatch(r'[A-Za-z0-9_]{3,}', contact_without_prefix):
return f'@{contact_without_prefix}'
+1 -1
View File
@@ -187,7 +187,7 @@ async def get_primary_user_promo_group(db: AsyncSession, user_id: int) -> PromoG
return None
# Первая в списке имеет максимальный приоритет (список уже отсортирован)
return user_promo_groups[0].promo_group if user_promo_groups[0].promo_group else None
return user_promo_groups[0].promo_group or None
except Exception as error:
logger.error('Ошибка получения primary промогруппы пользователя', user_id=user_id, error=error)
+2 -2
View File
@@ -8,7 +8,7 @@ def _aware(dt: datetime | None) -> datetime | None:
return dt
from enum import Enum
from enum import Enum, StrEnum
from sqlalchemy import (
JSON,
@@ -3124,7 +3124,7 @@ class LandingPage(Base):
return f"<LandingPage slug='{self.slug}' active={self.is_active}>"
class GuestPurchaseStatus(str, Enum):
class GuestPurchaseStatus(StrEnum):
PENDING = 'pending'
PAID = 'paid'
DELIVERED = 'delivered'
+1 -1
View File
@@ -29,7 +29,7 @@ async def show_blacklist_settings(callback: types.CallbackQuery, db_user: User,
blacklist_count = len(await blacklist_service.get_all_blacklisted_users())
status_text = '✅ Включена' if is_enabled else '❌ Отключена'
url_text = github_url if github_url else 'Не задан'
url_text = github_url or 'Не задан'
text = f"""
🔐 <b>Настройки черного списка</b>
+4 -4
View File
@@ -1350,12 +1350,12 @@ async def _do_reconcile_logs(callback: CallbackQuery):
await callback.answer('🔄 Анализирую логи платежей...', show_alert=False)
# Путь к файлу логов платежей (logs/current/)
log_file_path = Path(settings.LOG_FILE).resolve()
log_file_path = await asyncio.to_thread(Path(settings.LOG_FILE).resolve)
log_dir = log_file_path.parent
current_dir = log_dir / 'current'
payments_log = current_dir / settings.LOG_PAYMENTS_FILE
if not payments_log.exists():
if not await asyncio.to_thread(payments_log.exists):
try:
await callback.message.edit_text(
'❌ <b>Файл логов не найден</b>\n\n'
@@ -1491,12 +1491,12 @@ async def receipts_reconcile_logs_details_callback(callback: CallbackQuery):
await callback.answer('🔄 Загружаю детали...', show_alert=False)
# Путь к логам (logs/current/)
log_file_path = Path(settings.LOG_FILE).resolve()
log_file_path = await asyncio.to_thread(Path(settings.LOG_FILE).resolve)
log_dir = log_file_path.parent
current_dir = log_dir / 'current'
payments_log = current_dir / settings.LOG_PAYMENTS_FILE
if not payments_log.exists():
if not await asyncio.to_thread(payments_log.exists):
await callback.answer('❌ Файл логов не найден', show_alert=True)
return
+2 -2
View File
@@ -188,7 +188,7 @@ def _is_checkable(record: PendingPayment) -> bool:
if record.method == PaymentMethod.YOOKASSA:
return status in {'pending', 'waiting_for_capture'}
if record.method == PaymentMethod.CRYPTOBOT:
return status in {'active'}
return status == 'active'
if record.method == PaymentMethod.FREEKASSA:
return status in {'pending', 'created', ''}
if record.method == PaymentMethod.KASSA_AI:
@@ -380,7 +380,7 @@ def _build_payment_details_text(record: PendingPayment, *, texts, language: str)
amount = f'{crypto_amount} {crypto_asset}'
created = format_datetime(record.created_at)
age = format_time_ago(record.created_at, language)
raw_identifier = record.identifier if record.identifier else record.local_id
raw_identifier = record.identifier or record.local_id
identifier = html.escape(str(raw_identifier)) if raw_identifier is not None else ''
lines = [
texts.t('ADMIN_PAYMENT_DETAILS_TITLE', '💳 <b>Payment details</b>'),
+5 -4
View File
@@ -1,3 +1,4 @@
import asyncio
import json
from datetime import UTC, datetime, timedelta
@@ -756,8 +757,8 @@ async def _show_diagnostics_for_period(callback: types.CallbackQuery, db: AsyncS
# Информация о логах
log_path = referral_diagnostics_service.log_path
log_exists = log_path.exists()
log_size = log_path.stat().st_size if log_exists else 0
log_exists = await asyncio.to_thread(log_path.exists)
log_size = (await asyncio.to_thread(log_path.stat)).st_size if log_exists else 0
text += f'\n<i>📂 {log_path.name}'
if log_exists:
@@ -1434,9 +1435,9 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
finally:
# Удаляем временный файл
if temp_file_path and Path(temp_file_path).exists():
if temp_file_path and await asyncio.to_thread(Path(temp_file_path).exists):
try:
Path(temp_file_path).unlink()
await asyncio.to_thread(Path(temp_file_path).unlink)
logger.info('🗑️ Временный файл удалён', temp_file_path=temp_file_path)
except Exception as e:
logger.error('Ошибка удаления временного файла', error=e)
+1 -1
View File
@@ -81,7 +81,7 @@ def _split_text_into_pages(header: str, message_blocks: list[str], max_len: int
if current.strip():
pages.append(current)
return pages if pages else [header]
return pages or [header]
async def show_admin_tickets(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
+2 -2
View File
@@ -21,8 +21,8 @@ logger = structlog.get_logger(__name__)
FREEKASSA_SUB_METHODS = {
'freekassa_sbp': {'payment_system_id': 44, 'get_name': lambda: settings.get_freekassa_sbp_display_name()},
'freekassa_card': {'payment_system_id': 36, 'get_name': lambda: settings.get_freekassa_card_display_name()},
'freekassa_sbp': {'payment_system_id': 44, 'get_name': settings.get_freekassa_sbp_display_name},
'freekassa_card': {'payment_system_id': 36, 'get_name': settings.get_freekassa_card_display_name},
}
+1 -1
View File
@@ -169,7 +169,7 @@ def _split_into_pages(
pages.append((current_online, current_offline))
return pages if pages else [([], [])]
return pages or [([], [])]
def _format_server_lines(
+1 -1
View File
@@ -970,7 +970,7 @@ async def handle_simple_subscription_payment_method(
from aiogram.types import BufferedInputFile
# Используем qr_confirmation_data если доступно, иначе confirmation_url
qr_data = qr_confirmation_data if qr_confirmation_data else confirmation_url
qr_data = qr_confirmation_data or confirmation_url
# Создаем QR-код из полученных данных
qr = qrcode.QRCode(version=1, box_size=10, border=5)
+1 -1
View File
@@ -565,7 +565,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
if referral_code:
await state.update_data(referral_code=referral_code)
user = db_user if db_user else await get_user_by_telegram_id(db, message.from_user.id)
user = db_user or await get_user_by_telegram_id(db, message.from_user.id)
if campaign and not campaign_notification_sent:
try:
+4 -20
View File
@@ -277,11 +277,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
# Используем max_device_limit из тарифа если есть, иначе глобальную настройку
tariff_max_devices = getattr(tariff, 'max_device_limit', None) if tariff else None
effective_max = (
tariff_max_devices
if tariff_max_devices
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
)
effective_max = tariff_max_devices or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
if effective_max and new_devices_count > effective_max:
await callback.answer(
texts.t(
@@ -571,11 +567,7 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
# Re-validate: prevent double-charge and max-limit violation
if new_devices_count > current_devices:
tariff_max_recheck = getattr(tariff, 'max_device_limit', None) if tariff else None
max_devices = (
tariff_max_recheck
if tariff_max_recheck
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
)
max_devices = tariff_max_recheck or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
if max_devices and new_devices_count > max_devices:
if price > 0:
user_refund = await db.execute(
@@ -1139,11 +1131,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
# Используем max_device_limit из тарифа если есть, иначе глобальную настройку
tariff_max_devices = getattr(tariff, 'max_device_limit', None) if tariff else None
effective_max = (
tariff_max_devices
if tariff_max_devices
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
)
effective_max = tariff_max_devices or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
if effective_max and new_total_devices > effective_max:
await callback.answer(
texts.t(
@@ -1278,11 +1266,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
actual_current = subscription.device_limit or 1
actual_new = actual_current + devices_count
tariff_max_recheck = getattr(tariff, 'max_device_limit', None) if tariff else None
max_devices = (
tariff_max_recheck
if tariff_max_recheck
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
)
max_devices = tariff_max_recheck or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
if max_devices and actual_new > max_devices:
# Concurrent purchase exceeded limit — refund
user_refund = await db.execute(
+1 -1
View File
@@ -313,7 +313,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
devices_used_str = str(devices_used)
servers_names = await get_servers_display_names(subscription.connected_squads)
servers_display = servers_names if servers_names else texts.t('SUBSCRIPTION_NO_SERVERS', 'Нет серверов')
servers_display = servers_names or texts.t('SUBSCRIPTION_NO_SERVERS', 'Нет серверов')
# Получаем информацию о тарифе для режима тарифов
tariff_info_block = ''
+1 -1
View File
@@ -504,7 +504,7 @@ def _split_text_into_pages(header: str, message_blocks: list[str], max_len: int
if current.strip():
pages.append(current)
return pages if pages else [header]
return pages or [header]
async def view_ticket(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
+2 -2
View File
@@ -398,7 +398,7 @@ class ChannelCheckerMiddleware(BaseMiddleware):
# Notify user about deactivation
try:
normalized = _normalize_channels(channels)
texts = get_texts(user.language if user.language else DEFAULT_LANGUAGE)
texts = get_texts(user.language or DEFAULT_LANGUAGE)
notification_text = texts.t(
'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE',
'🚫 Ваша подписка приостановлена, так как вы отписались от канала.\n\n'
@@ -468,7 +468,7 @@ class ChannelCheckerMiddleware(BaseMiddleware):
# Notify user about reactivation
try:
texts = get_texts(user.language if user.language else DEFAULT_LANGUAGE)
texts = get_texts(user.language or DEFAULT_LANGUAGE)
notification_text = texts.t(
'SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE',
'✅ Ваша подписка восстановлена!\n\nСпасибо, что подписались на канал. VPN снова работает.',
+50 -42
View File
@@ -347,7 +347,7 @@ class BackupService:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
staging_dir = temp_path / 'backup'
staging_dir.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(staging_dir.mkdir, True, True)
database_info = await self._dump_database(staging_dir, include_logs=include_logs)
database_info.setdefault('tables_count', overview.get('tables_count', 0))
@@ -376,10 +376,10 @@ class BackupService:
mode = 'w:gz' if compress else 'w'
with tarfile.open(backup_path, mode) as tar:
for item in staging_dir.iterdir():
for item in await asyncio.to_thread(lambda: list(staging_dir.iterdir())):
tar.add(item, arcname=item.name)
file_size = backup_path.stat().st_size
file_size = (await asyncio.to_thread(backup_path.stat)).st_size
await self._cleanup_old_backups()
@@ -415,7 +415,7 @@ class BackupService:
logger.info('📄 Начинаем восстановление из', backup_file_path=backup_file_path)
backup_path = Path(backup_file_path)
if not backup_path.exists():
if not await asyncio.to_thread(backup_path.exists):
return False, f'❌ Файл бекапа не найден: {backup_file_path}'
if self._is_archive_backup(backup_path):
@@ -473,7 +473,11 @@ class BackupService:
if pg_dump_path:
dump_path = staging_dir / 'database.sql'
await self._dump_postgres(dump_path, pg_dump_path)
size = dump_path.stat().st_size if dump_path.exists() else 0
size = (
(await asyncio.to_thread(dump_path.stat)).st_size
if await asyncio.to_thread(dump_path.exists)
else 0
)
return {
'type': 'postgresql',
'path': dump_path.name,
@@ -488,7 +492,7 @@ class BackupService:
dump_path = staging_dir / 'database.sqlite'
await self._dump_sqlite(dump_path)
size = dump_path.stat().st_size if dump_path.exists() else 0
size = (await asyncio.to_thread(dump_path.stat)).st_size if await asyncio.to_thread(dump_path.exists) else 0
return {
'type': 'sqlite',
'path': dump_path.name,
@@ -516,9 +520,9 @@ class BackupService:
]
logger.info('📦 Экспорт PostgreSQL через pg_dump ...', pg_dump_path=pg_dump_path)
dump_path.parent.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(dump_path.parent.mkdir, True, True)
with dump_path.open('wb') as dump_file:
with open(dump_path, 'wb') as dump_file:
process = await asyncio.create_subprocess_exec(
*command,
stdout=dump_file,
@@ -558,7 +562,7 @@ class BackupService:
async with aiofiles.open(dump_path, 'w', encoding='utf-8') as dump_file:
await dump_file.write(json_lib.dumps(dump_structure, ensure_ascii=False, indent=2))
size = dump_path.stat().st_size if dump_path.exists() else 0
size = (await asyncio.to_thread(dump_path.stat)).st_size if await asyncio.to_thread(dump_path.exists) else 0
logger.info('✅ PostgreSQL экспортирован через ORM в JSON', dump_path=dump_path)
@@ -575,10 +579,10 @@ class BackupService:
async def _dump_sqlite(self, dump_path: Path):
sqlite_path = Path(settings.SQLITE_PATH)
if not sqlite_path.exists():
if not await asyncio.to_thread(sqlite_path.exists):
raise FileNotFoundError(f'SQLite база данных не найдена по пути {sqlite_path}')
dump_path.parent.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(dump_path.parent.mkdir, True, True)
await asyncio.to_thread(shutil.copy2, sqlite_path, dump_path)
logger.info('✅ SQLite база данных скопирована', dump_path=dump_path)
@@ -662,11 +666,11 @@ class BackupService:
async def _collect_files(self, staging_dir: Path, include_logs: bool) -> list[dict[str, Any]]:
files_info: list[dict[str, Any]] = []
files_dir = staging_dir / 'files'
files_dir.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(files_dir.mkdir, True, True)
if include_logs and settings.LOG_FILE:
log_path = Path(settings.LOG_FILE)
if log_path.exists():
if await asyncio.to_thread(log_path.exists):
dest = files_dir / log_path.name
await asyncio.to_thread(shutil.copy2, log_path, dest)
files_info.append(
@@ -676,8 +680,8 @@ class BackupService:
}
)
if not files_info and files_dir.exists():
files_dir.rmdir()
if not files_info and await asyncio.to_thread(files_dir.exists):
await asyncio.to_thread(files_dir.rmdir)
return files_info
@@ -688,7 +692,7 @@ class BackupService:
'items': 0,
}
if not self.data_dir.exists():
if not await asyncio.to_thread(self.data_dir.exists):
return snapshot_info
counter = {'items': 0}
@@ -732,7 +736,7 @@ class BackupService:
tar.extractall(temp_path, filter='data')
metadata_path = temp_path / 'metadata.json'
if not metadata_path.exists():
if not await asyncio.to_thread(metadata_path.exists):
return False, '❌ Метаданные бекапа отсутствуют'
async with aiofiles.open(metadata_path, encoding='utf-8') as meta_file:
@@ -758,7 +762,7 @@ class BackupService:
await self._restore_sqlite(dump_file, clear_existing)
data_dir = temp_path / 'data'
if data_dir.exists():
if await asyncio.to_thread(data_dir.exists):
await self._restore_data_snapshot(data_dir, clear_existing)
if files_info:
@@ -775,7 +779,7 @@ class BackupService:
return True, message
async def _restore_postgres(self, dump_path: Path, clear_existing: bool):
if not dump_path.exists():
if not await asyncio.to_thread(dump_path.exists):
raise FileNotFoundError(f'Dump PostgreSQL не найден: {dump_path}')
psql_path = self._resolve_command_path('psql', 'PSQL_PATH')
@@ -833,7 +837,7 @@ class BackupService:
logger.info('✅ PostgreSQL восстановлен', dump_path=dump_path)
async def _restore_postgres_json(self, dump_path: Path, clear_existing: bool):
if not dump_path.exists():
if not await asyncio.to_thread(dump_path.exists):
raise FileNotFoundError(f'JSON дамп PostgreSQL не найден: {dump_path}')
async with aiofiles.open(dump_path, encoding='utf-8') as dump_file:
@@ -853,20 +857,20 @@ class BackupService:
logger.info('✅ PostgreSQL восстановлен из ORM JSON', dump_path=dump_path)
async def _restore_sqlite(self, dump_path: Path, clear_existing: bool):
if not dump_path.exists():
if not await asyncio.to_thread(dump_path.exists):
raise FileNotFoundError(f'SQLite файл не найден: {dump_path}')
target_path = Path(settings.SQLITE_PATH)
target_path.parent.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(target_path.parent.mkdir, True, True)
if clear_existing and target_path.exists():
target_path.unlink()
if clear_existing and await asyncio.to_thread(target_path.exists):
await asyncio.to_thread(target_path.unlink)
await asyncio.to_thread(shutil.copy2, dump_path, target_path)
logger.info('✅ SQLite база восстановлена', target_path=target_path)
async def _restore_data_snapshot(self, source_dir: Path, clear_existing: bool):
if not source_dir.exists():
if not await asyncio.to_thread(source_dir.exists):
return
def _restore():
@@ -891,7 +895,7 @@ class BackupService:
logger.info('📁 Снимок директории data восстановлен')
async def _restore_files(self, files_info: list[dict[str, Any]], temp_path: Path):
allowed_base = self.data_dir.resolve()
allowed_base = await asyncio.to_thread(self.data_dir.resolve)
for file_info in files_info:
relative_path = file_info.get('relative_path')
@@ -899,21 +903,22 @@ class BackupService:
if not relative_path or not target_path:
continue
target_resolved = target_path.resolve()
target_resolved = await asyncio.to_thread(target_path.resolve)
if not str(target_resolved).startswith(str(allowed_base) + os.sep) and target_resolved != allowed_base:
logger.warning('Заблокирована запись за пределами data_dir', target_path=target_path)
continue
source_file = (temp_path / relative_path).resolve()
if not str(source_file).startswith(str(temp_path.resolve()) + os.sep):
source_file = await asyncio.to_thread((temp_path / relative_path).resolve)
temp_path_resolved = await asyncio.to_thread(temp_path.resolve)
if not str(source_file).startswith(str(temp_path_resolved) + os.sep):
logger.warning('Path traversal в relative_path', relative_path=relative_path)
continue
if not source_file.exists():
if not await asyncio.to_thread(source_file.exists):
logger.warning('Файл отсутствует в архиве', relative_path=relative_path)
continue
target_resolved.parent.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(target_resolved.parent.mkdir, True, True)
await asyncio.to_thread(shutil.copy2, source_file, target_resolved)
logger.info('📁 Файл восстановлен', target_resolved=target_resolved)
@@ -1573,8 +1578,10 @@ class BackupService:
backups = []
try:
for backup_file in sorted(self.backup_dir.glob('backup_*'), reverse=True):
if not backup_file.is_file():
for backup_file in sorted(
await asyncio.to_thread(lambda: list(self.backup_dir.glob('backup_*'))), reverse=True
):
if not await asyncio.to_thread(backup_file.is_file):
continue
try:
@@ -1598,7 +1605,7 @@ class BackupService:
backup_structure = json_lib.load(f)
metadata = backup_structure.get('metadata', {})
file_stats = backup_file.stat()
file_stats = await asyncio.to_thread(backup_file.stat)
backup_info = {
'filename': backup_file.name,
@@ -1626,7 +1633,7 @@ class BackupService:
except Exception as e:
logger.error('Ошибка чтения метаданных', backup_file=backup_file, error=e)
file_stats = backup_file.stat()
file_stats = await asyncio.to_thread(backup_file.stat)
backups.append(
{
'filename': backup_file.name,
@@ -1651,14 +1658,15 @@ class BackupService:
async def delete_backup(self, backup_filename: str) -> tuple[bool, str]:
try:
backup_path = (self.backup_dir / backup_filename).resolve()
if not str(backup_path).startswith(str(self.backup_dir.resolve()) + os.sep):
backup_path = await asyncio.to_thread((self.backup_dir / backup_filename).resolve)
backup_dir_resolved = await asyncio.to_thread(self.backup_dir.resolve)
if not str(backup_path).startswith(str(backup_dir_resolved) + os.sep):
return False, '❌ Недопустимое имя файла бекапа'
if not backup_path.is_file():
if not await asyncio.to_thread(backup_path.is_file):
return False, f'❌ Файл бекапа не найден: {backup_filename}'
backup_path.unlink()
await asyncio.to_thread(backup_path.unlink)
message = f'✅ Бекап {backup_filename} удален'
logger.info(message)
@@ -1826,9 +1834,9 @@ class BackupService:
await self.bot.send_document(**send_kwargs)
logger.info('Бекап отправлен в чат', chat_id=chat_id)
if temp_zip_path and Path(temp_zip_path).exists():
if temp_zip_path and await asyncio.to_thread(Path(temp_zip_path).exists):
try:
Path(temp_zip_path).unlink()
await asyncio.to_thread(Path(temp_zip_path).unlink)
except Exception as cleanup_error:
logger.warning('Не удалось удалить временный архив', cleanup_error=cleanup_error)
@@ -1838,7 +1846,7 @@ class BackupService:
async def _create_password_protected_archive(self, file_path: str, password: str) -> str | None:
try:
source_path = Path(file_path)
if not source_path.exists():
if not await asyncio.to_thread(source_path.exists):
logger.error('Исходный файл бекапа не найден', file_path=file_path)
return None
+4 -4
View File
@@ -1,9 +1,9 @@
"""Enum classes for contest system."""
from enum import Enum
from enum import StrEnum
class GameType(str, Enum):
class GameType(StrEnum):
"""Types of daily contest games."""
QUEST_BUTTONS = 'quest_buttons'
@@ -30,14 +30,14 @@ class GameType(str, Enum):
}
class RoundStatus(str, Enum):
class RoundStatus(StrEnum):
"""Contest round status."""
ACTIVE = 'active'
FINISHED = 'finished'
class PrizeType(str, Enum):
class PrizeType(StrEnum):
"""Types of prizes for contests."""
DAYS = 'days'
+9 -9
View File
@@ -78,8 +78,8 @@ class LogRotationService:
async def initialize(self) -> None:
"""Создать необходимые директории."""
self.current_dir.mkdir(parents=True, exist_ok=True)
self.archive_dir.mkdir(parents=True, exist_ok=True)
await asyncio.to_thread(self.current_dir.mkdir, True, True)
await asyncio.to_thread(self.archive_dir.mkdir, True, True)
async def start(self) -> None:
"""Запустить сервис ротации."""
@@ -170,7 +170,7 @@ class LogRotationService:
# Собираем файлы для архивации
files_to_archive: list[tuple[Path, str]] = []
for name, log_path in self.log_files.items():
if log_path.exists() and log_path.stat().st_size > 0:
if await asyncio.to_thread(log_path.exists) and (await asyncio.to_thread(log_path.stat)).st_size > 0:
files_to_archive.append((log_path, f'{name}.log'))
if not files_to_archive:
@@ -184,7 +184,7 @@ class LogRotationService:
if archive_path:
# Очищаем текущие лог-файлы
for log_path, _ in files_to_archive:
log_path.write_text('')
await asyncio.to_thread(log_path.write_text, '')
# Очистка старых архивов
await self._cleanup_old_archives()
@@ -247,12 +247,12 @@ class LogRotationService:
keep_days = settings.LOG_ROTATION_KEEP_DAYS
cutoff_date = datetime.now(get_local_timezone()) - timedelta(days=keep_days)
if not self.archive_dir.exists():
if not await asyncio.to_thread(self.archive_dir.exists):
return
# Ищем файлы вида logs_YYYY-MM-DD.tar.gz или logs_YYYY-MM-DD.tar
for archive_file in self.archive_dir.iterdir():
if not archive_file.is_file():
for archive_file in await asyncio.to_thread(lambda: list(self.archive_dir.iterdir())):
if not await asyncio.to_thread(archive_file.is_file):
continue
# Извлекаем дату из имени файла logs_YYYY-MM-DD.tar.gz
@@ -267,7 +267,7 @@ class LogRotationService:
file_date = file_date.replace(tzinfo=get_local_timezone())
if file_date < cutoff_date:
archive_file.unlink()
await asyncio.to_thread(archive_file.unlink)
logger.info('Удален старый архив логов', archive_file_name=archive_file.name)
except ValueError:
# Пропускаем файлы с некорректным форматом имени
@@ -287,7 +287,7 @@ class LogRotationService:
topic_id = settings.get_log_rotation_topic_id()
try:
file_size_kb = archive_path.stat().st_size / 1024
file_size_kb = (await asyncio.to_thread(archive_path.stat)).st_size / 1024
caption = (
f'<b>Логи бота</b>\n'
f'Дата: {date_str}\n'
+5 -1
View File
@@ -111,7 +111,11 @@ class MonitoringService:
logger.debug('Пропуск уведомления: пользователь недоступен', user_id=user.id, status=user.status)
return None
if settings.ENABLE_LOGO_MODE and LOGO_PATH.exists() and not caption_exceeds_telegram_limit(text):
if (
settings.ENABLE_LOGO_MODE
and await asyncio.to_thread(LOGO_PATH.exists)
and not caption_exceeds_telegram_limit(text)
):
try:
from app.utils.message_patch import _cache_logo_file_id, get_logo_media
+1 -1
View File
@@ -767,7 +767,7 @@ class YooKassaPaymentMixin:
)
# Используем full_user для форматирования реферальной информации, чтобы избежать проблем с ленивой загрузкой
user_for_referrer = full_user if full_user else user
user_for_referrer = full_user or user
referrer_info = format_referrer_info(user_for_referrer)
topup_status = '🆕 Первое пополнение' if was_first_topup else '🔄 Пополнение'
@@ -154,7 +154,7 @@ def _get_platega_sub_options() -> list[dict] | None:
'name': info.get('title') or info.get('name') or f'Platega {method_code}',
}
)
return options if options else None
return options or None
except Exception:
return None
@@ -527,7 +527,7 @@ class MiniAppSubscriptionPurchaseService:
value = int(package.get('gb') or 0)
price_per_month = int(package.get('price') or 0)
discounted_per_month, discount_value = _apply_percentage_discount(price_per_month, discount_percent)
label = texts.format_traffic(value if value else 0)
label = texts.format_traffic(value or 0)
options.append(
PurchaseTrafficOption(
value=value,
@@ -587,7 +587,7 @@ class MiniAppSubscriptionPurchaseService:
options=options,
min_selectable=1 if options else 0,
max_selectable=len(options),
default_selection=default_selection if default_selection else [opt.uuid for opt in options[:1]],
default_selection=default_selection or [opt.uuid for opt in options[:1]],
hint=None,
)
+1 -1
View File
@@ -1536,7 +1536,7 @@ class SubscriptionService:
logger.warning('Не удалось предзагрузить тариф подписки', subscription_id=sub.id, error=exc)
# Вычисляем стратегию сброса трафика один раз — все подписки одного тарифа
sample_tariff = subscriptions[0].tariff if subscriptions[0].tariff else None
sample_tariff = subscriptions[0].tariff or None
traffic_strategy = get_traffic_reset_strategy(sample_tariff)
# Параллельная синхронизация: один API-клиент, только HTTP-вызовы внутри gather
+4 -4
View File
@@ -429,7 +429,7 @@ async def get_service_rules(
language: str = Query('ru', min_length=2, max_length=10),
fallback: bool = Query(True),
) -> ServiceRulesResponse:
requested_lang = language.split('-')[0].lower()
requested_lang = language.split('-', maxsplit=1)[0].lower()
rules = await get_rules_by_language(db, requested_lang)
if not rules and fallback:
@@ -467,7 +467,7 @@ async def clear_service_rules(
db: AsyncSession = Depends(get_db_session),
language: str = Query('ru', min_length=2, max_length=10),
) -> Response:
lang = language.split('-')[0].lower()
lang = language.split('-', maxsplit=1)[0].lower()
await clear_all_rules(db, lang)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -479,7 +479,7 @@ async def get_service_rules_history(
language: str = Query('ru', min_length=2, max_length=10),
limit: int = Query(10, ge=1, le=100),
) -> ServiceRulesHistoryResponse:
lang = language.split('-')[0].lower()
lang = language.split('-', maxsplit=1)[0].lower()
history = await get_all_rules_versions(db, lang, limit=limit)
items = [_serialize_rules(item) for item in history]
return ServiceRulesHistoryResponse(
@@ -500,7 +500,7 @@ async def restore_service_rules_version(
db: AsyncSession = Depends(get_db_session),
language: str = Query('ru', min_length=2, max_length=10),
) -> ServiceRulesResponse:
lang = language.split('-')[0].lower()
lang = language.split('-', maxsplit=1)[0].lower()
restored = await restore_rules_version(db, rule_id, language=lang)
if not restored:
raise HTTPException(status.HTTP_404_NOT_FOUND, 'Rules version not found')
+4 -4
View File
@@ -3,13 +3,13 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
class ButtonType(str, Enum):
class ButtonType(StrEnum):
"""Тип кнопки меню."""
BUILTIN = 'builtin' # Встроенная кнопка с callback_data
@@ -18,7 +18,7 @@ class ButtonType(str, Enum):
CALLBACK = 'callback' # Кастомная кнопка с любым callback_data
class ButtonVisibility(str, Enum):
class ButtonVisibility(StrEnum):
"""Видимость кнопки."""
ALL = 'all' # Видна всем
@@ -27,7 +27,7 @@ class ButtonVisibility(str, Enum):
SUBSCRIBERS = 'subscribers' # Только подписчикам
class ButtonOpenMode(str, Enum):
class ButtonOpenMode(StrEnum):
"""Режим открытия кнопки."""
CALLBACK = 'callback' # Отправляет callback_data боту (по умолчанию)
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from enum import StrEnum
from typing import Any
from urllib.parse import urlparse
@@ -386,7 +386,7 @@ class MiniAppPaymentMethodsRequest(BaseModel):
init_data: str = Field(..., alias='initData')
class MiniAppPaymentIntegrationType(str, Enum):
class MiniAppPaymentIntegrationType(StrEnum):
IFRAME = 'iframe'
REDIRECT = 'redirect'
+1 -1
View File
@@ -723,7 +723,7 @@ async def main():
timeline.log_section(
'Активные webhook endpoints',
webhook_lines if webhook_lines else ['Нет активных endpoints'],
webhook_lines or ['Нет активных endpoints'],
icon='🎯',
)
@@ -1,7 +1,7 @@
"""add riopay_payments table
Revision ID: 0032
Revises: 0031
Revision ID: 0036
Revises: 0035
Create Date: 2026-03-08
"""
@@ -11,8 +11,8 @@ from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = '0032'
down_revision: Union[str, None] = '0031'
revision: str = '0036'
down_revision: Union[str, None] = '0035'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -42,4 +42,4 @@ def upgrade() -> None:
def downgrade() -> None:
op.drop_table('riopay_payments')
op.drop_table('riopay_payments')
+1
View File
@@ -166,6 +166,7 @@ ignore = [
'TC001',
'TC002',
'TC003',
'A003',
'N805',
'N815',
'RET504',
@@ -246,7 +246,7 @@ class TestPayloadIntegration:
mock_state = AsyncMock()
mock_state.get_data = AsyncMock(return_value=state_storage)
mock_state.set_data = AsyncMock(side_effect=lambda d: state_storage.update(d))
mock_state.set_data = AsyncMock(side_effect=state_storage.update)
mock_message = _create_mock_message('/start ref_flow_test', 111222)
@@ -341,7 +341,7 @@ async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService2(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminService2),
)
class DummyAsyncSession:
@@ -354,7 +354,7 @@ async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch
async def rollback(self): # pragma: no cover - defensive stub
return None
monkeypatch.setattr(cryptobot_module, 'AsyncSessionLocal', lambda: DummyAsyncSession())
monkeypatch.setattr(cryptobot_module, 'AsyncSessionLocal', DummyAsyncSession)
monkeypatch.setattr(payment_service_module.currency_converter, 'usd_to_rub', AsyncMock(return_value=140.0))
monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}', raising=False)
service.build_topup_success_keyboard = AsyncMock(return_value=None)
@@ -490,7 +490,7 @@ async def test_process_heleket_webhook_success(monkeypatch: pytest.MonkeyPatch)
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminService),
)
service.build_topup_success_keyboard = AsyncMock(return_value=None)
@@ -597,7 +597,7 @@ async def test_process_yookassa_webhook_success(monkeypatch: pytest.MonkeyPatch)
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminService),
)
service.build_topup_success_keyboard = AsyncMock(return_value=None)
@@ -694,7 +694,7 @@ async def test_process_yookassa_webhook_uses_remote_status(monkeypatch: pytest.M
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminService),
)
service.build_topup_success_keyboard = AsyncMock(return_value=None)
@@ -895,7 +895,7 @@ async def test_process_yookassa_webhook_restores_missing_payment(
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminService),
)
service.build_topup_success_keyboard = AsyncMock(return_value=None)
@@ -1050,7 +1050,7 @@ async def test_process_pal24_callback_success(monkeypatch: pytest.MonkeyPatch) -
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminServicePal(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminServicePal),
)
user_cart_stub = SimpleNamespace(user_cart_service=SimpleNamespace(has_user_cart=AsyncMock(return_value=True)))
@@ -1215,7 +1215,7 @@ async def test_get_pal24_payment_status_auto_finalize(monkeypatch: pytest.Monkey
monkeypatch.setitem(
sys.modules,
'app.services.admin_notification_service',
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
SimpleNamespace(AdminNotificationService=DummyAdminService),
)
user_cart_stub = SimpleNamespace(user_cart_service=SimpleNamespace(has_user_cart=AsyncMock(return_value=False)))
@@ -141,7 +141,7 @@ async def test_auto_purchase_saved_cart_after_topup_success(monkeypatch):
monkeypatch.setattr(
'app.services.subscription_auto_purchase_service.MiniAppSubscriptionPurchaseService',
lambda: DummyMiniAppService(),
DummyMiniAppService,
)
monkeypatch.setattr(
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
@@ -107,7 +107,7 @@ async def test_initialize_skips_db_value_for_env_override(monkeypatch):
monkeypatch.setattr(
'app.services.system_settings_service.AsyncSessionLocal',
lambda: DummySession(),
DummySession,
)
async def fake_sync():
@@ -52,7 +52,7 @@ def test_init_without_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
async def test_create_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
_prepare_config(monkeypatch)
monkeypatch.setattr(settings, 'YOOKASSA_DEFAULT_RECEIPT_EMAIL', None, raising=False)
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: DummyLoop(), raising=False)
monkeypatch.setattr(asyncio, 'get_running_loop', DummyLoop, raising=False)
captured_config: dict[str, tuple[str, str]] = {}
@@ -106,7 +106,7 @@ async def test_create_payment_without_contacts(monkeypatch: pytest.MonkeyPatch)
_prepare_config(monkeypatch)
monkeypatch.setattr(settings, 'YOOKASSA_DEFAULT_RECEIPT_EMAIL', None, raising=False)
monkeypatch.setattr(Configuration, 'configure', lambda *args, **kwargs: None, raising=False)
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: DummyLoop(), raising=False)
monkeypatch.setattr(asyncio, 'get_running_loop', DummyLoop, raising=False)
monkeypatch.setattr(
YooKassaPayment,
'create',
@@ -142,7 +142,7 @@ async def test_create_payment_returns_none_when_not_configured(monkeypatch: pyte
@pytest.mark.anyio('asyncio')
async def test_create_sbp_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
_prepare_config(monkeypatch)
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: DummyLoop(), raising=False)
monkeypatch.setattr(asyncio, 'get_running_loop', DummyLoop, raising=False)
monkeypatch.setattr(Configuration, 'configure', lambda *args, **kwargs: None, raising=False)
monkeypatch.setattr(settings, 'YOOKASSA_DEFAULT_RECEIPT_EMAIL', 'fallback@example.com', raising=False)
+1 -1
View File
@@ -1032,7 +1032,7 @@ async def test_create_payment_link_stars_normalizes_amount(monkeypatch):
async def fake_resolve_user(db, init_data):
return types.SimpleNamespace(id=7, language='ru'), {}
monkeypatch.setattr(miniapp, 'PaymentService', lambda bot: DummyPaymentService(bot))
monkeypatch.setattr(miniapp, 'PaymentService', DummyPaymentService)
monkeypatch.setattr(miniapp, 'Bot', DummyBot)
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
+1 -1
View File
@@ -103,7 +103,7 @@ def test_perform_sync_rebuilds_service_on_each_run(monkeypatch):
monkeypatch.setattr(
'app.services.remnawave_sync_service.AsyncSessionLocal',
lambda: DummySession(),
DummySession,
)
monkeypatch.setattr(
'app.services.remnawave_sync_service.sync_with_remnawave',
Generated
+1 -1
View File
@@ -1115,7 +1115,7 @@ wheels = [
[[package]]
name = "remnawave-bedolaga-telegram-bot"
version = "3.25.0"
version = "3.28.1"
source = { virtual = "." }
dependencies = [
{ name = "aiogram" },