refactor: global codebase refactoring

This commit is contained in:
ITQ
2025-02-21 18:00:42 +03:00
parent ea8f5cfd49
commit cebb87fa73
18 changed files with 89 additions and 240 deletions
-9
View File
@@ -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=
@@ -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
@@ -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
@@ -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(
@@ -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),
@@ -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)
@@ -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 = (
"<b>{levelname}</b>\n"
"\t<b>Guid:</b> <code>{correlation_id}</code>\n"
"\t<b>Timestamp:</b> <code>{asctime}</code>\n"
"\t<b>Logger:</b> <code>{name}</code>\n"
"\t<b>File:</b> <code>{pathname}</code> "
"(Line: <code>{lineno}</code>)\n\n"
'<pre><code class="language-message">{message}</code></pre>\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<pre><code class='language-traceback'>{exc_text}</code></pre>"
)
@classmethod
def shutdown_executor(cls) -> None:
cls._executor.shutdown(wait=True)
+2 -39
View File
@@ -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] = {}
@@ -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")