chore(): minor fixes around codebase
This commit is contained in:
@@ -5,7 +5,6 @@ from django.http import Http404, HttpRequest
|
|||||||
from ninja import Router
|
from ninja import Router
|
||||||
from prometheus_client import Counter
|
from prometheus_client import Counter
|
||||||
|
|
||||||
from api.v1.auth.endpoints import jwt_bearer
|
|
||||||
from api.v1.events.schemas import (
|
from api.v1.events.schemas import (
|
||||||
EventErrorOut,
|
EventErrorOut,
|
||||||
EventTypeCreateIn,
|
EventTypeCreateIn,
|
||||||
@@ -21,6 +20,7 @@ from apps.events.services import (
|
|||||||
event_type_update,
|
event_type_update,
|
||||||
process_events_batch,
|
process_events_batch,
|
||||||
)
|
)
|
||||||
|
from apps.users.auth.bearer import jwt_bearer
|
||||||
|
|
||||||
EVENTS_INGESTED = Counter(
|
EVENTS_INGESTED = Counter(
|
||||||
"lotty_events_ingested_total",
|
"lotty_events_ingested_total",
|
||||||
@@ -62,9 +62,7 @@ def list_event_types(
|
|||||||
is_active: bool | None = None, # noqa: FBT001
|
is_active: bool | None = None, # noqa: FBT001
|
||||||
) -> tuple[int, list[EventTypeOut]]:
|
) -> tuple[int, list[EventTypeOut]]:
|
||||||
qs = event_type_list(is_active=is_active)
|
qs = event_type_list(is_active=is_active)
|
||||||
return HTTPStatus.OK, [
|
return HTTPStatus.OK, [EventTypeOut.model_validate(et) for et in qs]
|
||||||
EventTypeOut.model_validate(metric) for metric in qs
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ def create_rule(
|
|||||||
try:
|
try:
|
||||||
experiment = Experiment.objects.get(pk=payload.experiment_id)
|
experiment = Experiment.objects.get(pk=payload.experiment_id)
|
||||||
except Experiment.DoesNotExist:
|
except Experiment.DoesNotExist:
|
||||||
raise Http404 from Experiment.DoesNotExist
|
raise Http404 from None
|
||||||
|
|
||||||
r = rule_create(
|
r = rule_create(
|
||||||
event_type=payload.event_type,
|
event_type=payload.event_type,
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.http import Http404, HttpRequest
|
from django.http import Http404, HttpRequest
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
from ninja import Router
|
from ninja import Router
|
||||||
|
|
||||||
from api.v1.auth.endpoints import jwt_bearer
|
|
||||||
from api.v1.reports.schemas import ExperimentReportOut
|
from api.v1.reports.schemas import ExperimentReportOut
|
||||||
from apps.experiments.models import Experiment
|
from apps.experiments.models import Experiment
|
||||||
from apps.reports.services import build_experiment_report
|
from apps.reports.services import build_experiment_report
|
||||||
|
from apps.users.auth.bearer import jwt_bearer
|
||||||
|
|
||||||
router = Router(tags=["reports"], auth=jwt_bearer)
|
router = Router(tags=["reports"], auth=jwt_bearer)
|
||||||
|
|
||||||
@@ -34,11 +35,15 @@ def get_experiment_report(
|
|||||||
if start_date:
|
if start_date:
|
||||||
parsed_start = parse_datetime(start_date)
|
parsed_start = parse_datetime(start_date)
|
||||||
if parsed_start is None:
|
if parsed_start is None:
|
||||||
raise Http404
|
raise ValidationError(
|
||||||
|
{"start_date": "Must be a valid ISO 8601 datetime."}
|
||||||
|
)
|
||||||
if end_date:
|
if end_date:
|
||||||
parsed_end = parse_datetime(end_date)
|
parsed_end = parse_datetime(end_date)
|
||||||
if parsed_end is None:
|
if parsed_end is None:
|
||||||
raise Http404
|
raise ValidationError(
|
||||||
|
{"end_date": "Must be a valid ISO 8601 datetime."}
|
||||||
|
)
|
||||||
|
|
||||||
report_data = build_experiment_report(
|
report_data = build_experiment_report(
|
||||||
experiment=experiment,
|
experiment=experiment,
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ def remove_approver_from_group(
|
|||||||
description=(
|
description=(
|
||||||
"Retrieve the approver group assigned to a specific experimenter. "
|
"Retrieve the approver group assigned to a specific experimenter. "
|
||||||
"Returns 404 if no explicit group exists. "
|
"Returns 404 if no explicit group exists. "
|
||||||
"Available to any authenticated user."
|
"Admin only."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -132,7 +132,8 @@ def _process_exposure_event(
|
|||||||
)
|
)
|
||||||
|
|
||||||
decision = decision_get(decision_id)
|
decision = decision_get(decision_id)
|
||||||
if decision:
|
has_decision = decision is not None
|
||||||
|
if has_decision:
|
||||||
with suppress(ConflictError):
|
with suppress(ConflictError):
|
||||||
Exposure.objects.create(
|
Exposure.objects.create(
|
||||||
decision_id=decision_id,
|
decision_id=decision_id,
|
||||||
@@ -149,9 +150,10 @@ def _process_exposure_event(
|
|||||||
subject_id=subject_id,
|
subject_id=subject_id,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
properties=event_data.get("properties", {}),
|
properties=event_data.get("properties", {}),
|
||||||
is_attributed=True,
|
is_attributed=has_decision,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if has_decision:
|
||||||
_promote_pending_events(decision_id)
|
_promote_pending_events(decision_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ def experiment_submit_for_review(
|
|||||||
def experiment_approve(
|
def experiment_approve(
|
||||||
*, experiment: Experiment, approver: User, comment: str = ""
|
*, experiment: Experiment, approver: User, comment: str = ""
|
||||||
) -> Experiment:
|
) -> Experiment:
|
||||||
|
experiment = Experiment.objects.select_for_update().get(pk=experiment.pk)
|
||||||
if experiment.status != ExperimentStatus.IN_REVIEW:
|
if experiment.status != ExperimentStatus.IN_REVIEW:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"status": "Experiment must be in 'in_review' status to approve."}
|
{"status": "Experiment must be in 'in_review' status to approve."}
|
||||||
@@ -386,6 +387,15 @@ def experiment_approve(
|
|||||||
def experiment_reject(
|
def experiment_reject(
|
||||||
*, experiment: Experiment, user: User, comment: str = ""
|
*, experiment: Experiment, user: User, comment: str = ""
|
||||||
) -> Experiment:
|
) -> Experiment:
|
||||||
|
if not can_user_approve_experimenter(user, experiment.owner):
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"user": (
|
||||||
|
f"User '{user.username}' is not authorized to "
|
||||||
|
f"reject experiments for '{experiment.owner.username}'."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
experiment = _transition(
|
experiment = _transition(
|
||||||
experiment,
|
experiment,
|
||||||
ExperimentStatus.REJECTED,
|
ExperimentStatus.REJECTED,
|
||||||
@@ -401,6 +411,15 @@ def experiment_reject(
|
|||||||
def experiment_request_changes(
|
def experiment_request_changes(
|
||||||
*, experiment: Experiment, user: User, comment: str = ""
|
*, experiment: Experiment, user: User, comment: str = ""
|
||||||
) -> Experiment:
|
) -> Experiment:
|
||||||
|
if not can_user_approve_experimenter(user, experiment.owner):
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"user": (
|
||||||
|
f"User '{user.username}' is not authorized to "
|
||||||
|
f"request changes for '{experiment.owner.username}'."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
experiment.approvals.all().delete()
|
experiment.approvals.all().delete()
|
||||||
return _transition(
|
return _transition(
|
||||||
experiment,
|
experiment,
|
||||||
|
|||||||
@@ -28,11 +28,6 @@ from apps.notifications.services import (
|
|||||||
)
|
)
|
||||||
from apps.reports.services import calculate_metric_value
|
from apps.reports.services import calculate_metric_value
|
||||||
|
|
||||||
_ACTION_SEVERITY = {
|
|
||||||
GuardrailAction.ROLLBACK: 0,
|
|
||||||
GuardrailAction.PAUSE: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def guardrail_create(
|
def guardrail_create(
|
||||||
@@ -69,6 +64,7 @@ def guardrail_update(
|
|||||||
guardrail: Guardrail,
|
guardrail: Guardrail,
|
||||||
**fields: Any,
|
**fields: Any,
|
||||||
) -> Guardrail:
|
) -> Guardrail:
|
||||||
|
guardrail.experiment.refresh_from_db(fields=["status"])
|
||||||
if guardrail.experiment.status in STARTED_STATUSES:
|
if guardrail.experiment.status in STARTED_STATUSES:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
@@ -96,6 +92,7 @@ def guardrail_update(
|
|||||||
|
|
||||||
|
|
||||||
def guardrail_delete(*, guardrail: Guardrail) -> None:
|
def guardrail_delete(*, guardrail: Guardrail) -> None:
|
||||||
|
guardrail.experiment.refresh_from_db(fields=["status"])
|
||||||
if guardrail.experiment.status in STARTED_STATUSES:
|
if guardrail.experiment.status in STARTED_STATUSES:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
@@ -171,7 +168,7 @@ def _execute_guardrail_action(
|
|||||||
guardrail: Guardrail,
|
guardrail: Guardrail,
|
||||||
experiment: Experiment,
|
experiment: Experiment,
|
||||||
actual_value: Decimal,
|
actual_value: Decimal,
|
||||||
) -> GuardrailTrigger:
|
) -> GuardrailTrigger | None:
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
experiment = Experiment.objects.select_for_update().get(pk=experiment.pk)
|
experiment = Experiment.objects.select_for_update().get(pk=experiment.pk)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.contrib.postgres.search import (
|
|||||||
SearchRank,
|
SearchRank,
|
||||||
SearchVector,
|
SearchVector,
|
||||||
)
|
)
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ def learning_update(
|
|||||||
changes: dict[str, Any] = {}
|
changes: dict[str, Any] = {}
|
||||||
for key in fields:
|
for key in fields:
|
||||||
if key not in allowed:
|
if key not in allowed:
|
||||||
raise ValueError(f"Field '{key}' cannot be updated.")
|
raise ValidationError({key: f"Field '{key}' cannot be updated."})
|
||||||
for key, value in fields.items():
|
for key, value in fields.items():
|
||||||
if value is not None:
|
if value is not None:
|
||||||
old_value = getattr(learning, key)
|
old_value = getattr(learning, key)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.experiments.tests.helpers import (
|
from apps.experiments.tests.helpers import (
|
||||||
@@ -146,7 +147,7 @@ class LearningUpdateTest(TestCase):
|
|||||||
self.assertEqual(edits.count(), 0)
|
self.assertEqual(edits.count(), 0)
|
||||||
|
|
||||||
def test_update_disallowed_field(self) -> None:
|
def test_update_disallowed_field(self) -> None:
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValidationError):
|
||||||
learning_update(
|
learning_update(
|
||||||
learning=self.learning,
|
learning=self.learning,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
|||||||
@@ -53,6 +53,22 @@ def _validate_calculation_rule(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
rule_type = rule.get("type")
|
||||||
|
if rule_type and rule_type != metric_type:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"calculation_rule": (
|
||||||
|
f"Rule type '{rule_type}' does not match "
|
||||||
|
f"metric_type '{metric_type}'."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if metric_type == MetricType.PERCENTILE:
|
||||||
|
percentile = rule.get("percentile")
|
||||||
|
if percentile is not None and not (0 <= percentile <= 100):
|
||||||
|
raise ValidationError(
|
||||||
|
{"calculation_rule": ("Percentile must be between 0 and 100.")}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.experiments.services import (
|
from apps.experiments.tests.helpers import make_experiment
|
||||||
experiment_approve,
|
|
||||||
experiment_start,
|
|
||||||
experiment_submit_for_review,
|
|
||||||
)
|
|
||||||
from apps.experiments.tests.helpers import add_two_variants, make_experiment
|
|
||||||
from apps.guardrails.models import GuardrailAction
|
|
||||||
from apps.guardrails.services import guardrail_create
|
|
||||||
from apps.metrics.models import ExperimentMetric, MetricDirection, MetricType
|
from apps.metrics.models import ExperimentMetric, MetricDirection, MetricType
|
||||||
from apps.metrics.services import (
|
from apps.metrics.services import (
|
||||||
experiment_metric_add,
|
experiment_metric_add,
|
||||||
@@ -19,8 +11,6 @@ from apps.metrics.services import (
|
|||||||
metric_definition_create,
|
metric_definition_create,
|
||||||
metric_definition_update,
|
metric_definition_update,
|
||||||
)
|
)
|
||||||
from apps.reviews.services import review_settings_update
|
|
||||||
from apps.reviews.tests.helpers import make_approver
|
|
||||||
from config.errors import ConflictError
|
from config.errors import ConflictError
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ class CalculateMetricValueTest(TestCase):
|
|||||||
variant_id=self.v_treatment.pk,
|
variant_id=self.v_treatment.pk,
|
||||||
)
|
)
|
||||||
self.assertIsNotNone(value)
|
self.assertIsNotNone(value)
|
||||||
self.assertGreaterEqual(value, Decimal("90"))
|
self.assertGreaterEqual(value, Decimal(90))
|
||||||
self.assertLessEqual(value, Decimal("100"))
|
self.assertLessEqual(value, Decimal(100))
|
||||||
|
|
||||||
def test_percentile_no_data_returns_none(self) -> None:
|
def test_percentile_no_data_returns_none(self) -> None:
|
||||||
metric = metric_definition_create(
|
metric = metric_definition_create(
|
||||||
|
|||||||
Reference in New Issue
Block a user