From cebb87fa73c91db7acc2b25408a359ad84c64e2a Mon Sep 17 00:00:00 2001 From: ITQ Date: Fri, 21 Feb 2025 18:00:42 +0300 Subject: [PATCH] refactor: global codebase refactoring --- solution/assets/images/backend-coverage.png | Bin 0 -> 68272 bytes solution/infrastructure/.gitignore | 2 +- solution/infrastructure/backend/.env.template | 5 +- solution/services/backend/.env.template | 9 -- .../services/backend/Dockerfile.staticfiles | 2 +- .../services/backend/api/v1/clients/tests.py | 1 - .../advertiser/tests/test_advertiser_model.py | 1 + .../services/backend/apps/campaign/models.py | 49 +++---- .../backend/apps/campaign/validators.py | 61 ++++++-- .../backend/config/notifiers/__init__.py | 0 .../backend/config/notifiers/telegram.py | 133 ------------------ solution/services/backend/config/settings.py | 41 +----- .../integrations/yandexai/healthcheck.py | 2 + solution/tests/README.md | 4 +- solution/tests/e2e/README.md | 2 +- solution/tests/e2e/pyproject.toml | 1 - .../e2e/tests/test_ad_text_generation.py | 14 +- .../tests/e2e/tests/test_backend_health.py | 2 +- 18 files changed, 89 insertions(+), 240 deletions(-) create mode 100644 solution/assets/images/backend-coverage.png delete mode 100644 solution/services/backend/config/notifiers/__init__.py delete mode 100644 solution/services/backend/config/notifiers/telegram.py diff --git a/solution/assets/images/backend-coverage.png b/solution/assets/images/backend-coverage.png new file mode 100644 index 0000000000000000000000000000000000000000..71bfd3940bba9b7aba823ad78cebef120f0d71b4 GIT binary patch literal 68272 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%0D93Q1>Ut*&0$~6S3 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 literal 0 HcmV?d00001 diff --git a/solution/infrastructure/.gitignore b/solution/infrastructure/.gitignore index 7d35adf..dad66cc 100644 --- a/solution/infrastructure/.gitignore +++ b/solution/infrastructure/.gitignore @@ -1,4 +1,4 @@ -# Custom evnironment files +# Custom environment files .env # Password files diff --git a/solution/infrastructure/backend/.env.template b/solution/infrastructure/backend/.env.template index e219fb4..2fac0be 100644 --- a/solution/infrastructure/backend/.env.template +++ b/solution/infrastructure/backend/.env.template @@ -8,15 +8,16 @@ DJANGO_LANGUAGE_CODE=en-us DJANGO_STATIC_URL=http://localhost:13241/ REDIS_URI=redis://redis:6379 DJANGO_DB_URI=postgresql://postgres:postgres@postgres/postgres + DJANGO_CREATE_SUPERUSER=True DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_EMAIL=admin@mail.com DJANGO_SUPERUSER_PASSWORD=admin + YANDEX_CLOUD_FOLDER_ID=b1grd7nb04jbfvgm9121 YANDEX_CLOUD_API_KEY=AQVN19lztDgUcBrHeW0HtJ0bbDGda6i6e3InXsDl + MINIO_ENDPOINT=minio:9000 MINIO_CUSTOM_ENDPOINT_URL=http://127.0.0.1:13244 MINIO_ACCESS_KEY=admin MINIO_SECRET_KEY=password -DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN=6196898691:AAFucgj7ieEuYMvWG_MZAn0Ao3UBuHpvaVY -DJANGO_NOTIFIER_TELEGRAM_CHAT_ID=-1002304409222 diff --git a/solution/services/backend/.env.template b/solution/services/backend/.env.template index 378245d..5988a69 100644 --- a/solution/services/backend/.env.template +++ b/solution/services/backend/.env.template @@ -31,12 +31,3 @@ DJANGO_CREATE_SUPERUSER=False DJANGO_SUPERUSER_USERNAME= DJANGO_SUPERUSER_EMAIL= DJANGO_SUPERUSER_PASSWORD= - - -# Notifiers settings (only with DJANGO_DEBUG=False) - -# Telegram - -DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN= -DJANGO_NOTIFIER_TELEGRAM_CHAT_ID= -DJANGO_NOTIFIER_TELEGRAM_THREAD_ID= diff --git a/solution/services/backend/Dockerfile.staticfiles b/solution/services/backend/Dockerfile.staticfiles index fb3fef2..5150bf5 100644 --- a/solution/services/backend/Dockerfile.staticfiles +++ b/solution/services/backend/Dockerfile.staticfiles @@ -19,7 +19,7 @@ COPY . . RUN uv run python manage.py collectstatic --noinput -# Stage 2: Start nginx to serve staticfiles +# Stage 2: Start nginx and serve staticfiles FROM docker.io/nginx:latest COPY --from=builder /app/static /usr/share/nginx/html diff --git a/solution/services/backend/api/v1/clients/tests.py b/solution/services/backend/api/v1/clients/tests.py index 03a323f..a61f8ad 100644 --- a/solution/services/backend/api/v1/clients/tests.py +++ b/solution/services/backend/api/v1/clients/tests.py @@ -1,6 +1,5 @@ from http import HTTPStatus as status from django.test import TestCase -from django.urls import reverse import json from uuid import uuid4 from apps.client.models import Client diff --git a/solution/services/backend/apps/advertiser/tests/test_advertiser_model.py b/solution/services/backend/apps/advertiser/tests/test_advertiser_model.py index 08b5806..c0e5e91 100644 --- a/solution/services/backend/apps/advertiser/tests/test_advertiser_model.py +++ b/solution/services/backend/apps/advertiser/tests/test_advertiser_model.py @@ -22,6 +22,7 @@ class AdvertiserModelTest(TestCase): new_id = uuid4() self.advertiser.advertiser_id = new_id + self.assertEqual(self.advertiser.id, new_id) @override_settings( diff --git a/solution/services/backend/apps/campaign/models.py b/solution/services/backend/apps/campaign/models.py index a60d5b8..c8ba5a8 100644 --- a/solution/services/backend/apps/campaign/models.py +++ b/solution/services/backend/apps/campaign/models.py @@ -6,7 +6,6 @@ from uuid import UUID from django.conf import settings from django.core.cache import cache -from django.core.exceptions import ValidationError from django.core.validators import ( MaxValueValidator, MinLengthValidator, @@ -20,6 +19,7 @@ from apps.campaign.validators import ( CampaignDurationValidator, CampaignLimitsValidator, CampaignReportMessageValidator, + CampaignStartDateValidator, CampaignTargetingLocationValidator, ) from apps.client.models import Client @@ -100,21 +100,7 @@ class Campaign(BaseModel): CampaignAgeValidator()(self) CampaignDurationValidator()(self) CampaignLimitsValidator()(self) - - current_date = cache.get("current_date", default=0) - - err = "start_date must be greater than the current date." - - try: - original = Campaign.objects.get(id=self.id or "") - if ( - original.start_date != self.start_date - and self.start_date < current_date - ): - raise ValidationError(err) - except Campaign.DoesNotExist: - if self.start_date < current_date: - raise ValidationError(err) from None + CampaignStartDateValidator()(self) def save(self, *args: Any, **kwargs: Any) -> None: created = self.pk is None @@ -134,6 +120,20 @@ class Campaign(BaseModel): ) cache.set(f"campaign_{self.id}_clicks_count", self.clicks.count()) + def inc_views(self) -> None: + try: + cache.incr(f"campaign_{self.id}_impressions_count", 1) + except ValueError: + self.setup_cache() + logger.warning("Seems that %s missing caches", self.campaign_id) + + def inc_clicks(self) -> None: + try: + cache.incr(f"campaign_{self.id}_clicks_count", 1) + except ValueError: + self.setup_cache() + logger.warning("Seems that %s missing caches", self.campaign_id) + @property def ad_id(self) -> UUID: return self.id @@ -175,13 +175,7 @@ class Campaign(BaseModel): price=self.cost_per_impression, date=cache.get("current_date", default=0), ) - try: - cache.incr(f"campaign_{self.id}_impressions_count", 1) - except ValueError: - self.setup_cache() - logger.warning( - "Seems that %s missing caches", self.campaign_id - ) + self.inc_views() except ConflictError: pass @@ -198,13 +192,7 @@ class Campaign(BaseModel): price=self.cost_per_click, date=cache.get("current_date", default=0), ) - try: - cache.incr(f"campaign_{self.id}_clicks_count", 1) - except ValueError: - self.setup_cache() - logger.warning( - "Seems that %s missing caches", self.campaign_id - ) + self.inc_clicks() except ConflictError: pass @@ -235,7 +223,6 @@ class Campaign(BaseModel): total=models.Count("id"), spent=models.Sum("price", default=0.0), ) - clicks = self.clicks.values("date").annotate( total=models.Count("id"), spent=models.Sum("price", default=0.0), diff --git a/solution/services/backend/apps/campaign/validators.py b/solution/services/backend/apps/campaign/validators.py index 98426a9..ff45719 100644 --- a/solution/services/backend/apps/campaign/validators.py +++ b/solution/services/backend/apps/campaign/validators.py @@ -1,48 +1,83 @@ +from typing import TYPE_CHECKING + +from django.core.cache import cache from django.core.exceptions import ValidationError +from django.db.models.fields import Field + +if TYPE_CHECKING: + from apps.campaign.models import Campaign, CampaignReport + + +class CampaignTargetingLocationValidator: + def __call__(self, instance: "Campaign") -> None: + if instance.location == "": + err = { + "targeting": { + type( + instance + ).location.field.name: Field.default_error_messages[ + "blank" + ] + } + } + raise ValidationError(err) class CampaignAgeValidator: - def __call__(self, instance) -> None: # noqa: ANN001 + def __call__(self, instance: "Campaign") -> None: if ( isinstance(instance.age_from, int) and isinstance(instance.age_to, int) and instance.age_from > instance.age_to ): - err = "age_from can't be greater than age_to" + err = "targeting.age_from can't be greater than targeting.age_to." raise ValidationError(err) class CampaignDurationValidator: - def __call__(self, instance) -> None: # noqa: ANN001 + def __call__(self, instance: "Campaign") -> None: if ( isinstance(instance.start_date, int) and isinstance(instance.end_date, int) and instance.start_date > instance.end_date ): - err = "start_date can't be greater than end_date" + err = "start_date can't be greater than end_date." raise ValidationError(err) class CampaignLimitsValidator: - def __call__(self, instance) -> None: # noqa: ANN001 + def __call__(self, instance: "Campaign") -> None: if ( isinstance(instance.impressions_limit, int) and isinstance(instance.clicks_limit, int) and instance.impressions_limit < instance.clicks_limit ): - err = "clicks_limit can't be greater than impressions_limit" + err = "clicks_limit can't be greater than impressions_limit." raise ValidationError(err) -class CampaignTargetingLocationValidator: - def __call__(self, instance) -> None: # noqa: ANN001 - if instance.location == "": - err = "targeting.location cannot be blank" - raise ValidationError(err) +class CampaignStartDateValidator: + def __call__(self, instance: "Campaign") -> None: + current_date = cache.get("current_date", default=0) + err = "start_date must be greater or equal than the current_date." + try: + original = type(instance).objects.get(id=instance.id or "") + if ( + original.start_date != instance.start_date + and instance.start_date < current_date + ): + raise ValidationError(err) + except type(instance).DoesNotExist: + if instance.start_date < current_date: + raise ValidationError(err) from None class CampaignReportMessageValidator: - def __call__(self, instance) -> None: # noqa: ANN001 + def __call__(self, instance: "CampaignReport") -> None: if instance.message == "": - err = "message cannot be blank" + err = { + instance.message.field.name: Field.default_error_messages[ + "blank" + ] + } raise ValidationError(err) diff --git a/solution/services/backend/config/notifiers/__init__.py b/solution/services/backend/config/notifiers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/solution/services/backend/config/notifiers/telegram.py b/solution/services/backend/config/notifiers/telegram.py deleted file mode 100644 index 6ca3a65..0000000 --- a/solution/services/backend/config/notifiers/telegram.py +++ /dev/null @@ -1,133 +0,0 @@ -import datetime -import logging -import time -import traceback -from concurrent.futures import ThreadPoolExecutor - -import httpx -from django.utils.timezone import get_current_timezone - -TELEGRAM_LOG_HANDLER = logging.getLogger("telegram_log_handler") - -LEVEL_EMOJIS = { - "DEBUG": "🐞", - "INFO": "ℹ️", - "WARNING": "⚠️", - "ERROR": "❌", - "CRITICAL": "🚨", -} - - -class LoggingHandler(logging.Handler): - _executor = ThreadPoolExecutor(max_workers=5) - - def __init__( - self, - token: str, - chat_id: int, - thread_id: int | None = None, - retries: int | None = 3, - delay: int | None = 2, - timeout: int | None = 5, - ) -> None: - super().__init__() - - self.token = token - self.chat_id = chat_id - self.thread_id = thread_id - self.retries = retries - self.delay = delay - self.timeout = timeout - self.api_url = f"https://api.telegram.org/bot{self.token}/sendMessage" - - self.template = ( - "{levelname}\n" - "\tGuid: {correlation_id}\n" - "\tTimestamp: {asctime}\n" - "\tLogger: {name}\n" - "\tFile: {pathname} " - "(Line: {lineno})\n\n" - '
{message}
\n' - ) - - def emit(self, record: logging.LogRecord) -> None: - try: - formatted_record = self.format(record) - self._executor.submit(self._send_message, formatted_record) - except Exception as e: # noqa: BLE001 - self.handleError(record) - TELEGRAM_LOG_HANDLER.exception(e) - - def _send_message(self, formatted_record: str) -> None: - payload = { - "chat_id": self.chat_id, - "text": formatted_record, - "parse_mode": "HTML", - } - if self.thread_id: - payload["reply_to_message_id"] = self.thread_id - - for attempt in range(1, self.retries + 1): - response = httpx.post( - self.api_url, - data=payload, - timeout=self.timeout, - ) - if response.status_code != httpx.codes.OK: - if attempt == self.retries: - TELEGRAM_LOG_HANDLER.exception( - "Failed to send to Telegram after %d attempts: %s", - self.retries, - response.text, - ) - else: - time.sleep(self.delay) - else: - return - - def format(self, record: logging.LogRecord) -> str: - try: - asctime = datetime.datetime.fromtimestamp( - record.created, - tz=get_current_timezone(), - ).strftime("%Y-%m-%d %H:%M:%S %Z") - level_emoji = LEVEL_EMOJIS.get(record.levelname, "") - - formatted_message = self.template.format( - levelname=f"{level_emoji} {record.levelname}", - correlation_id=getattr(record, "correlation_id", "N/A"), - asctime=asctime, - name=record.name, - pathname=record.pathname, - lineno=record.lineno, - message=record.getMessage(), - ) - - if record.exc_info: - formatted_message += self._format_exception(record.exc_info) - - formatted_message += ( - f"\n#{record.levelname.lower()} " - f"#{record.name.replace('.', '_')}" - ) - if hasattr(record, "correlation_id"): - formatted_message += f" #{record.correlation_id}" - except Exception as format_error: # noqa: BLE001 - TELEGRAM_LOG_HANDLER.exception( - "Error formatting log record: %s", - format_error, - ) - return f"Error formatting log record: {format_error}" - else: - return formatted_message - - @staticmethod - def _format_exception(exc_info: Exception) -> str: - exc_text = "".join(traceback.format_exception(*exc_info)) - return ( - f"\n
{exc_text}
" - ) - - @classmethod - def shutdown_executor(cls) -> None: - cls._executor.shutdown(wait=True) diff --git a/solution/services/backend/config/settings.py b/solution/services/backend/config/settings.py index 122b846..a1d1102 100644 --- a/solution/services/backend/config/settings.py +++ b/solution/services/backend/config/settings.py @@ -41,7 +41,8 @@ YANDEX_CLOUD_INTEGRATION_ENABLED = ( YANDEX_CLOUD_FOLDER_ID and YANDEX_CLOUD_API_KEY ) -# Register healthcheck + +# Register healthchecks plugin_dir.register(YandexAIHealthCheck) @@ -300,26 +301,6 @@ USE_X_FORWARDED_PORT = False WSGI_APPLICATION = "config.wsgi.application" -# Notifiers - -# Telegram - -NOTIFIER_TELEGRAM_BOT_TOKEN = env( - "DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN", - default=None, -) - -NOTIFIER_TELEGRAM_CHAT_ID = env( - "DJANGO_NOTIFIER_TELEGRAM_CHAT_ID", - default=None, -) - -NOTIFIER_TELEGRAM_THREAD_ID = env( - "DJANGO_NOTIFIER_TELEGRAM_THREAD_ID", - default=None, -) - - # Logging LOGGER_NAME = "adnova" @@ -435,24 +416,6 @@ LOGGING = { LOGGING_CONFIG = "logging.config.dictConfig" -if NOTIFIER_TELEGRAM_BOT_TOKEN and NOTIFIER_TELEGRAM_CHAT_ID: - LOGGING_HANDLERS["telegram"] = { - "class": "config.notifiers.telegram.LoggingHandler", - "level": "INFO", - "filters": ["require_debug_false"], - "token": NOTIFIER_TELEGRAM_BOT_TOKEN, - "chat_id": NOTIFIER_TELEGRAM_CHAT_ID, - "thread_id": NOTIFIER_TELEGRAM_THREAD_ID, - "retries": 5, - "delay": 2, - "timeout": 5, - } - LOGGING_LOGGERS["django"]["handlers"].append("telegram") - LOGGING_LOGGERS["django.request"]["handlers"].append("telegram") - LOGGING_LOGGERS["health-check"]["handlers"].append("telegram") - LOGGING_LOGGERS[LOGGER_NAME]["handlers"].append("telegram") - - # Models ABSOLUTE_URL_OVERRIDES: dict[str, Callable] = {} diff --git a/solution/services/backend/integrations/yandexai/healthcheck.py b/solution/services/backend/integrations/yandexai/healthcheck.py index 3f4659b..48fe99a 100644 --- a/solution/services/backend/integrations/yandexai/healthcheck.py +++ b/solution/services/backend/integrations/yandexai/healthcheck.py @@ -16,8 +16,10 @@ class YandexAIHealthCheck(BaseHealthCheckBackend): result = sdk.models.completions( "yandexgpt-lite", model_version="latest" ).tokenize("ping") + if not result: self.add_error("YandexAI API is unaccessible") + except YCloudMLError: self.add_error("YandexAI API is unaccessible") diff --git a/solution/tests/README.md b/solution/tests/README.md index d338986..04d8c23 100644 --- a/solution/tests/README.md +++ b/solution/tests/README.md @@ -1,6 +1,6 @@ # AdNova Tests -There is `unit` tests and `e2e` tests available, unit tests are placed all around `backend` serivce folder and `e2e` tests placed [here](./e2e/) +There is `unit` and `e2e` tests available, unit tests are placed all around `backend` serivce folder and `e2e` tests placed [here](./e2e/) ## Running unit tests @@ -12,7 +12,7 @@ See [services/backend/README.md](../services/backend/README.md#testing) ### Backend service --image- +![backend coverage](../assets/images/backend-coverage.png) ## Running e2e tests diff --git a/solution/tests/e2e/README.md b/solution/tests/e2e/README.md index dd4bbf5..6ed5ed4 100644 --- a/solution/tests/e2e/README.md +++ b/solution/tests/e2e/README.md @@ -31,7 +31,7 @@ cd devitq/solution/tests/e2e uv sync --no-dev ``` -## Customize environment +## Customize environment (optional) ```bash cp .env.template .env diff --git a/solution/tests/e2e/pyproject.toml b/solution/tests/e2e/pyproject.toml index cdaf1a1..2e81273 100644 --- a/solution/tests/e2e/pyproject.toml +++ b/solution/tests/e2e/pyproject.toml @@ -107,4 +107,3 @@ unfixable = [] [tool.ruff.lint.pylint] max-args = 6 - diff --git a/solution/tests/e2e/tests/test_ad_text_generation.py b/solution/tests/e2e/tests/test_ad_text_generation.py index dcd499a..98e836b 100644 --- a/solution/tests/e2e/tests/test_ad_text_generation.py +++ b/solution/tests/e2e/tests/test_ad_text_generation.py @@ -8,8 +8,15 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Tests integration between: backend, redis, yandexgpt and celery def test_generate_ad_text(client: Client) -> None: + """ + Tests integration between: + - backend + - redis + - yandexgpt + - celery + """ + payload = { "advertiser_name": "Центральный Университет", "ad_title": "Всероссийский кейс-чемпионат DEADLINE", @@ -25,10 +32,7 @@ def test_generate_ad_text(client: Client) -> None: while True: result_response = client.get(f"/generate/ad_text/{task_id}/result") - assert ( - result_response.status_code == status.OK - or result_response.status_code == status.NOT_FOUND - ) + assert result_response.status_code in (status.OK, status.NOT_FOUND) result_data = result_response.json() if ( diff --git a/solution/tests/e2e/tests/test_backend_health.py b/solution/tests/e2e/tests/test_backend_health.py index b00804e..7a9a33e 100644 --- a/solution/tests/e2e/tests/test_backend_health.py +++ b/solution/tests/e2e/tests/test_backend_health.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) def test_healthcheck(client: Client) -> None: """ - Checks that backend can use theese services: + Tests integration between: - redis - celery - postgres