from typing import override import django.core.validators from django.db import models from django.utils.translation import gettext_lazy as _ from apps.core.models import BaseModel METRIC_KEY_PATTERN = r"^[a-z][a-z0-9_]*$" class MetricType(models.TextChoices): RATIO = "ratio", _("Ratio") COUNT = "count", _("Count") AVERAGE = "average", _("Average") PERCENTILE = "percentile", _("Percentile") class MetricDirection(models.TextChoices): HIGHER_IS_BETTER = "higher_is_better", _("Higher is better") LOWER_IS_BETTER = "lower_is_better", _("Lower is better") NEUTRAL = "neutral", _("Neutral") class MetricDefinition(BaseModel): key = models.CharField( max_length=100, unique=True, verbose_name=_("key"), validators=[ django.core.validators.RegexValidator( regex=METRIC_KEY_PATTERN, message=( "Metric key must start with a lowercase letter " "and contain only lowercase letters, digits, " "and underscores." ), ) ], ) name = models.CharField( max_length=200, verbose_name=_("name"), ) description = models.TextField( blank=True, verbose_name=_("description"), ) metric_type = models.CharField( max_length=20, choices=MetricType.choices, verbose_name=_("metric type"), ) direction = models.CharField( max_length=20, choices=MetricDirection.choices, default=MetricDirection.NEUTRAL, verbose_name=_("direction"), ) calculation_rule = models.JSONField( verbose_name=_("calculation rule"), ) is_active = models.BooleanField( default=True, db_index=True, verbose_name=_("is active"), ) created_at = models.DateTimeField( auto_now_add=True, verbose_name=_("created at"), ) updated_at = models.DateTimeField( auto_now=True, verbose_name=_("updated at"), ) class Meta: verbose_name = _("metric definition") verbose_name_plural = _("metric definitions") ordering = ["key"] @override def __str__(self) -> str: return f"{self.key} ({self.metric_type})" class ExperimentMetric(BaseModel): experiment = models.ForeignKey( "experiments.Experiment", on_delete=models.CASCADE, related_name="experiment_metrics", verbose_name=_("experiment"), ) metric = models.ForeignKey( MetricDefinition, on_delete=models.PROTECT, related_name="experiment_usages", verbose_name=_("metric"), ) is_primary = models.BooleanField( default=False, verbose_name=_("is primary metric"), ) created_at = models.DateTimeField( auto_now_add=True, verbose_name=_("created at"), ) class Meta: verbose_name = _("experiment metric") verbose_name_plural = _("experiment metrics") constraints = [ models.UniqueConstraint( fields=["experiment", "metric"], name="unique_experiment_metric", ), ] indexes = [ models.Index( fields=["experiment", "is_primary"], name="idx_exp_metric_primary", ), ] @override def __str__(self) -> str: primary = " [primary]" if self.is_primary else "" return f"{self.experiment.name} → {self.metric.key}{primary}"