Ruff format/ Cleanup

This commit is contained in:
Vladless
2026-04-14 07:19:14 +00:00
parent 0e8b0d04f0
commit 39dd2432cc
224 changed files with 1583 additions and 1182 deletions
+1
View File
@@ -5,5 +5,6 @@ __all__ = ("router", "VERSION")
def __getattr__(name: str):
if name == "router":
from api.v2.router import router
return router
raise AttributeError(name)
+3 -7
View File
@@ -34,7 +34,7 @@ def generate_crud_router(
u = await resolve_user_optional(session, int(value))
if u is None:
return None
return getattr(model, "user_id"), u.id
return model.user_id, u.id
field = getattr(model, identifier_field)
return field, cast_identifier_type(field, value)
@@ -77,9 +77,7 @@ def generate_crud_router(
if resolved is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
field, casted = resolved
result = await session.execute(
_apply_user_relationship_loader(model, select(model).where(field == casted))
)
result = await session.execute(_apply_user_relationship_loader(model, select(model).where(field == casted)))
obj = result.scalar_one_or_none()
if not obj:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
@@ -97,9 +95,7 @@ def generate_crud_router(
if resolved is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
field, casted = resolved
result = await session.execute(
_apply_user_relationship_loader(model, select(model).where(field == casted))
)
result = await session.execute(_apply_user_relationship_loader(model, select(model).where(field == casted)))
objs = result.scalars().all()
if not objs:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
+16 -15
View File
@@ -1,27 +1,28 @@
from fastapi import APIRouter
from api.v2.routes import (
root_router,
auth,
users,
keys,
coupons,
servers,
tariffs,
gifts,
referrals,
misc,
partners,
modules,
management,
settings,
payment_links,
identities,
web,
flows,
gifts,
identities,
keys,
management,
misc,
modules,
notifications,
partners,
payment_links,
referrals,
root_router,
servers,
settings,
tariffs,
users,
web,
)
router = APIRouter()
router.include_router(root_router)
+1
View File
@@ -6,6 +6,7 @@
"""
import time
from collections import deque
from threading import Lock
+2 -3
View File
@@ -73,7 +73,6 @@ async def verify_email(
from sqlalchemy import update
from database.models import Identity as IdentityModel
await session.execute(
update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True)
)
await session.execute(update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True))
return {"ok": True}
+5 -1
View File
@@ -3,9 +3,11 @@ import hashlib
import hmac
import secrets
import time
from urllib.parse import urlencode
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
@@ -192,7 +194,9 @@ async def google_callback(
token = await idb.issue_token_for_identity(session, identity)
logger.info(
"[Auth] Login success: identity={}, google_sub={}, ip={}, method=google",
identity.id, google_sub, _client_ip(request),
identity.id,
google_sub,
_client_ip(request),
)
redirect = RedirectResponse(return_to, status_code=302)
set_auth_cookie(redirect, token, request)
+1 -3
View File
@@ -49,9 +49,7 @@ async def link_email_send_code(
if existing and existing.id != identity.id:
our_tg = identity.tg_id
their_tg = existing.tg_id
can_merge = their_tg is None or (
our_tg is not None and int(their_tg) == int(our_tg)
)
can_merge = their_tg is None or (our_tg is not None and int(their_tg) == int(our_tg))
if not can_merge:
raise HTTPException(
status_code=409,
+12 -4
View File
@@ -75,8 +75,9 @@ async def register_by_email(
)
ip = _client_ip(request)
try:
from core.redis_cache import cache_incr_checked
from api.v2.routes.auth._fallback_limiter import check_and_increment
from core.redis_cache import cache_incr_checked
count, redis_ok = await cache_incr_checked(f"register_rate:{ip}", 3600)
if not redis_ok:
count = check_and_increment(f"register_rate:{ip}", 5, 3600)
@@ -144,8 +145,9 @@ async def login(
raise HTTPException(status_code=400, detail="Email обязателен")
ip = _client_ip(request)
try:
from core.redis_cache import cache_get, cache_incr_checked
from api.v2.routes.auth._fallback_limiter import check_and_increment
from core.redis_cache import cache_get, cache_incr_checked
lockout_key = f"login_lockout:{email}"
locked = await cache_get(lockout_key)
if locked:
@@ -164,6 +166,7 @@ async def login(
if not result:
try:
from core.redis_cache import cache_incr, cache_set
fail_key = f"login_fail:{email}"
fails = await cache_incr(fail_key, 900)
if fails >= 10:
@@ -173,6 +176,7 @@ async def login(
raise HTTPException(status_code=401, detail="Неверный email или пароль")
try:
from core.redis_cache import cache_delete
await cache_delete(f"login_fail:{email}")
except Exception:
pass
@@ -193,8 +197,9 @@ async def send_login_code(
"""Отправить код входа на email (SMTP + Redis)."""
ip = _client_ip(request)
try:
from core.redis_cache import cache_incr_checked
from api.v2.routes.auth._fallback_limiter import check_and_increment
from core.redis_cache import cache_incr_checked
count, redis_ok = await cache_incr_checked(f"send_code_rate:{ip}", 3600)
if not redis_ok:
count = check_and_increment(f"send_code_rate:{ip}", 10, 3600)
@@ -292,7 +297,10 @@ async def login_by_code(
from sqlalchemy import update as sa_update
from database.models import Identity as IdentityModel
await session.execute(sa_update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True))
await session.execute(
sa_update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True)
)
await bind_identity_actor(request, session, identity)
token = await idb.issue_token_for_identity(session, identity)
logger.info("[Auth] Login success: identity={}, email={}, method=code", identity.id, email_norm)
+13 -2
View File
@@ -50,7 +50,12 @@ async def login_telegram(
identity = await idb.get_or_create_identity_for_tg(session, body.id)
await bind_identity_actor(request, session, identity)
token = await idb.issue_token_for_identity(session, identity)
logger.info("[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_widget", identity.id, body.id, _client_ip(request))
logger.info(
"[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_widget",
identity.id,
body.id,
_client_ip(request),
)
set_auth_cookie(response, token, request)
set_is_admin_cookie(response, identity, request)
return LoginResponse(identity_id=identity.id)
@@ -65,6 +70,7 @@ async def login_telegram_webapp(
):
"""Вход через Telegram WebApp initData. Валидирует HMAC, находит/создаёт Identity по tg_id."""
from utils.telegram_login import verify_webapp_init_data
result = verify_webapp_init_data(body.init_data, API_TOKEN)
if not result:
raise HTTPException(status_code=401, detail="Неверная подпись initData")
@@ -74,7 +80,12 @@ async def login_telegram_webapp(
identity = await idb.get_or_create_identity_for_tg(session, int(tg_id))
await bind_identity_actor(request, session, identity)
token = await idb.issue_token_for_identity(session, identity)
logger.info("[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_webapp", identity.id, tg_id, _client_ip(request))
logger.info(
"[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_webapp",
identity.id,
tg_id,
_client_ip(request),
)
set_auth_cookie(response, token, request)
set_is_admin_cookie(response, identity, request)
return LoginResponse(identity_id=identity.id)
+5 -1
View File
@@ -3,9 +3,11 @@ import hashlib
import hmac
import secrets
import time
from urllib.parse import urlencode
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.responses import RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
@@ -189,7 +191,9 @@ async def yandex_callback(
token = await idb.issue_token_for_identity(session, identity)
logger.info(
"[Auth] Login success: identity={}, yandex_sub={}, ip={}, method=yandex",
identity.id, yandex_sub, _client_ip(request),
identity.id,
yandex_sub,
_client_ip(request),
)
redirect = RedirectResponse(return_to, status_code=302)
set_auth_cookie(redirect, token, request)
+3 -2
View File
@@ -1,14 +1,15 @@
from fastapi import Depends, HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_request_actor, get_session, verify_identity_token
from api.v2.base_crud import generate_crud_router
from api.v2.schemas import CouponBase, CouponResponse, CouponUpdate
from api.v2.schemas.web_public import CouponApplyRequest, CouponApplyResponse
from api.depends import get_request_actor, get_session, verify_identity_token
from database import identities as idb
from database.models import Coupon
from services.coupons import apply_fixed_coupon
from services.errors import LimitExceededError, NotFoundError, ServiceError, ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
router = generate_crud_router(
model=Coupon,
+2 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, UTC
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
@@ -10,6 +10,7 @@ from api.depends import get_session, verify_identity_admin
from api.v2.schemas.flows import FlowCreate, FlowResponse, FlowUpdate
from database.models import WebFlow
router = APIRouter()
+4 -2
View File
@@ -36,8 +36,10 @@ from database.tariffs import get_tariff_by_id
from database.temporary_data import create_temporary_data
from services.errors import NotFoundError, ValidationError
from services.formatting import get_site_gift_link
from services.gifts import create_gift as service_create_gift
from services.gifts import redeem_gift as service_redeem_gift
from services.gifts import (
create_gift as service_create_gift,
redeem_gift as service_redeem_gift,
)
from services.payments.payment_links import PaymentLinkRequest, create_payment_link
from services.tariffs import calculate_config_price
+1
View File
@@ -10,6 +10,7 @@ from api.v2.schemas.identities import (
)
from database import identities as idb
router = APIRouter(tags=["Identities"])
+2 -1
View File
@@ -1,4 +1,5 @@
from ._common import router, user_router
from . import admin, user # noqa: F401 — import triggers endpoint registration
from ._common import router, user_router
__all__ = ["router", "user_router"]
+3 -12
View File
@@ -92,7 +92,6 @@ from services.tariffs import calculate_config_price
from services.tariffs.tariff_display import GB, get_effective_limits_for_key, get_key_tariff_addons_state
router = generate_crud_router(
model=Key,
schema_response=KeyResponse,
@@ -105,8 +104,6 @@ router = generate_crud_router(
user_router = APIRouter()
def _key_actions_config() -> AccountKeyActionsConfigResponse:
addons_mode = str(TARIFFS_CONFIG.get("KEY_ADDONS_PACK_MODE", "") or "").strip().lower()
if addons_mode not in {"", "traffic", "devices", "all"}:
@@ -223,13 +220,9 @@ async def _resolve_available_location_servers(session: AsyncSession, db_key: Key
bound_servers = [s for s in available_servers if special in (s.get("special_groups") or [])]
if bound_servers:
available_servers = bound_servers
names = sorted(
{
str(s.get("server_name") or "").strip()
for s in available_servers
if str(s.get("server_name") or "").strip()
}
)
names = sorted({
str(s.get("server_name") or "").strip() for s in available_servers if str(s.get("server_name") or "").strip()
})
return names
@@ -275,5 +268,3 @@ def _normalize_expiry_ms(raw_value: int | float | None) -> int:
elif value < 10**10:
value *= 1000
return value
+1 -1
View File
@@ -1,6 +1,7 @@
from ._common import * # noqa: F401,F403
from ._common import router, user_router # noqa: F401
@router.delete("/by_email/{email}", response_model=dict)
async def delete_key_by_email(
email: str = Path(..., description="Email клиента"),
@@ -114,4 +115,3 @@ async def create_key_api(
except Exception as e:
logger.error(f"[API] Ошибка при создании ключа: {e}")
raise HTTPException(status_code=500, detail="Ошибка при создании ключа")
+100 -56
View File
@@ -7,11 +7,11 @@
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
from .._common import (
_key_actions_config,
_normalize_expiry_ms,
_resolve_available_location_servers,
_resolve_billing_user_id,
_resolve_default_web_payment_provider,
_resolve_public_base_url,
_normalize_expiry_ms,
router,
user_router,
)
@@ -35,9 +35,7 @@ async def user_key_addons_preview(
raise HTTPException(status_code=403, detail="Доп. опции отключены в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
@@ -118,26 +116,32 @@ async def user_key_addons_preview(
if not has_device_option and not has_traffic_option:
raise HTTPException(status_code=400, detail="Доп. опции для этой подписки недоступны")
if pack_mode:
include_device_effective = bool(include_device) if include_device is not None else selected_device_limit is not None
include_traffic_effective = bool(include_traffic) if include_traffic is not None else selected_traffic_gb is not None
selected_device = (
selected_device_limit
if selected_device_limit is not None
else None
include_device_effective = (
bool(include_device) if include_device is not None else selected_device_limit is not None
)
selected_traffic = (
selected_traffic_gb
if selected_traffic_gb is not None
else None
include_traffic_effective = (
bool(include_traffic) if include_traffic is not None else selected_traffic_gb is not None
)
selected_device = selected_device_limit if selected_device_limit is not None else None
selected_traffic = selected_traffic_gb if selected_traffic_gb is not None else None
else:
include_device_effective = has_device_option
include_traffic_effective = has_traffic_option
selected_device = selected_device_limit if selected_device_limit is not None else current_device_limit
selected_traffic = selected_traffic_gb if selected_traffic_gb is not None else current_traffic_gb
if has_device_option and include_device_effective and selected_device is not None and int(selected_device) not in device_options:
if (
has_device_option
and include_device_effective
and selected_device is not None
and int(selected_device) not in device_options
):
raise HTTPException(status_code=400, detail="Выбранный пакет устройств недоступен")
if has_traffic_option and include_traffic_effective and selected_traffic is not None and int(selected_traffic) not in traffic_options:
if (
has_traffic_option
and include_traffic_effective
and selected_traffic is not None
and int(selected_traffic) not in traffic_options
):
raise HTTPException(status_code=400, detail="Выбранный пакет трафика недоступен")
current_devices_for_price = int(current_device_limit) if current_device_limit is not None else None
current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
@@ -154,8 +158,12 @@ async def user_key_addons_preview(
tariff=tariff,
has_device_option=bool(has_device_option and include_device_effective),
has_traffic_option=bool(has_traffic_option and include_traffic_effective),
selected_devices=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
selected_devices=int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
selected_traffic_gb=int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
)
)
recalc_enabled = bool(
@@ -177,8 +185,12 @@ async def user_key_addons_preview(
total_price_rub = int(
calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
selected_device_limit=int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
selected_traffic_gb=int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
)
)
extra_price_rub = int(max(0, total_price_rub - base_price_for_current))
@@ -196,8 +208,12 @@ async def user_key_addons_preview(
has_traffic_option=bool(has_traffic_option),
current_device_limit=int(current_device_limit) if current_device_limit is not None else None,
current_traffic_gb=int(current_traffic_gb) if current_traffic_gb is not None else None,
selected_device_limit=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
selected_device_limit=int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
selected_traffic_gb=int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
device_options=[
AccountKeyAddonOptionResponse(
value=int(val),
@@ -212,11 +228,7 @@ async def user_key_addons_preview(
traffic_options=[
AccountKeyAddonOptionResponse(
value=int(val),
label=(
"Безлимит трафика"
if int(val) <= 0
else (f"+{int(val)} ГБ" if pack_mode else f"{int(val)} ГБ")
),
label=("Безлимит трафика" if int(val) <= 0 else (f"+{int(val)} ГБ" if pack_mode else f"{int(val)} ГБ")),
)
for val in traffic_options
],
@@ -243,9 +255,7 @@ async def user_key_apply_addons(
raise HTTPException(status_code=403, detail="Доп. опции отключены в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
@@ -332,24 +342,26 @@ async def user_key_apply_addons(
include_traffic_effective = (
bool(body.include_traffic) if body.include_traffic is not None else body.selected_traffic_gb is not None
)
selected_device = (
body.selected_device_limit
if body.selected_device_limit is not None
else None
)
selected_traffic = (
body.selected_traffic_gb
if body.selected_traffic_gb is not None
else None
)
selected_device = body.selected_device_limit if body.selected_device_limit is not None else None
selected_traffic = body.selected_traffic_gb if body.selected_traffic_gb is not None else None
else:
include_device_effective = has_device_option
include_traffic_effective = has_traffic_option
selected_device = body.selected_device_limit if body.selected_device_limit is not None else current_device_limit
selected_traffic = body.selected_traffic_gb if body.selected_traffic_gb is not None else current_traffic_gb
if has_device_option and include_device_effective and selected_device is not None and int(selected_device) not in device_options:
if (
has_device_option
and include_device_effective
and selected_device is not None
and int(selected_device) not in device_options
):
raise HTTPException(status_code=400, detail="Выбранный пакет устройств недоступен")
if has_traffic_option and include_traffic_effective and selected_traffic is not None and int(selected_traffic) not in traffic_options:
if (
has_traffic_option
and include_traffic_effective
and selected_traffic is not None
and int(selected_traffic) not in traffic_options
):
raise HTTPException(status_code=400, detail="Выбранный пакет трафика недоступен")
current_devices_for_price = int(current_device_limit) if current_device_limit is not None else None
current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
@@ -367,8 +379,12 @@ async def user_key_apply_addons(
tariff=tariff,
has_device_option=bool(has_device_option and include_device_effective),
has_traffic_option=bool(has_traffic_option and include_traffic_effective),
selected_devices=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
selected_devices=int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
selected_traffic_gb=int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
)
)
recalc_enabled = bool(
@@ -390,8 +406,12 @@ async def user_key_apply_addons(
selected_total_price = int(
calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_device) if has_device_option and selected_device is not None else None,
selected_traffic_gb=int(selected_traffic) if has_traffic_option and selected_traffic is not None else None,
selected_device_limit=int(selected_device)
if has_device_option and selected_device is not None
else None,
selected_traffic_gb=int(selected_traffic)
if has_traffic_option and selected_traffic is not None
else None,
)
)
extra_price_rub = int(max(0, selected_total_price - base_price_for_current))
@@ -458,8 +478,12 @@ async def user_key_apply_addons(
"payment_flow": "key_addons",
"tariff_id": int(tariff_id),
"email": email,
"selected_device_limit": int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
"selected_traffic_gb": int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
"selected_device_limit": int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
"selected_traffic_gb": int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
"current_device_limit": int(current_device_limit) if current_device_limit is not None else None,
"current_traffic_gb": int(current_traffic_gb) if current_traffic_gb is not None else None,
"original_price": int(base_price_for_current),
@@ -480,8 +504,12 @@ async def user_key_apply_addons(
"tariff_id": int(tariff_id),
"email": email,
"required_amount": int(required_amount),
"selected_device_limit": int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
"selected_traffic_gb": int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
"selected_device_limit": int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
"selected_traffic_gb": int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
"current_device_limit": int(current_device_limit) if current_device_limit is not None else None,
"current_traffic_gb": int(current_traffic_gb) if current_traffic_gb is not None else None,
"original_price": int(base_price_for_current),
@@ -561,22 +589,34 @@ async def user_key_apply_addons(
session=session,
email=email,
selected_devices=int(new_device_limit_effective) if new_device_limit_effective is not None else None,
selected_traffic_gb=int(new_traffic_limit_gb_effective) if new_traffic_limit_gb_effective is not None else None,
selected_traffic_gb=int(new_traffic_limit_gb_effective)
if new_traffic_limit_gb_effective is not None
else None,
total_price=int(total_price_after_purchase),
has_device_choice=bool(has_device_option and include_device_effective),
has_traffic_choice=bool(has_traffic_option and include_traffic_effective),
config_mode="pack",
)
else:
selected_device_for_effective = int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None
selected_traffic_for_effective = int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else 0
selected_device_for_effective = (
int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None
)
selected_traffic_for_effective = (
int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else 0
)
device_limit_effective_new, traffic_limit_bytes_effective_new = await get_effective_limits_for_key(
session=session,
tariff_id=int(tariff_id),
selected_device_limit=selected_device_for_effective,
selected_traffic_gb=selected_traffic_for_effective,
)
traffic_limit_gb_effective = int(traffic_limit_bytes_effective_new / GB) if traffic_limit_bytes_effective_new else 0
traffic_limit_gb_effective = (
int(traffic_limit_bytes_effective_new / GB) if traffic_limit_bytes_effective_new else 0
)
await renew_key_in_cluster(
cluster_id=server_id,
email=email,
@@ -593,8 +633,12 @@ async def user_key_apply_addons(
await save_key_config_with_mode(
session=session,
email=email,
selected_devices=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
selected_devices=int(selected_device)
if has_device_option and include_device_effective and selected_device is not None
else None,
selected_traffic_gb=int(selected_traffic)
if has_traffic_option and include_traffic_effective and selected_traffic is not None
else None,
total_price=int(total_price_after_purchase),
has_device_choice=bool(has_device_option and include_device_effective),
has_traffic_choice=bool(has_traffic_option and include_traffic_effective),
+5 -13
View File
@@ -7,11 +7,11 @@
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
from .._common import (
_key_actions_config,
_normalize_expiry_ms,
_resolve_available_location_servers,
_resolve_billing_user_id,
_resolve_default_web_payment_provider,
_resolve_public_base_url,
_normalize_expiry_ms,
router,
user_router,
)
@@ -69,9 +69,7 @@ async def user_key_details(
):
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
@@ -153,9 +151,7 @@ async def user_key_qr(
raise HTTPException(status_code=403, detail="QR для подписок отключен в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
@@ -194,9 +190,7 @@ async def user_key_update_alias(
raise HTTPException(status_code=400, detail="Alias содержит недопустимые символы")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
@@ -229,9 +223,7 @@ async def user_key_delete(
raise HTTPException(status_code=403, detail="Удаление подписки отключено в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
+2 -4
View File
@@ -7,11 +7,11 @@
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
from .._common import (
_key_actions_config,
_normalize_expiry_ms,
_resolve_available_location_servers,
_resolve_billing_user_id,
_resolve_default_web_payment_provider,
_resolve_public_base_url,
_normalize_expiry_ms,
router,
user_router,
)
@@ -30,9 +30,7 @@ async def user_key_reset_hwid(
raise HTTPException(status_code=403, detail="Сброс устройств отключен в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
+3 -7
View File
@@ -7,11 +7,11 @@
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
from .._common import (
_key_actions_config,
_normalize_expiry_ms,
_resolve_available_location_servers,
_resolve_billing_user_id,
_resolve_default_web_payment_provider,
_resolve_public_base_url,
_normalize_expiry_ms,
router,
user_router,
)
@@ -30,9 +30,7 @@ async def user_key_locations(
raise HTTPException(status_code=403, detail="Смена локации отключена в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
@@ -61,9 +59,7 @@ async def user_key_change_location(
raise HTTPException(status_code=400, detail="Укажите целевую локацию")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
+2 -4
View File
@@ -7,11 +7,11 @@
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
from .._common import (
_key_actions_config,
_normalize_expiry_ms,
_resolve_available_location_servers,
_resolve_billing_user_id,
_resolve_default_web_payment_provider,
_resolve_public_base_url,
_normalize_expiry_ms,
router,
user_router,
)
@@ -39,9 +39,7 @@ async def user_key_renew(
raise HTTPException(status_code=403, detail="Продление подписки отключено в настройках")
billing_user_id = await _resolve_billing_user_id(request, identity, session)
db_key = (
await session.execute(
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
)
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
).scalar_one_or_none()
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
+2 -1
View File
@@ -3,6 +3,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_session, verify_identity_admin
from api.v2.base_crud import generate_crud_router
from api.v2.schemas import (
BlockedUserResponse,
ManualBanResponse,
@@ -11,7 +12,6 @@ from api.v2.schemas import (
TemporaryDataResponse,
TrackingSourceResponse,
)
from api.v2.base_crud import generate_crud_router
from database import get_tracking_source_stats
from database.access.resolution import resolve_user_optional
from database.models import (
@@ -23,6 +23,7 @@ from database.models import (
TrackingSource,
)
router = APIRouter()
router.include_router(
+5 -2
View File
@@ -3,8 +3,9 @@ from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_session, verify_identity_token
from database.models import Identity
from database import web_notifications as wn_db
from database.models import Identity
router = APIRouter()
@@ -55,7 +56,9 @@ async def get_notifications(
identity: Identity = Depends(verify_identity_token),
):
notifications = await wn_db.get_notifications_for_identity(
session, identity.id, limit=limit,
session,
identity.id,
limit=limit,
)
unread_count = await wn_db.count_unread_for_identity(session, identity.id)
+15 -14
View File
@@ -1,13 +1,15 @@
import csv
import re
from base64 import b64encode
from datetime import datetime
from io import BytesIO, StringIO
import re
from urllib.parse import urlsplit
import qrcode
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
import qrcode
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,17 +18,18 @@ from api.v2.schemas.web_public import (
PartnerApplyRequest,
PartnerApplyResponse,
PartnerConditionsResponse,
PartnerQrResponse,
PartnerTopEntryResponse,
PartnerTopResponse,
PartnerPayoutEntryResponse,
PartnerPayoutHistoryResponse,
PartnerPayoutRequestCreate,
PartnerPayoutRequestResponse,
PartnerQrResponse,
PartnerTopEntryResponse,
PartnerTopResponse,
)
from database import identities as idb
from utils.referral_codes import decode_partner_code, encode_partner_code
try:
from modules.partner_program.settings import PARTNER_BONUS_PERCENTAGES
except Exception:
@@ -63,10 +66,10 @@ def _row_dt_iso(value) -> str | None:
def _resolve_public_base_url(request: Request) -> str:
origin = str(request.headers.get("origin") or "").strip()
if origin.startswith("http://") or origin.startswith("https://"):
if origin.startswith(("http://", "https://")):
return origin.rstrip("/")
referer = str(request.headers.get("referer") or request.headers.get("referrer") or "").strip()
if referer.startswith("http://") or referer.startswith("https://"):
if referer.startswith(("http://", "https://")):
parsed = urlsplit(referer)
if parsed.scheme and parsed.netloc:
return f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
@@ -351,9 +354,9 @@ async def partner_conditions(
("ENABLE_PAYOUT_USDT", "USDT"),
("ENABLE_PAYOUT_TON", "TON"),
]
payout_methods = [
title for key, title in method_map if bool(getattr(partner_settings, key, False))
] if partner_settings else []
payout_methods = (
[title for key, title in method_map if bool(getattr(partner_settings, key, False))] if partner_settings else []
)
level_lines: list[str] = []
all_levels = sorted({int(k) for k in [*percent_levels_raw.keys(), *flat_levels_raw.keys()] if str(k).isdigit()})
for level in all_levels:
@@ -422,9 +425,7 @@ async def partner_payouts_me(
"""
)
total = int((await session.scalar(count_sql, {"tg_id": tg_id})) or 0)
rows = (
await session.execute(rows_sql, {"tg_id": tg_id, "limit": int(limit), "offset": int(offset)})
).fetchall()
rows = (await session.execute(rows_sql, {"tg_id": tg_id, "limit": int(limit), "offset": int(offset)})).fetchall()
items = [
PartnerPayoutEntryResponse(
id=int(row[0]),
@@ -1094,13 +1095,13 @@ async def reset_disabled_payout_methods(
):
"""Сбрасывает реквизиты для отключённых способов вывода."""
try:
from modules.partner_program import buttons as B
from modules.partner_program.settings import (
ENABLE_PAYOUT_CARD,
ENABLE_PAYOUT_SBP,
ENABLE_PAYOUT_TON,
ENABLE_PAYOUT_USDT,
)
from modules.partner_program import buttons as B
except Exception:
ENABLE_PAYOUT_CARD = True
ENABLE_PAYOUT_USDT = True
+21 -7
View File
@@ -3,6 +3,7 @@ from io import BytesIO
from urllib.parse import urlsplit
import qrcode
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,14 +16,25 @@ from api.v2.schemas.web_public import (
ReferralTopEntryResponse,
ReferralTopResponse,
)
from config import CHECK_REFERRAL_REWARD_ISSUED, REFERRAL_BONUS_PERCENTAGES, REFERRAL_BUTTON, REFERRAL_QR, TOP_REFERRAL_BUTTON
from config import (
CHECK_REFERRAL_REWARD_ISSUED,
REFERRAL_BONUS_PERCENTAGES,
REFERRAL_BUTTON,
REFERRAL_QR,
TOP_REFERRAL_BUTTON,
)
from core.bootstrap import BUTTONS_CONFIG
from database import add_referral, get_referral_by_referred_id, get_user_referral_count
from database.referrals import get_referral_position, get_top_referrals
from database import identities as idb
from database import (
add_referral,
get_referral_by_referred_id,
get_user_referral_count,
identities as idb,
)
from database.access.resolution import resolve_user_optional
from database.referrals import get_referral_position, get_top_referrals
from utils.referral_codes import decode_referral_code, encode_referral_code
router = APIRouter()
@@ -44,10 +56,10 @@ def _normalize_referrer_code(value: str | None, fallback_tg_id: int | None) -> i
def _resolve_public_base_url(request: Request) -> str:
origin = str(request.headers.get("origin") or "").strip()
if origin.startswith("http://") or origin.startswith("https://"):
if origin.startswith(("http://", "https://")):
return origin.rstrip("/")
referer = str(request.headers.get("referer") or request.headers.get("referrer") or "").strip()
if referer.startswith("http://") or referer.startswith("https://"):
if referer.startswith(("http://", "https://")):
parsed = urlsplit(referer)
if parsed.scheme and parsed.netloc:
return f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
@@ -170,7 +182,9 @@ async def referral_conditions(
level_lines.append(f"{level} уровень: {label}")
one_time_mode = bool(CHECK_REFERRAL_REWARD_ISSUED)
bonus_mode = "one_time" if one_time_mode else "each_payment"
bonus_mode_label = "Бонус за первую успешную оплату реферала" if one_time_mode else "Бонус за каждую успешную оплату реферала"
bonus_mode_label = (
"Бонус за первую успешную оплату реферала" if one_time_mode else "Бонус за каждую успешную оплату реферала"
)
rules = [
"Бонус начисляется только за реальных приглашённых пользователей.",
"Нельзя использовать собственную реферальную ссылку.",
+11 -13
View File
@@ -4,6 +4,7 @@ import re
import time
import aiohttp
from fastapi import APIRouter
from config import (
@@ -21,18 +22,19 @@ from config import (
REFERRAL_QR,
REMNAWAVE_WEBAPP,
REMNAWAVE_WEBAPP_OPEN_IN_BROWSER,
TOP_REFERRAL_BUTTON,
TRIAL_TIME_DISABLE,
USE_COUNTRY_SELECTION,
USERNAME_BOT,
TELEGRAM_WEBAPP_DIRECT_LINK,
TELEGRAM_WEBAPP_SHORT_NAME,
TOP_REFERRAL_BUTTON,
TRIAL_TIME_DISABLE,
USERNAME_BOT,
USE_COUNTRY_SELECTION,
)
from core.bootstrap import BUTTONS_CONFIG, MODES_CONFIG, MONEY_CONFIG, PAYMENTS_CONFIG
from core.settings.web_config import WEB_CONFIG
from core.settings.money_config import get_currency_mode
from core.settings.web_config import WEB_CONFIG
from services.payments.providers import PROVIDERS_BASE, TELEGRAM_ONLY_PROVIDER_IDS, WEB_LINK_PROVIDER_IDS
router = APIRouter(tags=["Root"])
@@ -113,9 +115,7 @@ async def site_config():
"email_code_login_enabled": bool(MODES_CONFIG.get("WEB_EMAIL_CODE_LOGIN_ENABLED", True)),
},
"mobile": {
"prefer_mini_app_on_telegram_mobile": bool(
MODES_CONFIG.get("PREFER_MINI_APP_ON_TELEGRAM_MOBILE", False)
),
"prefer_mini_app_on_telegram_mobile": bool(MODES_CONFIG.get("PREFER_MINI_APP_ON_TELEGRAM_MOBILE", False)),
},
"features": {
"channel_enabled": bool(BUTTONS_CONFIG.get("CHANNEL_BUTTON_ENABLE", CHANNEL_EXISTS)),
@@ -129,9 +129,7 @@ async def site_config():
"coupon_enabled": bool(BUTTONS_CONFIG.get("COUPON_BUTTON_ENABLE", True)),
"qr_subscription_enabled": bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK)),
"hwid_reset_enabled": bool(BUTTONS_CONFIG.get("HWID_RESET_BUTTON_ENABLE", HWID_RESET_BUTTON)),
"country_selection_enabled": bool(
MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION)
),
"country_selection_enabled": bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION)),
"captcha_enabled": bool(MODES_CONFIG.get("CAPTCHA_ENABLED", CAPTCHA_ENABLE)),
"channel_check_enabled": bool(MODES_CONFIG.get("CHANNEL_CHECK_ENABLED", CHANNEL_REQUIRED)),
"trial_enabled": not bool(MODES_CONFIG.get("TRIAL_TIME_DISABLED", TRIAL_TIME_DISABLE)),
@@ -201,7 +199,7 @@ def _parse_semver(tag: str) -> tuple[int, int, int, int, tuple[tuple[int, int |
async def _fetch_ghcr_latest_tag(image: str) -> str | None:
"""Анонимно тянем список тегов публичного GHCR-пакета и возвращаем максимальный семвер."""
"""Возвращает максимальный semver-тег образа в GHCR."""
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
token_url = f"https://ghcr.io/token?scope=repository:{image}:pull"
async with session.get(token_url) as token_resp:
@@ -231,7 +229,7 @@ async def _fetch_ghcr_latest_tag(image: str) -> str | None:
@router.get("/api/meta/update-check", include_in_schema=True)
async def update_check():
"""Сравнивает текущую версию Solo-brick с последним тегом публичного GHCR-образа."""
"""Сравнивает текущую версию Solo-brick с последним доступным релизом."""
current = (os.environ.get("APP_VERSION") or "").strip()
image = (os.environ.get("GHCR_IMAGE") or "").strip()
now = time.time()
+2 -1
View File
@@ -1,9 +1,10 @@
from fastapi import APIRouter
from api.v2.schemas import ServerBase, ServerResponse, ServerUpdate
from api.v2.base_crud import generate_crud_router
from api.v2.schemas import ServerBase, ServerResponse, ServerUpdate
from database.models import Server
router = generate_crud_router(
model=Server,
schema_response=ServerResponse,
+2 -1
View File
@@ -7,7 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_session, verify_identity_admin
from api.v2.schemas import SettingResponse, SettingUpsert
from database.settings_cache import settings_cache
from core.settings.buttons_config import BUTTONS_CONFIG, update_buttons_config
from core.settings.modes_config import MODES_CONFIG, update_modes_config
from core.settings.money_config import MONEY_CONFIG, update_money_config
@@ -17,6 +16,8 @@ from core.settings.providers_order_config import PROVIDERS_ORDER, update_provide
from core.settings.tariffs_config import TARIFFS_CONFIG, update_tariffs_config
from database.models import Setting
from database.settings import set_setting
from database.settings_cache import settings_cache
router = APIRouter()
+6 -2
View File
@@ -27,8 +27,8 @@ from database.coupons import mark_coupon_used
from database.models import Tariff
from database.tariffs import get_tariff_by_id
from database.temporary_data import create_temporary_data
from services.keys import create_vpn_key_headless
from logger import logger
from services.keys import create_vpn_key_headless
from services.payments.payment_links import PaymentLinkRequest, create_payment_link
from services.payments.providers import WEB_LINK_PROVIDER_IDS
from services.tariffs import calculate_config_price
@@ -143,7 +143,11 @@ async def get_tariffs_public(
if isinstance(cached, list):
return cached
q = select(Tariff).where(Tariff.is_active.is_(True)).order_by(Tariff.sort_order.asc().nulls_last(), Tariff.price_rub.asc())
q = (
select(Tariff)
.where(Tariff.is_active.is_(True))
.order_by(Tariff.sort_order.asc().nulls_last(), Tariff.price_rub.asc())
)
if tariff_ids:
try:
ids = [int(x.strip()) for x in tariff_ids.split(",") if x.strip()]
+4 -3
View File
@@ -5,13 +5,14 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_session, verify_identity_admin
from api.v2.schemas import UserBase, UserResponse, UserUpdate
from api.v2.base_crud import generate_crud_router
from api.v2.schemas import UserBase, UserResponse, UserUpdate
from database import async_session_maker, delete_user_data, get_servers
from database.models import Key, User
from database.access.resolution import resolve_user_optional
from services.operations import delete_key_from_cluster
from database.models import Key, User
from logger import logger
from services.operations import delete_key_from_cluster
router = generate_crud_router(
model=User,
+10 -13
View File
@@ -2,12 +2,11 @@ import hashlib
import re
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
from pydantic import BaseModel
from datetime import datetime, timedelta, timezone
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -54,6 +53,7 @@ EXTENSION_CONTENT_TYPES: dict[str, frozenset[str]] = {
def _sanitize_svg(data: bytes) -> bytes:
import re as _re
text = data.decode("utf-8", errors="replace")
text = _re.sub(r"<script[^>]*>.*?</script>", "", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"<style[^>]*>.*?</style>", "", text, flags=_re.DOTALL | _re.IGNORECASE)
@@ -535,9 +535,7 @@ async def list_custom_element_builds(
session: AsyncSession = Depends(get_session),
_identity=Depends(verify_identity_admin),
):
result = await session.execute(
select(WebCustomElementBuild).order_by(WebCustomElementBuild.created_at.desc())
)
result = await session.execute(select(WebCustomElementBuild).order_by(WebCustomElementBuild.created_at.desc()))
builds = result.scalars().all()
return [_build_to_dict(b) for b in builds]
@@ -630,8 +628,9 @@ async def ingest_flow_events(
session: AsyncSession = Depends(get_session),
):
try:
from core.redis_cache import cache_incr_checked
from api.v2.routes.auth._fallback_limiter import check_and_increment
from core.redis_cache import cache_incr_checked
ip = (request.client.host if request.client else "") or "unknown"
count, redis_ok = await cache_incr_checked(f"analytics_rate:{ip}", 60)
if not redis_ok:
@@ -709,9 +708,7 @@ async def get_flow_funnel(
for i, node in enumerate(funnel):
prev_entered = funnel[i - 1]["entered"] if i > 0 else node["entered"]
node["dropOff"] = round(
(1 - node["entered"] / prev_entered) * 100, 1
) if prev_entered > 0 else 0
node["dropOff"] = round((1 - node["entered"] / prev_entered) * 100, 1) if prev_entered > 0 else 0
return {"flowId": flow_id, "days": days, "funnel": funnel}
@@ -732,6 +729,7 @@ def _error_signature(name: str, message: str, stack: str | None, url: str | None
if url:
try:
from urllib.parse import urlparse
pathname = urlparse(url).path[:100]
except Exception:
pass
@@ -757,8 +755,9 @@ async def ingest_error_report(
session: AsyncSession = Depends(get_session),
):
try:
from core.redis_cache import cache_incr_checked
from api.v2.routes.auth._fallback_limiter import check_and_increment
from core.redis_cache import cache_incr_checked
ip = (request.client.host if request.client else "") or "unknown"
count, redis_ok = await cache_incr_checked(f"error_report_rate:{ip}", 60)
if not redis_ok:
@@ -773,9 +772,7 @@ async def ingest_error_report(
signature = _error_signature(body.name, body.message, body.stack, body.url)
existing = (
await session.execute(
select(WebErrorReport).where(WebErrorReport.signature == signature)
)
await session.execute(select(WebErrorReport).where(WebErrorReport.signature == signature))
).scalar_one_or_none()
if existing:
+7 -7
View File
@@ -1,4 +1,5 @@
from api.v1.schemas import (
BlockedUserResponse,
CouponBase,
CouponResponse,
CouponUpdate,
@@ -9,6 +10,9 @@ from api.v1.schemas import (
GiftUsageResponse,
KeyDetailsResponse,
KeyResponse,
ManualBanResponse,
NotificationResponse,
PaymentResponse,
ReferralResponse,
ServerBase,
ServerResponse,
@@ -16,25 +20,21 @@ from api.v1.schemas import (
TariffBase,
TariffResponse,
TariffUpdate,
TemporaryDataResponse,
TrackingSourceResponse,
UserBase,
UserResponse,
UserUpdate,
BlockedUserResponse,
ManualBanResponse,
NotificationResponse,
PaymentResponse,
TemporaryDataResponse,
TrackingSourceResponse,
)
from api.v1.schemas.keys import KeyBase, KeyCreateRequest, KeyUpdate
from api.v1.schemas.settings import SettingResponse, SettingUpsert
from api.v2.schemas.web import (
WebBlockResponse,
WebTheme,
WebPageResponse,
WebPageUpdate,
WebPageVariantCreate,
WebPageVariantSummary,
WebPageVariantUpdate,
WebPageVariantsResponse,
WebTheme,
)
+2
View File
@@ -3,11 +3,13 @@ from pydantic import BaseModel
class TariffGroup(BaseModel):
"""Группа тарифов (group_code) для выбора в лендинге и др."""
group_code: str
class TariffPublic(BaseModel):
"""Публичный список тарифов (без авторизации)."""
id: int
name: str
group_code: str
+2
View File
@@ -1,8 +1,10 @@
import json
from typing import Any
from pydantic import BaseModel, Field, model_validator
_MAX_BLOCK_DATA_SIZE = 256 * 1024