Backend (tasks 1.1, 1.5–1.8): - pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x, structlog, bcrypt; ruff + mypy strict configured - Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation) - SQLite schema: settings, sessions, blocklist_sources, import_log; WAL mode + foreign keys; idempotent init_db() - FastAPI app factory with lifespan (DB, aiohttp session, scheduler), CORS, unhandled-exception handler, GET /api/health - Fail2BanClient: async Unix-socket wrapper using run_in_executor, custom error types, async context manager - Utility modules: ip_utils, time_utils, constants - 47 tests; ruff 0 errors; mypy --strict 0 errors Frontend (tasks 1.2–1.4): - Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier - Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants - Typed fetch API client (ApiError, get/post/put/del) + endpoints constants - tsc --noEmit 0 errors
68 lines
1.9 KiB
Python
68 lines
1.9 KiB
Python
"""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
|
|
|
|
|
|
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)
|