init
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, time as dt_time
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DateTimeBundle:
|
||||
"""Unified datetime payload for storage and API usage."""
|
||||
|
||||
dt: datetime
|
||||
db_datetime: datetime
|
||||
epoch_seconds: int
|
||||
epoch_millis: int
|
||||
yyyymmdd: str
|
||||
date_str: str
|
||||
datetime_str: str
|
||||
iso_str: str
|
||||
|
||||
|
||||
class DateTimeGenerator:
|
||||
"""Parse and generate datetime values in multiple common formats."""
|
||||
|
||||
DEFAULT_TZ = ZoneInfo("Asia/Shanghai")
|
||||
SUPPORTED_FORMATS = (
|
||||
"%Y%m%d",
|
||||
"%Y%m%d%H%M%S",
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y/%m/%d %H:%M",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y/%m/%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M:%S.%f",
|
||||
"%Y/%m/%d %H:%M:%S.%f",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def now(cls) -> DateTimeBundle:
|
||||
return cls.bundle()
|
||||
|
||||
@classmethod
|
||||
def bundle(cls, value: Any = None, *, default_to_now: bool = True) -> DateTimeBundle:
|
||||
dt = cls.parse(value, default_to_now=default_to_now)
|
||||
epoch_seconds = int(dt.timestamp())
|
||||
epoch_millis = int(dt.timestamp() * 1000)
|
||||
return DateTimeBundle(
|
||||
dt=dt,
|
||||
db_datetime=dt.replace(tzinfo=None),
|
||||
epoch_seconds=epoch_seconds,
|
||||
epoch_millis=epoch_millis,
|
||||
yyyymmdd=dt.strftime("%Y%m%d"),
|
||||
date_str=dt.strftime("%Y-%m-%d"),
|
||||
datetime_str=dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
iso_str=dt.isoformat(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def pair(cls, created_value: Any = None, updated_value: Any = None) -> tuple[DateTimeBundle, DateTimeBundle]:
|
||||
created = cls.bundle(created_value, default_to_now=True)
|
||||
updated = cls.bundle(updated_value if updated_value is not None else created.epoch_millis, default_to_now=True)
|
||||
return created, updated
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: Any = None, *, default_to_now: bool = True) -> datetime:
|
||||
if value is None:
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise ValueError("datetime value is None")
|
||||
|
||||
if isinstance(value, datetime):
|
||||
return cls._normalize_datetime(value)
|
||||
|
||||
if isinstance(value, date):
|
||||
return datetime.combine(value, dt_time.min).replace(tzinfo=cls.DEFAULT_TZ)
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
return cls._parse_numeric(str(int(value)), default_to_now=default_to_now)
|
||||
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
if not text:
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise ValueError("datetime value is blank")
|
||||
|
||||
if text.isdigit():
|
||||
return cls._parse_numeric(text, default_to_now=default_to_now)
|
||||
|
||||
iso_candidate = text.replace("Z", "+00:00")
|
||||
try:
|
||||
return cls._normalize_datetime(datetime.fromisoformat(iso_candidate))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for fmt in cls.SUPPORTED_FORMATS:
|
||||
try:
|
||||
parsed = datetime.strptime(text, fmt)
|
||||
return parsed.replace(tzinfo=cls.DEFAULT_TZ)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise ValueError(f"unsupported datetime value: {value!r}")
|
||||
|
||||
@classmethod
|
||||
def _parse_numeric(cls, text: str, *, default_to_now: bool) -> datetime:
|
||||
if len(text) == 8:
|
||||
try:
|
||||
return datetime.strptime(text, "%Y%m%d").replace(tzinfo=cls.DEFAULT_TZ)
|
||||
except Exception:
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise
|
||||
|
||||
if len(text) == 14:
|
||||
try:
|
||||
return datetime.strptime(text, "%Y%m%d%H%M%S").replace(tzinfo=cls.DEFAULT_TZ)
|
||||
except Exception:
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise
|
||||
|
||||
if len(text) == 10:
|
||||
try:
|
||||
return datetime.fromtimestamp(int(text), tz=cls.DEFAULT_TZ)
|
||||
except Exception:
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise
|
||||
|
||||
if len(text) == 13:
|
||||
try:
|
||||
return datetime.fromtimestamp(int(text) / 1000, tz=cls.DEFAULT_TZ)
|
||||
except Exception:
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise
|
||||
|
||||
if default_to_now:
|
||||
return datetime.now(cls.DEFAULT_TZ)
|
||||
raise ValueError(f"unsupported numeric datetime value: {text!r}")
|
||||
|
||||
@classmethod
|
||||
def _normalize_datetime(cls, value: datetime) -> datetime:
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=cls.DEFAULT_TZ)
|
||||
return value.astimezone(cls.DEFAULT_TZ)
|
||||
|
||||
Reference in New Issue
Block a user