mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
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:
parent
5d0c4d0e3a
commit
ec0946f64e
@ -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$"]
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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", []
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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 "
|
||||
|
||||
@ -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 "
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 "
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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] = []
|
||||
|
||||
@ -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"])),
|
||||
)
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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"}}),
|
||||
)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -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]))
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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": ""}}})
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, ""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user