{"record":{"id":"72333746f6da50f9","repo":"ZhuLinsen/daily_stock_analysis","slug":"zoneinfo-is-unavailable","errorCode":null,"errorMessage":"zoneinfo is unavailable","messagePattern":"zoneinfo is unavailable","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/notification_noise.py","lineNumber":118,"sourceCode":"    raw = str(value or \"\").strip()\n    if not raw:\n        return None\n\n    match = _QUIET_HOURS_RE.match(raw)\n    if not match:\n        raise ValueError(\"NOTIFICATION_QUIET_HOURS must be in HH:MM-HH:MM format\")\n\n    start_hour, start_minute, end_hour, end_minute = [int(group) for group in match.groups()]\n    return start_hour * 60 + start_minute, end_hour * 60 + end_minute\n\n\ndef validate_notification_timezone(value: Optional[str]) -> None:\n    \"\"\"Validate an optional IANA timezone name.\"\"\"\n    raw = str(value or \"\").strip()\n    if not raw:\n        return\n    if ZoneInfo is None:\n        raise ValueError(\"zoneinfo is unavailable\")\n    try:\n        ZoneInfo(raw)\n    except ZoneInfoNotFoundError as exc:\n        raise ValueError(f\"unknown timezone: {raw}\") from exc\n\n\ndef is_time_in_quiet_hours(now: datetime, quiet_hours: Tuple[int, int]) -> bool:\n    \"\"\"Return whether *now* falls inside a quiet-hours interval.\"\"\"\n    start_minute, end_minute = quiet_hours\n    minute_of_day = now.hour * 60 + now.minute\n\n    if start_minute == end_minute:\n        return False\n    if start_minute < end_minute:\n        return start_minute <= minute_of_day < end_minute\n    return minute_of_day >= start_minute or minute_of_day < end_minute\n\n","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/notification_noise.py#L100-L136","documentation":"ValueError from validate_notification_timezone when a NOTIFICATION_TIMEZONE value is supplied but the ZoneInfo class could not be imported in the running interpreter. This is an environment capability failure, not a bad timezone name: the code guards the import and only raises here when a timezone is actually requested while zoneinfo is unavailable.","triggerScenarios":"Running on Python older than 3.9 (zoneinfo added in 3.9) with NOTIFICATION_TIMEZONE set to a non-empty value, or on a stripped interpreter where the stdlib zoneinfo module was removed. Any non-empty timezone config triggers it.","commonSituations":"Deploying to a legacy Python 3.8 image; a minimal/alpine-like custom build pruning stdlib modules; a virtualenv built against an old system Python; CI matrix still including EOL Python versions.","solutions":["Upgrade the runtime to Python 3.9 or newer where zoneinfo is in the standard library","Leave NOTIFICATION_TIMEZONE empty/unset — quiet hours then use local time and the import is never required","On custom builds, ensure the stdlib zoneinfo module and tzdata are included in the image","Pin CI/deploy images to python:3.10+ or equivalent"],"exampleFix":"# before (python 3.8 runtime, timezone set)\nNOTIFICATION_TIMEZONE=Asia/Shanghai\n\n# after\n# either run on Python >= 3.9, or unset:\n# NOTIFICATION_TIMEZONE=","handlingStrategy":"validation","validationCode":"import sys\n\ndef zoneinfo_available() -> bool:\n    if sys.version_info < (3, 9):\n        return False\n    try:\n        from zoneinfo import ZoneInfo  # noqa: F401\n        return True\n    except ImportError:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    validate_notification_timezone(tz)\nexcept ValueError as exc:\n    if str(exc) == \"zoneinfo is unavailable\":\n        log.warning(\"Timezone ignored: needs Python >= 3.9; continuing with local time\")\n    else:\n        raise","preventionTips":["Assert Python >= 3.9 in deployment metadata when timezone config is used","Include stdlib zoneinfo (and tzdata fallback package) in minimal container images","Skip timezone validation instead of crashing when the feature is optional"],"tags":["environment","python-version","notification","timezone"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}