"""Timezone-aware datetime helpers. All datetimes in BanGUI are stored and transmitted in UTC. Conversion to the user's display timezone happens only at the presentation layer (frontend). These utilities provide a consistent, safe foundation for working with time throughout the backend. """ import datetime import time def utc_now() -> datetime.datetime: """Return the current UTC time as a timezone-aware :class:`datetime.datetime`. Returns: Current UTC datetime with ``tzinfo=datetime.UTC``. """ return datetime.datetime.now(datetime.UTC) def utc_from_timestamp(ts: float) -> datetime.datetime: """Convert a POSIX timestamp to a timezone-aware UTC datetime. Args: ts: POSIX timestamp (seconds since Unix epoch). Returns: Timezone-aware UTC :class:`datetime.datetime`. """ return datetime.datetime.fromtimestamp(ts, tz=datetime.UTC) def add_minutes(dt: datetime.datetime, minutes: int) -> datetime.datetime: """Return a new datetime that is *minutes* ahead of *dt*. Args: dt: The source datetime (must be timezone-aware). minutes: Number of minutes to add. May be negative. Returns: A new timezone-aware :class:`datetime.datetime`. """ return dt + datetime.timedelta(minutes=minutes) def is_expired(expires_at: datetime.datetime) -> bool: """Return ``True`` if *expires_at* is in the past relative to UTC now. Args: expires_at: The expiry timestamp to check (must be timezone-aware). Returns: ``True`` when the timestamp is past, ``False`` otherwise. """ return utc_now() >= expires_at def hours_ago(hours: int) -> datetime.datetime: """Return a timezone-aware UTC datetime *hours* before now. Args: hours: Number of hours to subtract from the current time. Returns: Timezone-aware UTC :class:`datetime.datetime`. """ return utc_now() - datetime.timedelta(hours=hours) def since_unix(range_: str) -> int: """Return the Unix timestamp for the start of a time-range window. Uses :func:`time.time` (always UTC epoch seconds on all platforms) to be consistent with how fail2ban stores ``timeofban`` values in its SQLite database. fail2ban records :func:`time.time()` values directly, so using a timezone-aware :func:`datetime.datetime.now`\\ ``(UTC).timestamp()`` would theoretically produce the same result but using :func:`time.time` avoids any timezone-aware datetime pitfalls on misconfigured systems. A 60-second slack window is applied to accommodate clock drift and test seeding delays. This ensures consistent query windows across services (e.g., dashboard vs. history). Args: range_: One of the supported time-range presets (e.g., ``"24h"``). Returns: Unix timestamp (seconds since epoch) representing the start of the time window: *now − range_ − slack*. """ from app.models.ban import TIME_RANGE_SECONDS # noqa: F401 from app.utils.constants import TIME_RANGE_SLACK_SECONDS seconds: int = TIME_RANGE_SECONDS[range_] return int(time.time()) - seconds - TIME_RANGE_SLACK_SECONDS