ForcePilot/backend/package/yuxi/utils/datetime_utils.py
Kris f9f08221fc chore: 整理代码风格与优化细节
本次提交包含多类代码优化:
1. 修复多处单行代码换行格式,统一代码排版
2. 为外部系统模块新增快捷菜单配置
3. 优化前端API请求参数命名一致性
4. 补充媒体下载超限错误码与领域异常类
5. 完善仓储层更新逻辑,支持显式清空字段
6. 优化部分测试用例与工具函数代码结构
7. 为微信插件白名单缓存增加过期时间
2026-07-14 15:13:29 +08:00

172 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Datetime helper utilities for consistent timezone handling.
The backend stores timestamps in UTC and exposes ISO 8601 strings with an
explicit timezone designator. For user-facing displays we typically convert to
Asia/Shanghai.
"""
from __future__ import annotations
import datetime as dt
from collections.abc import Iterable
from zoneinfo import ZoneInfo
UTC = dt.UTC
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
_ISO_Z_SUFFIX = "+00:00"
def utc_now() -> dt.datetime:
"""Return the current UTC time as an aware datetime."""
return dt.datetime.now(UTC)
def utc_now_naive() -> dt.datetime:
"""Return the current UTC time as a naive datetime (for DB fields without timezone)."""
return dt.datetime.now(UTC).replace(tzinfo=None)
def shanghai_now() -> dt.datetime:
"""Return the current Asia/Shanghai time as an aware datetime."""
return utc_now().astimezone(SHANGHAI_TZ)
def ensure_utc(value: dt.datetime) -> dt.datetime:
"""
Convert a datetime to UTC.
Naive values are assumed to be in Asia/Shanghai.
"""
if value.tzinfo is None:
value = value.replace(tzinfo=SHANGHAI_TZ)
return value.astimezone(UTC)
def to_naive_utc(value: dt.datetime | None) -> dt.datetime | None:
"""将 aware datetime 转为 naive UTC用于 ``TIMESTAMP WITHOUT TIME ZONE`` 列。
与 DB 存储约定一致:所有 DateTime 列以 UTC naive 存储(见 ``utc_now_naive``)。
naive 输入视为已 UTC原样返回``None`` 原样返回。
"""
if value is None:
return None
if value.tzinfo is None:
return value
return value.astimezone(UTC).replace(tzinfo=None)
def ensure_shanghai(value: dt.datetime) -> dt.datetime:
"""
Convert a datetime to Asia/Shanghai.
Naive values are assumed to be in Asia/Shanghai.
"""
if value.tzinfo is None:
value = value.replace(tzinfo=SHANGHAI_TZ)
return value.astimezone(SHANGHAI_TZ)
def utc_isoformat(value: dt.datetime | None = None) -> str:
"""Return an ISO 8601 string in UTC with a trailing Z suffix."""
value = ensure_utc(value or utc_now())
iso_string = value.isoformat()
if iso_string.endswith(_ISO_Z_SUFFIX):
return iso_string.replace(_ISO_Z_SUFFIX, "Z")
return iso_string
def shanghai_isoformat(value: dt.datetime | None = None) -> str:
"""Return an ISO 8601 string in Asia/Shanghai timezone."""
value = ensure_shanghai(value or shanghai_now())
return value.isoformat()
def coerce_datetime(value: dt.datetime | None) -> dt.datetime | None:
"""Normalize persisted datetimes to UTC, handling nulls gracefully."""
if value is None:
return None
return ensure_utc(value)
def coerce_any_to_utc_datetime(value: dt.datetime | int | float | str | None) -> dt.datetime | None:
"""
Convert heterogeneous timestamp representations to an aware UTC datetime.
Supports:
* aware or naive datetime objects
* unix timestamps (seconds) as int/float
* ISO 8601 strings
"""
if value is None:
return None
if isinstance(value, dt.datetime):
return ensure_utc(value)
if isinstance(value, (int, float)):
return dt.datetime.fromtimestamp(value, tz=UTC)
if isinstance(value, str):
# Attempt to parse ISO 8601 strings.
try:
parsed = dt.datetime.fromisoformat(value.replace("Z", _ISO_Z_SUFFIX))
return ensure_utc(parsed)
except ValueError:
# Attempt fallback to numeric string
try:
as_number = float(value)
return dt.datetime.fromtimestamp(as_number, tz=UTC)
except ValueError:
raise ValueError(f"Unsupported datetime string format: {value!r}") from None
raise TypeError(f"Unsupported datetime value: {value!r}")
def normalize_iterable_to_utc(values: Iterable[dt.datetime | None]) -> list[dt.datetime | None]:
"""Normalize each datetime in iterable to UTC."""
return [coerce_datetime(item) if isinstance(item, dt.datetime) else None for item in values]
def format_utc_datetime(value: dt.datetime | None) -> str | None:
"""
Format a datetime to UTC ISO 8601 string, handling naive datetimes.
Returns None for None input.
Naive datetimes are assumed to be in UTC (与 DB 存储约定一致:所有 DateTime
列以 UTC naive 存储)。aware datetimes 统一转换为 UTC。
"""
if value is None:
return None
if value.tzinfo is None:
# naive datetime 视为 UTC直接附加 Z 后缀
return value.isoformat() + "Z"
# aware datetime 转换为 UTC
return utc_isoformat(value)
def utc_isoformat_from_timestamp(timestamp: float | int | None) -> str | None:
"""Format a Unix timestamp as an ISO 8601 UTC datetime string."""
if timestamp is None:
return None
return dt.datetime.fromtimestamp(timestamp, tz=UTC).isoformat()
__all__ = [
"UTC",
"SHANGHAI_TZ",
"utc_now",
"utc_now_naive",
"shanghai_now",
"ensure_utc",
"to_naive_utc",
"ensure_shanghai",
"utc_isoformat",
"shanghai_isoformat",
"coerce_datetime",
"coerce_any_to_utc_datetime",
"normalize_iterable_to_utc",
"format_utc_datetime",
"utc_isoformat_from_timestamp",
]