diff --git a/pyproject.toml b/pyproject.toml index bf893a4..0d44763 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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$"] diff --git a/screen_locker/_compliance_predicates.py b/screen_locker/_compliance_predicates.py index 309f5ce..6f20f3d 100644 --- a/screen_locker/_compliance_predicates.py +++ b/screen_locker/_compliance_predicates.py @@ -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( diff --git a/screen_locker/_compliance_state.py b/screen_locker/_compliance_state.py index a17c637..384616a 100644 --- a/screen_locker/_compliance_state.py +++ b/screen_locker/_compliance_state.py @@ -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 diff --git a/screen_locker/_decision_log.py b/screen_locker/_decision_log.py index 0aabe90..09800ab 100644 --- a/screen_locker/_decision_log.py +++ b/screen_locker/_decision_log.py @@ -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, diff --git a/screen_locker/_early_bird.py b/screen_locker/_early_bird.py index 07eb740..7c56937 100644 --- a/screen_locker/_early_bird.py +++ b/screen_locker/_early_bird.py @@ -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: diff --git a/screen_locker/_extra_benefits.py b/screen_locker/_extra_benefits.py index 14f0ddb..c6ec6e7 100644 --- a/screen_locker/_extra_benefits.py +++ b/screen_locker/_extra_benefits.py @@ -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", [] diff --git a/screen_locker/_log_mixin.py b/screen_locker/_log_mixin.py index 6972c33..4b8a398 100644 --- a/screen_locker/_log_mixin.py +++ b/screen_locker/_log_mixin.py @@ -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) diff --git a/screen_locker/_manual_cli.py b/screen_locker/_manual_cli.py index 479d15e..b27865e 100644 --- a/screen_locker/_manual_cli.py +++ b/screen_locker/_manual_cli.py @@ -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 diff --git a/screen_locker/_manual_push.py b/screen_locker/_manual_push.py index 31192f4..a3ffd7d 100644 --- a/screen_locker/_manual_push.py +++ b/screen_locker/_manual_push.py @@ -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) diff --git a/screen_locker/_manual_workout.py b/screen_locker/_manual_workout.py index 6c80a32..aaa9ca7 100644 --- a/screen_locker/_manual_workout.py +++ b/screen_locker/_manual_workout.py @@ -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 " diff --git a/screen_locker/_manual_workout_types.py b/screen_locker/_manual_workout_types.py index 34956af..e4b0b27 100644 --- a/screen_locker/_manual_workout_types.py +++ b/screen_locker/_manual_workout_types.py @@ -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 " diff --git a/screen_locker/_phone_verification.py b/screen_locker/_phone_verification.py index 1c25d56..70a39b6 100644 --- a/screen_locker/_phone_verification.py +++ b/screen_locker/_phone_verification.py @@ -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) diff --git a/screen_locker/_runnerup_backfill.py b/screen_locker/_runnerup_backfill.py index 9fa64c6..116cb8f 100644 --- a/screen_locker/_runnerup_backfill.py +++ b/screen_locker/_runnerup_backfill.py @@ -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()) diff --git a/screen_locker/_runnerup_verification.py b/screen_locker/_runnerup_verification.py index 66c3ada..2c72261 100644 --- a/screen_locker/_runnerup_verification.py +++ b/screen_locker/_runnerup_verification.py @@ -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( diff --git a/screen_locker/_shutdown.py b/screen_locker/_shutdown.py index 3382307..3212586 100644 --- a/screen_locker/_shutdown.py +++ b/screen_locker/_shutdown.py @@ -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") diff --git a/screen_locker/_shutdown_base.py b/screen_locker/_shutdown_base.py index b65db7c..9ecbbd5 100644 --- a/screen_locker/_shutdown_base.py +++ b/screen_locker/_shutdown_base.py @@ -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: diff --git a/screen_locker/_shutdown_sick_state.py b/screen_locker/_shutdown_sick_state.py index 1638734..88213ff 100644 --- a/screen_locker/_shutdown_sick_state.py +++ b/screen_locker/_shutdown_sick_state.py @@ -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: diff --git a/screen_locker/_sick_budget.py b/screen_locker/_sick_budget.py index fecfcae..428944e 100644 --- a/screen_locker/_sick_budget.py +++ b/screen_locker/_sick_budget.py @@ -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 " diff --git a/screen_locker/_sick_tracker.py b/screen_locker/_sick_tracker.py index 7cab148..3a17633 100644 --- a/screen_locker/_sick_tracker.py +++ b/screen_locker/_sick_tracker.py @@ -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), diff --git a/screen_locker/_source_health.py b/screen_locker/_source_health.py index 492f917..c07d1b9 100644 --- a/screen_locker/_source_health.py +++ b/screen_locker/_source_health.py @@ -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( diff --git a/screen_locker/_status.py b/screen_locker/_status.py index e7ee762..bb20a64 100644 --- a/screen_locker/_status.py +++ b/screen_locker/_status.py @@ -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() diff --git a/screen_locker/_status_data.py b/screen_locker/_status_data.py index a43ed63..20d6a97 100644 --- a/screen_locker/_status_data.py +++ b/screen_locker/_status_data.py @@ -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() diff --git a/screen_locker/_surface_group.py b/screen_locker/_surface_group.py index 76b5157..027ac11 100644 --- a/screen_locker/_surface_group.py +++ b/screen_locker/_surface_group.py @@ -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] = [] diff --git a/screen_locker/_sync_client.py b/screen_locker/_sync_client.py index dc9aaef..8c6a073 100644 --- a/screen_locker/_sync_client.py +++ b/screen_locker/_sync_client.py @@ -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"])), ) ) diff --git a/screen_locker/_sync_retry.py b/screen_locker/_sync_retry.py index 5df6708..f698ae2 100644 --- a/screen_locker/_sync_retry.py +++ b/screen_locker/_sync_retry.py @@ -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, diff --git a/screen_locker/_sync_status.py b/screen_locker/_sync_status.py index 73ea57a..a84740f 100644 --- a/screen_locker/_sync_status.py +++ b/screen_locker/_sync_status.py @@ -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") ) diff --git a/screen_locker/_wake_alarm.py b/screen_locker/_wake_alarm.py index 237def7..cc4845c 100644 --- a/screen_locker/_wake_alarm.py +++ b/screen_locker/_wake_alarm.py @@ -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()) diff --git a/screen_locker/_wake_state.py b/screen_locker/_wake_state.py index c6209fc..0116287 100644 --- a/screen_locker/_wake_state.py +++ b/screen_locker/_wake_state.py @@ -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 diff --git a/screen_locker/_weekly_check.py b/screen_locker/_weekly_check.py index c4c1edd..c141ff7 100644 --- a/screen_locker/_weekly_check.py +++ b/screen_locker/_weekly_check.py @@ -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( diff --git a/screen_locker/tests/_unlock_helpers.py b/screen_locker/tests/_unlock_helpers.py index 889344f..b350cd3 100644 --- a/screen_locker/tests/_unlock_helpers.py +++ b/screen_locker/tests/_unlock_helpers.py @@ -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( { diff --git a/screen_locker/tests/conftest.py b/screen_locker/tests/conftest.py index 6332173..48ad25b 100644 --- a/screen_locker/tests/conftest.py +++ b/screen_locker/tests/conftest.py @@ -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} diff --git a/screen_locker/tests/test_compliance_state.py b/screen_locker/tests/test_compliance_state.py index 4c6b668..1d105ee 100644 --- a/screen_locker/tests/test_compliance_state.py +++ b/screen_locker/tests/test_compliance_state.py @@ -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: diff --git a/screen_locker/tests/test_compliance_state_part2.py b/screen_locker/tests/test_compliance_state_part2.py index ec824d2..630bbbb 100644 --- a/screen_locker/tests/test_compliance_state_part2.py +++ b/screen_locker/tests/test_compliance_state_part2.py @@ -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" diff --git a/screen_locker/tests/test_early_bird_part2.py b/screen_locker/tests/test_early_bird_part2.py index c553a1e..505cf16 100644 --- a/screen_locker/tests/test_early_bird_part2.py +++ b/screen_locker/tests/test_early_bird_part2.py @@ -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" diff --git a/screen_locker/tests/test_early_bird_part3.py b/screen_locker/tests/test_early_bird_part3.py index df44eb9..f61122f 100644 --- a/screen_locker/tests/test_early_bird_part3.py +++ b/screen_locker/tests/test_early_bird_part3.py @@ -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() diff --git a/screen_locker/tests/test_extra_benefits.py b/screen_locker/tests/test_extra_benefits.py index 98abf9c..1226e9c 100644 --- a/screen_locker/tests/test_extra_benefits.py +++ b/screen_locker/tests/test_extra_benefits.py @@ -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" diff --git a/screen_locker/tests/test_extra_benefits_part2.py b/screen_locker/tests/test_extra_benefits_part2.py index e69af61..e23a679 100644 --- a/screen_locker/tests/test_extra_benefits_part2.py +++ b/screen_locker/tests/test_extra_benefits_part2.py @@ -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 diff --git a/screen_locker/tests/test_home_isolation.py b/screen_locker/tests/test_home_isolation.py index 448e0fa..e205754 100644 --- a/screen_locker/tests/test_home_isolation.py +++ b/screen_locker/tests/test_home_isolation.py @@ -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), ) diff --git a/screen_locker/tests/test_init_and_log.py b/screen_locker/tests/test_init_and_log.py index 94d23ec..450569a 100644 --- a/screen_locker/tests/test_init_and_log.py +++ b/screen_locker/tests/test_init_and_log.py @@ -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"}}), ) diff --git a/screen_locker/tests/test_init_and_log_part2.py b/screen_locker/tests/test_init_and_log_part2.py index cee5980..1cb5fd1 100644 --- a/screen_locker/tests/test_init_and_log_part2.py +++ b/screen_locker/tests/test_init_and_log_part2.py @@ -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. diff --git a/screen_locker/tests/test_init_and_log_part3.py b/screen_locker/tests/test_init_and_log_part3.py index b28d770..2f2d2d3 100644 --- a/screen_locker/tests/test_init_and_log_part3.py +++ b/screen_locker/tests/test_init_and_log_part3.py @@ -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( diff --git a/screen_locker/tests/test_manual_push.py b/screen_locker/tests/test_manual_push.py index 1a49060..b40feb7 100644 --- a/screen_locker/tests/test_manual_push.py +++ b/screen_locker/tests/test_manual_push.py @@ -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: diff --git a/screen_locker/tests/test_manual_sync.py b/screen_locker/tests/test_manual_sync.py index c631347..410cdb8 100644 --- a/screen_locker/tests/test_manual_sync.py +++ b/screen_locker/tests/test_manual_sync.py @@ -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) diff --git a/screen_locker/tests/test_manual_workout.py b/screen_locker/tests/test_manual_workout.py index bd35056..24f1dcb 100644 --- a/screen_locker/tests/test_manual_workout.py +++ b/screen_locker/tests/test_manual_workout.py @@ -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) diff --git a/screen_locker/tests/test_phone_verification_part5.py b/screen_locker/tests/test_phone_verification_part5.py index ee7b44e..4ed455c 100644 --- a/screen_locker/tests/test_phone_verification_part5.py +++ b/screen_locker/tests/test_phone_verification_part5.py @@ -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() diff --git a/screen_locker/tests/test_scheduled_skip.py b/screen_locker/tests/test_scheduled_skip.py index 55c98e6..62a2de8 100644 --- a/screen_locker/tests/test_scheduled_skip.py +++ b/screen_locker/tests/test_scheduled_skip.py @@ -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])) diff --git a/screen_locker/tests/test_screen_lock_coverage_part3.py b/screen_locker/tests/test_screen_lock_coverage_part3.py index 325da05..b9cd345 100644 --- a/screen_locker/tests/test_screen_lock_coverage_part3.py +++ b/screen_locker/tests/test_screen_lock_coverage_part3.py @@ -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" diff --git a/screen_locker/tests/test_session_sync.py b/screen_locker/tests/test_session_sync.py index 5ba444e..98c302a 100644 --- a/screen_locker/tests/test_session_sync.py +++ b/screen_locker/tests/test_session_sync.py @@ -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: diff --git a/screen_locker/tests/test_shutdown_base.py b/screen_locker/tests/test_shutdown_base.py index 5d16286..b753b04 100644 --- a/screen_locker/tests/test_shutdown_base.py +++ b/screen_locker/tests/test_shutdown_base.py @@ -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 diff --git a/screen_locker/tests/test_shutdown_part3.py b/screen_locker/tests/test_shutdown_part3.py index 1d8365e..741bd23 100644 --- a/screen_locker/tests/test_shutdown_part3.py +++ b/screen_locker/tests/test_shutdown_part3.py @@ -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( diff --git a/screen_locker/tests/test_shutdown_part5.py b/screen_locker/tests/test_shutdown_part5.py index aaa62df..a94d577 100644 --- a/screen_locker/tests/test_shutdown_part5.py +++ b/screen_locker/tests/test_shutdown_part5.py @@ -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 diff --git a/screen_locker/tests/test_source_health.py b/screen_locker/tests/test_source_health.py index f799f21..429c112 100644 --- a/screen_locker/tests/test_source_health.py +++ b/screen_locker/tests/test_source_health.py @@ -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) diff --git a/screen_locker/tests/test_status.py b/screen_locker/tests/test_status.py index 12f3937..3d933e7 100644 --- a/screen_locker/tests/test_status.py +++ b/screen_locker/tests/test_status.py @@ -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": ""}}}) diff --git a/screen_locker/tests/test_status_data.py b/screen_locker/tests/test_status_data.py index 3a147c2..94f2e90 100644 --- a/screen_locker/tests/test_status_data.py +++ b/screen_locker/tests/test_status_data.py @@ -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]: diff --git a/screen_locker/tests/test_status_data_part2.py b/screen_locker/tests/test_status_data_part2.py index 8fc918b..3f8fa49 100644 --- a/screen_locker/tests/test_status_data_part2.py +++ b/screen_locker/tests/test_status_data_part2.py @@ -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: diff --git a/screen_locker/tests/test_status_data_part3.py b/screen_locker/tests/test_status_data_part3.py index 146e963..e2a2a68 100644 --- a/screen_locker/tests/test_status_data_part3.py +++ b/screen_locker/tests/test_status_data_part3.py @@ -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 ): diff --git a/screen_locker/tests/test_status_data_part4.py b/screen_locker/tests/test_status_data_part4.py index e7fb0cf..a06d184 100644 --- a/screen_locker/tests/test_status_data_part4.py +++ b/screen_locker/tests/test_status_data_part4.py @@ -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: diff --git a/screen_locker/tests/test_status_part2.py b/screen_locker/tests/test_status_part2.py index 30233f1..9bb5376 100644 --- a/screen_locker/tests/test_status_part2.py +++ b/screen_locker/tests/test_status_part2.py @@ -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 diff --git a/screen_locker/tests/test_status_part3.py b/screen_locker/tests/test_status_part3.py index c8d5f2f..2855b29 100644 --- a/screen_locker/tests/test_status_part3.py +++ b/screen_locker/tests/test_status_part3.py @@ -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( diff --git a/screen_locker/tests/test_verify_workout.py b/screen_locker/tests/test_verify_workout.py index 8f57af1..535065b 100644 --- a/screen_locker/tests/test_verify_workout.py +++ b/screen_locker/tests/test_verify_workout.py @@ -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 diff --git a/screen_locker/tests/test_wake_shutdown.py b/screen_locker/tests/test_wake_shutdown.py index 07fca1e..06c62a6 100644 --- a/screen_locker/tests/test_wake_shutdown.py +++ b/screen_locker/tests/test_wake_shutdown.py @@ -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 diff --git a/screen_locker/tests/test_weekly_check.py b/screen_locker/tests/test_weekly_check.py index 97a3bc8..275e505 100644 --- a/screen_locker/tests/test_weekly_check.py +++ b/screen_locker/tests/test_weekly_check.py @@ -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) diff --git a/screen_locker/tests/test_weekly_logic_part5.py b/screen_locker/tests/test_weekly_logic_part5.py index 5206f94..ff2f408 100644 --- a/screen_locker/tests/test_weekly_logic_part5.py +++ b/screen_locker/tests/test_weekly_logic_part5.py @@ -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 ( diff --git a/scripts/add_scheduled_skip.py b/scripts/add_scheduled_skip.py index 34648e9..155b91a 100755 --- a/scripts/add_scheduled_skip.py +++ b/scripts/add_scheduled_skip.py @@ -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 diff --git a/scripts/check_enforcement_armed.py b/scripts/check_enforcement_armed.py index 584ce45..ca1599c 100755 --- a/scripts/check_enforcement_armed.py +++ b/scripts/check_enforcement_armed.py @@ -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, "" diff --git a/scripts/fix_2026_07_26_sport.py b/scripts/fix_2026_07_26_sport.py index d5b8135..a6772bc 100755 --- a/scripts/fix_2026_07_26_sport.py +++ b/scripts/fix_2026_07_26_sport.py @@ -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) diff --git a/scripts/revoke_sick_day.py b/scripts/revoke_sick_day.py index 1dcb0ac..932a5aa 100755 --- a/scripts/revoke_sick_day.py +++ b/scripts/revoke_sick_day.py @@ -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"