Buckets:
| """ | |
| Timezone-aware clock for Hermes. | |
| Provides a single ``now()`` helper that returns a timezone-aware datetime | |
| based on the user's configured IANA timezone (e.g. ``Asia/Kolkata``). | |
| Resolution order: | |
| 1. ``HERMES_TIMEZONE`` environment variable | |
| 2. ``timezone`` key in ``~/.hermes/config.yaml`` | |
| 3. Falls back to the server's local time (``datetime.now().astimezone()``) | |
| Invalid timezone values log a warning and fall back safely — Hermes never | |
| crashes due to a bad timezone string. | |
| """ | |
| import logging | |
| import os | |
| from datetime import datetime | |
| from hermes_constants import get_config_path | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| try: | |
| from zoneinfo import ZoneInfo | |
| except ImportError: | |
| # Python 3.8 fallback (shouldn't be needed — Hermes requires 3.9+) | |
| from backports.zoneinfo import ZoneInfo # type: ignore[no-redef] | |
| # Cached state — resolved once, reused on every call. | |
| # Call reset_cache() to force re-resolution (e.g. after config changes). | |
| _cached_tz: Optional[ZoneInfo] = None | |
| _cached_tz_name: Optional[str] = None | |
| _cache_resolved: bool = False | |
| def _resolve_timezone_name() -> str: | |
| """Read the configured IANA timezone string (or empty string). | |
| This does file I/O when falling through to config.yaml, so callers | |
| should cache the result rather than calling on every ``now()``. | |
| """ | |
| # 1. Environment variable (highest priority — set by Supervisor, etc.) | |
| tz_env = os.getenv("HERMES_TIMEZONE", "").strip() | |
| if tz_env: | |
| return tz_env | |
| # 2. config.yaml ``timezone`` key | |
| try: | |
| import yaml | |
| config_path = get_config_path() | |
| if config_path.exists(): | |
| with open(config_path, encoding="utf-8") as f: | |
| cfg = yaml.safe_load(f) or {} | |
| tz_cfg = cfg.get("timezone", "") | |
| if isinstance(tz_cfg, str) and tz_cfg.strip(): | |
| return tz_cfg.strip() | |
| except Exception: | |
| pass | |
| return "" | |
| def _get_zoneinfo(name: str) -> Optional[ZoneInfo]: | |
| """Validate and return a ZoneInfo, or None if invalid.""" | |
| if not name: | |
| return None | |
| try: | |
| return ZoneInfo(name) | |
| except (KeyError, Exception) as exc: | |
| logger.warning( | |
| "Invalid timezone '%s': %s. Falling back to server local time.", | |
| name, exc, | |
| ) | |
| return None | |
| def get_timezone() -> Optional[ZoneInfo]: | |
| """Return the user's configured ZoneInfo, or None (meaning server-local). | |
| Resolved once and cached. Call ``reset_cache()`` after config changes. | |
| """ | |
| global _cached_tz, _cached_tz_name, _cache_resolved | |
| if not _cache_resolved: | |
| _cached_tz_name = _resolve_timezone_name() | |
| _cached_tz = _get_zoneinfo(_cached_tz_name) | |
| _cache_resolved = True | |
| return _cached_tz | |
| def now() -> datetime: | |
| """ | |
| Return the current time as a timezone-aware datetime. | |
| If a valid timezone is configured, returns wall-clock time in that zone. | |
| Otherwise returns the server's local time (via ``astimezone()``). | |
| """ | |
| tz = get_timezone() | |
| if tz is not None: | |
| return datetime.now(tz) | |
| # No timezone configured — use server-local (still tz-aware) | |
| return datetime.now().astimezone() | |
Xet Storage Details
- Size:
- 3.23 kB
- Xet hash:
- 04ad3c746cc5f5abbcd79d21f51fa97028c89678946dacdc36bc328613785939
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.