mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2024-11-14 17:40:24 -08:00
263 lines
8.9 KiB
Python
263 lines
8.9 KiB
Python
#!/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
|