Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions app/modules/quota_planner/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sqlalchemy import Integer, and_, cast, func, literal, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.utils.time import utcnow
from app.core.utils.time import to_utc_naive, utcnow
from app.db.models import (
Account,
QuotaPlannerDecision,
Expand Down Expand Up @@ -100,8 +100,8 @@ async def log_decision(
mode=mode,
action=action,
account_id=account_id,
scheduled_at=scheduled_at,
executed_at=executed_at,
scheduled_at=_quota_planner_db_timestamp(scheduled_at),
executed_at=_quota_planner_db_timestamp(executed_at),
score=score,
reason=reason,
forecast_snapshot_hash=forecast_snapshot_hash,
Expand Down Expand Up @@ -138,7 +138,7 @@ async def update_decision_status(
if reason is not None:
values["reason"] = reason
if executed_at is not None:
values["executed_at"] = executed_at
values["executed_at"] = _quota_planner_db_timestamp(executed_at)
if state_after_json is not None:
values["state_after_json"] = state_after_json
stmt = update(QuotaPlannerDecision).where(QuotaPlannerDecision.id == decision_id).values(**values)
Expand All @@ -160,6 +160,7 @@ async def update_decision_status(
return row

async def count_executed_warmups_since(self, since: datetime) -> int:
since = to_utc_naive(since)
stmt = select(func.count(QuotaPlannerDecision.id)).where(
and_(
QuotaPlannerDecision.action == "warmup",
Expand All @@ -170,6 +171,7 @@ async def count_executed_warmups_since(self, since: datetime) -> int:
return int(await self._session.scalar(stmt) or 0)

async def warmup_cost_since(self, since: datetime) -> float:
since = to_utc_naive(since)
stmt = select(func.coalesce(func.sum(RequestLog.cost_usd), 0.0)).where(
and_(
RequestLog.request_kind == "warmup",
Expand Down Expand Up @@ -215,7 +217,7 @@ async def add_window_observation(
) -> QuotaWindowObservation:
row = QuotaWindowObservation(
account_id=account_id,
observed_at=observed_at or utcnow(),
observed_at=_quota_planner_db_timestamp(observed_at) or utcnow(),
model=model,
primary_remaining_percent=primary_remaining_percent,
primary_reset_at=primary_reset_at,
Expand All @@ -236,7 +238,7 @@ async def aggregate_demand_bins(
since: datetime | None = None,
bucket_seconds: int = 900,
) -> list[DemandBin]:
since = since or (utcnow() - timedelta(days=28))
since = to_utc_naive(since) if since is not None else (utcnow() - timedelta(days=28))
bind = self._session.get_bind()
dialect = bind.dialect.name if bind else "sqlite"
if dialect == "postgresql":
Expand Down Expand Up @@ -296,6 +298,12 @@ async def aggregate_demand_bins(
]


def _quota_planner_db_timestamp(value: datetime | None) -> datetime | None:
if value is None:
return None
return to_utc_naive(value)


def _settings_from_row(row: QuotaPlannerSettings) -> PlannerSettings:
return PlannerSettings(
mode=row.mode,
Expand Down
9 changes: 8 additions & 1 deletion tests/unit/test_quota_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
plan_shadow_actions,
simulate_pool,
)
from app.modules.quota_planner.repository import DemandBin
from app.modules.quota_planner.repository import DemandBin, _quota_planner_db_timestamp

pytestmark = pytest.mark.unit

Expand Down Expand Up @@ -92,6 +92,13 @@ def test_planner_settings_default_to_nonblocking_shadow_mode() -> None:
assert settings.dry_run is True


def test_quota_planner_db_timestamp_normalizes_aware_datetimes_to_naive_utc() -> None:
scheduled_at = datetime(2026, 6, 18, 8, 30, tzinfo=timezone(timedelta(hours=3)))

assert _quota_planner_db_timestamp(scheduled_at) == datetime(2026, 6, 18, 5, 30)
assert _quota_planner_db_timestamp(None) is None
Comment thread
zvladru marked this conversation as resolved.


def test_candidate_start_times_do_not_floor_now_into_the_past() -> None:
settings = PlannerSettings(
mode="shadow",
Expand Down