Created
March 20, 2026 01:34
-
-
Save yeiichi/d5148b5dc4972a7a0bfe2b729fd5001e to your computer and use it in GitHub Desktop.
Utility helpers for working with timezone-aware datetimes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| datetime_util.py | |
| Utility helpers for working with timezone-aware datetimes. | |
| This module provides: | |
| - Predefined, commonly used timezone constants (UTC, JST, ET, etc.) | |
| - Convenience functions for getting the current time in specific regions | |
| - Safe conversion utilities that enforce timezone awareness | |
| - ISO 8601 serialization and parsing helpers | |
| - A simple wrapper for constructing ZoneInfo instances | |
| Design principles: | |
| - Always prefer *aware* datetimes (naive datetimes are rejected) | |
| - Keep APIs small, explicit, and predictable | |
| - Use standard library only (datetime, zoneinfo) | |
| Typical usage: | |
| from datetime_util import now_jst, to_iso, from_iso, JST, ET | |
| dt_jst = now_jst() | |
| dt_et = dt_jst.astimezone(ET) | |
| iso_str = to_iso(dt_jst) | |
| parsed = from_iso(iso_str) | |
| assert parsed.tzinfo is not None | |
| Notes: | |
| - All conversion helpers raise ValueError when given naive datetimes | |
| - Timezones are based on IANA names via zoneinfo | |
| - Daylight Saving Time (DST) is handled automatically by ZoneInfo | |
| This module is intended to be a lightweight, dependency-free foundation | |
| for consistent datetime handling across applications. | |
| """ | |
| from __future__ import annotations | |
| from datetime import UTC, datetime | |
| from zoneinfo import ZoneInfo | |
| # ---- Public timezone constants ---- | |
| UTC_TZ = UTC | |
| JST = ZoneInfo("Asia/Tokyo") | |
| KST = ZoneInfo("Asia/Seoul") | |
| CST_CN = ZoneInfo("Asia/Shanghai") | |
| IST = ZoneInfo("Asia/Kolkata") | |
| SGT = ZoneInfo("Asia/Singapore") | |
| ET = ZoneInfo("America/New_York") | |
| PT = ZoneInfo("America/Los_Angeles") | |
| GMT = ZoneInfo("Europe/London") | |
| CET = ZoneInfo("Europe/Paris") | |
| __all__ = [ | |
| "UTC_TZ", | |
| "JST", | |
| "KST", | |
| "CST_CN", | |
| "IST", | |
| "SGT", | |
| "ET", | |
| "PT", | |
| "GMT", | |
| "CET", | |
| "now_utc", | |
| "now_in", | |
| "now_jst", | |
| "now_et", | |
| "to_timezone", | |
| "to_iso", | |
| "from_iso", | |
| "tz", | |
| ] | |
| def now_utc() -> datetime: | |
| """Return the current UTC datetime.""" | |
| return datetime.now(UTC_TZ) | |
| def now_in(timezone: ZoneInfo) -> datetime: | |
| """Return the current datetime in the given timezone.""" | |
| return now_utc().astimezone(timezone) | |
| def now_jst() -> datetime: | |
| """Return the current datetime in Asia/Tokyo.""" | |
| return now_in(JST) | |
| def now_et() -> datetime: | |
| """Return the current datetime in America/New_York.""" | |
| return now_in(ET) | |
| def to_timezone(dt: datetime, timezone: ZoneInfo) -> datetime: | |
| """Convert an aware datetime to another timezone. | |
| Raises: | |
| ValueError: If ``dt`` is naive. | |
| """ | |
| if dt.tzinfo is None: | |
| raise ValueError("Naive datetime is not allowed") | |
| return dt.astimezone(timezone) | |
| def to_iso(dt: datetime) -> str: | |
| """Return an ISO 8601 string for an aware datetime. | |
| Raises: | |
| ValueError: If ``dt`` is naive. | |
| """ | |
| if dt.tzinfo is None: | |
| raise ValueError("Naive datetime is not allowed") | |
| return dt.isoformat() | |
| def from_iso(value: str) -> datetime: | |
| """Parse an ISO 8601 string into an aware datetime. | |
| Raises: | |
| ValueError: If the parsed datetime is naive. | |
| """ | |
| dt = datetime.fromisoformat(value) | |
| if dt.tzinfo is None: | |
| raise ValueError("Naive datetime string is not allowed") | |
| return dt | |
| def tz(name: str) -> ZoneInfo: | |
| """Return a timezone from an IANA name. | |
| Example: | |
| tz("Australia/Sydney") | |
| """ | |
| return ZoneInfo(name) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment