#!/usr/bin/env python import operator import ctypes import datetime from ctypes.wintypes import WORD, WCHAR, BOOL, LONG from jaraco.windows.util import Extended from jaraco.collections import RangeMap class AnyDict(object): "A dictionary that returns the same value regardless of key" def __init__(self, value): self.value = value def __getitem__(self, key): return self.value class SYSTEMTIME(Extended, ctypes.Structure): _fields_ = [ ('year', WORD), ('month', WORD), ('day_of_week', WORD), ('day', WORD), ('hour', WORD), ('minute', WORD), ('second', WORD), ('millisecond', WORD), ] class REG_TZI_FORMAT(Extended, ctypes.Structure): _fields_ = [ ('bias', LONG), ('standard_bias', LONG), ('daylight_bias', LONG), ('standard_start', SYSTEMTIME), ('daylight_start', SYSTEMTIME), ] class TIME_ZONE_INFORMATION(Extended, ctypes.Structure): _fields_ = [ ('bias', LONG), ('standard_name', WCHAR * 32), ('standard_start', SYSTEMTIME), ('standard_bias', LONG), ('daylight_name', WCHAR * 32), ('daylight_start', SYSTEMTIME), ('daylight_bias', LONG), ] class DYNAMIC_TIME_ZONE_INFORMATION(TIME_ZONE_INFORMATION): """ Because the structure of the DYNAMIC_TIME_ZONE_INFORMATION extends the structure of the TIME_ZONE_INFORMATION, this structure can be used as a drop-in replacement for calls where the structure is passed by reference. For example, dynamic_tzi = DYNAMIC_TIME_ZONE_INFORMATION() ctypes.windll.kernel32.GetTimeZoneInformation(ctypes.byref(dynamic_tzi)) (although the key_name and dynamic_daylight_time_disabled flags will be set to the default (null)). >>> isinstance(DYNAMIC_TIME_ZONE_INFORMATION(), TIME_ZONE_INFORMATION) True """ _fields_ = [ # ctypes automatically includes the fields from the parent ('key_name', WCHAR * 128), ('dynamic_daylight_time_disabled', BOOL), ] def __init__(self, *args, **kwargs): """Allow initialization from args from both this class and its superclass. Default ctypes implementation seems to assume that this class is only initialized with its own _fields_ (for non-keyword-args).""" super_self = super(DYNAMIC_TIME_ZONE_INFORMATION, self) super_fields = super_self._fields_ super_args = args[: len(super_fields)] self_args = args[len(super_fields) :] # convert the super args to keyword args so they're also handled for field, arg in zip(super_fields, super_args): field_name, spec = field kwargs[field_name] = arg super(DYNAMIC_TIME_ZONE_INFORMATION, self).__init__(*self_args, **kwargs) class Info(DYNAMIC_TIME_ZONE_INFORMATION): """ A time zone definition class based on the win32 DYNAMIC_TIME_ZONE_INFORMATION structure. Describes a bias against UTC (bias), and two dates at which a separate additional bias applies (standard_bias and daylight_bias). """ def field_names(self): return map(operator.itemgetter(0), self._fields_) def __init__(self, *args, **kwargs): """ Try to construct a timezone.Info from a) [DYNAMIC_]TIME_ZONE_INFORMATION args b) another Info c) a REG_TZI_FORMAT d) a byte structure """ funcs = ( super(Info, self).__init__, self.__init_from_other, self.__init_from_reg_tzi, self.__init_from_bytes, ) for func in funcs: try: func(*args, **kwargs) return except TypeError: pass raise TypeError("Invalid arguments for %s" % self.__class__) def __init_from_bytes(self, bytes, **kwargs): reg_tzi = REG_TZI_FORMAT() # todo: use buffer API in Python 3 buffer = memoryview(bytes) ctypes.memmove(ctypes.addressof(reg_tzi), buffer, len(buffer)) self.__init_from_reg_tzi(self, reg_tzi, **kwargs) def __init_from_reg_tzi(self, reg_tzi, **kwargs): if not isinstance(reg_tzi, REG_TZI_FORMAT): raise TypeError("Not a REG_TZI_FORMAT") for field_name, type in reg_tzi._fields_: setattr(self, field_name, getattr(reg_tzi, field_name)) for name, value in kwargs.items(): setattr(self, name, value) def __init_from_other(self, other): if not isinstance(other, TIME_ZONE_INFORMATION): raise TypeError("Not a TIME_ZONE_INFORMATION") for name in other.field_names(): # explicitly get the value from the underlying structure value = super(Info, other).__getattribute__(other, name) setattr(self, name, value) # consider instead of the loop above just copying the memory directly # size = max(ctypes.sizeof(DYNAMIC_TIME_ZONE_INFO), ctypes.sizeof(other)) # ctypes.memmove(ctypes.addressof(self), other, size) def __getattribute__(self, attr): value = super(Info, self).__getattribute__(attr) def make_minute_timedelta(m): datetime.timedelta(minutes=m) if 'bias' in attr: value = make_minute_timedelta(value) return value @classmethod def current(class_): "Windows Platform SDK GetTimeZoneInformation" tzi = class_() kernel32 = ctypes.windll.kernel32 getter = kernel32.GetTimeZoneInformation getter = getattr(kernel32, 'GetDynamicTimeZoneInformation', getter) code = getter(ctypes.byref(tzi)) return code, tzi def set(self): kernel32 = ctypes.windll.kernel32 setter = kernel32.SetTimeZoneInformation setter = getattr(kernel32, 'SetDynamicTimeZoneInformation', setter) return setter(ctypes.byref(self)) def copy(self): return self.__class__(self) def locate_daylight_start(self, year): info = self.get_info_for_year(year) return self._locate_day(year, info.daylight_start) def locate_standard_start(self, year): info = self.get_info_for_year(year) return self._locate_day(year, info.standard_start) def get_info_for_year(self, year): return self.dynamic_info[year] @property def dynamic_info(self): "Return a map that for a given year will return the correct Info" if self.key_name: dyn_key = self.get_key().subkey('Dynamic DST') del dyn_key['FirstEntry'] del dyn_key['LastEntry'] years = map(int, dyn_key.keys()) values = map(Info, dyn_key.values()) # create a range mapping that searches by descending year and matches # if the target year is greater or equal. return RangeMap(zip(years, values), RangeMap.descending, operator.ge) else: return AnyDict(self) @staticmethod def _locate_day(year, cutoff): """ Takes a SYSTEMTIME object, such as retrieved from a TIME_ZONE_INFORMATION structure or call to GetTimeZoneInformation and interprets it based on the given year to identify the actual day. This method is necessary because the SYSTEMTIME structure refers to a day by its day of the week and week of the month (e.g. 4th saturday in March). >>> SATURDAY = 6 >>> MARCH = 3 >>> st = SYSTEMTIME(2000, MARCH, SATURDAY, 4, 0, 0, 0, 0) # according to my calendar, the 4th Saturday in March in 2009 was the 28th >>> expected_date = datetime.datetime(2009, 3, 28) >>> Info._locate_day(2009, st) == expected_date True """ # MS stores Sunday as 0, Python datetime stores Monday as zero target_weekday = (cutoff.day_of_week + 6) % 7 # For SYSTEMTIMEs relating to time zone inforamtion, cutoff.day # is the week of the month week_of_month = cutoff.day # so the following is the first day of that week day = (week_of_month - 1) * 7 + 1 result = datetime.datetime( year, cutoff.month, day, cutoff.hour, cutoff.minute, cutoff.second, cutoff.millisecond, ) # now the result is the correct week, but not necessarily # the correct day of the week days_to_go = (target_weekday - result.weekday()) % 7 result += datetime.timedelta(days_to_go) # if we selected a day in the month following the target month, # move back a week or two. # This is necessary because Microsoft defines the fifth week in a month # to be the last week in a month and adding the time delta might have # pushed the result into the next month. while result.month == cutoff.month + 1: result -= datetime.timedelta(weeks=1) return result