Grade against 3.14 too, not just run on it

`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.

What the stale target was hiding: 118 sites on `datetime.timezone.utc` instead
of `datetime.UTC` (UP017), two classes still written with `Generic[...]` rather
than PEP 695 syntax, and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That last one makes the source
3.14-only at the syntax level, which is what `requires-python = ">=3.14"`
already promised; kuhy chose it deliberately over holding ruff a release back
(2026-08-29).

The PEP 695 fix needed a hand: ruff rewrote `class WidgetGroup(Generic[_W])`
to `class WidgetGroup[W: tk.Misc]` but left the body annotated with the
module-level `_W`, a *different* type variable -- mypy caught it as "a function
returning TypeVar should receive at least one argument containing the same
TypeVar". Renamed the body to the new parameter, gave `FrameGroup.__init__`
the concrete `list[tk.Frame]` it always wrapped, and deleted the two now-dead
module-level TypeVars.

ruff, ruff-format, mypy --strict and both pylint passes clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
This commit is contained in:
Krzysztof Rudnicki 2026-08-29 11:42:47 +02:00
parent 5d0c4d0e3a
commit ec0946f64e
67 changed files with 206 additions and 210 deletions

View File

@ -30,7 +30,7 @@ mcp = ["mcp==2.1.1"]
include = ["screen_locker*"]
[tool.ruff]
target-version = "py310"
target-version = "py314"
include = ["*.py", "**/*.py"]
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
@ -140,7 +140,7 @@ line-ending = "auto"
docstring-code-format = true
[tool.mypy]
python_version = "3.10"
python_version = "3.14"
strict = true
warn_return_any = true
warn_unused_configs = true
@ -174,7 +174,7 @@ init-hook = "import sys; sys.path.insert(0, '.')"
analyse-fallback-blocks = true
persistent = true
jobs = 0
py-version = "3.10"
py-version = "3.14"
ignore = [".venv", "__pycache__"]
ignore-patterns = [".*\\.pyi$"]

View File

@ -14,7 +14,7 @@ rather than at the old ``_compliance_state`` path.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from typing import TYPE_CHECKING
@ -40,7 +40,7 @@ _logger = logging.getLogger(__name__)
def _today_str() -> str:
"""Return today's date as ``YYYY-MM-DD`` in UTC."""
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
def is_scheduled_skip_today(

View File

@ -18,7 +18,7 @@ surfaces the pending opportunity separately via ``AutoUpgradeOpportunity``.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from screen_locker._compliance_predicates import (
@ -79,8 +79,8 @@ def explain_lock_decision(
order. Heat-skip is never evaluated (it needs a live ``wttr.in`` call) —
reported via ``heat_skip_evaluated=False``, not guessed.
"""
instant = now if now is not None else datetime.now(tz=timezone.utc)
today_str = instant.astimezone(timezone.utc).strftime("%Y-%m-%d")
instant = now if now is not None else datetime.now(tz=UTC)
today_str = instant.astimezone(UTC).strftime("%Y-%m-%d")
local_dt = instant.astimezone()
local_minutes = local_dt.hour * 60 + local_dt.minute

View File

@ -27,7 +27,7 @@ must never be re-tracked.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from pathlib import Path
@ -94,7 +94,7 @@ class LockDecision:
def as_record(self) -> dict[str, object]:
"""Render the durable JSON object written to the trail."""
record: dict[str, object] = {
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
"timestamp": datetime.now(tz=UTC).isoformat(),
"locked": self.locked,
"reason": self.reason,
}
@ -179,7 +179,7 @@ def record_no_decision(mode: str, *, log_file: Path | None = None) -> None:
)
_append_record(
{
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
"timestamp": datetime.now(tz=UTC).isoformat(),
"locked": None,
"reason": "mode_makes_no_decision",
"mode": mode,

View File

@ -9,7 +9,7 @@ its own self-expiring, HMAC-signed state file, mirroring the pattern used by
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
@ -30,7 +30,7 @@ _logger = logging.getLogger(__name__)
def _today_str() -> str:
"""Return today's date as YYYY-MM-DD in UTC."""
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
class EarlyBirdMixin:
@ -38,7 +38,7 @@ class EarlyBirdMixin:
def _get_local_time_minutes(self) -> int:
"""Return current local time as minutes from midnight."""
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
return now.hour * 60 + now.minute
def _is_early_bird_time(self) -> bool:

View File

@ -13,7 +13,7 @@ State is persisted in ``extra_benefits_state.json`` next to this file.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import json
import logging
from typing import TYPE_CHECKING, Any
@ -91,7 +91,7 @@ def process_week_transition(log_file: Path, state_file: Path) -> list[str]:
Returns a list of human-readable reward strings (empty if no transition).
"""
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
current_week_str = _current_iso_week(now)
state = _load_state(state_file)
@ -108,7 +108,7 @@ def process_week_transition(log_file: Path, state_file: Path) -> list[str]:
23,
59,
59,
tzinfo=timezone.utc,
tzinfo=UTC,
)
prev_week_count = count_weekly_workouts(log_file, today=prev_week_dt)
@ -166,7 +166,7 @@ def weekly_shutdown_bonus_hours(
state_file: Path, *, today: datetime | None = None
) -> int:
"""Return the banked shutdown-time bonus (hours) for the current ISO week."""
now = today if today is not None else datetime.now(tz=timezone.utc).astimezone()
now = today if today is not None else datetime.now(tz=UTC).astimezone()
current_week_str = _current_iso_week(now)
bonus_hours: dict[str, int] = _load_state(state_file).get(
"weekly_shutdown_bonus_hours", {}
@ -176,7 +176,7 @@ def weekly_shutdown_bonus_hours(
def has_extended_early_bird(state_file: Path, *, today: datetime | None = None) -> bool:
"""Return True if the current ISO week has an extended early-bird window (09:00)."""
now = today if today is not None else datetime.now(tz=timezone.utc).astimezone()
now = today if today is not None else datetime.now(tz=UTC).astimezone()
current_week_str = _current_iso_week(now)
eb_weeks: list[str] = _load_state(state_file).get(
"extended_early_bird_iso_weeks", []

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from typing import TYPE_CHECKING
@ -107,7 +107,7 @@ def write_signed_entry(
return RecordResult(appended=False, prior_entries=prior)
entry: dict[str, object] = {
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
"timestamp": datetime.now(tz=UTC).isoformat(),
"workout_data": workout_data,
}
if workout_id is not None:
@ -154,5 +154,5 @@ class LogMixin:
credit only for a genuinely new workout and scale it by whether today
already had one.
"""
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
return write_signed_entry(self.log_file, today, self.workout_data)

View File

@ -18,7 +18,7 @@ that can drift.
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import UTC, datetime
import logging
from typing import TYPE_CHECKING
@ -132,7 +132,7 @@ def run_manual_log(log_file: Path, argv: Sequence[str]) -> int:
# UTC, matching how the budget windows compute "today" -- a local date
# near midnight would file the workout in a different window than the one
# it is then counted in.
date = args.date or datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
date = args.date or datetime.now(tz=UTC).strftime("%Y-%m-%d")
draft = _draft(args)
# Validated here as well as inside the ingest, purely so a person running
# this by hand gets the concrete field error on stderr and a non-zero exit

View File

@ -22,7 +22,7 @@ what happened, and logs it.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from typing import TYPE_CHECKING
@ -117,7 +117,7 @@ def _entry_wall_ms(entry: dict, date: str) -> int:
"for its sync clock",
date,
)
midnight = datetime.fromisoformat(date).replace(tzinfo=timezone.utc)
midnight = datetime.fromisoformat(date).replace(tzinfo=UTC)
return int(midnight.timestamp() * 1000)

View File

@ -15,7 +15,7 @@ see :class:`ManualWorkoutDraft` and :data:`SPORT_CHOICES`.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import logging
from screen_locker._constants import (
@ -60,7 +60,7 @@ _logger = logging.getLogger(__name__)
def _parse_hhmm(value: str) -> datetime | None:
"""Parse an ``HH:MM`` string into a datetime on an arbitrary fixed date."""
try:
return datetime.strptime(value.strip(), "%H:%M").replace(tzinfo=timezone.utc)
return datetime.strptime(value.strip(), "%H:%M").replace(tzinfo=UTC)
except ValueError as exc:
_logger.warning(
"Manual workout time %r is not HH:MM (%s) — the workout's duration "

View File

@ -11,7 +11,7 @@ manual workouts are self-reported, so only a few count in any rolling window.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import logging
from typing import TYPE_CHECKING
@ -39,13 +39,13 @@ SPORT_LABELS: dict[str, str] = {
def _today_iso() -> str:
"""Return today's date as ``YYYY-MM-DD`` (UTC)."""
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
def _parse_iso(date_str: str) -> datetime | None:
"""Parse ``YYYY-MM-DD`` into a UTC datetime, or return None."""
try:
return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC)
except ValueError as exc:
_logger.warning(
"Workout log has an unparsable date key %r (%s) — that day is "

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
from datetime import UTC, date, datetime, timedelta
from http import client as _http_client
import json
import logging
@ -119,7 +119,7 @@ class PhoneVerificationMixin(AdbTransportMixin, HttpWorkoutFetchMixin):
date_str = data.get("date") if data else None
if not date_str:
return 0
today = datetime.now(tz=timezone.utc).astimezone().date()
today = datetime.now(tz=UTC).astimezone().date()
week_start = today - timedelta(days=today.weekday())
try:
entry_date = date.fromisoformat(date_str)

View File

@ -10,7 +10,7 @@ filled in from whatever RunnerUp kept rather than only from today's check.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import logging
from typing import TYPE_CHECKING
@ -72,7 +72,7 @@ class RunnerUpBackfillMixin:
)
return 0
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
today = now.date()
week_start = today - timedelta(days=today.weekday())

View File

@ -7,7 +7,7 @@ Root DB fallback lives in ``_runnerup_db.py``.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import logging
from typing import Any
@ -77,7 +77,7 @@ class RunnerUpVerificationMixin(
fails validation), or ``None`` if no today's file exists at all
(caller should try the root DB path instead).
"""
today = datetime.now(tz=timezone.utc).astimezone().strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d")
exports = self._find_runnerup_exports_for_date(today)
if not exports:
_logger.warning(

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import logging
import subprocess
from typing import TYPE_CHECKING
@ -76,7 +76,7 @@ class ShutdownMixin(SickDayStateMixin, WakeAlarmMixin):
Returns True if successful, False otherwise.
"""
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
self._restore_original_config_if_needed()
if self._sick_mode_used_today():
_logger.warning("Sick mode already used today")

View File

@ -10,7 +10,7 @@ overwrite the fresh base when it runs later in the same startup.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from typing import TYPE_CHECKING, Any
@ -62,7 +62,7 @@ def reset_to_base_if_new_day(
Returns True if a reset was performed, False if today was already reset.
"""
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
if state_file.exists():
try:

View File

@ -10,7 +10,7 @@ earlier, so the following day can put them back.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
@ -30,7 +30,7 @@ class SickDayStateMixin:
try:
with SICK_DAY_STATE_FILE.open() as f:
state = json.load(f)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
return state.get("date") == today
except (OSError, json.JSONDecodeError) as exc:
_logger.warning(
@ -112,7 +112,7 @@ class SickDayStateMixin:
if loaded is None:
return
state_date, orig_mw, orig_ts = loaded
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
if state_date != today:
self._write_restored_config(orig_mw, orig_ts, state_date)
except (OSError, json.JSONDecodeError) as e:

View File

@ -11,7 +11,7 @@ Loading and saving that history stays in ``_sick_tracker``.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import logging
from typing import Any
@ -44,13 +44,13 @@ class SickHistory:
def _today_iso() -> str:
"""Return today's date as ``YYYY-MM-DD`` (UTC)."""
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
def _parse_iso(date_str: str) -> datetime | None:
"""Parse ``YYYY-MM-DD`` into a UTC datetime, or return None."""
try:
return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC)
except ValueError as exc:
_logger.warning(
"Sick history holds an unparsable date %r (%s) — that entry is "

View File

@ -7,7 +7,7 @@ state via :func:`save_history`.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import json
import logging
from typing import Any
@ -60,7 +60,7 @@ def load_history() -> SickHistory:
try:
with SICK_HISTORY_FILE.open() as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
except OSError, json.JSONDecodeError:
_logger.warning("Could not read sick history; starting fresh")
return SickHistory()
return SickHistory(
@ -198,7 +198,7 @@ def add_justification(
today_str = today or _today_iso()
entry: dict[str, Any] = {
"date": today_str,
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
"timestamp": datetime.now(tz=UTC).isoformat(),
"symptom": draft.symptom.strip(),
"onset": draft.onset.strip(),
"severity": int(draft.severity),

View File

@ -19,7 +19,7 @@ accusing the user of skipping a workout they actually did.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from screen_locker._degraded_sources import degraded_sources
@ -49,7 +49,7 @@ def describe_staleness(
it returned nothing at all.
now: Injected for tests.
"""
instant = now if now is not None else datetime.now(tz=timezone.utc)
instant = now if now is not None else datetime.now(tz=UTC)
if newest is None:
return SourceFinding(
name=name,
@ -60,7 +60,7 @@ def describe_staleness(
"your phone writes to."
),
)
age = instant - newest.astimezone(timezone.utc)
age = instant - newest.astimezone(UTC)
if age < STALE_AFTER:
return None
return SourceFinding(

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
from datetime import UTC, date, datetime, timedelta
import json
import logging
import sys
@ -73,7 +73,7 @@ def _print_day_line(d: date, entries: list[dict], sick_days: set[str]) -> bool:
def run_status(locker: ScreenLocker) -> None:
"""Print weekly workout status, run RunnerUp scan, apply bonus, then exit."""
today = datetime.now(tz=timezone.utc).astimezone().date()
today = datetime.now(tz=UTC).astimezone().date()
monday = today - timedelta(days=today.weekday())
log_file: Path = locker.log_file
log_data = load_workout_log(log_file)
@ -120,7 +120,7 @@ def run_status(locker: ScreenLocker) -> None:
eb_str = "Yes — until 09:00" if eb_ext else "No"
# Heat skips this month
this_month = datetime.now(tz=timezone.utc).astimezone().date().strftime("%Y-%m")
this_month = datetime.now(tz=UTC).astimezone().date().strftime("%Y-%m")
heat_entries = [
(d, entry)
for d, entries in log_data.items()

View File

@ -10,7 +10,7 @@ status view are built on.
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING
@ -181,7 +181,7 @@ def gather_status(
Never touches ADB, sudo, or the network — safe to call on every i3blocks
tick and every status-window open/refresh.
"""
instant = now if now is not None else datetime.now(tz=timezone.utc)
instant = now if now is not None else datetime.now(tz=UTC)
today_local = instant.astimezone()
today_str = today_local.date().isoformat()

View File

@ -23,16 +23,13 @@ from __future__ import annotations
import contextlib
import tkinter as tk
from typing import TYPE_CHECKING, Generic, TypeVar
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
_W = TypeVar("_W", bound=tk.Misc)
"""The widget each group mirrors, so a subclass keeps its own methods typed."""
class WidgetGroup(Generic[_W]):
class WidgetGroup[W: tk.Misc]:
"""One logical widget, mirrored onto every monitor.
Only the operations the locker actually performs after creation are
@ -40,16 +37,16 @@ class WidgetGroup(Generic[_W]):
from the first copy, since all copies are built identically.
"""
def __init__(self, widgets: list[_W]) -> None:
def __init__(self, widgets: list[W]) -> None:
"""Wrap the per-monitor copies of one widget."""
self._widgets = widgets
def __iter__(self) -> Iterator[_W]:
def __iter__(self) -> Iterator[W]:
"""Iterate the per-monitor copies, for callers that need each one."""
return iter(self._widgets)
@property
def first(self) -> _W:
def first(self) -> W:
"""The primary monitor's copy, for reads that cannot fan out."""
return self._widgets[0]
@ -111,7 +108,7 @@ class FrameGroup(WidgetGroup[tk.Frame]):
it to ``_button()`` creates one button inside each of its frames.
"""
def __init__(self, widgets: list[_W]) -> None:
def __init__(self, widgets: list[tk.Frame]) -> None:
"""Wrap the per-monitor frames, tracking which output each came from."""
super().__init__(widgets)
self._outputs: list[str] = []

View File

@ -10,7 +10,7 @@ because a silent None here is exactly how the PC stopped syncing for weeks.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import logging
from pathlib import Path
@ -86,7 +86,7 @@ def try_recover_firebase_session() -> RecoveryResult:
FirebaseCredentials(
id_token=body["id_token"],
refresh_token=body["refresh_token"],
expires_at=datetime.now(tz=timezone.utc)
expires_at=datetime.now(tz=UTC)
+ timedelta(seconds=int(body["expires_in"])),
)
)

View File

@ -27,7 +27,7 @@ from __future__ import annotations
import logging
from time import sleep
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING
from crdt_sync import RemoteSyncError
@ -41,10 +41,8 @@ if TYPE_CHECKING:
_logger = logging.getLogger(__name__)
T = TypeVar("T")
def with_sync_retry(
def with_sync_retry[T](
operation: Callable[[], T],
*,
description: str,

View File

@ -131,7 +131,7 @@ def gather_sync_status(
# astimezone() with no argument resolves the system local zone, which
# is what a human reading the status window expects to see.
last_push = (
dt.datetime.fromtimestamp(mtime, tz=dt.timezone.utc)
dt.datetime.fromtimestamp(mtime, tz=dt.UTC)
.astimezone()
.strftime("%Y-%m-%d %H:%M")
)

View File

@ -8,7 +8,7 @@ change.
from __future__ import annotations
import calendar
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import logging
import subprocess
@ -27,7 +27,7 @@ class WakeAlarmMixin:
@staticmethod
def _is_tomorrow_alarm_day() -> bool:
"""Check if tomorrow is an alarm day."""
tomorrow = datetime.now(tz=timezone.utc) + timedelta(days=1)
tomorrow = datetime.now(tz=UTC) + timedelta(days=1)
return tomorrow.weekday() in ALARM_DAYS
@staticmethod
@ -37,7 +37,7 @@ class WakeAlarmMixin:
Returns:
Epoch seconds WAKE_AFTER_HOURS from now.
"""
wake_time = datetime.now(tz=timezone.utc) + timedelta(
wake_time = datetime.now(tz=UTC) + timedelta(
hours=WAKE_AFTER_HOURS,
)
return calendar.timegm(wake_time.utctimetuple())

View File

@ -11,7 +11,7 @@ by patching ``screen_locker._wake_state.WAKE_STATE_FILE``.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
@ -24,7 +24,7 @@ _logger = logging.getLogger(__name__)
def _today_str() -> str:
"""Return today's date as YYYY-MM-DD in UTC."""
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
def load_wake_state() -> dict[str, object] | None:
@ -39,7 +39,7 @@ def load_wake_state() -> dict[str, object] | None:
try:
with WAKE_STATE_FILE.open() as f:
state = json.load(f)
except (OSError, json.JSONDecodeError):
except OSError, json.JSONDecodeError:
_logger.warning("Cannot read wake state file")
return None

View File

@ -11,7 +11,7 @@ current ISO week (Mon-Sun).
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import logging
from typing import TYPE_CHECKING
@ -67,7 +67,7 @@ def is_relaxed_day(*, today: datetime | None = None) -> bool:
Returns:
True when the current weekday is Tuesday, Wednesday, or Thursday.
"""
dt = today if today is not None else datetime.now(tz=timezone.utc).astimezone()
dt = today if today is not None else datetime.now(tz=UTC).astimezone()
return dt.weekday() in _RELAXED_WEEKDAYS
@ -92,7 +92,7 @@ def count_weekly_workouts(
Returns:
The weekly workout count (Mon-Sun, up to and including today).
"""
dt = today if today is not None else datetime.now(tz=timezone.utc).astimezone()
dt = today if today is not None else datetime.now(tz=UTC).astimezone()
week_start = (dt - timedelta(days=dt.weekday())).date()
today_date = dt.date()
@ -100,9 +100,7 @@ def count_weekly_workouts(
for date_str, entries in load_workout_log(log_file).items():
try:
entry_date = (
datetime.strptime(date_str, "%Y-%m-%d")
.replace(tzinfo=timezone.utc)
.date()
datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC).date()
)
except ValueError as exc:
_logger.warning(

View File

@ -7,7 +7,7 @@ leading underscore so pre-commit's name-tests-test hook skips it.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
@ -36,7 +36,7 @@ def setup_unlock(
if seed_today_type is None:
log_file.write_text("{}")
else:
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
log_file.write_text(
json.dumps(
{

View File

@ -11,7 +11,7 @@ Safety:
from __future__ import annotations
from contextlib import ExitStack
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from pathlib import Path
from typing import TYPE_CHECKING
@ -183,7 +183,7 @@ def _isolate_shutdown_base(tmp_path: Path) -> Iterator[None]:
same as the rest of the suite already does.
"""
target = tmp_path / "shutdown_base.json"
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
target.write_text(
json.dumps(
{"base_mon_wed_hour": 21, "base_thu_sun_hour": 21, "last_reset_date": today}

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -20,7 +20,7 @@ if TYPE_CHECKING:
def _today() -> str:
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
class TestIsScheduledSkipToday:

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -15,7 +15,7 @@ if TYPE_CHECKING:
def _today() -> str:
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
class TestExplainLockDecision:
@ -47,7 +47,7 @@ class TestExplainLockDecision:
files["early_bird_pending_file"].write_text(
json.dumps({"date": _today(), "hmac": "sig"})
)
now = datetime.now(tz=timezone.utc).astimezone().replace(hour=6, minute=0)
now = datetime.now(tz=UTC).astimezone().replace(hour=6, minute=0)
with patch(
"screen_locker._compliance_predicates.verify_entry_hmac", return_value=True
):
@ -69,7 +69,7 @@ class TestExplainLockDecision:
files["early_bird_pending_file"].write_text(
json.dumps({"date": _today(), "hmac": "sig"})
)
now = datetime.now(tz=timezone.utc).astimezone().replace(hour=10, minute=0)
now = datetime.now(tz=UTC).astimezone().replace(hour=10, minute=0)
with patch(
"screen_locker._compliance_predicates.verify_entry_hmac", return_value=True
):
@ -97,7 +97,7 @@ class TestExplainLockDecision:
extended_early_bird=False,
weekly_minimum_met=False,
relaxed_day=False,
now=datetime.now(tz=timezone.utc).astimezone().replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is False
assert result.stage == "sick_day"
@ -114,9 +114,7 @@ class TestExplainLockDecision:
extended_early_bird=False,
weekly_minimum_met=False,
relaxed_day=False,
now=datetime.now(tz=timezone.utc)
.astimezone()
.replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is False
assert result.stage == "already_logged"
@ -130,14 +128,14 @@ class TestExplainLockDecision:
weekly_minimum_met=False,
relaxed_day=False,
wake_skip=True,
now=datetime.now(tz=timezone.utc).astimezone().replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is False
assert result.stage == "wake_alarm_skip"
def test_fresh_early_bird_time_skips(self, tmp_path: Path) -> None:
files = self._files(tmp_path)
now = datetime.now(tz=timezone.utc).astimezone().replace(hour=6, minute=0)
now = datetime.now(tz=UTC).astimezone().replace(hour=6, minute=0)
result = explain_lock_decision(
**files,
sick_history=SickHistory(),
@ -156,7 +154,7 @@ class TestExplainLockDecision:
) -> None:
"""The re-check time isn't ambiguous — it's whichever one actually applies."""
files = self._files(tmp_path)
now = datetime.now(tz=timezone.utc).astimezone().replace(hour=6, minute=0)
now = datetime.now(tz=UTC).astimezone().replace(hour=6, minute=0)
result = explain_lock_decision(
**files,
sick_history=SickHistory(),
@ -178,7 +176,7 @@ class TestExplainLockDecision:
extended_early_bird=False,
weekly_minimum_met=False,
relaxed_day=True,
now=datetime.now(tz=timezone.utc).astimezone().replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is False
assert result.stage == "relaxed_day"
@ -202,7 +200,7 @@ class TestExplainLockDecision:
extended_early_bird=False,
weekly_minimum_met=False,
relaxed_day=True,
now=datetime.now(tz=timezone.utc).astimezone().replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is False
assert result.stage == "relaxed_day_already_skipped"
@ -215,7 +213,7 @@ class TestExplainLockDecision:
extended_early_bird=False,
weekly_minimum_met=True,
relaxed_day=False,
now=datetime.now(tz=timezone.utc).astimezone().replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is False
assert result.stage == "weekly_minimum_met"
@ -228,7 +226,7 @@ class TestExplainLockDecision:
extended_early_bird=False,
weekly_minimum_met=False,
relaxed_day=False,
now=datetime.now(tz=timezone.utc).astimezone().replace(hour=12, minute=0),
now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0),
)
assert result.fired is True
assert result.stage == "full_lock_pending_heat_check"

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from pathlib import Path
from typing import Any
@ -49,7 +49,7 @@ class TestTryAutoUpgradeEarlyBird:
assert result is True
with log_file.open() as f:
data: dict[str, Any] = json.load(f)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
assert data[today][0]["workout_data"]["type"] == "phone_verified"
assert data[today][0]["workout_data"]["after_early_bird"] == "true"

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
@ -81,7 +81,7 @@ class TestIsEarlyBirdPending:
) -> None:
"""Return False when HMAC verification fails."""
locker = create_locker(mock_tk, tmp_path)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
pending_file = tmp_path / "early_bird_pending.json"
pending_file.write_text(json.dumps({"date": today, "hmac": "bad"}))
with (
@ -105,7 +105,7 @@ class TestIsEarlyBirdPending:
) -> None:
"""Return True when today's marker is present and HMAC-valid."""
locker = create_locker(mock_tk, tmp_path)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
pending_file = tmp_path / "early_bird_pending.json"
pending_file.write_text(json.dumps({"date": today, "hmac": "sig"}))
with (
@ -125,7 +125,7 @@ class TestIsEarlyBirdPending:
) -> None:
"""Unsigned marker is accepted when no HMAC key is configured."""
locker = create_locker(mock_tk, tmp_path)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
pending_file = tmp_path / "early_bird_pending.json"
pending_file.write_text(json.dumps({"date": today}))
with (
@ -163,7 +163,7 @@ class TestSaveEarlyBirdPending:
assert pending_file.exists()
with pending_file.open() as f:
data: dict[str, Any] = json.load(f)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
assert data["date"] == today
assert not locker.log_file.exists()

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -73,7 +73,7 @@ class TestProcessWeekTransition:
self, tmp_path: Path
) -> None:
"""Early return when ISO week already processed (line 63)."""
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
year, week, _ = now.isocalendar()
f = tmp_path / "state.json"
f.write_text(json.dumps({"last_processed_iso_week": f"{year}-W{week:02d}"}))
@ -81,7 +81,7 @@ class TestProcessWeekTransition:
@staticmethod
def _current_week_str() -> str:
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
year, week, _ = now.isocalendar()
return f"{year}-W{week:02d}"
@ -148,7 +148,7 @@ class TestProcessWeekTransition:
):
process_week_transition(tmp_path / "log.json", f)
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
year, week, _ = now.isocalendar()
state = json.loads(f.read_text())
assert f"{year}-W{week:02d}" in state["extended_early_bird_iso_weeks"]
@ -202,7 +202,7 @@ class TestProcessWeekTransition:
def test_duplicate_eb_week_not_added_twice(self, tmp_path: Path) -> None:
"""Current week already in EB list: not added again (line 91 branch False)."""
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
year, week, _ = now.isocalendar()
current_week = f"{year}-W{week:02d}"
f = tmp_path / "state.json"

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
@ -26,7 +26,7 @@ class TestWeeklyShutdownBonusHours:
def test_returns_current_week_bonus(self, tmp_path: Path) -> None:
"""Returns the banked bonus for the current ISO week."""
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
year, week, _ = now.isocalendar()
current_week = f"{year}-W{week:02d}"
f = tmp_path / "state.json"
@ -71,7 +71,7 @@ class TestHasExtendedEarlyBird:
def test_returns_true_when_current_week_is_in_list(self, tmp_path: Path) -> None:
"""Current ISO week present in list → True."""
now = datetime.now(tz=timezone.utc).astimezone()
now = datetime.now(tz=UTC).astimezone()
year, week, _ = now.isocalendar()
current_week = f"{year}-W{week:02d}"
f = tmp_path / "state.json"
@ -82,6 +82,6 @@ class TestHasExtendedEarlyBird:
"""An explicit `today` is used instead of the real wall clock."""
f = tmp_path / "state.json"
f.write_text(json.dumps({"extended_early_bird_iso_weeks": ["2024-W01"]}))
fixed_today = datetime(2024, 1, 5, tzinfo=timezone.utc)
fixed_today = datetime(2024, 1, 5, tzinfo=UTC)
assert has_extended_early_bird(f, today=fixed_today) is True
assert has_extended_early_bird(f) is False

View File

@ -16,7 +16,7 @@ which is the only warning that arrives before the credential is already gone.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import os
from pathlib import Path
@ -32,7 +32,7 @@ def _fixture_credentials() -> FirebaseCredentials:
return FirebaseCredentials(
id_token=f"{fixture}-id-token",
refresh_token=f"{fixture}-refresh-token",
expires_at=datetime.now(tz=timezone.utc) + timedelta(hours=1),
expires_at=datetime.now(tz=UTC) + timedelta(hours=1),
)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import tkinter as tk
from typing import TYPE_CHECKING
@ -142,7 +142,7 @@ class TestHasLoggedToday:
) -> None:
"""Test when today's workout is logged with valid HMAC."""
log_file = tmp_path / "log.json"
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
log_file.write_text(
json.dumps({today: {"workout": "data", "hmac": "valid"}}),
)
@ -163,7 +163,7 @@ class TestHasLoggedToday:
) -> None:
"""Test rejects entry when HMAC verification fails."""
log_file = tmp_path / "log.json"
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
log_file.write_text(
json.dumps({today: {"workout": "data", "hmac": "tampered"}}),
)
@ -184,7 +184,7 @@ class TestHasLoggedToday:
) -> None:
"""Accept unsigned entry when HMAC key is unavailable."""
log_file = tmp_path / "log.json"
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
log_file.write_text(
json.dumps({today: {"workout": "data"}}),
)
@ -211,7 +211,7 @@ class TestHasLoggedToday:
) -> None:
"""Reject unsigned entry when HMAC key IS available."""
log_file = tmp_path / "log.json"
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
log_file.write_text(
json.dumps({today: {"workout": "data"}}),
)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import tkinter as tk
from typing import TYPE_CHECKING, Any
@ -66,7 +66,7 @@ class TestAutoUpgradeSickDay:
assert locker._try_auto_upgrade_sick_day() is True
mock_adjust.assert_called_once()
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
with log_file.open() as f:
data: dict[str, Any] = json.load(f)
# The log is day-keyed with a LIST of workouts; the upgrade appends one.

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
@ -36,7 +36,7 @@ class TestSaveWorkoutLog:
assert log_file.exists()
with log_file.open() as f:
data: dict[str, Any] = json.load(f)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
assert today in data
# The log now holds a list of entries per day (multiple workouts/day).
assert data[today][0]["workout_data"]["type"] == "running"
@ -61,7 +61,7 @@ class TestSaveWorkoutLog:
with log_file.open() as f:
data: dict[str, Any] = json.load(f)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
assert "hmac" not in data[today][0]
def test_save_to_existing_file(
@ -86,7 +86,7 @@ class TestSaveWorkoutLog:
with log_file.open() as f:
data: dict[str, Any] = json.load(f)
assert "2020-01-01" in data
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
assert today in data
def test_save_with_corrupted_existing_file(
@ -110,7 +110,7 @@ class TestSaveWorkoutLog:
with log_file.open() as f:
data: dict[str, Any] = json.load(f)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
assert today in data
def test_save_with_write_error(

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -61,9 +61,7 @@ class TestEntryWallMs:
def test_uses_the_entry_timestamp(self) -> None:
"""Uses the entry timestamp."""
expected = int(
datetime(2026, 7, 13, 12, tzinfo=timezone.utc).timestamp() * 1000
)
expected = int(datetime(2026, 7, 13, 12, tzinfo=UTC).timestamp() * 1000)
ms = _entry_wall_ms({"timestamp": "2026-07-13T12:00:00+00:00"}, "2026-07-13")
assert ms == expected
@ -74,7 +72,7 @@ class TestEntryWallMs:
def test_missing_timestamp_falls_back_to_midnight(self) -> None:
"""Missing timestamp falls back to midnight."""
expected = int(datetime(2026, 7, 13, tzinfo=timezone.utc).timestamp() * 1000)
expected = int(datetime(2026, 7, 13, tzinfo=UTC).timestamp() * 1000)
assert _entry_wall_ms({}, "2026-07-13") == expected
def test_fallback_is_stable_across_calls(self) -> None:

View File

@ -3,6 +3,7 @@
from __future__ import annotations
from datetime import UTC
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -189,6 +190,6 @@ class TestIngestManualRecords:
def _dt(date: str) -> object:
from datetime import datetime, timezone
from datetime import datetime
return datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
return datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC)

View File

@ -4,7 +4,7 @@
from __future__ import annotations
import dataclasses
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import json
from typing import TYPE_CHECKING
@ -145,7 +145,7 @@ class TestIsBudgetExhausted:
# MANUAL_WORKOUT_BUDGET_PER_30_DAYS distinct dates, all strictly after
# the 30d cutoff (2026-06-05) but at/before the 7d cutoff (2026-06-28),
# so only the 30d window sees them.
today_dt = datetime.strptime(_TODAY, "%Y-%m-%d").replace(tzinfo=timezone.utc)
today_dt = datetime.strptime(_TODAY, "%Y-%m-%d").replace(tzinfo=UTC)
dates = [
(today_dt - timedelta(days=8 + i)).strftime("%Y-%m-%d")
for i in range(MANUAL_WORKOUT_BUDGET_PER_30_DAYS)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
from datetime import UTC, date, datetime, timedelta
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -17,7 +17,7 @@ if TYPE_CHECKING:
# ISO week. On a Monday no such day exists (week_start == today), so using
# date.today() made them fail every Monday and pass Tue-Sun. Pin "now" to a
# Wednesday instead: yesterday is then Tuesday, comfortably inside the week.
_FROZEN_NOW = datetime(2026, 7, 22, 12, 0, tzinfo=timezone.utc) # a Wednesday
_FROZEN_NOW = datetime(2026, 7, 22, 12, 0, tzinfo=UTC) # a Wednesday
_FROZEN_YESTERDAY = (_FROZEN_NOW.date() - timedelta(days=1)).isoformat()

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -46,7 +46,7 @@ class TestIsScheduledSkipToday:
) -> None:
"""Returns True when today's date is in the skips list."""
locker = self._make_locker(mock_tk, tmp_path)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
skip_file = tmp_path / "scheduled_skips.json"
skip_file.write_text(json.dumps([today]))
with patch(
@ -128,7 +128,7 @@ class TestScheduledSkipEarlyExit:
@staticmethod
def _write_today_skip(tmp_path: Path) -> None:
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
skip_file = tmp_path / "scheduled_skips.json"
skip_file.write_text(json.dumps([today]))

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -64,7 +64,7 @@ class TestTryAutoUpgradeSickDayRunnerUp:
result = locker._try_auto_upgrade_sick_day()
assert result is True
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
data = json.loads(log_file.read_text())
assert data[today][0]["workout_data"]["type"] == "runnerup_verified"
assert data[today][0]["workout_data"]["after_sick_day"] == "true"
@ -141,7 +141,7 @@ class TestTryAutoUpgradeEarlyBirdRunnerUp:
result = locker._try_auto_upgrade_early_bird()
assert result is True
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
data = json.loads(log_file.read_text())
assert data[today][0]["workout_data"]["type"] == "runnerup_verified"
assert data[today][0]["workout_data"]["after_early_bird"] == "true"

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -19,7 +19,7 @@ if TYPE_CHECKING:
_DATE = "2026-08-21"
# The ISO week _DATE falls in, for count_weekly_workouts.
_TODAY = datetime(2026, 8, 21, tzinfo=timezone.utc)
_TODAY = datetime(2026, 8, 21, tzinfo=UTC)
def _session(**overrides: object) -> dict:

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
@ -58,7 +58,7 @@ class TestResetToBaseIfNewDay:
def test_returns_false_when_already_reset_today(self, tmp_path: Path) -> None:
"""Same-day last_reset_date → early False return (line 63)."""
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
f = tmp_path / "state.json"
f.write_text(json.dumps({"last_reset_date": today}))
assert reset_to_base_if_new_day(f, self._make_mixin()) is False

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import UTC
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -68,9 +69,9 @@ class TestRestoreOriginalConfigIfNeeded:
) -> None:
"""Test does not restore when state date matches today."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timezone
from datetime import datetime
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
state_file = tmp_path / "state.json"
state_file.write_text(
json.dumps(

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import UTC
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -44,9 +45,9 @@ class TestSickModeUsedToday:
"screen_locker._shutdown_sick_state.SICK_DAY_STATE_FILE",
state_file,
):
from datetime import datetime, timezone
from datetime import datetime
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
state_file.write_text(json.dumps({"date": today}))
assert locker._sick_mode_used_today() is True

View File

@ -7,7 +7,7 @@ correctly while the phone had stopped writing to it nine days earlier.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from screen_locker import _sync_client
from screen_locker._source_health import (
@ -17,7 +17,7 @@ from screen_locker._source_health import (
explain_findings,
)
_NOW = datetime(2026, 8, 24, 12, 0, tzinfo=timezone.utc)
_NOW = datetime(2026, 8, 24, 12, 0, tzinfo=UTC)
class TestCollectSourceFindings:
@ -50,7 +50,7 @@ class TestDescribeStaleness:
def test_nine_day_silence_is_reported_with_its_age(self) -> None:
"""The real 2026-08-15 mirror silence, named in days."""
newest = datetime(2026, 8, 15, 18, 0, tzinfo=timezone.utc)
newest = datetime(2026, 8, 15, 18, 0, tzinfo=UTC)
finding = describe_staleness("GitHub mirror", newest, now=_NOW)

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import UTC
import json
from pathlib import Path
from typing import TYPE_CHECKING
@ -82,9 +83,9 @@ class TestRunStatusNormal:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Today == the week's Sunday: the per-day loop runs all 7 days, never breaking early."""
from datetime import datetime, timezone
from datetime import datetime
fixed_now = datetime(2026, 7, 5, 12, 0, tzinfo=timezone.utc) # a real Sunday
fixed_now = datetime(2026, 7, 5, 12, 0, tzinfo=UTC) # a real Sunday
eb_file = tmp_path / "eb.json"
log_file = tmp_path / "log.json"
locker = _make_locker(log_file, n_filled=0)
@ -106,9 +107,9 @@ class TestRunStatusNormal:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""A date with no workout_log entry but in sick_history → shown as sick_day."""
from datetime import datetime, timezone
from datetime import datetime
today = datetime.now(tz=timezone.utc).astimezone().date().isoformat()
today = datetime.now(tz=UTC).astimezone().date().isoformat()
eb_file = tmp_path / "eb.json"
log_file = tmp_path / "log.json"
history_file = tmp_path / "sick_history.json"
@ -188,9 +189,9 @@ class TestRunStatusWorkoutLog:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Log entry with counted type → ✓ mark printed."""
from datetime import datetime, timezone
from datetime import datetime
today = datetime.now(tz=timezone.utc).astimezone().date().isoformat()
today = datetime.now(tz=UTC).astimezone().date().isoformat()
log_file = tmp_path / "log.json"
log_file.write_text(
json.dumps(
@ -223,9 +224,9 @@ class TestRunStatusWorkoutLog:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Log entry with uncounted type → ✗ mark printed."""
from datetime import datetime, timezone
from datetime import datetime
today = datetime.now(tz=timezone.utc).astimezone().date().isoformat()
today = datetime.now(tz=UTC).astimezone().date().isoformat()
log_file = tmp_path / "log.json"
log_file.write_text(
json.dumps({today: {"workout_data": {"type": "heat_skip", "source": ""}}})

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@ -11,9 +11,9 @@ if TYPE_CHECKING:
# Fixed reference instant: Friday 2024-01-05, 12:00 UTC == 13:00 Europe/Warsaw.
# Outside the 05:00-09:00 early-bird window and not a Tue/Wed/Thu relaxed day,
# so lock-decision branches are fully deterministic regardless of wall clock.
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=timezone.utc)
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=UTC)
# Monday of that same ISO week, for Mon-Wed shutdown-band assertions.
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)
def _files(tmp_path: Path) -> dict[str, Path]:

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import date, datetime, timezone
from datetime import UTC, date, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -16,9 +16,9 @@ if TYPE_CHECKING:
# Fixed reference instant: Friday 2024-01-05, 12:00 UTC == 13:00 Europe/Warsaw.
# Outside the 05:00-09:00 early-bird window and not a Tue/Wed/Thu relaxed day,
# so lock-decision branches are fully deterministic regardless of wall clock.
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=timezone.utc)
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=UTC)
# Monday of that same ISO week, for Mon-Wed shutdown-band assertions.
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)
class TestGatherStatus:

View File

@ -7,7 +7,7 @@ weekly minimum) means no lock is needed.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -21,9 +21,9 @@ if TYPE_CHECKING:
# Fixed reference instant: Friday 2024-01-05, 12:00 UTC == 13:00 Europe/Warsaw.
# Outside the 05:00-09:00 early-bird window and not a Tue/Wed/Thu relaxed day,
# so lock-decision branches are fully deterministic regardless of wall clock.
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=timezone.utc)
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=UTC)
# Monday of that same ISO week, for Mon-Wed shutdown-band assertions.
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)
class TestGatherStatusLockSuppression:
@ -84,7 +84,7 @@ class TestGatherStatusLockSuppression:
def test_relaxed_day_stops_lock(self, tmp_path: Path) -> None:
"""Tuesday is a relaxed day — is_relaxed_day derives it from `now`."""
files = _files(tmp_path)
tuesday_noon = datetime(2024, 1, 2, 12, 0, tzinfo=timezone.utc)
tuesday_noon = datetime(2024, 1, 2, 12, 0, tzinfo=UTC)
with patch(
"screen_locker._status_data.has_workout_skip_today", return_value=False
):

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -16,9 +16,9 @@ if TYPE_CHECKING:
# Fixed reference instant: Friday 2024-01-05, 12:00 UTC == 13:00 Europe/Warsaw.
# Outside the 05:00-09:00 early-bird window and not a Tue/Wed/Thu relaxed day,
# so lock-decision branches are fully deterministic regardless of wall clock.
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=timezone.utc)
_FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=UTC)
# Monday of that same ISO week, for Mon-Wed shutdown-band assertions.
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
_MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)
class TestFormatSummaryLine:

View File

@ -5,6 +5,7 @@ Split from test_status.py to stay under the repo's 400-line file limit.
from __future__ import annotations
from datetime import UTC
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -142,9 +143,9 @@ class TestRunStatusMinimumStatus:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Pin today to Monday so the loop hits d > today on day 2, covering line 64."""
from datetime import datetime, timezone
from datetime import datetime
fake_now = datetime(2026, 6, 22, 12, 0, tzinfo=timezone.utc)
fake_now = datetime(2026, 6, 22, 12, 0, tzinfo=UTC)
class _FakeDatetime(datetime):
@classmethod

View File

@ -5,6 +5,7 @@ Split out of test_status.py for the 250-line cap.
from __future__ import annotations
from datetime import UTC
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -32,9 +33,9 @@ class TestWeeklyTotalUsesPerWorkoutRule:
days are supposed to support. Also covers manual workouts counting
individually (no once-per-day collapse), same as verified ones.
"""
from datetime import datetime, timezone
from datetime import datetime
today = datetime.now(tz=timezone.utc).astimezone().date().isoformat()
today = datetime.now(tz=UTC).astimezone().date().isoformat()
log_file = tmp_path / "log.json"
log_file.write_text(
json.dumps(

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
@ -54,7 +54,7 @@ class TestIsSickDayToday:
) -> None:
"""Return True when today is in sick_history's sick_days list."""
locker = create_locker(mock_tk, tmp_path)
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
history_file = tmp_path / "sick_history.json"
history_file.write_text(json.dumps({"sick_days": [today]}))
assert locker._is_sick_day_today() is True

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import UTC
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -22,13 +23,13 @@ class TestIsTomorrowAlarmDay:
) -> None:
"""Sunday evening → Monday is alarm day (weekday=0)."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timezone
from datetime import datetime
# Sunday 2026-04-12 → tomorrow Monday
with patch(
"screen_locker._wake_alarm.datetime",
) as mock_dt:
mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=timezone.utc)
mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=UTC)
mock_dt.side_effect = datetime
from datetime import timedelta
@ -47,7 +48,7 @@ class TestIsTomorrowAlarmDay:
) -> None:
"""Monday evening → Tuesday is NOT an alarm day."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
# Monday 2026-04-13 → tomorrow Tuesday (weekday=1)
with (
@ -59,7 +60,7 @@ class TestIsTomorrowAlarmDay:
timedelta,
),
):
mock_dt.now.return_value = datetime(2026, 4, 13, 23, 0, tzinfo=timezone.utc)
mock_dt.now.return_value = datetime(2026, 4, 13, 23, 0, tzinfo=UTC)
mock_dt.side_effect = datetime
assert locker._is_tomorrow_alarm_day() is False
@ -71,7 +72,7 @@ class TestIsTomorrowAlarmDay:
) -> None:
"""Thursday evening → Friday is alarm day (weekday=4)."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
# Thursday 2026-04-16 → tomorrow Friday (weekday=4)
with (
@ -83,7 +84,7 @@ class TestIsTomorrowAlarmDay:
timedelta,
),
):
mock_dt.now.return_value = datetime(2026, 4, 16, 23, 0, tzinfo=timezone.utc)
mock_dt.now.return_value = datetime(2026, 4, 16, 23, 0, tzinfo=UTC)
mock_dt.side_effect = datetime
assert locker._is_tomorrow_alarm_day() is True

View File

@ -3,7 +3,7 @@ has_weekly_minimum."""
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
@ -25,7 +25,7 @@ if TYPE_CHECKING:
def _dt(weekday: int, hour: int = 10) -> datetime:
"""Return a UTC-aware datetime for the given ISO weekday (0=Mon, 6=Sun)."""
# 2025-05-19 is a Monday (weekday 0)
base = datetime(2025, 5, 19, hour, 0, 0, tzinfo=timezone.utc)
base = datetime(2025, 5, 19, hour, 0, 0, tzinfo=UTC)
from datetime import timedelta
return base + timedelta(days=weekday)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -34,7 +34,7 @@ class TestRelaxedDayAlreadySkippedBranch:
# script_dir.parent/"log.json" resolves to tmp_path/"log.json" -- NOT
# tmp_path/"screen_locker"/"log.json".
log_file = tmp_path / "log.json"
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
write_signed_entry(log_file, today, {"type": "relaxed_day_skip"})
with (

View File

@ -20,7 +20,7 @@ Usage:
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
from pathlib import Path
import sys
@ -97,7 +97,7 @@ def main(argv: list[str] | None = None) -> int:
date = _today() if args.date == "today" else args.date
try:
datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC)
except ValueError:
msg = f"ERROR: {date!r} is not a YYYY-MM-DD date"
raise SystemExit(msg) from None

View File

@ -94,7 +94,7 @@ def _run(args: list[str]) -> tuple[int, str]:
timeout=_TIMEOUT_SECONDS,
check=False,
)
except (OSError, subprocess.SubprocessError):
except OSError, subprocess.SubprocessError:
# A check that cannot run must not look like a check that passed.
_logger.exception("Could not run %s", " ".join(args))
return 1, ""

View File

@ -29,7 +29,7 @@ Usage (needs the root-owned key at /etc/workout-locker/hmac.key):
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from pathlib import Path
@ -152,7 +152,7 @@ def main() -> int:
_logger.info("\nDry run. Re-run with --apply to write.")
return 0
stamp = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ")
stamp = datetime.now(tz=UTC).strftime("%Y%m%dT%H%M%SZ")
backup = LOG_FILE.with_suffix(f".json.bak-{stamp}")
shutil.copy2(LOG_FILE, backup)
_logger.info("\nBacked up to %s", backup)

View File

@ -22,7 +22,7 @@ Usage:
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import UTC, datetime
import json
import logging
from pathlib import Path
@ -73,7 +73,7 @@ def revoke(history: dict, log: dict, date: str) -> str:
revocations.append(
{
"date": date,
"revoked_at": datetime.now(tz=timezone.utc).isoformat(),
"revoked_at": datetime.now(tz=UTC).isoformat(),
"reason": (
"workout was completed but the locker could not read it; "
"no local log entry survives for this date"
@ -92,7 +92,7 @@ def revoke(history: dict, log: dict, date: str) -> str:
revocations.append(
{
"date": date,
"revoked_at": datetime.now(tz=timezone.utc).isoformat(),
"revoked_at": datetime.now(tz=UTC).isoformat(),
"reason": (
"workout verified after the fact; the sick day was spent only "
"because the locker could not read the synced workout"