class acetz(tzinfo): """An implementation of datetime.tzinfo using the ZoneSpecifier class from AceTime/tools. """ def __init__(self, zone_info: ZoneInfo): self.zone_info = zone_info self.zs = ZoneSpecifier(zone_info, use_python_transition=True) def utcoffset(self, dt: Optional[datetime]) -> timedelta: assert dt info = self.zs.get_timezone_info_for_datetime(dt) if not info: raise Exception(f'Unknown timezone info for ' f'{dt.year:04}-{dt.month:02}-{dt.day:02} ' f'{dt.hour:02}:{dt.minute:02}:{dt.second:02}') return timedelta(seconds=info.total_offset) def dst(self, dt: Optional[datetime]) -> timedelta: assert dt offset_info = self.zs.get_timezone_info_for_datetime(dt) if not offset_info: raise Exception(f'Unknown timezone info for ' f'{dt.year:04}-{dt.month:02}-{dt.day:02} ' f'{dt.hour:02}:{dt.minute:02}:{dt.second:02}') return timedelta(seconds=offset_info.dst_offset) def tzname(self, dt: Optional[datetime]) -> str: assert dt offset_info = self.zs.get_timezone_info_for_datetime(dt) if not offset_info: raise Exception(f'Unknown timezone info for ' f'{dt.year:04}-{dt.month:02}-{dt.day:02} ' f'{dt.hour:02}:{dt.minute:02}:{dt.second:02}') return offset_info.abbrev def fromutc(self, dt: Optional[datetime]) -> datetime: """Override the default implementation in tzinfo which does not make sense for acetz. The 'dt' passed into this function from datetime.astimezone() is a weird one: the components are in UTC time, but the timezone is the target tzinfo, in other words, the same acetz as self. Warning: Do NOT call dt.isoformat() from this method, because it causes an infinite recursion as it tries to figure out the UTC offset. Use {dt.date()} and {dt.time()} instead. """ if not isinstance(dt, datetime): raise TypeError("fromutc() requires a datetime argument") if dt.tzinfo is not self: raise ValueError("dt.tzinfo is not self") # Extract the epoch_seconds of the source 'dt' assert dt is not None utcdt = dt.replace(tzinfo=timezone.utc) unix_seconds = int(utcdt.timestamp()) epoch_seconds = unix_seconds - SECONDS_SINCE_UNIX_EPOCH # Search the transitions for the matching Transition offset_info = self.zs.get_timezone_info_for_seconds(epoch_seconds) if not offset_info: raise ValueError(f"transition not found for {epoch_seconds}") # Convert the date/time fields into local date/time and attach # the current acetz object. newutcdt = utcdt + timedelta(seconds=offset_info.total_offset) newdt = newutcdt.replace(tzinfo=self, fold=offset_info.fold) return newdt def zone_specifier(self) -> ZoneSpecifier: return self.zs
def main() -> None: # Configure command line flags. parser = argparse.ArgumentParser(description='Zone Agent.') parser.add_argument( '--viewing_months', help='Number of months to use for calculations (12, 13, 14, 36)', type=int, default=14) parser.add_argument('--transition', help='Print the transition instead of timezone info', action='store_true') parser.add_argument('--debug', help='Print debugging info', action='store_true') parser.add_argument( '--in_place_transitions', help='Use in-place Transition array to determine Active Transitions', action="store_true", default=True) parser.add_argument('--no_in_place_transitions', help='Disable --in_place_transitions', action="store_false", dest='in_place_transitions') parser.add_argument('--optimize_candidates', help='Optimize the candidate transitions', action='store_true', default=True) parser.add_argument('--no_optimize_candidates', help='Disable --optimize_candidates', action='store_false', dest='optimize_candidates') parser.add_argument('--zone', help='Name of time zone', required=True) parser.add_argument('--year', help='Year of interest', type=int) parser.add_argument('--date', help='DateTime of interest') args = parser.parse_args() # Configure logging logging.basicConfig(level=logging.INFO) # Find the zone. zone_info = cast(ZoneInfo, zone_infos.ZONE_INFO_MAP.get(args.zone)) if not zone_info: logging.error("Zone '%s' not found", args.zone) sys.exit(1) # Create the ZoneSpecifier for zone zone_specifier = ZoneSpecifier( zone_info_data=zone_info, viewing_months=args.viewing_months, debug=args.debug, in_place_transitions=args.in_place_transitions, optimize_candidates=args.optimize_candidates) if args.year: zone_specifier.init_for_year(args.year) if args.debug: logging.info('==== Final matches and transitions') zone_specifier.print_matches_and_transitions() elif args.date: dt: datetime = datetime.strptime(args.date, "%Y-%m-%dT%H:%M") if args.transition: transition = zone_specifier.get_transition_for_datetime(dt) if transition: logging.info(transition) else: logging.error('Transition not found') else: offset_info = zone_specifier.get_timezone_info_for_datetime(dt) if not offset_info: logging.info('Invalid time') else: logging.info( '%s (%s)', to_utc_string( offset_info.utc_offset, offset_info.dst_offset, ), offset_info.abbrev, ) else: print("One of --year or --date must be provided") sys.exit(1)