"""Original educational fixture, Python 3.11+. No scheduler or network I/O. Run: python3 timezone-schedule-lab.py Optional: --tzdir DIRECTORY containing compiled IANA TZif files. The report contains zone hashes, not local filesystem paths. """ import argparse import hashlib import json import platform from datetime import datetime, timedelta, timezone from pathlib import Path from zoneinfo import ZoneInfo, ZoneInfoNotFoundError UTC = timezone.utc def candidates(local, zone): """Return distinct valid UTC instants for one naive local datetime. Deliberately does not choose a gap/fold business policy. UTC keys avoid same-zone datetime comparisons ignoring fold. Intended for ZoneInfo. """ if local.tzinfo is not None: raise ValueError('Expected naive local calendar fields') found = set() for fold in (0, 1): instant = local.replace(tzinfo=zone, fold=fold).astimezone(UTC) back = instant.astimezone(zone) if back.replace(tzinfo=None) == local: found.add(instant) return sorted(found) def run(tzdir=None): zone_hashes = {} def zone(key): if tzdir is None: return ZoneInfo(key) # All keys below are fixed fixture constants, never external input. raw = (Path(tzdir) / key).read_bytes() zone_hashes[key] = hashlib.sha256(raw).hexdigest() with (Path(tzdir) / key).open('rb') as f: return ZoneInfo.from_file(f, key=key) ny, seoul, lord = (zone(k) for k in ('America/New_York', 'Asia/Seoul', 'Australia/Lord_Howe')) cases = [] def expect(name, actual, expected): if actual != expected: raise AssertionError(f'{name}: {actual!r} != {expected!r}') cases.append({'case': name, 'observed': actual, 'passed': True}) def iso(values): return [v.isoformat().replace('+00:00', 'Z') for v in values] expect('normal-new-york', iso(candidates(datetime(2026, 10, 12, 9), ny)), ['2026-10-12T13:00:00Z']) expect('spring-gap', iso(candidates(datetime(2026, 3, 8, 2, 30), ny)), []) fold = candidates(datetime(2026, 11, 1, 1, 30), ny) expect('autumn-fold', iso(fold), ['2026-11-01T05:30:00Z', '2026-11-01T06:30:00Z']) expect('fold-elapsed-minutes', (fold[1] - fold[0]).total_seconds() / 60, 60) spring = [candidates(datetime(2026, 3, d, 9), ny)[0] for d in (7, 8)] expect('daily-nine-spring', iso(spring), ['2026-03-07T14:00:00Z', '2026-03-08T13:00:00Z']) expect('daily-nine-spring-hours', (spring[1] - spring[0]).total_seconds() / 3600, 23) expect('fixed-24h-spring-local', (spring[0] + timedelta(hours=24)).astimezone(ny).isoformat(), '2026-03-08T10:00:00-04:00') autumn = [candidates(datetime(2026, 10, 31, 9), ny)[0], candidates(datetime(2026, 11, 1, 9), ny)[0]] expect('daily-nine-autumn', iso(autumn), ['2026-10-31T13:00:00Z', '2026-11-01T14:00:00Z']) expect('daily-nine-autumn-hours', (autumn[1] - autumn[0]).total_seconds() / 3600, 25) expect('fixed-24h-autumn-local', (autumn[0] + timedelta(hours=24)).astimezone(ny).isoformat(), '2026-11-01T08:00:00-05:00') expect('seoul-normal', iso(candidates(datetime(2026, 11, 1, 9), seoul)), ['2026-11-01T00:00:00Z']) half = candidates(datetime(2026, 4, 5, 1, 45), lord) expect('lord-howe-fold', iso(half), ['2026-04-04T14:45:00Z', '2026-04-04T15:15:00Z']) expect('lord-howe-fold-minutes', (half[1] - half[0]).total_seconds() / 60, 30) expect('lord-howe-gap', iso(candidates(datetime(2026, 10, 4, 2, 15), lord)), []) try: candidates(datetime(2026, 1, 1, tzinfo=UTC), ny) except ValueError: expect('aware-input-rejected', True, True) else: raise AssertionError('aware input was accepted') try: ZoneInfo('Example/Invalid_Zone') except ZoneInfoNotFoundError: expect('unknown-zone-rejected', True, True) else: raise AssertionError('unknown zone was accepted') # Constructing an aware datetime is not a validity check. imaginary = datetime(2026, 3, 8, 2, 30, tzinfo=ny) expect('unchecked-gap-roundtrip', imaginary.astimezone(UTC).astimezone(ny).isoformat(), '2026-03-08T03:30:00-04:00') return { 'python': platform.python_version(), 'dataMode': 'explicit-TZif' if tzdir else 'runtime-default-not-version-pinned', 'zoneFileSha256': zone_hashes, 'passed': len(cases), 'cases': cases, 'limits': 'Fixed synthetic dates; conversion only. No cron, recurrence engine, ' 'calendar sync, DB, queue, delivery, real-time waiting or performance test. ' 'Python datetime does not model leap seconds.' } if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--tzdir') args = parser.parse_args() print(json.dumps(run(args.tzdir), ensure_ascii=False, indent=2))