Also rename services/tz.user_briefing_date → user_day_date with a backwards compat alias (briefing modules using the old name will be deleted in the upcoming briefing tear-down stage). Update services/chat.py to_dict to use day_date. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
53 lines
2.0 KiB
Python
53 lines
2.0 KiB
Python
"""User-timezone helpers.
|
||
|
||
All datetimes in the DB are stored as UTC. The helpers here bridge between
|
||
that UTC storage and the user's configured local timezone (IANA string in
|
||
the ``user_timezone`` setting). Use these anywhere the model or UI talks
|
||
in terms of "today", "tomorrow", or a bare calendar date — never
|
||
``date.today()`` or ``datetime.now(timezone.utc)`` inside a per-user flow.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, datetime, timedelta
|
||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||
|
||
from fabledassistant.services.settings import get_setting
|
||
|
||
# Day-rollover boundary for journal/day-anchored views. The "day" flips at
|
||
# this local hour (not midnight) so the 00:00–04:00 local window still
|
||
# shows yesterday's content until the user "starts" the new day.
|
||
DAY_ROLLOVER_HOUR = 4
|
||
|
||
# Backwards-compat alias for code paths still referencing the old name.
|
||
BRIEFING_DAY_START_HOUR = DAY_ROLLOVER_HOUR
|
||
|
||
|
||
async def get_user_tz(user_id: int) -> ZoneInfo:
|
||
"""Return the user's IANA ``ZoneInfo``, falling back to UTC."""
|
||
tz_str = await get_setting(user_id, "user_timezone") or "UTC"
|
||
try:
|
||
return ZoneInfo(tz_str)
|
||
except (ZoneInfoNotFoundError, KeyError):
|
||
return ZoneInfo("UTC")
|
||
|
||
|
||
async def user_today(user_id: int) -> date:
|
||
"""Return today's calendar date in the user's local timezone."""
|
||
tz = await get_user_tz(user_id)
|
||
return datetime.now(tz).date()
|
||
|
||
|
||
async def user_day_date(user_id: int) -> date:
|
||
"""Return the current "day" in the user's local timezone.
|
||
|
||
The day flips at ``DAY_ROLLOVER_HOUR`` (4am local) rather than midnight,
|
||
so the 00:00–04:00 local window still returns *yesterday* — the user's
|
||
journal stays anchored to yesterday until they cross the rollover.
|
||
"""
|
||
tz = await get_user_tz(user_id)
|
||
return (datetime.now(tz) - timedelta(hours=DAY_ROLLOVER_HOUR)).date()
|
||
|
||
|
||
# Backwards-compat alias used by briefing-only modules being torn down in Stage B.
|
||
user_briefing_date = user_day_date
|