@@ -1,18 +1,23 @@ import os from channels.routing import ProtocolTypeRouter, URLRouter +from django.conf import settings +from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler from django.core.asgi import get_asgi_application from django.urls import path os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') -application = get_asgi_application() +django_app = get_asgi_application() + +if settings.DEBUG: + django_app = ASGIStaticFilesHandler(django_app) from tools.copywrite.routes.v1 import ws_router as copywrite_ws_router # noqa: E402 application = ProtocolTypeRouter( { - 'http': application, + 'http': django_app, 'websocket': URLRouter([path('rtc/copywrite/', copywrite_ws_router)]), } ) @@ -321,7 +321,8 @@ USE_I18N = True USE_TZ = True # Static files -STATIC_URL = f'{env.str("STATIC_PATH_PREFIX")}/' +STATIC_PATH_PREFIX = env.str('STATIC_PATH_PREFIX', default='static').strip('/') +STATIC_URL = f'/{STATIC_PATH_PREFIX}/' STATIC_ROOT = BASE_DIR / 'static' MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'static/media' @@ -497,9 +498,10 @@ if CACHEOPS_REDIS: } # UNLEASH settings -FEATURE_FLAG_API_URL = env.str('FEATURE_FLAG_API_URL') +# Пусто = без Unleash (иначе в Docker «localhost» даёт /client/features Connection refused) +FEATURE_FLAG_API_URL = env.str('FEATURE_FLAG_API_URL', '') FEATURE_FLAG_APP_NAME = env.str('FEATURE_FLAG_APP_NAME', 'staging') -FEATURE_FLAG_INSTANCE_ID = env.str('FEATURE_FLAG_INSTANCE_ID') +FEATURE_FLAG_INSTANCE_ID = env.str('FEATURE_FLAG_INSTANCE_ID', 'default') FEATURE_FLAG_WEBHOOK_SECRET_KEY = env.str( 'FEATURE_FLAG_WEBHOOK_SECRET_KEY', 'FEATURE_FLAG_WEBHOOK_SECRET_KEY' ) @@ -1,4 +1,14 @@ +from django.conf import settings + +from lib.services.noop_feature_flag import NoopFeatureFlagService from lib.services.unleash_feature_flag import UnleashFeatureFlagService -web_client = UnleashFeatureFlagService() -celery_client = UnleashFeatureFlagService() \ No newline at end of file + +def _feature_flag_service(): + if not (getattr(settings, 'FEATURE_FLAG_API_URL', None) or '').strip(): + return NoopFeatureFlagService() + return UnleashFeatureFlagService() + + +web_client = _feature_flag_service() +celery_client = _feature_flag_service() \ No newline at end of file @@ -749,8 +749,6 @@ msgid "Available only in paid plan" msgstr "Доступно только в платном тарифе" #: ml_model/exceptions.py:163 -#, fuzzy -#| msgid "Image analysis error. Please try another image." msgid "Face not found in the image. Please try another image with a face." msgstr "Не найдено лицо на картинке. Попробуйте другую картинку с лицом." @@ -35,3 +35,5 @@ def calculate_predict_price(request, body: PredictPriceInputSchema): return PredictPriceSchema(price=predicted_price) + + @@ -107,7 +107,8 @@ class Chatgpt_5_4(Chatgpt): model_name = info.pop('version', 'gpt-5.4') user_system_prompt = info.pop('system_prompt', '') plan_info = self.store.user.payment_plan - is_free_plan = plan_info and plan_info.plan.price <= 0 + is_regular_user = self.store.user.account_type == 'regular' + is_free_plan = is_regular_user and plan_info and plan_info.plan.price <= 0 if is_free_plan and model_name == 'gpt-5.4-pro': raise PaidPlanRequiredError() if is_free_plan: @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -58,12 +57,7 @@ class Dalle(SimpleService): **input_message.info, } ) - try: - images = replicate_run(self._CALLBACK, callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - raise GenerationException from exc + images = replicate_run(self._CALLBACK, callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message=input_message) msgs = self.save_results(input_message.content, images, process_time, save) @@ -12,24 +12,32 @@ from tools.public_api.models import APIStore class Deepseek(SimpleService): TOKENS_COST = { - 'deepseek/deepseek-chat': { - 'input': Decimal('390') / 1_000_000, - 'output': Decimal('390') / 1_000_000, + # 'deepseek/deepseek-chat': { + # 'input': Decimal('390') / 1_000_000, + # 'output': Decimal('390') / 1_000_000, + # }, + # 'deepseek/deepseek-r1': { + # 'input': Decimal('900') / 1_000_000, + # 'output': Decimal('900') / 1_000_000, + # }, + # 'deepseek/deepseek-r1:free': { + # 'input': Decimal('0'), + # 'output': Decimal('0'), + # }, + 'deepseek/deepseek-v4-pro': { + 'input': Decimal('1050') / 1_000_000, + 'output': Decimal('2200') / 1_000_000, }, - 'deepseek/deepseek-r1': { - 'input': Decimal('900') / 1_000_000, - 'output': Decimal('900') / 1_000_000, - }, - 'deepseek/deepseek-r1:free': { - 'input': Decimal('0'), - 'output': Decimal('0') + 'deepseek/deepseek-v4-flash': { + 'input': Decimal('100') / 1_000_000, + 'output': Decimal('175') / 1_000_000, }, } - PRICE_BIAS = Decimal('0.05') def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: price_map = self.TOKENS_COST[version] - price = input_tokens * price_map['input'] + output_tokens * price_map['output'] + self.PRICE_BIAS + price = input_tokens * price_map['input'] + output_tokens * price_map['output'] + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, content: str, t: timedelta, save: bool = True) -> list[Message]: @@ -47,15 +55,17 @@ class Deepseek(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: info = input_message.info.copy() version = info.pop('version') - system_prompt = input_message.info.pop('system_prompt', '') + system_prompt = info.pop('system_prompt', '') + callback_data = { - **input_message.info, + **info, } messages = [ {'role': 'system', 'content': system_prompt}, *self.get_chat_history(), {'role': 'user', 'content': input_message.content} ] + start_time = time.time() result = openrouter_run(version, messages, callback_data, 'Deepseek') process_time = timedelta(seconds=(time.time() - start_time)) @@ -66,8 +76,11 @@ class Deepseek(SimpleService): output_tokens=result[2], ) 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( @@ -100,3 +113,4 @@ class Deepseek(SimpleService): while character_length > max_character_limit: character_length -= len(memory.pop(0)['content']) return memory + @@ -7,7 +7,6 @@ from typing import Any import requests from django.core.files import File from django.utils.translation import gettext as _ -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException, InvalidParameterError @@ -54,12 +53,7 @@ class Elevenlabs_Music(SimpleService): **input_message.info, } start_time = time.time() - try: - audio = replicate_run('elevenlabs/music', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + audio = replicate_run('elevenlabs/music', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, duration=duration) msgs = self.save_results(input_message.content, process_time, audio, save) @@ -8,7 +8,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions import RequestBlocked, PredictionInterruptedError, GenerationException from ml_model.models import ( NeuronModel, ) @@ -87,17 +86,10 @@ class Flux(SimpleService): **input_message.info, } ) - try: - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data.get("version", "flux-schnell")}', - callback_data, - ) - except Exception as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - elif 'PA' in str(exc): - raise PredictionInterruptedError - raise GenerationException from exc + runner = replicate_run( + f'{self._CALLBACK_BASE}{callback_data.get("version", "flux-schnell")}', + callback_data, + ) images = runner if isinstance(runner, list) else [runner] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message=input_message, version=version) @@ -10,7 +10,6 @@ import requests from PIL import Image from django.core.files import File from django.core.files.images import get_image_dimensions -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import PredictionInterruptedError, RequestBlocked, GenerationException, \ @@ -96,14 +95,7 @@ class Flux_2(SimpleService): image = f'data:image/{format};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' buf.close() callback_data.update({'input_images': [image]}) - try: - images = [replicate_run(f'black-forest-labs/{version}', callback_data)] - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - elif 'PA' in str(exc): - raise PredictionInterruptedError - raise GenerationException from exc + images = [replicate_run(f'black-forest-labs/{version}', callback_data)] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version, input_mp=input_mp, output_mp=output_mp) msgs = self.save_results(input_message.content, images, process_time, save) @@ -5,7 +5,6 @@ import time from typing import Any from django.core.files import File -from replicate.exceptions import ModelError import requests from messages.models.message import Message @@ -73,15 +72,7 @@ class Fluxpulid(SimpleService): } ) start_time = time.time() - try: - images = replicate_run(self.ENDPOINT, callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - elif exc.prediction.error == 'facexlib align face fail': - raise FaceNotFoundError - raise GenerationException from exc - + images = replicate_run(self.ENDPOINT, callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, len(images)) msgs = self.save_results(input_message.content, images, process_time, save) @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import ModelTimeoutError, ImageContentNotFound, RequestBlocked, GenerationException @@ -44,25 +43,15 @@ class Geminiimage(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: if input_message.content: - try: - callback_data = dict( - {'prompt': self.translate_prompt(input_message.content), **input_message.info} - ) - start_time = time.time() - images = replicate_run('google/gemini-2.5-flash-image', callback_data) - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, num_images=input_message.info.get('num_images', 1) - ) - msgs = self.save_results(input_message.content, process_time, images, save) - return msgs - except ModelError as exc: - if exc.prediction.error in ( - 'No image content found in response', - 'Failed to generate image.', - ): - raise ImageContentNotFound - elif any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + callback_data = dict( + {'prompt': self.translate_prompt(input_message.content), **input_message.info} + ) + start_time = time.time() + images = replicate_run('google/gemini-2.5-flash-image', callback_data) + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, num_images=input_message.info.get('num_images', 1) + ) + msgs = self.save_results(input_message.content, process_time, images, save) + return msgs raise ModelTimeoutError @@ -7,9 +7,10 @@ from io import BytesIO import filetype from PIL import Image +from PIL.Image import DecompressionBombError from messages.models import Message -from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError +from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError, ImageTooLargeError from ml_model.services.EmbeddingService import EmbeddingService from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService @@ -27,6 +28,9 @@ class Grok_4_1_Fast(SimpleService): TOOLS_TOKEN_COSTS = {'text-embedding-3-small': {'output': Decimal('0.00001')}} + MAX_PIXELS = 178956970 + + def calculate_price(self, input_tokens: int, output_tokens: int, embedding_tokens: int) -> Decimal: price = ( input_tokens * self.TOKENS_COST['input'] / 1_000_000 @@ -57,6 +61,7 @@ class Grok_4_1_Fast(SimpleService): if input_message.file: file_service = FileProcessingService file_bytes = input_message.file.read() + input_message.file.close() kind = filetype.guess(file_bytes[:550]) if not kind: raise CorruptedFileError @@ -100,14 +105,20 @@ class Grok_4_1_Fast(SimpleService): f'{chunks}. Вопрос: {input_message.content}' ) elif file_extension in ('jpg', 'jpeg', 'png', 'webp'): - kind = filetype.guess(file_bytes[:20]) + try: + with Image.open(BytesIO(file_bytes)) as normalized_image: + current_pixels = normalized_image.width * normalized_image.height + if current_pixels > self.MAX_PIXELS: + raise ImageTooLargeError(self.MAX_PIXELS) + except DecompressionBombError: + raise ImageTooLargeError(self.MAX_PIXELS) + mime = kind.mime if kind else 'application/octet-stream' - normalized_image = Image.open(input_message.file) - format = 'jpeg' if kind.extension == 'jpg' else kind.extension - buf = BytesIO() - normalized_image.save(buf, format=format) - image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' - buf.close() + image_url = ( + f'data:{mime};base64,' + f'{base64.b64encode(file_bytes).decode("utf-8")}' + ) + messages[-1]['content'] = [ {'type': 'text', 'text': input_message.content}, {'type': 'image_url', 'image_url': {'url': image_url}}, @@ -7,7 +7,6 @@ from io import BytesIO from typing import Any from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import ( @@ -54,14 +53,7 @@ class Grok_Image(SimpleService): if image := input_message.file: callback_data.update({'image': image.url}) start_time = time.time() - try: - images = replicate_run('xai/grok-imagine-image', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - elif exc.prediction.error == 'No image content found in response': - raise ImageContentNotFound - raise GenerationException from exc + images = replicate_run('xai/grok-imagine-image', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, process_time, images, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -52,12 +51,7 @@ class Grok_Imagine_Video(SimpleService): if image := input_message.file: callback_data.update({'image': image.url}) start_time = time.time() - try: - video = replicate_run('xai/grok-imagine-video', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run('xai/grok-imagine-video', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -70,15 +69,10 @@ class Hailuo(SimpleService): input_message.file.close() callback_data.update({'first_frame_image': image}) start_time = time.time() - try: - video = replicate_run( - f'minimax/{version}', - callback_data - ) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run( + f'minimax/{version}', + callback_data + ) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version, resolution=resolution) msgs = self.save_results(input_message.content, process_time, video, save) @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import InvalidStyleCombinationError, RequestBlocked, GenerationException @@ -80,15 +79,10 @@ class Ideogram(SimpleService): **input_message.info, } ) - try: - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data.get("version", "ideogram-v3-turbo")}', - callback_data, - ) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + runner = replicate_run( + f'{self._CALLBACK_BASE}{callback_data.get("version", "ideogram-v3-turbo")}', + callback_data, + ) images = runner if isinstance(runner, list) else [runner] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message=input_message, version=version) @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import ImageContentNotFound, GenerationException, RequestBlocked @@ -38,25 +37,18 @@ class Imagen(SimpleService): return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - try: - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: - raise InsufficientBalance(balance, self.TOKENS_COST) - callback_data = dict( - { - 'prompt': self.translate_prompt(input_message.content), - 'safety_filter_level': 'block_medium_and_above', - **input_message.info, - } - ) - start_time = time.time() - images = replicate_run('google/imagen-3-fast', callback_data) - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model) - msgs = self.save_results(input_message.content, process_time, images, save) - return msgs - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - elif exc.prediction.error == 'No image content found in response': - raise ImageContentNotFound - raise GenerationException from exc + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < self.TOKENS_COST: + raise InsufficientBalance(balance, self.TOKENS_COST) + callback_data = dict( + { + 'prompt': self.translate_prompt(input_message.content), + 'safety_filter_level': 'block_medium_and_above', + **input_message.info, + } + ) + start_time = time.time() + images = replicate_run('google/imagen-3-fast', callback_data) + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model) + msgs = self.save_results(input_message.content, process_time, images, save) + return msgs @@ -9,7 +9,6 @@ import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException, FileNotProvided @@ -60,12 +59,7 @@ class Kling(SimpleService): input_message.file.close() callback_data.update({'start_image': image}) start_time = time.time() - try: - video = replicate_run('kwaivgi/kling-v2.1', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run('kwaivgi/kling-v2.1', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, mode=mode, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -73,15 +72,10 @@ class Leonardo(SimpleService): **input_message.info, } ) - try: - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data.get("version", "lucid-origin")}', - callback_data, - ) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + runner = replicate_run( + f'{self._CALLBACK_BASE}{callback_data.get("version", "lucid-origin")}', + callback_data, + ) images = runner if isinstance(runner, list) else [runner] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -47,12 +46,7 @@ class Lyria(SimpleService): if file := input_message.file: callback_data.update({'images': [file.url]}) start_time = time.time() - try: - video = replicate_run(f'google/{version}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run(f'google/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version) msgs = self.save_results(input_message.content, process_time, video, save) @@ -7,7 +7,6 @@ from typing import Any import requests from django.core.files import File from django.utils.translation import gettext as _ -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, InvalidParameterError, GenerationException @@ -51,14 +50,7 @@ class Minimaxmusic(SimpleService): **input_message.info, } start_time = time.time() - try: - audio = replicate_run('minimax/music-1.5', callback_data) - except ModelError as exc: - if 'lyrics is too long' in str(exc): - raise InvalidParameterError(_('Lyrics is too long')) - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + audio = replicate_run('minimax/music-1.5', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, process_time, audio, save) @@ -7,7 +7,6 @@ from typing import Any import requests from django.core.files import File from django.utils.translation import gettext as _ -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, InvalidParameterError, GenerationException @@ -55,14 +54,7 @@ class Minimaxmusic_Lite(SimpleService): file_url = Preset.objects.get(slug=instrumental).file.url callback_data.update({'instrumental_file': file_url}) start_time = time.time() - try: - audio = replicate_run('minimax/music-01', callback_data) - except ModelError as exc: - if 'lyrics is too long' in str(exc): - raise InvalidParameterError(_('Lyrics is too long')) - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + audio = replicate_run('minimax/music-01', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, process_time, audio, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -55,12 +54,7 @@ class Minimaxvideo(SimpleService): input_message.file.close() callback_data.update({'subject_reference': image}) start_time = time.time() - try: - video = replicate_run(f'minimax/{version}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run(f'minimax/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version) msgs = self.save_results(input_message.content, process_time, video, save) @@ -8,7 +8,6 @@ from typing import Optional, Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import ( @@ -78,16 +77,7 @@ class Nanobanana(SimpleService): image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' input_message.file.close() callback_data.update({'image_input': [image]}) - try: - image = replicate_run(f'google/{version}', callback_data) - except ModelError as exc: - if exc.prediction.error == 'No image content found in response': - raise ImageContentNotFound - elif exc.prediction.error in ('400', 'Failed to generate image.'): - raise ModelCouldNotInterpretPrompt from exc - elif any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + image = replicate_run(f'google/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version, resolution=resolution) msgs = self.save_results(input_message.content, image, process_time, save) @@ -8,7 +8,6 @@ from typing import Any, Optional import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import ( @@ -77,16 +76,7 @@ class Nanobanana_2(SimpleService): image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' input_message.file.close() callback_data.update({'image_input': [image]}) - try: - image = replicate_run(f'google/{version}', callback_data) - except ModelError as exc: - if exc.prediction.error == 'No image content found in response': - raise ImageContentNotFound - elif exc.prediction.error in ('400', 'Failed to generate image.'): - raise ModelCouldNotInterpretPrompt from exc - elif any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + image = replicate_run(f'google/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version, resolution=resolution) msgs = self.save_results(input_message.content, image, process_time, save) @@ -10,7 +10,6 @@ import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import ( @@ -65,14 +64,7 @@ class Photon(SimpleService): input_message.file.close() callback_data.update({'image_reference': image}) start_time = time.time() - try: - images = replicate_run('luma/photon-flash', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - elif exc.prediction.error == 'No image content found in response': - raise ImageContentNotFound - raise GenerationException from exc + images = replicate_run('luma/photon-flash', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, process_time, images, save) @@ -7,7 +7,6 @@ from io import BytesIO from typing import Any from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -63,12 +62,7 @@ class Pixverse(SimpleService): if image := input_message.file: callback_data.update({'image': image.url}) start_time = time.time() - try: - video = replicate_run('pixverse/pixverse-v5.6', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run('pixverse/pixverse-v5.6', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, quality=quality, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) @@ -9,7 +9,6 @@ import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import GenerationException, RequestBlocked @@ -74,14 +73,9 @@ class Pruna_V(SimpleService): else: callback_data.update({'image': input_message.file.url}) start_time = time.time() - try: - images = replicate_run('prunaai/p-video', callback_data) - if 'nsfw.jpeg' == str(images).split('/')[-1]: - raise RequestBlocked - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - raise GenerationException from exc + images = replicate_run('prunaai/p-video', callback_data) + if 'nsfw.jpeg' == str(images).split('/')[-1]: + raise RequestBlocked process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice( input_message.content_object.model, @@ -8,7 +8,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import GenerationException, RequestBlocked @@ -70,12 +69,7 @@ class Prunaai(SimpleService): } callback_data.update({'speed_mode': speed_mode[s_m]}) start_time = time.time() - try: - images = replicate_run(f'prunaai/{version}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual', 'NSFW')): - raise RequestBlocked - raise GenerationException from exc + images = replicate_run(f'prunaai/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version) msgs = self.save_results(input_message.content, process_time, images, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -59,12 +58,7 @@ class Ray(SimpleService): input_message.file.close() callback_data.update({'start_image': image}) start_time = time.time() - try: - video = replicate_run(f'luma/{version}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run(f'luma/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version=version, duration=duration) msgs = self.save_results(input_message.content, process_time, video, save) @@ -5,7 +5,6 @@ from decimal import Decimal from io import BytesIO from typing import Any -from replicate.exceptions import ModelError from ml_model.exceptions import ( RequestBlocked, GenerationException, @@ -71,16 +70,7 @@ class Reve(SimpleService): input_message.file.close() callback_data.update({'image': image}) type = 'edit-fast' - try: - image = replicate_run(f'reve/{type}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - if 'INPUT_ANALYSIS_FAILURE' in str(exc): - raise ImageAnalysisError - if 'PROMPT_TOO_LONG' in str(exc): - raise ExceededContextLengthError - raise GenerationException from exc + image = replicate_run(f'reve/{type}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, type=type) msgs = self.save_results(input_message.content, image, process_time, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import FileNotProvided, RequestBlocked, GenerationException @@ -69,12 +68,7 @@ class Runway(SimpleService): 'image': media } start_time = time.time() - try: - video = replicate_run(f'runwayml/{version}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run(f'runwayml/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, duration=duration, version=version) msgs = self.save_results(input_message.content, process_time, video, save) @@ -7,7 +7,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException, FileExtensionNotSupported @@ -93,14 +92,9 @@ class Seedance(SimpleService): ) callback_data.update({f'reference_{reference_type}': [file.url]}) start_time = time.time() - try: - video = replicate_run( - f'bytedance/{version}', callback_data - ) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run( + f'bytedance/{version}', callback_data + ) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, resolution=resolution, duration=duration, version=version, generation_type=generation_type) msgs = self.save_results(input_message.content, process_time, video, save) @@ -6,7 +6,6 @@ from typing import Any import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -52,12 +51,7 @@ class Seedream(SimpleService): } if image := input_message.file: callback_data.update({'image_input': [image.url]}) - try: - images = replicate_run('bytedance/seedream-5-lite', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + images = replicate_run('bytedance/seedream-5-lite', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model) msgs = self.save_results(input_message.content, images, process_time, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import RequestBlocked, GenerationException @@ -56,12 +55,7 @@ class Veo(SimpleService): input_message.file.close() callback_data.update({'image': image}) start_time = time.time() - try: - video = replicate_run(f'google/{version}', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + video = replicate_run(f'google/{version}', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, version) msgs = self.save_results(input_message.content, process_time, video, save) @@ -8,7 +8,6 @@ from typing import Any import filetype import requests from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message from ml_model.exceptions import FileExtensionNotSupported, CorruptedFileError, GenerationException @@ -64,12 +63,7 @@ class Wan_Lite(SimpleService): input_message.file.close() callback_data.update({'image': image}) start_time = time.time() - try: - video = replicate_run('wan-video/wan-2.2-5b-fast', callback_data) - except ModelError as exc: - if 'image file' in exc.prediction.error: - raise CorruptedFileError from exc - raise GenerationException from exc + video = replicate_run('wan-video/wan-2.2-5b-fast', callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, resolution=resolution) msgs = self.save_results(input_message.content, process_time, video, save) @@ -39,6 +39,14 @@ class UnsupportedSize(Exception): self.current_size | self.required_size ) +class ImageTooLargeError(Exception): + def __init__(self, max_pixels: int) -> None: + self.max_pixels = max_pixels + + def __str__(self) -> str: + return _('Image exceeds the maximum allowed pixel count (%(max_pixels)d).') % { + 'max_pixels': self.max_pixels + } class ModelTimeoutError(Exception): def __str__(self): @@ -153,6 +161,11 @@ class ServiceHighDemandError(Exception): return _('Service is currently unavailable due to high demand. Please try again later') +class OpenRouterCreditsError(Exception): + def __str__(self) -> str: + return _('OpenRouter credits are exhausted. Please try again later.') + + class PaidPlanRequiredError(Exception): def __str__(self) -> str: return _('Available only in paid plan') @@ -160,4 +173,4 @@ class PaidPlanRequiredError(Exception): class FaceNotFoundError(Exception): def __str__(self) -> str: - return _('Face not found in the image. Please try another image with a face.') + return _('Face not found in the image. Please try another image with a face.') \ No newline at end of file @@ -1,5 +1,4 @@ from typing import List, Optional, Any - from ninja import ModelSchema, Schema from pydantic import condecimal @@ -15,17 +15,86 @@ import replicate import requests from celery import shared_task from deepl.translator import TextResult +from django.utils.translation import gettext as _ +from replicate.exceptions import ModelError from requests import Response from backend import settings from ml_model.adapters.bytedance_model_ark import BytedanceContentType, BytedanceModelArkAdapter -from ml_model.exceptions import DeploymentDisabled, ModelTimeoutError, GenerationException, RequestBlocked +from ml_model.exceptions import ( + CorruptedFileError, + DeploymentDisabled, + ExceededContextLengthError, + FaceNotFoundError, + GenerationException, + ImageAnalysisError, + ImageContentNotFound, + InvalidParameterError, + ModelCouldNotInterpretPrompt, + ModelTimeoutError, + PredictionInterruptedError, + RequestBlocked, +) from ml_model.utils import count_openrouter_tokens from poller.models import Proxy logger = logging.getLogger(__name__) +def _httpx_proxy_url(proxy: Proxy) -> str: + """httpx не принимает схему socks:// — только socks5:// / socks4://.""" + if proxy.protocol == Proxy.ProtocolChoices.SOCKS: + return f'socks5://{proxy.address}' + return f'{proxy.protocol}://{proxy.address}' + + +def _openrouter_result_from_response(resp: httpx.Response, model_name: str, messages: list) -> tuple[str, int, int]: + if resp.status_code == 402: + logger.warning('OpenRouter 402 for %s: %s', model_name, resp.text[:800]) + raise OpenRouterCreditsError + if (data := resp.json()) and data.get('choices'): + raw_content = [ + c['message']['content'] + for c in data.get('choices', []) + if c.get('message') and c['message'].get('content') is not None + ] + if not raw_content: + raise GenerationException + content = ','.join(raw_content) + reasoning = ','.join( + reasoning + for choice in data.get('choices', []) + if (reasoning := choice['message'].get('reasoning')) is not None + ) + reasoning = re.sub(r'Вывод:|Основная мысль:|Рассуждение:|\*\*', '', reasoning) + answer = reasoning + if any(m in data['model'] for m in ('google/gemini', 'x-ai/grok-4.1-fast')) or re.match( + r'^qwen/qwen3\.5-.*$', data['model'] + ): + answer = content + elif reasoning and content: + # TODO: переделать рендеринг сообщения на Jinja 2 + answer = f'**Рассуждение:**\n\n{reasoning}\n\n**Основная мысль:**\n\n{content}' + elif content: + answer = content + if int(data.get('choices')[0].get('error', {}).get('code', 0)) == 502: + error_type = re.sub(r'["\']', '', str(data['choices'][0]['error']['message'])) + if error_type == 'Overloaded': + logger.warning(f'Model {model_name} overloaded') + input_tokens, output_tokens = count_openrouter_tokens( + model_name, messages, content + reasoning + ) + else: + logger.error(f'Model {model_name} disabled') + raise DeploymentDisabled + 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') + + @shared_task def create_d_image(payload: dict): if payload.get('image'): @@ -96,10 +165,35 @@ def transcript_audio(payload: dict[str, Any]): @shared_task def replicate_run(callback_url: str, payload: dict[str, Any]): replicate_client = replicate.Client(settings.REPLICATE_API_KEY) - return replicate_client.run( - ref=callback_url, - input=payload, - ) + try: + return replicate_client.run( + ref=callback_url, + input=payload, + ) + except ModelError as exc: + prediction_error = getattr(getattr(exc, 'prediction', None), 'error', '') or '' + error_text = str(exc) + if any(error in error_text for error in ('E005', 'E006', 'sexual', 'NSFW')): + raise RequestBlocked + if 'PA' in error_text: + raise PredictionInterruptedError + if prediction_error == 'No image content found in response': + raise ImageContentNotFound + if prediction_error == 'Failed to generate image.': + raise ModelCouldNotInterpretPrompt from exc + if prediction_error == '400': + raise ModelCouldNotInterpretPrompt from exc + if prediction_error == 'facexlib align face fail': + raise FaceNotFoundError + if prediction_error and 'image file' in prediction_error: + raise CorruptedFileError from exc + if 'lyrics is too long' in error_text: + raise InvalidParameterError(_('Lyrics is too long')) + if 'INPUT_ANALYSIS_FAILURE' in error_text: + raise ImageAnalysisError + if 'PROMPT_TOO_LONG' in error_text: + raise ExceededContextLengthError + raise GenerationException from exc @shared_task @@ -172,6 +266,7 @@ def fal_ai_run(model, payload): headers={'Authorization': f'Key {settings.FAL_API_KEY}'}, timeout=600, proxy=f'{proxy.protocol}://{proxy.address}', + trust_env=False, ) resp = client.post(model, json=payload) if resp.status_code == 403: @@ -29,6 +29,7 @@ class PaymentPlanSelector: else: pp = self.user.payment_plan balance = pp.current_token_balance + pp.referral_balance + logging.info(f'Balance: {balance} for user: {self.user.email} and plan: {pp.plan.price} and current token balance: {pp.current_token_balance} and referral balance: {pp.referral_balance}') return balance @@ -2,6 +2,7 @@ from django.apps import AppConfig from django.core.signals import setting_changed from django.utils.translation import gettext_lazy as _ +from lib.services.unleash_feature_flag import UnleashFeatureFlagService from lib.unleash.client import web_client @@ -13,7 +14,8 @@ class PaymentsConfig(AppConfig): def ready(self): import payments.signals - web_client.client.initialize_client() + if isinstance(web_client, UnleashFeatureFlagService): + web_client.client.initialize_client() setting_changed.connect(payments.signals.init_referral_account) return super().ready() @@ -6,6 +6,10 @@ STATIC_PATH_PREFIX=static RELEASE=1.0.0 ENVIRONMENT=dev +# REQUIRED RUNTIME ENV +RELEASE=local +ENVIRONMENT=local + # NEURON MODELS OPENAI_API_KEY=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 STABLE_DIFFUSION_API_KEY=sk-fztQxZobaL0SD7PgpmK7XMQlyNivpKFZNJnqAVG2CcbvAP6Z @@ -17,8 +21,9 @@ MISTRAL_API_KEY=CYtZSCQXZFzHcpJvWOjWNx4EHjf5kWQc DEEPL_API_KEY=4bb58b98-ca95-5978-9be0-ed437df6c15c:fx SERPER_API_KEY=ed8e0dbcc26dacf3f7f99fbc8b3add9ada0c793e FLUX_API_KEY=dccaf377-aecf-4cf0-aff4-dde47cee340d -OPENROUTER_API_KEY=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 -BYTEDANCE_MODEL_ARK_API_KEY=ark-151e9e89-7275-4dbf-bbb3-d2b32bb69d61-3ca5b +OPENROUTER_API_KEY=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 +# При ENVIRONMENT=local прямой OpenRouter уже включён в settings; для prod-like локально: OPENROUTER_ALLOW_DIRECT=0 +# OPENROUTER_ALLOW_DIRECT=0 FAL_API_KEY=617f0fe4-c627-4119-9681-11af2c3e416a:3d618ccd0ee11ed82543801d7da96d1d # EXTERNAL SERVICES @@ -96,7 +101,7 @@ LOG_LEVEL=debug DOMAIN=localhost PROVIDER=docker -# UNLEASH +# UNLEASH (пустой URL = без запросов к Unleash; иначе в Docker «localhost» даёт /client/features Connection refused) FEATURE_FLAG_API_URL=https://gitlab.kisulkens.ru/api/v4/feature_flags/unleash/243 FEATURE_FLAG_INSTANCE_ID=glffct-ic8xsVF5eR9BaUySR-_w FEATURE_FLAG_APP_NAME=Production @@ -1,6 +1,11 @@ services: app: + extends: + file: docker-compose.yml + service: app build: + context: . + dockerfile: Dockerfile args: EXPORT_FLAGS: "--with test --with debug" volumes: @@ -8,15 +13,95 @@ services: ports: - "8000:8000" - "5678:5678" + networks: + - default + # Доступ к прокси на хосте (Windows/macOS/Linux): в poller_proxy.address — host.docker.internal:ПОРТ + extra_hosts: + - "host.docker.internal:host-gateway" + command: + - /bin/sh + - -c + - | + python manage.py create_indexes + python manage.py compilemessages + python -m uvicorn backend.asgi:application --host 0.0.0.0 --ws wsproto --http httptools --lifespan off --log-level info --reload --reload-dir /app + celery: + extends: + file: docker-compose.yml + service: celery + build: + context: . + dockerfile: Dockerfile + args: + EXPORT_FLAGS: "--with test --with debug" volumes: - ./:/app + networks: + - default + extra_hosts: + - "host.docker.internal:host-gateway" + command: + - /bin/sh + - -c + - watchmedo auto-restart --directory=/app --patterns='*.py' --recursive -- celery -A backend worker -l INFO --concurrency 3 + depends_on: + - celery-mdb + celery-beat: + extends: + file: docker-compose.yml + service: celery-beat + build: + context: . + dockerfile: Dockerfile + args: + EXPORT_FLAGS: "--with test --with debug" volumes: - ./:/app + networks: + - default + depends_on: + - celery-mdb + housekeeper: + extends: + file: docker-compose.yml + service: housekeeper + build: + context: . + dockerfile: Dockerfile + args: + EXPORT_FLAGS: "--with test --with debug" volumes: - ./:/app + networks: + - default + depends_on: + - db + - s3 + + cache-mdb: + extends: + file: docker-compose.yml + service: cache-mdb + networks: + - default + + celery-mdb: + extends: + file: docker-compose.yml + service: celery-mdb + networks: + - default + + channels-mdb: + extends: + file: docker-compose.yml + service: channels-mdb + networks: + - default + db: image: postgres:alpine restart: unless-stopped @@ -39,9 +124,9 @@ services: - "9001:9001" volumes: - pgdata: { } - s3data: { } + pgdata: {} + s3data: {} + static: {} networks: - infrastructure: - external: false \ No newline at end of file + infrastructure: {} \ No newline at end of file @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -5083,6 +5083,18 @@ allpy3 = ["cryptography (>=2.1.1)", "python3-saml (>=1.5.0)"] azuread = ["cryptography (>=2.1.1)"] saml = ["python3-saml (>=1.5.0)"] +[[package]] +name = "socksio" +version = "1.0.0" +description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, + {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, +] + [[package]] name = "sqlalchemy" version = "2.0.37" @@ -5578,6 +5590,49 @@ files = [ {file = "wasmer_compiler_cranelift-1.1.0-py3-none-any.whl", hash = "sha256:200fea80609cfb088457327acf66d5aa61f4c4f66b5a71133ada960b534c7355"}, ] +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["debug"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -5851,13 +5906,13 @@ files = [ [[package]] name = "yookassa" -version = "2.5.0" +version = "3.10.1" description = "YooKassa API SDK Python Library" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "yookassa-2.5.0.tar.gz", hash = "sha256:5ddb279d6e867c74b66549e3096196606b5f04bf4927bde2513072b7a08ee3ff"}, + {file = "yookassa-3.10.1.tar.gz", hash = "sha256:6eddff428be5eb86c001c79c334ce8ad265b52b315baba70dc5f19bec0f25955"}, ] [package.dependencies] @@ -5945,4 +6000,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "062dda7a1043076481446c18a95fa53ce5e21da80cdbe045621204ecf8a72811" +content-hash = "6d8d326ffa25ad4bc05f2319238735e45ac25671682a40765e79d603685635d4" @@ -13,7 +13,7 @@ django = "5.0.*" django-cors-headers = "^4.2.0" djangorestframework-simplejwt = "^5.2.2" djangorestframework = "^3.14.0" -yookassa = "^2.4.0" +yookassa = "^3.10.1" environs = "^9.5.0" dj-rest-auth = "^4.0.1" django-celery-beat = "^2.5.0" @@ -39,6 +39,7 @@ psycopg2-binary = "^2.9.10" filetype = "^1.2.0" django-polymorphic = "^3.1.0" httpx = "^0.28.1" +socksio = "^1.0.0" django-ninja = "^1.3.0" channels = {extras = ["daphne"], version = "^4.2.0"} channels-redis = "^4.2.1" @@ -82,6 +83,7 @@ pytest-cov = "^4.1.0" [tool.poetry.group.debug.dependencies] debugpy = "^1.8.1" +watchdog = "^6.0.0" [tool.ruff]