@@ -1,6 +1,5 @@ import logging -from asgiref.sync import sync_to_async from ninja import Router from ninja.errors import HttpError @@ -16,16 +15,16 @@ router = Router(tags=['auth']) @router.post('confirm', auth=None, tags=['auth/confirm'], response=TokensOutSchema) -async def confirm_user(request, payload: ConfirmInSchema): +def confirm_user(request, payload: ConfirmInSchema): try: - token = await EmailToken.objects.select_related('user').aget(key=payload.token) + token = EmailToken.objects.select_related('user').get(key=payload.token) user = token.user - await token.adelete() + token.delete() if user.is_confirmed: raise HttpError(400, _('User is already confirmed')) user.is_confirmed = True - await user.asave() - refresh = await sync_to_async(RefreshToken.for_user)(user) + user.save() + refresh = RefreshToken.for_user(user) access = refresh.access_token return TokensOutSchema(access=str(access), refresh=str(refresh)) except EmailToken.DoesNotExist: @@ -35,4 +34,3 @@ async def confirm_user(request, payload: ConfirmInSchema): except Exception as exc: logger.exception(exc) raise HttpError(500, _('Could not confirm email, please try again.')) - @@ -77,7 +77,7 @@ class TokenService: } @classmethod - async def decode(cls, *, token: str) -> dict[str, Any]: + def decode(cls, *, token: str) -> dict[str, Any]: return cls._decode(token=token) @classmethod @@ -63,7 +63,9 @@ class UserService: user.set_password(user_data['password']) else: user = CustomUserModel.objects.create_user( - email=email, password=user_data['password'], is_subscribed_to_emails=user_data.get('is_subscribed_to_emails', False) + email=email, + password=user_data['password'], + is_subscribed_to_emails=user_data.get('is_subscribed_to_emails', False), ) UTMService(user).create_utm( user_data.get('utm_source'), @@ -271,7 +273,7 @@ class UserService: @atomic def delete_user(self, request: Request): logout(request) - + if self.user.account_type in ('business_admin', 'business_security', 'business_account'): self.user.business_account.delete() @@ -290,12 +292,12 @@ class UserService: return UserSetting.objects.filter(filters) @classmethod - async def add_setting(cls, user_id: UUID, device: str, type: str, value: Any) -> UserSetting: - return await UserSetting.objects.acreate(user_id=user_id, device=device, type=type, value=value) + def add_setting(cls, user_id: UUID, device: str, type: str, value: Any) -> UserSetting: + return UserSetting.objects.create(user_id=user_id, device=device, type=type, value=value) @classmethod - async def update_setting(cls, setting_id: UUID, value: Any) -> None: - return await UserSetting.objects.filter(id=setting_id).aupdate(value=value) + def update_setting(cls, setting_id: UUID, value: Any) -> None: + return UserSetting.objects.filter(id=setting_id).update(value=value) # For Social Auth pipeline @@ -2,7 +2,6 @@ import logging from typing import Any import jwt -from asgiref.sync import async_to_sync, sync_to_async from django.conf import settings from django.http import HttpRequest from django.utils.translation import gettext as _ @@ -76,7 +75,7 @@ class SyncAuthBearer(HttpBearer): def authenticate(self, request: HttpRequest, token: str) -> Any | None: mapper = PATH_PREFETCH_MAP.get(request.path, {}) try: - user_payload = async_to_sync(TokenService.decode)(token=token) + user_payload = TokenService.decode(token=token) request.provider = 'air' user = ( CustomUserModel.objects.select_related(*mapper.get('select', [])) @@ -98,31 +97,3 @@ class SyncAuthBearer(HttpBearer): raise HttpError(401, _('Access token expired or does not exist')) from exc _check_ip_client(user, request) return user - - -class AsyncAuthBearer(HttpBearer): - async def authenticate(self, request: HttpRequest, token: str) -> Any | None: - mapper = PATH_PREFETCH_MAP.get(request.path, {}) - try: - user_payload = await TokenService.decode(token=token) - request.provider = 'air' - user = ( - await CustomUserModel.objects.select_related(*mapper.get('select', [])) - .prefetch_related(*mapper.get('prefetch', [])) - .only(*mapper.get('only', [])) - .aget(**{key: user_payload[f'{key}'] for key in settings.JWT_SETTINGS['encode_attributes']}) - ) - except InvalidToken: - try: - user = ( - await CustomUserModel.objects.select_related(*mapper.get('select', [])) - .prefetch_related(*mapper.get('prefetch', [])) - .only(*mapper.get('only', [])) - .filter(oauth2_provider_accesstoken__token=token) - .alatest('oauth2_provider_accesstoken__created') - ) - request.provider = 'yandex' - except Exception as exc: - raise HttpError(401, _('Access token expired or does not exist')) from exc - await sync_to_async(_check_ip_client)(user, request) - return user @@ -378,17 +378,17 @@ ENVIRONMENT = env.str('ENVIRONMENT') LOGGING = { 'version': 1, 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '[{asctime}] [{process}] [{thread}] [{levelname}] [{pathname}:{lineno}] {message}', + 'style': '{', + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', - 'level': 'INFO', - 'stream': 'ext://sys.stdout', - }, - }, - 'formatters': { - 'console_pretty': { - '()': 'lib.logging.formatters.ConsoleFormatter', - 'datefmt': '%Y-%m-%dT%H:%M:%S%z', + 'formatter': 'verbose', }, }, 'loggers': { @@ -402,25 +402,16 @@ LOGGING = { }, '': { 'handlers': ['console'], - 'formatter': 'console_pretty', 'level': 'INFO', 'propagate': False, }, 'django': { 'handlers': ['console'], - 'formatter': 'console_pretty', - 'level': 'INFO', - 'propagate': False, - }, - 'uvicorn': { - 'handlers': ['console'], - 'formatter': 'console_pretty', 'level': 'INFO', 'propagate': False, }, - '__main__': { + 'gunicorn': { 'handlers': ['console'], - 'formatter': 'console_pretty', 'level': 'INFO', 'propagate': False, }, @@ -6,11 +6,11 @@ from decimal import Decimal from django.core.cache import cache from ninja import Router -from authentication.security import AsyncAuthBearer +from authentication.security import SyncAuthBearer from ml_model.schemas import PredictPriceSchema, PredictPriceInputSchema from ml_model.services.base import SimpleService -router = Router(auth=AsyncAuthBearer(), tags=['ml_model']) +router = Router(auth=SyncAuthBearer(), tags=['ml_model']) @router.post('predict-price/', tags=['ml_model/predict-price'], response=PredictPriceSchema) @@ -26,7 +26,9 @@ def calculate_predict_price(request, body: PredictPriceInputSchema): service: type[SimpleService] = getattr( sys.modules['ml_model.services'], f'{body.model_slug.title()}' ) - predicted_price = service.predict_price(content=content, file_exists=body.file_exists, info=body.info) + predicted_price = service.predict_price( + content=content, file_exists=body.file_exists, info=body.info + ) if predicted_price: cache.set(cache_key, predicted_price) @@ -34,6 +36,3 @@ def calculate_predict_price(request, body: PredictPriceInputSchema): predicted_price = predicted_price.quantize(Decimal('0.01')) return PredictPriceSchema(price=predicted_price) - - - @@ -59,6 +59,7 @@ from ml_model.services.pulid import Pulid from ml_model.services.qwen import Qwen from ml_model.services.qwen_235B import Qwen_235B from ml_model.services.qwen_3_6 import Qwen_3_6 +from ml_model.services.qwen_3_7 import Qwen_3_7 from ml_model.services.qwen_3_max_thinking import Qwen_3_Max_Thinking from ml_model.services.raifgpt import Raifgpt from ml_model.services.ray import Ray @@ -0,0 +1,183 @@ +import time +from datetime import timedelta +from decimal import Decimal +from pathlib import Path + +import filetype + +from messages.models import Message +from ml_model.exceptions import CorruptedFileError, FileExtensionNotSupported +from ml_model.services.EmbeddingService import EmbeddingService +from ml_model.services.FileService import FileProcessingService +from ml_model.exceptions import ModelVersionNotAvailable +from ml_model.services.base import SimpleService +from ml_model.tasks import openrouter_run +from poller.models import Proxy +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + + +class Qwen_3_7(SimpleService): + COEFFICIENT = Decimal('300.0') + + TOKENS_COST = { + 'qwen3.7-max': {'input': Decimal('750'), 'output': Decimal('2250')}, + 'qwen3.7-plus': {'input': Decimal('120'), 'output': Decimal('480')}, + } + + MAX_OUTPUT_TOKENS = 30_000 + + TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} + + SYSTEM = """ + You are a reliable and practical AI assistant. + + Provide accurate, direct, and useful answers. + Focus on the user's actual goal and solve the task with minimal unnecessary explanation. + + Rules: + - Follow the user's instructions exactly. + - Start with the answer, not with introductions or disclaimers. + - Be concise when the question is simple. + - Be detailed when the task is complex or requires analysis. + - Ask clarifying questions only when necessary. + - Do not make up facts, data, sources, or capabilities. + - If information is uncertain, state the uncertainty clearly. + - Prefer actionable recommendations over theory. + - Structure long answers with headings, lists, and examples. + - Preserve important details and constraints from the conversation. + - When writing code, prioritize correctness, readability, and maintainability. + - Adapt the level of detail, terminology, and tone to the user's apparent expertise. + + Your goal is to maximize usefulness, clarity, and task completion. + """ + + def calculate_price(self, version: str, cost: float, embedding_tokens: int) -> Decimal: + price = Decimal(cost) * self.COEFFICIENT + if embedding_tokens > 0: + price += self.TOOLS_TOKEN_COSTS['text-embedding-3-small']['output'] * embedding_tokens + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results(self, content: str, time: timedelta, save: bool = True) -> list[Message]: + msgs = [ + Message( + content=content, + content_object=self.store, + elapsed_time=time, + ) + ] + if save: + return Message.objects.bulk_create(msgs) + return msgs + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + start_time = time.time() + version_slug = input_message.info.pop('version', None) + if version_slug is None or version_slug not in self.TOKENS_COST: + raise ModelVersionNotAvailable(version_slug, self.TOKENS_COST) + callback_data = {'max_tokens': self.MAX_OUTPUT_TOKENS, 'tools': [], **input_message.info} + messages = [ + {'role': 'system', 'content': self.SYSTEM}, + *self.get_chat_history(), + {'role': 'user', 'content': input_message.content}, + ] + embedding_tokens = 0 + if input_message.file: + file_service = FileProcessingService + file_bytes = input_message.file.read() + input_message.file.close() + kind = filetype.guess(file_bytes[:550]) + supported_extensions = ['PDF', 'DOC', 'DOCX', 'XLSX'] + if not kind: + if Path(input_message.file.name).suffix[1:].upper() not in supported_extensions: + raise FileExtensionNotSupported(supported_extensions) + raise CorruptedFileError + raw_file_extension = kind.extension + file_extension = file_service.get_file_extension(raw_file_extension, file_bytes) + if file_extension in ('pdf', 'doc', 'docx', 'xlsx'): + text = file_service.get_file_data(file_extension, file_bytes) + chunks = EmbeddingService.split_text_to_chunks(text) + if len(text) > 20_000: + for proxy in Proxy.objects.all(): + document_name = chunks[0].partition(f':{chr(10)}')[2].split(f'{chr(10)}')[0][:100] + embedding_tokens, file_data = EmbeddingService.get_large_file_data( + self.store.messages.first().pk, + chunks, + proxy, + input_message.content, + model='text-embedding-3-small', + index_name='ml_model-index-1536', + ) + messages[-1]['content'] = EmbeddingService.make_embeddings_prompt( + document_name=document_name, + section_texts=file_data, + question=input_message.content, + ) + else: + messages[-1]['content'] = ( + f'Используй системный промпт. Содержание файла: ' + f'{chunks}. Вопрос: {input_message.content}' + ) + else: + raise FileExtensionNotSupported(supported_extensions) + callback_data['tools'].append( + { + 'type': 'openrouter:web_search', + 'parameters': { + 'engine': 'parallel', + 'max_results': 1, + 'max_total_results': 3, + 'search_context_size': 'low', + }, + } + ) + model_slug = f'qwen/{version_slug}' + result = openrouter_run(model_slug, messages, callback_data, 'Qwen 3.7') + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + version=version_slug, + cost=result[1], + embedding_tokens=embedding_tokens, + ) + msgs = self.save_results(result[0], process_time) + return msgs + + def get_chat_history( + self, message_limit: int = 10, max_character_limit: int = 1500 + ) -> list[dict[str, str | list]]: + if isinstance(self.store, Chat): + air_messages = list( + reversed( + Message.objects.filter( + chats_chats_messages=self.store, is_deleted=False, is_sent=True + ).order_by('-created_at')[1 : message_limit + 1] + ) + ) + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] + ) + ) + else: + air_messages = [] + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({'role': 'assistant', 'content': content}) + else: + memory.append({'role': 'user', 'content': content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + character_length -= len(memory.pop(0)['content']) + + return memory @@ -190,7 +190,7 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name reasoning = re.sub(r'Вывод:|Основная мысль:|Рассуждение:|\*\*', '', reasoning) answer = reasoning if any(m in data['model'] for m in ('google/gemini', 'x-ai/grok-4.3')) or re.match( - r'^qwen/qwen3\.5-.*$', data['model'] + r'^qwen/qwen3\.(?:5|6|7)-.*$', data['model'] ): answer = content elif reasoning and content: @@ -209,8 +209,12 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name logger.error(f'Model {model_name} disabled') raise DeploymentDisabled else: - input_tokens = data['usage']['prompt_tokens'] - output_tokens = data['usage']['completion_tokens'] + if re.match(r'^qwen/qwen3\.7-.*$', data['model']): + input_tokens = data['usage']['cost'] + output_tokens = 0 + else: + input_tokens = data['usage']['prompt_tokens'] + output_tokens = data['usage']['completion_tokens'] return (re.sub(r'\\+["n*]', '', answer), input_tokens, output_tokens) logger.error(f'Error occured via model {model_name}. Data: {resp.content}') raise Exception(f'No answer from {model_name}, please retry later') @@ -17,7 +17,7 @@ from ninja import Query, Router from ninja.errors import HttpError from authentication.models import CustomUserModel -from authentication.security import AsyncAuthBearer, SyncAuthBearer +from authentication.security import SyncAuthBearer, SyncAuthBearer from payments.exceptions.payer_not_found import PayerNotFound from authentication.exceptions.business_host_exceptions.access_denied import AccessDenied from payments.models import ( @@ -58,15 +58,15 @@ def get_user_balance(request): @router.post('payment-result', tags=['payments/payment-result'], auth=None) -async def handle_yookassa_webhook(request): +def handle_yookassa_webhook(request): data = orjson.loads(request.body) logger.info('YooKassa webhook received: payment_id=%s', data['object']['id']) try: - payment = await PaymentService.handle_payment(data['object']['id']) - payer = await CustomUserModel.objects.prefetch_related('payment_plan', 'payment_plan__plan', 'payment_plan__method').aget( - uid=payment.description - ) - await PaymentService(payer).do_payment(payment) + payment = PaymentService.handle_payment(data['object']['id']) + payer = CustomUserModel.objects.prefetch_related( + 'payment_plan', 'payment_plan__plan', 'payment_plan__method' + ).get(uid=payment.description) + PaymentService(payer).do_payment(payment) logger.info( 'YooKassa webhook processed: payment_id=%s payer_email=%s status=%s', payment.id, @@ -158,8 +158,8 @@ def list_expenses(request, data: ExpensesParamsSchema = Query(...)): raise HttpError(400, f'{exc}') -@router.get('plans', tags=['payments/plans'], auth=AsyncAuthBearer(), response=list[PaymentPlanSchema]) -async def list_payment_plans(request): +@router.get('plans', tags=['payments/plans'], auth=SyncAuthBearer(), response=list[PaymentPlanSchema]) +def list_payment_plans(request): try: if request.auth.account_type not in {'regular', 'business_host'}: raise AccessDenied @@ -169,17 +169,17 @@ async def list_payment_plans(request): ).prefetch_related( Prefetch( 'features', - queryset=PaymentPlanFeature.objects.filter( - model__model_settings__is_active=True - ).select_related( + queryset=PaymentPlanFeature.objects.filter(model__model_settings__is_active=True) + .select_related( 'model__category', 'model__model_settings', - ).order_by('model__category__order', 'order'), + ) + .order_by('model__category__order', 'order'), to_attr='active_features', ) ) result = [] - async for plan in plans: + for plan in plans: grouped = defaultdict(list) for feature in plan.active_features: grouped[feature.model.category.title].append( @@ -207,16 +207,16 @@ async def list_payment_plans(request): raise HttpError(400, f'{exc}') -@router.post('plans', tags=['payments/plans'], auth=AsyncAuthBearer(), response=PaymentLinkSchema) -async def create_payment_link(request, body: NewSubscriptionSchema): +@router.post('plans', tags=['payments/plans'], auth=SyncAuthBearer(), response=PaymentLinkSchema) +def create_payment_link(request, body: NewSubscriptionSchema): try: if request.auth.account_type not in {'regular', 'business_host'}: raise AccessDenied is_corporate = request.auth.account_type == 'business_host' - payment_plan = await PaymentPlan.objects.aget( + payment_plan = PaymentPlan.objects.get( uid=body.uid, price__gt=0, is_corporate=is_corporate, is_visible=True, individual=False ) - payment_url = await PaymentService(request.auth).create_payment_link(payment_plan) + payment_url = PaymentService(request.auth).create_payment_link(payment_plan) logger.info( 'Payment link endpoint completed: email=%s plan_uid=%s', request.auth.email, @@ -230,24 +230,24 @@ async def create_payment_link(request, body: NewSubscriptionSchema): @router.post('gitlab-webhook', tags=['payments/gitlab-webhook'], auth=None) -async def handle_gitlab_webhook(request): - ''' +def handle_gitlab_webhook(request): + """ Currently disabled, pending future feature flags - ''' + """ # try: # data = orjson.loads(request.body)['object_attributes'] # if data['name'] == 'recurring_payments': # is_active = data['active'] # if not is_active: - # deleted_methods_count, deleted_details = await PaymentMethod.objects.all().adelete() + # deleted_methods_count, deleted_details = PaymentMethod.objects.all().delete() # logger.info( # 'Recurring feature disabled: all payment methods removed count=%s', # deleted_methods_count, # ) - # updated_count = await PaymentPlanUserInfo.objects.filter( + # updated_count = PaymentPlanUserInfo.objects.filter( # plan__price__gt=0, # plan__individual=False, - # ).aupdate(next_payment_at=None if not is_active else (timezone.now() + timedelta(days=30))) + # ).update(next_payment_at=None if not is_active else (timezone.now() + timedelta(days=30))) # logger.info( # 'Recurring feature flag synced: active=%s updated_subscriptions=%s', # is_active, @@ -5,7 +5,6 @@ from datetime import timedelta from decimal import Decimal from uuid import UUID, uuid4 -from asgiref.sync import sync_to_async from django.conf import settings from django.db import transaction from django.utils import timezone @@ -29,7 +28,7 @@ class PaymentService: def __init__(self, user: CustomUserModel): self.user = user - async def create_payment_link(self, plan: PaymentPlan) -> str: + def create_payment_link(self, plan: PaymentPlan) -> str: receipt_data = { 'customer': {'email': self.user.email}, 'items': [ @@ -59,7 +58,6 @@ class PaymentService: ) return payment.confirmation.confirmation_url - @sync_to_async def do_payment(self, payment: YookassaPaymentResponse) -> PaymentModel: from payments.services.payment_plan_service import PaymentPlanService @@ -93,8 +91,8 @@ class PaymentService: return payment_instance @classmethod - async def handle_payment(cls, payment_id: UUID) -> YookassaPaymentResponse: - return await sync_to_async(YookassaPayment.find_one)(payment_id) + def handle_payment(cls, payment_id: UUID) -> YookassaPaymentResponse: + return YookassaPayment.find_one(payment_id) def handle_captured_payment(self, payment_id: UUID) -> None: idempotency_key = hashlib.sha256(f'capture:{payment_id}'.encode('utf-8')).hexdigest() @@ -158,7 +156,9 @@ class PaymentService: self.user.payment_plan.method.attempts, payment.cancellation_details.reason, ) - payment_method_service.compare_attempts_with_max(attempts=self.user.payment_plan.method.attempts) + payment_method_service.compare_attempts_with_max( + attempts=self.user.payment_plan.method.attempts + ) else: logger.info( 'Recurring payment retry skipped, payment method not found: email=%s', @@ -1,12 +1,11 @@ from typing import List -from asgiref.sync import sync_to_async from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ from ninja import File, Form, Router, UploadedFile from ninja.errors import HttpError -from authentication.security import SyncAuthBearer, AsyncAuthBearer +from authentication.security import SyncAuthBearer from ml_model.models import NeuronModel from ml_model.schemas import NeuronModelLink from tools.media.models import Preset, Voice @@ -31,8 +30,8 @@ def get_links(request): ) -@router.post('voices/', tags=['media/voices'], auth=AsyncAuthBearer(), response={201: None, 400: str}) -async def upload_voice( +@router.post('voices/', tags=['media/voices'], auth=SyncAuthBearer(), response={201: None, 400: str}) +def upload_voice( request, file: File[UploadedFile], title: str | None = Form(None), @@ -40,52 +39,52 @@ async def upload_voice( ): voice = Voice(user=request.auth, title=title, file=file, transcription=transcription) try: - await sync_to_async(voice.full_clean)() + voice.full_clean() except ValidationError as exc: raise HttpError(400, ';\n'.join(exc.messages)) - await voice.asave() + voice.save() return 201, None -@router.get('voices/', tags=['media/voices'], auth=AsyncAuthBearer(), response=List[VoiceSchema]) -async def list_voices(request): - return [voice async for voice in Voice.objects.filter(user=request.auth)] +@router.get('voices/', tags=['media/voices'], auth=SyncAuthBearer(), response=List[VoiceSchema]) +def list_voices(request): + return [voice for voice in Voice.objects.filter(user=request.auth)] @router.delete( 'voices/{voice_id}/', tags=['media/voices'], - auth=AsyncAuthBearer(), + auth=SyncAuthBearer(), response={204: None, 404: str}, ) -async def delete_voice(request, voice_id: int): - await Voice.objects.filter(pk=voice_id, user=request.auth).adelete() +def delete_voice(request, voice_id: int): + Voice.objects.filter(pk=voice_id, user=request.auth).delete() return 204, None @router.patch( 'voices/{voice_id}/', tags=['media/voices'], - auth=AsyncAuthBearer(), + auth=SyncAuthBearer(), response={200: VoiceSchema, 400: str, 404: str}, ) -async def update_voice_title(request, voice_id: int, body: UpdateVoiceSchema): +def update_voice_title(request, voice_id: int, body: UpdateVoiceSchema): try: - voice = await Voice.objects.aget(pk=voice_id, user=request.auth) + voice = Voice.objects.get(pk=voice_id, user=request.auth) except Voice.DoesNotExist: raise HttpError(404, _('Voice not found')) voice.title = body.title voice.transcription = body.transcription - await voice.asave() + voice.save() return voice @router.get( 'presets/', tags=['media/presets'], - auth=AsyncAuthBearer(), + auth=SyncAuthBearer(), response=List[PresetSchema], ) -async def list_presets(request, kind: PresetKindEnum | None = None): +def list_presets(request, kind: PresetKindEnum | None = None): qs = Preset.objects.filter(kind=kind) if kind else Preset.objects.all() - return [preset async for preset in qs] + return [preset for preset in qs] @@ -9,22 +9,22 @@ from users.schemas import ( UserSettingFilterSchema, UserSettingSchema, ) -from authentication.security import AsyncAuthBearer +from authentication.security import SyncAuthBearer from authentication.services.user_services import UserService -router = Router(auth=AsyncAuthBearer(), tags=['users']) +router = Router(auth=SyncAuthBearer(), tags=['users']) @router.get('settings/', tags=['users/settings'], response=List[UserSettingSchema]) -async def get_user_settings(request, filters: UserSettingFilterSchema = Query(...)): - return [setting async for setting in UserService.list_settings(filters=filters.get_filter_expression())] +def get_user_settings(request, filters: UserSettingFilterSchema = Query(...)): + return UserService.list_settings(filters=filters.get_filter_expression()) @router.post('settings/', tags=['users/settings'], response=UserSettingSchema) -async def add_setting(request, data: CreateUserSettingSchema): - return await UserService.add_setting(user_id=request.auth.uid, **data.model_dump()) +def add_setting(request, data: CreateUserSettingSchema): + return UserService.add_setting(user_id=request.auth.uid, **data.model_dump()) @router.put('settings/{id}', tags=['users/settings'], response={204: None}) -async def update_setting(request, id: UUID, data: UpdateUserSettingSchema): - await UserService.update_setting(setting_id=id, **data.model_dump()) +def update_setting(request, id: UUID, data: UpdateUserSettingSchema): + UserService.update_setting(setting_id=id, **data.model_dump()) @@ -101,4 +101,7 @@ ZROK2_ENABLE_TOKEN="your zrok token" # DOCKER COMPOSE_FILE=docker-compose.yml:docker-compose.local.yml COMPOSE_PROFILES="" # use-tunnel available -BUILDKIT_PROGRESS=plain \ No newline at end of file +BUILDKIT_PROGRESS=plain + +# HTTP FRAMEWORK +GUNICORN_CMD_ARGS="-b 0.0.0.0:8000 -k gthread -w 3 -t 600 --preload --reload" \ No newline at end of file @@ -8,7 +8,7 @@ x-default-ulimit: &default-ulimit hard: 98304 x-app-config: &app-config - <<: [*app-image, *default-ulimit] + <<: [ *app-image, *default-ulimit ] restart: on-failure:3 env_file: ${ENV:-.env} environment: @@ -28,7 +28,6 @@ x-default-deploy: &default-deploy placement: constraints: - node.role == worker - - node.labels.type != observer services: app: @@ -40,13 +39,13 @@ services: - /bin/sh - -c - | - python manage.py compilemessages --locale ru_RU --skip-system-checks - python -m uvicorn backend.asgi:application --host 0.0.0.0 --ws wsproto --http httptools --lifespan off --log-level info + python manage.py compilemessages --locale ru_RU + gunicorn backend.wsgi:application networks: - default - infrastructure volumes: - - static:/code/static + - static:/app/static labels: &app-labels - traefik.enable=true - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure} @@ -89,7 +88,7 @@ services: deploy: replicas: ${APP_REPLICAS:-1} labels: *app-labels - <<: [*default-deploy] + <<: [ *default-deploy ] init: <<: *app-config @@ -114,26 +113,21 @@ services: command: celery -A backend worker -l INFO --concurrency 3 deploy: replicas: 1 - <<: [*default-deploy] + <<: [ *default-deploy ] celery-beat: <<: *app-config command: celery -A backend beat -l INFO deploy: replicas: 1 - <<: [*default-deploy] + <<: [ *default-deploy ] static-server: image: nginx:alpine command: - /bin/sh - -c - - echo ' - server { - listen 80 default_server; - access_log off; - location ${STATIC_PATH_PREFIX:-/static/} { autoindex on; expires 365d; alias /var/www/static/; } - }' > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;' + - echo ' server { listen 80 default_server; access_log off; location ${STATIC_PATH_PREFIX:-/static/} { autoindex on; expires 365d; alias /var/www/static/; } }' > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;' networks: - infrastructure volumes: @@ -157,21 +151,21 @@ services: deploy: replicas: 1 labels: *static-labels - <<: [*default-deploy] + <<: [ *default-deploy ] cache-mdb: image: redis:alpine - <<: [*default-ulimit] + <<: [ *default-ulimit ] deploy: replicas: 1 - <<: [*default-deploy] + <<: [ *default-deploy ] celery-mdb: image: redis:alpine - <<: [*default-ulimit] + <<: [ *default-ulimit ] deploy: replicas: 1 - <<: [*default-deploy] + <<: [ *default-deploy ] networks: infrastructure: @@ -30,7 +30,7 @@ dependencies = [ "environs==9.5.0", "filetype==1.2.0", "googletrans==4.0.2", - "httptools==0.6.4", + "gunicorn>=26.0.0", "httpx==0.28.1", "langchain==0.3.19", "langchain-community==0.3.17", @@ -47,7 +47,6 @@ dependencies = [ "pymupdf==1.26.1", "pypandoc==1.15", "pypdf2==3.0.1", - "pyroscope-io==0.8.11", "python-dateutil==2.9.0.post0", "python-docx==1.1.2", "redis>=6.4.0", @@ -57,8 +56,6 @@ dependencies = [ "social-auth-app-django==5.3.0", "tiktoken==0.9.0", "unleashclient==6.4.0", - "uvicorn==0.35.0", - "wsproto==1.2.0", "yookassa==3.10.1", ] @@ -289,7 +289,7 @@ dependencies = [ { name = "environs" }, { name = "filetype" }, { name = "googletrans" }, - { name = "httptools" }, + { name = "gunicorn" }, { name = "httpx" }, { name = "langchain" }, { name = "langchain-community" }, @@ -306,7 +306,6 @@ dependencies = [ { name = "pymupdf" }, { name = "pypandoc" }, { name = "pypdf2" }, - { name = "pyroscope-io" }, { name = "python-dateutil" }, { name = "python-docx" }, { name = "redis" }, @@ -316,8 +315,6 @@ dependencies = [ { name = "social-auth-app-django" }, { name = "tiktoken" }, { name = "unleashclient" }, - { name = "uvicorn" }, - { name = "wsproto" }, { name = "yookassa" }, ] @@ -358,7 +355,7 @@ requires-dist = [ { name = "environs", specifier = "==9.5.0" }, { name = "filetype", specifier = "==1.2.0" }, { name = "googletrans", specifier = "==4.0.2" }, - { name = "httptools", specifier = "==0.6.4" }, + { name = "gunicorn", specifier = ">=26.0.0" }, { name = "httpx", specifier = "==0.28.1" }, { name = "langchain", specifier = "==0.3.19" }, { name = "langchain-community", specifier = "==0.3.17" }, @@ -375,7 +372,6 @@ requires-dist = [ { name = "pymupdf", specifier = "==1.26.1" }, { name = "pypandoc", specifier = "==1.15" }, { name = "pypdf2", specifier = "==3.0.1" }, - { name = "pyroscope-io", specifier = "==0.8.11" }, { name = "python-dateutil", specifier = "==2.9.0.post0" }, { name = "python-docx", specifier = "==1.1.2" }, { name = "redis", specifier = ">=6.4.0" }, @@ -385,8 +381,6 @@ requires-dist = [ { name = "social-auth-app-django", specifier = "==5.3.0" }, { name = "tiktoken", specifier = "==0.9.0" }, { name = "unleashclient", specifier = "==6.4.0" }, - { name = "uvicorn", specifier = "==0.35.0" }, - { name = "wsproto", specifier = "==1.2.0" }, { name = "yookassa", specifier = "==3.10.1" }, ] @@ -1528,6 +1522,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424, upload-time = "2025-06-28T04:23:42.136Z" }, ] +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1584,28 +1590,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, ] -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, -] - [[package]] name = "httpx" version = "0.28.1" @@ -3039,20 +3023,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, ] -[[package]] -name = "pyroscope-io" -version = "0.8.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/d6/323d3ebcee1dab2a6839d9259159328c876ed6e6900ad7c3103f1f945591/pyroscope_io-0.8.11-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:644dbd81b162b6d678ef9989649bf936c62373fd7bba16fb6490272f453ff159", size = 2415169, upload-time = "2025-05-06T10:31:04.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/72/24cbeda11a911ac0f1752881c400b4f130d5843c0ed37abc0bd7879b3000/pyroscope_io-0.8.11-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:2df4cd4cbfb451c27cad20f905bf612ffc306a96820e316f7575c84433eadb24", size = 2576820, upload-time = "2025-05-06T10:31:06.431Z" }, - { url = "https://files.pythonhosted.org/packages/97/34/41ce83d98ba8ad6da5d3002c1f81fd99b7eea2a199269f904d7c288b8d99/pyroscope_io-0.8.11-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50164c96cf5533ce795a114c6181c5d2aa162c9dae59277b6ac557820c411b7a", size = 2599444, upload-time = "2025-05-06T10:31:08.056Z" }, - { url = "https://files.pythonhosted.org/packages/ad/72/336cb95dc629ade6aa27b9b28562dfc21a17b049216168e6a85e7c905350/pyroscope_io-0.8.11-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a415072dd7e8964d66001fff2446d7426a505cfca1a89f3c912314537622a4cd", size = 2714690, upload-time = "2025-05-06T10:31:09.329Z" }, -] - [[package]] name = "python-crontab" version = "3.3.0" @@ -3978,19 +3948,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", size = 171733, upload-time = "2026-05-19T07:45:29.283Z" }, ] -[[package]] -name = "uvicorn" -version = "0.35.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, -] - [[package]] name = "vine" version = "5.1.0" @@ -4073,18 +4030,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, ] -[[package]] -name = "wsproto" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, -] - [[package]] name = "yarl" version = "1.24.2"