@@ -317,12 +317,11 @@ USE_I18N = True USE_TZ = True # Static files -STATIC_URL = 'djangostatic/' +STATIC_URL = f'{env.str("STATIC_PATH_PREFIX")}/' STATIC_ROOT = BASE_DIR / 'static' MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'static/media' - # API keys for Generative APIs OPENAI_API_KEY = env.str('OPENAI_API_KEY', default='defaultapikey') DEEPL_API_KEY = env.str('DEEPL_API_KEY', default='defaultapikey') @@ -342,7 +341,6 @@ UPSCALE_MULTIPLIER_HOST = env.str('UPSCALE_MULTIPLIER_HOST', 'packet:8080') # FILES ImageFile.LOAD_TRUNCATED_IMAGES = True - # Payments YOOKASSA_ACCOUNT_ID = env.str('YOOKASSA_ACCOUNT_ID', default='defaultapikey') YOOKASSA_SECRET_KEY = env.str('YOOKASSA_SECRET_KEY', default='defaultapikey') @@ -1,13 +1,19 @@ import re import subprocess import zipfile +from uuid import UUID + import docx2txt import fitz import openpyxl from io import BytesIO +from django.db.models.fields.files import FieldFile + +from authentication.models import CustomUserModel from ml_model.exceptions import UnrecognizedFileError +from tools.media.models import Voice, Preset class FileProcessingService: @@ -85,3 +91,19 @@ class FileProcessingService: else: return 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' + @classmethod + def get_voice_file( + cls, + voice_id: int | None, + preset_id: UUID | None, + user: CustomUserModel, + default_voice_slug: str = 'russian_1', + ) -> FieldFile: + if voice_id: + voice = Voice.objects.get(pk=voice_id, user=user) + elif preset_id: + voice = Preset.objects.get(uid=preset_id) + else: + voice = Preset.objects.get(slug=default_voice_slug) + return voice.file + @@ -1,19 +1,28 @@ import time +from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import timedelta from decimal import Decimal from io import BytesIO from typing import Any +import filetype import requests +from backend import settings from django.core.files import File -from replicate.exceptions import ModelError from messages.models import Message -from ml_model.exceptions import GenerationException, RequestBlocked +from ml_model.exceptions import ( + GenerationException, + CorruptedFileError, + FileExtensionNotSupported, +) +from ml_model.services.EmbeddingService import EmbeddingService +from ml_model.services.FileService import FileProcessingService from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run from payments.exceptions.insufficient_balance import InsufficientBalance from payments.selectors.payment_plan_selector import PaymentPlanSelector +from pydub import AudioSegment class Elevenlabs(SimpleService): @@ -21,6 +30,8 @@ class Elevenlabs(SimpleService): @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: + if file_exists: + return None chars = len(content) price = cls.TOKENS_PER_1K_CHARS * Decimal(chars) / Decimal(1000) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') @@ -30,36 +41,85 @@ class Elevenlabs(SimpleService): price = self.TOKENS_PER_1K_CHARS * Decimal(chars) / Decimal(1000) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, content: str, t: timedelta, audio_url: str, save: bool = True) -> list[Message]: + def save_results( + self, content: str, t: timedelta, audio_file: BytesIO, save: bool = True + ) -> list[Message]: msg = Message( content=content, content_object=self.store, elapsed_time=t, - file=File(BytesIO(requests.get(audio_url).content), '.mp3'), + file=File(audio_file, name='result.mp3'), ) if save: return Message.objects.bulk_create([msg]) return [msg] - def make(self, input_message: Message, save: bool = True) -> list[Message]: - cost = self.calculate_price(input_message.content) - if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < cost: - raise InsufficientBalance(balance, cost) - callback_data = { - 'text': input_message.content, - 'mode': 'voice_clone', - 'reference_audio': input_message.file.url, - } - if transcription := input_message.info.get('transcription', ''): - callback_data.update({'reference_text': transcription}) - start_time = time.time() + def _run_one_chunk(self, payload: dict[str, Any]) -> str | None: try: - result = replicate_run('qwen/qwen3-tts', callback_data) - except ModelError as exc: - if any(error in str(exc) for error in ('E005', 'E006', 'sexual')): - raise RequestBlocked - raise GenerationException from exc + return replicate_run('qwen/qwen3-tts', payload) + except Exception: + return None + def make(self, input_message: Message, save: bool = True) -> list[Message]: + raw_text = input_message.content + file_service = FileProcessingService + balance = PaymentPlanSelector(self.store.user).get_current_balance() + if file := input_message.file: + file_bytes = file.read() + kind = filetype.guess(file_bytes[:20]) + if not kind: + 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'): + raw_text = ( + file_service.get_file_data(file_extension, file_bytes) + .replace('\n', ' ') + ) + else: + raise FileExtensionNotSupported(['PDF', 'DOC', 'DOCX']) + max_affordable_chars = max( + int((balance * 1000) / self.TOKENS_PER_1K_CHARS) + - int((Decimal('0.1') * 1000) / self.TOKENS_PER_1K_CHARS), + 0, + ) + text_chunks = EmbeddingService.split_text_to_chunks( + raw_text[:max_affordable_chars], + chunk_size=1000, + overlap=0, + ) + if not text_chunks: + raise InsufficientBalance(balance, Decimal('1')) + reference_audio = file_service.get_voice_file( + voice_id=input_message.info.get('voice_id'), + preset_id=input_message.info.get('preset_id'), + user=self.store.user, + ) + callback_data = {'mode': 'voice_clone', 'reference_audio': reference_audio.url} + total_cost = self.calculate_price(''.join(text_chunks)) + if balance < total_cost: + raise InsufficientBalance(balance, total_cost) + start_time = time.time() + chunk_results: list[tuple[int, str, str | None]] = [] + with ThreadPoolExecutor(max_workers=settings.MAX_THREADS) as executor: + future_to_data = { + executor.submit(self._run_one_chunk, callback_data | {'text': chunk}): (index, chunk) + for index, chunk in enumerate(text_chunks) + } + for future in as_completed(future_to_data): + index, chunk = future_to_data[future] + chunk_results.append((index, chunk, future.result())) + chunk_results.sort(key=lambda item: item[0]) + parts = [url for _, _, url in chunk_results if url] + final_text = ''.join(chunk for _, chunk, url in chunk_results if url) + if not parts: + raise GenerationException + audio = AudioSegment.empty() + for part in parts: + audio += AudioSegment.from_file(BytesIO(requests.get(part).content)) + result = BytesIO() + audio.export(result, format='mp3') + result.seek(0) process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, content=input_message.content) + self.handle_invoice(input_message.content_object.model, content=final_text) return self.save_results(input_message.content, process_time, result, save) @@ -70,9 +70,20 @@ class Flux(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() version = input_message.info.get('version') + user_prompt = self.translate_prompt(input_message.content) + prompt = f""" + {user_prompt}. + Clear main subject and coherent scene context. + Balanced composition with readable foreground, midground, and background. + Natural, consistent lighting with clean shadows and good tonal balance. + Accurate proportions, clean geometry, and stable spatial relationships. + Crisp important details, clear textures, and minimal visual artifacts. + """ callback_data = dict( { - 'prompt': self.translate_prompt(input_message.content), + 'prompt': prompt, + 'go_fast': False, + 'output_quality': 100, **input_message.info, } ) @@ -17,17 +17,14 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector class Lyria(SimpleService): - TOKENS_COST = Decimal('0.6') # per 1 sec of output audio + TOKENS_COST = {'lyria-3': Decimal('12'), 'lyria-3-pro': Decimal('24')} @classmethod def predict_price(cls, content: str, file_exists: bool, info: dict[str, Any]) -> Decimal | None: - duration = info.get('duration', 32) - price = cls.TOKENS_COST * duration - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + return cls.TOKENS_COST[info['version']] - def calculate_price(self, duration: int) -> Decimal: - price = self.TOKENS_COST * duration - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + def calculate_price(self, version: str) -> Decimal: + return self.TOKENS_COST[version] def save_results(self, content: str, t: timedelta, audio: str, save: bool = True) -> list[Message]: msg = Message( @@ -41,23 +38,22 @@ class Lyria(SimpleService): return [msg] def make(self, input_message: Message, save: bool = True) -> list[Message]: - if ( - (balance := PaymentPlanSelector(self.store.user).get_current_balance()) - < (cost := self.TOKENS_COST * 32) + version = input_message.info.pop('version', 'lyria-3') + if (balance := PaymentPlanSelector(self.store.user).get_current_balance()) < ( + cost := self.TOKENS_COST[version] ): raise InsufficientBalance(balance, cost) - callback_data = { - 'prompt': self.translate_prompt(input_message.content), - **input_message.info - } + callback_data = {'prompt': input_message.content} + if file := input_message.file: + callback_data.update({'images': [file.url]}) start_time = time.time() try: - video = replicate_run(f'google/lyria-2', callback_data) + 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 process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, duration=32) + self.handle_invoice(input_message.content_object.model, version) msgs = self.save_results(input_message.content, process_time, video, save) return msgs @@ -52,9 +52,36 @@ class Midjourney(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() - translated_prompt = self.translate_prompt(input_message.content) - activation_prompt = f'mdjrny-v4 style a highly detailed {translated_prompt}' - callback_data = dict(prompt=activation_prompt, **input_message.info) + base_prompt = input_message.content + activation_prompt = f""" + Transform the user input into a single high-quality photographic image prompt. + USER INPUT: + {base_prompt} + RULES: + - Convert input into a realistic visual scene + - Keep one clear main subject unless multiple are explicitly required + - Ensure the scene looks like a real photograph, not an illustration or CGI + STYLE: + real-world photography, natural and believable scene, no artificial or cartoon look + COMPOSITION: + clear subject focus, natural framing, balanced but unposed layout, + real depth between foreground, subject, and background + CAMERA: + professional camera look, 35–85mm lens perspective, + natural depth of field, realistic focus and perspective + LIGHTING: + natural or practical lighting only (sunlight, indoor lamps, ambient light), + soft realistic shadows, no artificial glow or dramatic effects + COLOR: + neutral and realistic color reproduction, + slight natural tonal variation, no heavy grading or stylization + DETAIL: + real materials and textures, natural imperfections, + avoid smooth plastic-like or CGI surfaces + OUTPUT: + Return ONLY a single concise image-generation prompt describing the final scene. + """ + callback_data = dict(prompt=activation_prompt, prompt_optimizer=False,**input_message.info) results = 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) @@ -271,23 +271,10 @@ class ModelVoiceCloneAPIView(MediaAPIView): }, ) def post(self, request, model: str, *args, **kwargs): - if not (request.FILES.get('file') or request.data.get('file')): - try: - if voice_id := request.data.pop('voice_id', None): - voice = Voice.objects.get(pk=voice_id, user=request.user) - transcription = voice.transcription - elif preset_id := request.data.pop('preset_id', None): - voice = Preset.objects.get(uid=preset_id) - transcription = voice.metadata.get('transcription', '') - else: - voice = Preset.objects.get(slug='russian_1') - transcription = voice.metadata.get('transcription', '') - except (Voice.DoesNotExist, Preset.DoesNotExist): - return Response( - {'detail': _('Voice not found.')}, - status=HTTP_400_BAD_REQUEST, - ) - request.data.update( - {'file': voice.file, 'info': {'transcription': transcription, **request.data['info']}} - ) + info = request.data.get('info', {}) or {} + if voice_id := request.data.get('voice_id'): + info.update({'voice_id': voice_id}) + elif preset_id := request.data.get('preset_id'): + info.update({'preset_id': preset_id}) + request.data.update({'info': info}) return super().post(request, model, *args, **kwargs) @@ -1,6 +1,7 @@ # CORE SETTINGS SECRET_KEY=testtest DEBUG=true +STATIC_PATH_PREFIX=static/ # NEURON MODELS OPENAI_API_KEY=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 @@ -67,6 +68,7 @@ YOOKASSA_ACCOUNT_ID=322563 YOOKASSA_SECRET_KEY=test_i_Au0KbXnOmdVf1icljT7v4CuDHLG8mXVkyofJQFBns YOOKASSA_RESULT_PAYMENT_URL=http://localhost RECURRENT_RATE=days + USER_CONFIRMATION_URL='https://app.air.fail/confirm' USER_PASSWORD_RESET_URL='https://app.air.fail/changePassword' INVITATION_RESPONSE_URL='https://app.air.fail/business/confirm' @@ -90,4 +92,6 @@ CHANNELS_PORT_MDB=6379 LOG_LEVEL=debug DOMAIN=localhost -PROVIDER=docker \ No newline at end of file +PROVIDER=docker + +COMPOSE_FILE=docker-compose.yml:docker-compose.local.yml \ No newline at end of file @@ -1,86 +1,66 @@ stages: - - Lint - - Test - Build - Deploy default: image: docker:cli - services: - - docker:dind before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin -build_staging: - stage: Build - script: - - touch .env && export ENV=.env - - export TAG=$CI_COMMIT_SHA - - docker compose build - - docker compose push - only: - - staging - when: on_success - -build_production: +build: stage: Build + variables: script: - - touch .env && export ENV=.env - - export TAG=latest - - docker rmi $CI_REGISTRY_IMAGE:latest || true - - docker compose -f stack.yml build - - docker compose -f stack.yml push + - touch .env + - docker compose build --push only: - main + - staging when: on_success -deploy_staging: +.deploy_template: &default_deploy_job stage: Deploy + services: + - docker:dind variables: - DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376 + ENVIRONMENT: $CI_ENVIRONMENT_TIER DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" - environment: - name: Staging - deployment_tier: staging - url: $DOMAIN - only: - - staging + COMPOSE_ENV_FILES: $ENV + COMPOSE_REMOVE_ORPHANS: true + COMPOSE_PROJECT_NAME: $CI_PROJECT_NAME when: on_success before_script: - mkdir -p $DOCKER_CERT_PATH - - echo "$STAGING_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem - - echo "$STAGING_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem - - echo "$STAGING_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem + - export ENVNAME=$(echo ${ENVIRONMENT} | tr '[:lower:]' '[:upper:]') + - export DOCKER_HOST="tcp://$(printenv "${ENVNAME}_CLUSTER_HOST"):2376" + - cp "$(printenv ${ENVNAME}_CLUSTER_CA)" $DOCKER_CERT_PATH/ca.pem + - cp "$(printenv ${ENVNAME}_CLUSTER_CERT)" $DOCKER_CERT_PATH/cert.pem + - cp "$(printenv ${ENVNAME}_CLUSTER_KEY)" $DOCKER_CERT_PATH/key.pem - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin script: - - export TAG=$CI_COMMIT_SHA - export RELEASE=$(echo -n $(date '+%D %X') | md5sum | awk '{print $1}') - - echo -e "\nRELEASE=$RELEASE\nENVIRONMENT=$CI_ENVIRONMENT_TIER" >> $ENV - - docker compose pull - - docker compose --project-name air-backend up -d + - if [ "$(docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null)" = "active" ]; then + docker stack deploy --prune --with-registry-auth -c <(docker compose config | grep -v "^name:") $COMPOSE_PROJECT_NAME; + else + docker compose up -d; + fi + + +deploy_staging: + <<: *default_deploy_job + environment: + name: Staging + deployment_tier: staging + url: $DOMAIN + only: + - staging deploy_production: - stage: Deploy - variables: - DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376 - DOCKER_TLS_VERIFY: 1 - DOCKER_CERT_PATH: "/certs" + <<: *default_deploy_job environment: name: Production deployment_tier: production url: $DOMAIN only: - - main - when: on_success - before_script: - - mkdir -p $DOCKER_CERT_PATH - - echo "$PRODUCTION_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem - - echo "$PRODUCTION_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem - - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin - script: - - export TAG=latest - - export RELEASE=$(echo -n $(date '+%D %X') | md5sum | awk '{print $1}') - - echo -e "\nRELEASE=$RELEASE\nENVIRONMENT=$CI_ENVIRONMENT_TIER" >> $ENV - - docker stack deploy --prune --with-registry-auth --resolve-image=always --compose-file stack.yml --detach backend \ No newline at end of file + - main \ No newline at end of file @@ -1,10 +1,12 @@ -FROM python:3.12-slim as build +FROM python:3.12-slim AS build -WORKDIR /code +WORKDIR /app -COPY pyproject.toml poetry.lock /code/ +ARG EXPORT_FLAGS="--only main" + +COPY pyproject.toml poetry.lock /app/ RUN --mount=type=cache,target=/root/.cache/pip pip install poetry && poetry self add poetry-plugin-export -RUN poetry export --only main --output=requirements.txt +RUN poetry export ${EXPORT_FLAGS} --output=requirements.txt FROM python:3.12-slim @@ -14,16 +16,15 @@ ENV PYTHONFAULTHANDLER=1 \ PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DEFAULT_TIMEOUT=100 -WORKDIR /code +WORKDIR /app -COPY --from=build /code/requirements.txt . +COPY --from=build /app/requirements.txt . RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ --mount=target=/var/cache/apt,type=cache,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update \ - && apt-get -y --no-install-recommends install -y gettext \ - && apt-get -y install antiword + && apt-get -y --no-install-recommends install gettext antiword ffmpeg RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt @@ -1,31 +0,0 @@ -FROM python:3.12-slim as build - -WORKDIR /code - -COPY pyproject.toml poetry.lock /code/ -RUN --mount=type=cache,target=/root/.cache/pip pip install poetry && poetry self add poetry-plugin-export - -RUN poetry export --with test --with debug --with dev --output=requirements.txt - -FROM python:3.12-slim - -ENV PYTHONFAULTHANDLER=1 \ - PYTHONHASHSEED=random \ - PIP_NO_CACHE_DIR=on \ - PIP_DISABLE_PIP_VERSION_CHECK=on \ - PIP_DEFAULT_TIMEOUT=100 - -WORKDIR /code - -COPY --from=build /code/requirements.txt . - -RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ - --mount=target=/var/cache/apt,type=cache,sharing=locked \ - rm -f /etc/apt/apt.conf.d/docker-clean \ - && apt-get update \ - && apt-get -y --no-install-recommends install gettext \ - && apt-get -y install antiword - -RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt - -COPY . . @@ -1,26 +0,0 @@ -.DEFAULT_GOAL=start - -start: - cp -n .env.dist .env - docker compose -f docker-compose.debug.yml --project-name air up -.PHONY=start - -rebuild: - cp -n .env.dist .env - docker compose -f docker-compose.debug.yml --project-name air up --build -.PHONY=rebuild - -stop: - cp -n .env.dist .env - docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -.PHONY=stop - -cleanup: - cp -n .env.dist .env - docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -v -.PHONY=cleanup - -full-cleanup: - cp -n .env.dist .env - docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -v --rmi local -.PHONY=full-cleanup \ No newline at end of file @@ -1,123 +0,0 @@ -services: - app: - restart: unless-stopped - container_name: app - user: '1000' - build: - context: . - dockerfile: Dockerfile.dev - command: - - /bin/sh - - -c - - | - python manage.py initialize_buckets - python manage.py collectstatic --no-input - python manage.py compilemessages - (python manage.py createsuperuser --no-input || true) - python -m uvicorn backend.asgi:application --host 0.0.0.0 --ws wsproto --http httptools --lifespan off --log-level info --reload - volumes: - - .:/code - ports: - - "8000:8000" - - "5678:5678" - env_file: - - .env - depends_on: - migrator: - condition: service_completed_successfully - cache-mdb: - condition: service_started - s3: - condition: service_started - db: - condition: service_started - - migrator: - restart: on-failure:3 - container_name: migrator - volumes: - - .:/code - build: - context: . - dockerfile: Dockerfile.dev - command: - - /bin/sh - - -c - - python manage.py migrate - env_file: - - .env - - cache-mdb: - container_name: cache-mdb - image: redis:alpine - restart: unless-stopped - - celery-mdb: - container_name: celery-mdb - image: redis:alpine - restart: unless-stopped - - channels-mdb: - container_name: channels-mdb - image: redis:alpine - restart: unless-stopped - - celery: - restart: unless-stopped - container_name: celery - build: - context: . - dockerfile: Dockerfile.dev - command: celery -A backend worker -l INFO --concurrency 1 - volumes: - - .:/code - env_file: - - .env - environment: - - C_FORCE_ROOT=true - depends_on: - - celery-mdb - - celery_beat: - restart: unless-stopped - container_name: celery-beat - build: - context: . - dockerfile: Dockerfile.dev - command: celery -A backend beat -l INFO - volumes: - - .:/code - env_file: - - .env - depends_on: - - celery-mdb - db: - restart: unless-stopped - container_name: db - image: postgres:alpine - volumes: - - pgdata:/var/lib/postgresql/data - env_file: - - .env - ports: - - "5432:5432" - - s3: - image: webcenter/alpine-minio - container_name: s3 - restart: unless-stopped - volumes: - - s3data:/data - env_file: - - .env - ports: - - "9000:9000" - - "9001:9001" - -networks: - default: - name: "air" - -volumes: - pgdata: { } - s3data: { } @@ -0,0 +1,47 @@ +services: + app: + build: + args: + EXPORT_FLAGS: "--with test --with debug" + volumes: + - ./:/app + ports: + - "8000:8000" + - "5678:5678" + celery: + volumes: + - ./:/app + celery-beat: + volumes: + - ./:/app + housekeeper: + volumes: + - ./:/app + db: + image: postgres:alpine + restart: unless-stopped + volumes: + - pgdata:/var/lib/postgresql + env_file: + - .env + ports: + - "5432:5432" + + s3: + image: webcenter/alpine-minio + restart: unless-stopped + volumes: + - s3data:/data + env_file: + - .env + ports: + - "9000:9000" + - "9001:9001" + +volumes: + pgdata: { } + s3data: { } + +networks: + infrastructure: + external: false \ No newline at end of file @@ -1,144 +1,179 @@ +x-app-config: &app-config + image: ${CI_REGISTRY_IMAGE:-air/backend}:${CI_COMMIT_SHA:-latest} + restart: on-failure:3 + environment: + - RELEASE + - ENVIRONMENT + +x-default-deploy: &default-deploy + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + - node.labels.type != observer + services: app: - restart: unless-stopped - image: $CI_REGISTRY_IMAGE:$TAG + <<: *app-config build: context: . dockerfile: Dockerfile volumes: - static:/code/static - networks: - - infrastructure - - default command: - /bin/sh - -c - | - python manage.py collectstatic --no-input python manage.py compilemessages python -m uvicorn backend.asgi:application --host 0.0.0.0 --ws wsproto --http httptools --lifespan off --log-level info - labels: + networks: + - default + - infrastructure + labels: &app-labels - traefik.enable=true - - traefik.docker.network=infrastructure + - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure} - - traefik.http.routers.backend-http.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/api/v1`) + - traefik.http.routers.backend-http.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/api/v1`) - traefik.http.routers.backend-http.entrypoints=web - - traefik.http.routers.backend-http.tls=false - traefik.http.routers.backend-http.service=backend - traefik.http.routers.backend-http.middlewares=sts-header@file,https-redirect@file - - traefik.http.routers.backend-https.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/api/v1`) + - traefik.http.routers.backend-https.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/api/v1`) - traefik.http.routers.backend-https.entrypoints=websecure - traefik.http.routers.backend-https.service=backend - traefik.http.routers.backend-https.tls=true - traefik.http.routers.backend-https.tls.certresolver=defaultresolver - - traefik.http.routers.backend-admin-http.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/admin`) + - traefik.http.routers.backend-public-api-http.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/public`) + - traefik.http.routers.backend-public-api-http.entrypoints=web + - traefik.http.routers.backend-public-api-http.service=backend + - traefik.http.routers.backend-public-api-http.middlewares=sts-header@file,https-redirect@file + + - traefik.http.routers.backend-public-api-https.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/public`) + - traefik.http.routers.backend-public-api-https.entrypoints=websecure + - traefik.http.routers.backend-public-api-https.service=backend + - traefik.http.routers.backend-public-api-https.tls=true + - traefik.http.routers.backend-public-api-https.tls.certresolver=defaultresolver + + - traefik.http.routers.backend-admin-http.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/admin`) - traefik.http.routers.backend-admin-http.entrypoints=web - - traefik.http.routers.backend-admin-http.tls=false - traefik.http.routers.backend-admin-http.service=backend - traefik.http.routers.backend-admin-http.middlewares=sts-header@file,https-redirect@file,internal-allowlist@file - - traefik.http.routers.backend-admin-https.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/admin`) + - traefik.http.routers.backend-admin-https.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/admin`) - traefik.http.routers.backend-admin-https.entrypoints=websecure - traefik.http.routers.backend-admin-https.service=backend - traefik.http.routers.backend-admin-https.tls=true - traefik.http.routers.backend-admin-https.tls.certresolver=defaultresolver - traefik.http.routers.backend-admin-https.middlewares=internal-allowlist@file - - traefik.http.routers.backend-public-api-http.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/public`) - - traefik.http.routers.backend-public-api-http.entrypoints=web - - traefik.http.routers.backend-public-api-http.tls=false - - traefik.http.routers.backend-public-api-http.service=backend - - traefik.http.routers.backend-public-api-http.middlewares=sts-header@file,https-redirect@file - - - traefik.http.routers.backend-public-api-https.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/public`) - - traefik.http.routers.backend-public-api-https.entrypoints=websecure - - traefik.http.routers.backend-public-api-https.service=backend - - traefik.http.routers.backend-public-api-https.tls=true - - traefik.http.routers.backend-public-api-https.tls.certresolver=defaultresolver - - traefik.http.services.backend.loadbalancer.server.port=8000 - env_file: - - $ENV - depends_on: - migrator: - condition: service_completed_successfully - cache-mdb: - condition: service_started - - migrator: - restart: on-failure:1 - image: $CI_REGISTRY_IMAGE:$TAG + deploy: + replicas: ${APP_REPLICAS:-1} + labels: *app-labels + <<: [*default-deploy] + env_file: ${ENV:-.env} + + housekeeper: + <<: *app-config + deploy: + replicas: 1 + <<: [*default-deploy] command: - /bin/sh - -c - - python manage.py migrate - env_file: - - $ENV + - | + python manage.py initialize_buckets + python manage.py collectstatic --no-input + python manage.py migrate + env_file: ${ENV:-.env} celery: - restart: unless-stopped - image: $CI_REGISTRY_IMAGE:$TAG - command: celery -A backend worker -l INFO --concurrency 8 - env_file: - - $ENV + <<: *app-config + command: celery -A backend worker -l INFO --concurrency 3 + deploy: + replicas: 1 + <<: [*default-deploy] + env_file: ${ENV:-.env} environment: - C_FORCE_ROOT=true - depends_on: - - celery-mdb + - RELEASE + - ENVIRONMENT - celery_beat: - restart: unless-stopped - image: $CI_REGISTRY_IMAGE:$TAG + celery-beat: + <<: *app-config command: celery -A backend beat -l INFO - env_file: - - $ENV - depends_on: - - celery-mdb + deploy: + replicas: 1 + <<: [*default-deploy] + env_file: ${ENV:-.env} static-server: - image: $CI_REGISTRY_IMAGE/static-server:$CI_COMMIT_SHA - build: - context: nginx - dockerfile: Dockerfile - labels: + 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;' + labels: &static-labels - traefik.enable=true - - traefik.docker.network=infrastructure - - traefik.http.routers.backend-static.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/djangostatic`) - - traefik.http.routers.backend-static.entrypoints=web,websecure - - traefik.http.routers.backend-static.tls=true - - traefik.http.routers.backend-static.tls.certresolver=defaultresolver - - traefik.http.routers.backend-static.service=backend-static + - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure} + + - traefik.http.routers.backend-static-http.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/${STATIC_PATH_PREFIX:-static}`) + - traefik.http.routers.backend-static-http.entrypoints=web + - traefik.http.routers.backend-static-http.service=backend-static + - traefik.http.routers.backend-static-http.middlewares=sts-header@file,https-redirect@file + + - traefik.http.routers.backend-static-https.rule=HostRegexp(`$DOMAIN`) && PathPrefix(`/${STATIC_PATH_PREFIX:-static}`) + - traefik.http.routers.backend-static-https.entrypoints=websecure + - traefik.http.routers.backend-static-https.tls=true + - traefik.http.routers.backend-static-https.tls.certresolver=defaultresolver + - traefik.http.routers.backend-static-https.service=backend-static + - traefik.http.services.backend-static.loadbalancer.server.port=80 - - traefik.http.middlewares.backend-static.redirectscheme.scheme=https - - traefik.http.middlewares.backend-static.redirectscheme.permanent=true + deploy: + replicas: 1 + labels: *static-labels + <<: [*default-deploy] networks: - infrastructure volumes: - static:/var/www/static - env_file: - - $ENV + env_file: ${ENV:-.env} cache-mdb: image: redis:alpine - restart: unless-stopped + deploy: + replicas: 1 + <<: [*default-deploy] celery-mdb: image: redis:alpine - restart: unless-stopped + deploy: + replicas: 1 + <<: [*default-deploy] channels-mdb: image: redis:alpine - restart: unless-stopped + deploy: + replicas: 1 + <<: [*default-deploy] networks: - default: {} infrastructure: + name: ${PROXY_NETWORK:-infrastructure} external: true volumes: static: name: "backend-static" locales: - name: "backend-locales" + name: "backend-locales" \ No newline at end of file @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -239,7 +239,7 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -395,29 +395,13 @@ yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] -[[package]] -name = "celery-stubs" -version = "0.1.3" -description = "celery stubs" -optional = false -python-versions = "*" -groups = ["typing"] -files = [ - {file = "celery-stubs-0.1.3.tar.gz", hash = "sha256:0fb5345820f8a2bd14e6ffcbef2d10181e12e40f8369f551d7acc99d8d514919"}, - {file = "celery_stubs-0.1.3-py3-none-any.whl", hash = "sha256:dfb9ad27614a8af028b2055bb4a4ae99ca5e9a8d871428a506646d62153218d7"}, -] - -[package.dependencies] -mypy = ">=0.950" -typing-extensions = ">=4.2.0" - [[package]] name = "certifi" version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -552,7 +536,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -1051,7 +1035,7 @@ version = "5.0.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "Django-5.0.11-py3-none-any.whl", hash = "sha256:09e8128f717266bf382d82ffa4933f13da05d82579abf008ede86acb15dec88b"}, {file = "Django-5.0.11.tar.gz", hash = "sha256:e7d98fa05ce09cb3e8d5ad6472fb602322acd1740bfdadc29c8404182d664f65"}, @@ -1279,44 +1263,6 @@ redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" [package.extras] hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] -[[package]] -name = "django-stubs" -version = "4.2.7" -description = "Mypy stubs for Django" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"}, - {file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"}, -] - -[package.dependencies] -django = "*" -django-stubs-ext = ">=4.2.7" -types-pytz = "*" -types-PyYAML = "*" -typing-extensions = "*" - -[package.extras] -compatible-mypy = ["mypy (>=1.7.0,<1.8.0)"] - -[[package]] -name = "django-stubs-ext" -version = "5.1.2" -description = "Monkey-patching and extensions for django-stubs" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "django_stubs_ext-5.1.2-py3-none-any.whl", hash = "sha256:6c559214538d6a26f631ca638ddc3251a0a891d607de8ce01d23d3201ad8ad6c"}, - {file = "django_stubs_ext-5.1.2.tar.gz", hash = "sha256:421c0c3025a68e3ab8e16f065fad9ba93335ecefe2dd92a0cff97a665680266c"}, -] - -[package.dependencies] -django = "*" -typing-extensions = "*" - [[package]] name = "django-timezone-field" version = "7.1" @@ -1372,30 +1318,6 @@ lint = ["flake8", "isort", "pep8"] python-jose = ["python-jose (==3.3.0)"] test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] -[[package]] -name = "djangorestframework-stubs" -version = "3.14.5" -description = "PEP-484 stubs for django-rest-framework" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"}, - {file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"}, -] - -[package.dependencies] -django-stubs = ">=4.2.7" -requests = ">=2.0.0" -types-PyYAML = ">=5.4.3" -types-requests = ">=0.1.12" -typing-extensions = ">=3.10.0" - -[package.extras] -compatible-mypy = ["django-stubs[compatible-mypy]", "mypy (>=1.7.0,<1.8.0)"] -coreapi = ["coreapi (>=2.0.0)"] -markdown = ["types-Markdown (>=0.1.5)"] - [[package]] name = "dnspython" version = "2.7.0" @@ -1744,13 +1666,13 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0.dev0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" proto-plus = [ - {version = ">=1.22.3,<2.0.0dev"}, - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0.dev0"}, + {version = ">=1.25.0,<2.0.0.dev0", markers = "python_version >= \"3.13\""}, ] -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" [[package]] name = "google-api-core" @@ -1767,18 +1689,18 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" -grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ - {version = ">=1.22.3,<2.0.0dev"}, - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0.dev0"}, + {version = ">=1.25.0,<2.0.0.dev0", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpc = ["grpcio (>=1.33.2,<2.0.dev0)", "grpcio (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -2075,28 +1997,6 @@ googleapis-common-protos = ">=1.5.5" grpcio = ">=1.62.3" protobuf = ">=4.21.6" -[[package]] -name = "gunicorn" -version = "23.0.0" -description = "WSGI HTTP Server for UNIX" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, - {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] - [[package]] name = "h11" version = "0.14.0" @@ -2301,7 +2201,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -2479,7 +2379,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -2928,21 +2828,6 @@ dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] tests = ["pytest", "simplejson"] -[[package]] -name = "memory-profiler" -version = "0.61.0" -description = "A module for monitoring memory usage of a python program" -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84"}, - {file = "memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0"}, -] - -[package.dependencies] -psutil = "*" - [[package]] name = "minio" version = "7.2.14" @@ -3150,72 +3035,13 @@ files = [ {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, ] -[[package]] -name = "mypy" -version = "1.14.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, - {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, - {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, - {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, - {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, - {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, - {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, - {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, - {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, - {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, - {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, - {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, - {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, - {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, - {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, - {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, - {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, - {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, - {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, - {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, - {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, - {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, - {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, - {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, - {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, - {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, - {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, - {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, - {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, - {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, - {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, - {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, - {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, - {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, - {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, - {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, - {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, - {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -3299,21 +3125,6 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] -[[package]] -name = "objgraph" -version = "3.6.2" -description = "Draws Python object reference graphs with graphviz" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "objgraph-3.6.2-py3-none-any.whl", hash = "sha256:8114c97712291c3ba30d882406a384d0a7651b307ea9a06e0d83836ccde85e15"}, - {file = "objgraph-3.6.2.tar.gz", hash = "sha256:00b9f2f40f7422e3c7f45a61c4dafdaf81f03ff0649d6eaec866f01030e51ad8"}, -] - -[package.extras] -ipython = ["graphviz"] - [[package]] name = "openai" version = "1.59.7" @@ -3701,7 +3512,7 @@ files = [ ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" +protobuf = ">=3.19.0,<6.0.0.dev0" [package.extras] testing = ["google-api-core (>=1.31.5)"] @@ -3727,30 +3538,6 @@ files = [ {file = "protobuf-4.25.5.tar.gz", hash = "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584"}, ] -[[package]] -name = "psutil" -version = "7.0.0" -description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, - {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, - {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, - {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, - {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, - {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, - {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, -] - -[package.extras] -dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] - [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -4065,6 +3852,18 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0 toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] +[[package]] +name = "pydub" +version = "0.25.1" +description = "Manipulate audio with an simple and easy high level interface" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, + {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, +] + [[package]] name = "pyjwt" version = "2.10.1" @@ -4083,21 +3882,6 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] -[[package]] -name = "pympler" -version = "1.1" -description = "A development tool to measure, monitor and analyze the memory behavior of Python objects." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "Pympler-1.1-py3-none-any.whl", hash = "sha256:5b223d6027d0619584116a0cbc28e8d2e378f7a79c1e5e024f9ff3b673c58506"}, - {file = "pympler-1.1.tar.gz", hash = "sha256:1eaa867cb8992c218430f1708fdaccda53df064144d1c5656b1e6f1ee6000424"}, -] - -[package.dependencies] -pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} - [[package]] name = "pymupdf" version = "1.26.3" @@ -4184,16 +3968,16 @@ image = ["Pillow"] [[package]] name = "pyroscope-io" -version = "0.8.11" +version = "0.8.16" description = "Pyroscope Python integration" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pyroscope_io-0.8.11-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:644dbd81b162b6d678ef9989649bf936c62373fd7bba16fb6490272f453ff159"}, - {file = "pyroscope_io-0.8.11-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:2df4cd4cbfb451c27cad20f905bf612ffc306a96820e316f7575c84433eadb24"}, - {file = "pyroscope_io-0.8.11-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50164c96cf5533ce795a114c6181c5d2aa162c9dae59277b6ac557820c411b7a"}, - {file = "pyroscope_io-0.8.11-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a415072dd7e8964d66001fff2446d7426a505cfca1a89f3c912314537622a4cd"}, + {file = "pyroscope_io-0.8.16-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:e07edcfd59f5bdce42948b92c9b118c824edbd551730305f095a6b9af401a9e8"}, + {file = "pyroscope_io-0.8.16-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:dc98355e27c0b7b61f27066500fe1045b70e9459bb8b9a3082bc4755cb6392b6"}, + {file = "pyroscope_io-0.8.16-py2.py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:86f0f047554ff62bd92c3e5a26bc2809ccd467d11fbacb9fef898ba299dbda59"}, + {file = "pyroscope_io-0.8.16-py2.py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b91ce5b240f8de756c16a17022ca8e25ef8a4eed461c7d074b8a0841cf7b445"}, ] [package.dependencies] @@ -4430,37 +4214,6 @@ files = [ {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] -[[package]] -name = "pywin32" -version = "311" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -groups = ["dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, - {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, - {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, - {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, - {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, - {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, - {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, - {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, - {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, - {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, - {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, - {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, - {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, - {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, - {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, - {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, - {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, - {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, - {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, - {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, -] - [[package]] name = "pyyaml" version = "6.0.2" @@ -4684,7 +4437,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -5186,7 +4939,7 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -5374,61 +5127,13 @@ all = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] dev = ["pep8 (>=1.6.2)", "pyenchant (>=1.6.6)", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "sphinx (>=1.2.3)", "sphinx-rtd-theme (>=0.1.9)", "sphinxcontrib-spelling (>=2.1.2)", "tox (>=2.1.1)", "tox-gh-actions (>=2.2.0)", "twine (>=1.6.5)", "wheel"] twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] -[[package]] -name = "types-pillow" -version = "10.2.0.20240822" -description = "Typing stubs for Pillow" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3"}, - {file = "types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d"}, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, -] - -[[package]] -name = "types-pytz" -version = "2024.2.0.20241221" -description = "Typing stubs for pytz" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5"}, - {file = "types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20241230" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.8" -groups = ["typing"] -files = [ - {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, - {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, -] - [[package]] name = "types-requests" version = "2.32.0.20241016" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, @@ -5443,7 +5148,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["main", "test", "typing"] +groups = ["main", "test"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -5471,12 +5176,11 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] -markers = {typing = "sys_platform == \"win32\""} [[package]] name = "uritemplate" @@ -5496,7 +5200,7 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main", "typing"] +groups = ["main"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, @@ -5876,4 +5580,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "edd413e469f523e2a363dd9b28c0a4aeea799ac371f2995b0b327c933a456b56" +content-hash = "be19d70adce9ad107dd4b610d061d2477ea062690bad3e553131bf6d61acbf11" @@ -57,12 +57,12 @@ deepl = "^1.21.1" python-docx = "^1.1.2" pymupdf = "^1.26.1" pyroscope-io = "^0.8.11" -gunicorn = "^23.0.0" uvicorn = "^0.35.0" httptools = "^0.6.4" wsproto = "^1.2.0" sentry-sdk = {extras = ["django"], version = "^2.39.0"} googletrans = "^4.0.2" +pydub = "^0.25.1" [tool.poetry.group.test.dependencies] @@ -78,26 +78,10 @@ pytest-factoryboy = "^2.5.1" pytest-cov = "^4.1.0" -[tool.poetry.group.typing.dependencies] -mypy = "^1.5.1" -django-stubs = "^4.2.4" -types-pillow = "^10.0.0.3" -types-python-dateutil = "^2.8.19.14" -types-requests = "^2.31.0.2" -celery-stubs = "^0.1.3" -djangorestframework-stubs = "^3.14.2" - - [tool.poetry.group.debug.dependencies] debugpy = "^1.8.1" -[tool.poetry.group.dev.dependencies] -psutil = "^7.0.0" -objgraph = "^3.6.2" -pympler = "^1.1" -memory-profiler = "^0.61.0" - [tool.ruff] exclude = [ ".bzr", @@ -1,232 +0,0 @@ -services: - app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - build: - context: . - dockerfile: Dockerfile - tags: - - $CI_REGISTRY_IMAGE:latest - - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - volumes: - - static:/code/static - command: - - /bin/sh - - -c - - | - python manage.py collectstatic --no-input - python manage.py compilemessages - python -m uvicorn backend.asgi:application --host 0.0.0.0 --ws wsproto --http httptools --lifespan off --log-level info - networks: - - default - - infrastructure - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 1s - order: start-first - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - - node.labels.type != observer - labels: - - traefik.enable=true - - traefik.swarm.network=infrastructure - - - traefik.http.routers.backend-http.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/api/v1`) - - traefik.http.routers.backend-http.entrypoints=web - - traefik.http.routers.backend-http.tls=false - - traefik.http.routers.backend-http.service=backend - - traefik.http.routers.backend-http.middlewares=sts-header@file,https-redirect@file - - - traefik.http.routers.backend-https.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/api/v1`) - - traefik.http.routers.backend-https.entrypoints=websecure - - traefik.http.routers.backend-https.service=backend - - traefik.http.routers.backend-https.tls=true - - traefik.http.routers.backend-https.tls.certresolver=defaultresolver - - - traefik.http.routers.backend-public-api-http.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/public`) - - traefik.http.routers.backend-public-api-http.entrypoints=web - - traefik.http.routers.backend-public-api-http.tls=false - - traefik.http.routers.backend-public-api-http.service=backend - - traefik.http.routers.backend-public-api-http.middlewares=sts-header@file,https-redirect@file - - - traefik.http.routers.backend-public-api-https.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/public`) - - traefik.http.routers.backend-public-api-https.entrypoints=websecure - - traefik.http.routers.backend-public-api-https.service=backend - - traefik.http.routers.backend-public-api-https.tls=true - - traefik.http.routers.backend-public-api-https.tls.certresolver=defaultresolver - - - traefik.http.routers.backend-admin-http.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/admin`) - - traefik.http.routers.backend-admin-http.entrypoints=web - - traefik.http.routers.backend-admin-http.tls=false - - traefik.http.routers.backend-admin-http.service=backend - - traefik.http.routers.backend-admin-http.middlewares=sts-header@file,https-redirect@file,internal-allowlist@file - - - traefik.http.routers.backend-admin-https.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/admin`) - - traefik.http.routers.backend-admin-https.entrypoints=websecure - - traefik.http.routers.backend-admin-https.service=backend - - traefik.http.routers.backend-admin-https.tls=true - - traefik.http.routers.backend-admin-https.tls.certresolver=defaultresolver - - traefik.http.routers.backend-admin-https.middlewares=internal-allowlist@file - - - traefik.http.services.backend.loadbalancer.server.port=8000 - env_file: - - $ENV - - migrator: - image: $CI_REGISTRY_IMAGE:latest - deploy: - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - - node.labels.type != observer - command: - - /bin/sh - - -c - - python manage.py migrate - env_file: - - $ENV - - celery: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - command: celery -A backend worker -l INFO --concurrency 3 - networks: - - default - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 10s - order: start-first - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - - node.labels.type != observer - env_file: - - $ENV - environment: - - C_FORCE_ROOT=true - - celery_beat: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - command: celery -A backend beat -l INFO - networks: - - default - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 10s - order: start-first - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - env_file: - - $ENV - - static-server: - image: $CI_REGISTRY_IMAGE/static-server:$CI_COMMIT_SHA - build: - context: nginx - dockerfile: Dockerfile - deploy: - replicas: 1 - placement: - constraints: - - node.role == worker - - node.labels.type != observer - labels: - - traefik.enable=true - - traefik.$PROVIDER.network=infrastructure - - traefik.http.routers.backend-static.rule=(Host(`$DOMAIN`) || Host(`$RESERVE_DOMAIN`)) && PathPrefix(`/djangostatic`) - - traefik.http.routers.backend-static.entrypoints=web,websecure - - traefik.http.routers.backend-static.tls=true - - traefik.http.routers.backend-static.tls.certresolver=defaultresolver - - traefik.http.routers.backend-static.service=backend-static - - traefik.http.services.backend-static.loadbalancer.server.port=80 - - traefik.http.middlewares.backend-static.redirectscheme.scheme=https - - traefik.http.middlewares.backend-static.redirectscheme.permanent=true - networks: - - infrastructure - volumes: - - static:/var/www/static - env_file: - - $ENV - - cache-mdb: - image: redis:alpine - deploy: - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - - node.labels.type != observer - - celery-mdb: - image: redis:alpine - networks: - - default - deploy: - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - - node.labels.type != observer - - channels-mdb: - image: redis:alpine - deploy: - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - - node.labels.type != observers - -networks: - infrastructure: - name: infrastructure - external: true - default: {} - -volumes: - static: - name: "backend-static" - locales: - name: "backend-locales" \ No newline at end of file