From 0b763b70f43000d7ce2faf46947bdc506ac140df Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Fri, 19 Apr 2019 15:35:05 -0400 Subject: [PATCH] Local filtering of CSV output. Fixes #895. --- src/dateutil/__init__.py | 8 + src/dateutil/_common.py | 43 + src/dateutil/_version.py | 4 + src/dateutil/easter.py | 89 + src/dateutil/parser/__init__.py | 60 + src/dateutil/parser/_parser.py | 1580 ++++++++++++++ src/dateutil/parser/isoparser.py | 411 ++++ src/dateutil/relativedelta.py | 599 ++++++ src/dateutil/rrule.py | 1736 ++++++++++++++++ src/dateutil/tz/__init__.py | 17 + src/dateutil/tz/_common.py | 419 ++++ src/dateutil/tz/_factories.py | 73 + src/dateutil/tz/tz.py | 1836 +++++++++++++++++ src/dateutil/tz/win.py | 370 ++++ src/dateutil/tzwin.py | 2 + src/dateutil/utils.py | 71 + src/dateutil/zoneinfo/__init__.py | 167 ++ .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 154405 bytes src/dateutil/zoneinfo/rebuild.py | 53 + src/gam.py | 43 +- src/var.py | 8 + 21 files changed, 7586 insertions(+), 3 deletions(-) create mode 100644 src/dateutil/__init__.py create mode 100644 src/dateutil/_common.py create mode 100644 src/dateutil/_version.py create mode 100644 src/dateutil/easter.py create mode 100644 src/dateutil/parser/__init__.py create mode 100644 src/dateutil/parser/_parser.py create mode 100644 src/dateutil/parser/isoparser.py create mode 100644 src/dateutil/relativedelta.py create mode 100644 src/dateutil/rrule.py create mode 100644 src/dateutil/tz/__init__.py create mode 100644 src/dateutil/tz/_common.py create mode 100644 src/dateutil/tz/_factories.py create mode 100644 src/dateutil/tz/tz.py create mode 100644 src/dateutil/tz/win.py create mode 100644 src/dateutil/tzwin.py create mode 100644 src/dateutil/utils.py create mode 100644 src/dateutil/zoneinfo/__init__.py create mode 100644 src/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 src/dateutil/zoneinfo/rebuild.py diff --git a/src/dateutil/__init__.py b/src/dateutil/__init__.py new file mode 100644 index 00000000..0defb82e --- /dev/null +++ b/src/dateutil/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +try: + from ._version import version as __version__ +except ImportError: + __version__ = 'unknown' + +__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', + 'utils', 'zoneinfo'] diff --git a/src/dateutil/_common.py b/src/dateutil/_common.py new file mode 100644 index 00000000..4eb2659b --- /dev/null +++ b/src/dateutil/_common.py @@ -0,0 +1,43 @@ +""" +Common code used in multiple modules. +""" + + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + def __hash__(self): + return hash(( + self.weekday, + self.n, + )) + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) + +# vim:ts=4:sw=4:et diff --git a/src/dateutil/_version.py b/src/dateutil/_version.py new file mode 100644 index 00000000..670d7ab7 --- /dev/null +++ b/src/dateutil/_version.py @@ -0,0 +1,4 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = '2.8.0' diff --git a/src/dateutil/easter.py b/src/dateutil/easter.py new file mode 100644 index 00000000..53b7c789 --- /dev/null +++ b/src/dateutil/easter.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic easter computing method for any given year, using +Western, Orthodox or Julian algorithms. +""" + +import datetime + +__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] + +EASTER_JULIAN = 1 +EASTER_ORTHODOX = 2 +EASTER_WESTERN = 3 + + +def easter(year, method=EASTER_WESTERN): + """ + This method was ported from the work done by GM Arts, + on top of the algorithm by Claus Tondering, which was + based in part on the algorithm of Ouding (1940), as + quoted in "Explanatory Supplement to the Astronomical + Almanac", P. Kenneth Seidelmann, editor. + + This algorithm implements three different easter + calculation methods: + + 1 - Original calculation in Julian calendar, valid in + dates after 326 AD + 2 - Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3 - Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well + + These methods are represented by the constants: + + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` + + The default method is method 3. + + More about the algorithm may be found at: + + `GM Arts: Easter Algorithms `_ + + and + + `The Calendar FAQ: Easter `_ + + """ + + if not (1 <= method <= 3): + raise ValueError("invalid method") + + # g - Golden year - 1 + # c - Century + # h - (23 - Epact) mod 30 + # i - Number of days from March 21 to Paschal Full Moon + # j - Weekday for PFM (0=Sunday, etc) + # p - Number of days from March 21 to Sunday on or before PFM + # (-6 to 28 methods 1 & 3, to 56 for method 2) + # e - Extra days to add for method 2 (converting Julian + # date to Gregorian date) + + y = year + g = y % 19 + e = 0 + if method < 3: + # Old method + i = (19*g + 15) % 30 + j = (y + y//4 + i) % 7 + if method == 2: + # Extra dates to convert Julian to Gregorian date + e = 10 + if y > 1600: + e = e + y//100 - 16 - (y//100 - 16)//4 + else: + # New method + c = y//100 + h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 + i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) + j = (y + y//4 + i + 2 - c + c//4) % 7 + + # p can be from -6 to 56 corresponding to dates 22 March to 23 May + # (later dates apply to method 2, although 23 May never actually occurs) + p = i - j + e + d = 1 + (p + 27 + (p + 6)//40) % 31 + m = 3 + (p + 26)//30 + return datetime.date(int(y), int(m), int(d)) diff --git a/src/dateutil/parser/__init__.py b/src/dateutil/parser/__init__.py new file mode 100644 index 00000000..216762c0 --- /dev/null +++ b/src/dateutil/parser/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + 'UnknownTimezoneWarning'] + + +### +# Deprecate portions of the private interface so that downstream code that +# is improperly relying on it is given *some* notice. + + +def __deprecated_private_func(f): + from functools import wraps + import warnings + + msg = ('{name} is a private function and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=f.__name__) + + @wraps(f) + def deprecated_func(*args, **kwargs): + warnings.warn(msg, DeprecationWarning) + return f(*args, **kwargs) + + return deprecated_func + +def __deprecate_private_class(c): + import warnings + + msg = ('{name} is a private class and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=c.__name__) + + class private_class(c): + __doc__ = c.__doc__ + + def __init__(self, *args, **kwargs): + warnings.warn(msg, DeprecationWarning) + super(private_class, self).__init__(*args, **kwargs) + + private_class.__name__ = c.__name__ + + return private_class + + +from ._parser import _timelex, _resultbase +from ._parser import _tzparser, _parsetz + +_timelex = __deprecate_private_class(_timelex) +_tzparser = __deprecate_private_class(_tzparser) +_resultbase = __deprecate_private_class(_resultbase) +_parsetz = __deprecated_private_func(_parsetz) diff --git a/src/dateutil/parser/_parser.py b/src/dateutil/parser/_parser.py new file mode 100644 index 00000000..0da0f3e6 --- /dev/null +++ b/src/dateutil/parser/_parser.py @@ -0,0 +1,1580 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +This module attempts to be forgiving with regards to unlikely input formats, +returning a datetime object even for dates which are ambiguous. If an element +of a date/time stamp is omitted, the following rules are applied: + +- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour + on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is + specified. +- If a time zone is omitted, a timezone-naive datetime is returned. + +If any other elements are missing, they are taken from the +:class:`datetime.datetime` object passed to the parameter ``default``. If this +results in a day number exceeding the valid number of days per month, the +value falls back to the end of the month. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + `_ +- `W3C Date and Time Formats `_ +- `Time Formats (Planetary Rings Node) `_ +- `CPAN ParseDate module + `_ +- `Java SimpleDateFormat Class + `_ +""" +from __future__ import unicode_literals + +import datetime +import re +import string +import time +import warnings + +from calendar import monthrange +from io import StringIO + +import six +from six import integer_types, text_type + +from decimal import Decimal + +from warnings import warn + +from .. import relativedelta +from .. import tz + +__all__ = ["parse", "parserinfo"] + + +# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth +# making public and/or figuring out if there is something we can +# take off their plate. +class _timelex(object): + # Fractional seconds are sometimes split by a comma + _split_decimal = re.compile("([.,])") + + def __init__(self, instream): + if six.PY2: + # In Python 2, we can't duck type properly because unicode has + # a 'decode' function, and we'd be double-decoding + if isinstance(instream, (bytes, bytearray)): + instream = instream.decode() + else: + if getattr(instream, 'decode', None) is not None: + instream = instream.decode() + + if isinstance(instream, text_type): + instream = StringIO(instream) + elif getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + + self.instream = instream + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered + one unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + + while not self.eof: + # We only realize that we've reached the end of a token when we + # find a character that's not part of the current token - since + # that character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if self.isword(nextchar): + state = 'a' + elif self.isnum(nextchar): + state = '0' + elif self.isspace(nextchar): + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if self.isword(nextchar): + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if self.isnum(nextchar): + token += nextchar + elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or self.isword(nextchar): + token += nextchar + elif self.isnum(nextchar) and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or self.isnum(nextchar): + token += nextchar + elif self.isword(nextchar) and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] in '.,')): + l = self._split_decimal.split(token) + token = l[0] + for tok in l[1:]: + if tok: + self.tokenstack.append(tok) + + if state == '0.' and token.count('.') == 0: + token = token.replace(',', '.') + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + @classmethod + def split(cls, s): + return list(cls(s)) + + @classmethod + def isword(cls, nextchar): + """ Whether or not the next character is part of a word """ + return nextchar.isalpha() + + @classmethod + def isnum(cls, nextchar): + """ Whether the next character is part of a number """ + return nextchar.isdigit() + + @classmethod + def isspace(cls, nextchar): + """ Whether the next character is whitespace """ + return nextchar.isspace() + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __len__(self): + return (sum(getattr(self, attr) is not None + for attr in self.__slots__)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize + the language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. Default is ``False``. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + Default is ``False``. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), # TODO: "Tues" + ("Wed", "Wednesday"), + ("Thu", "Thursday"), # TODO: "Thurs" + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), # TODO: "Febr" + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z", "z"] + PERTAIN = ["of"] + TZOFFSET = {} + # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", + # "Anno Domini", "Year of Our Lord"] + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100 * 100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + try: + return self._months[name.lower()] + 1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + + if year < 100 and not century_specified: + # assume current century to start + year += self._century + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year, res.century_specified) + + if ((res.tzoffset == 0 and not res.tzname) or + (res.tzname == 'Z' or res.tzname == 'z')): + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class _ymd(list): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.century_specified = False + self.dstridx = None + self.mstridx = None + self.ystridx = None + + @property + def has_year(self): + return self.ystridx is not None + + @property + def has_month(self): + return self.mstridx is not None + + @property + def has_day(self): + return self.dstridx is not None + + def could_be_day(self, value): + if self.has_day: + return False + elif not self.has_month: + return 1 <= value <= 31 + elif not self.has_year: + # Be permissive, assume leapyear + month = self[self.mstridx] + return 1 <= value <= monthrange(2000, month)[1] + else: + month = self[self.mstridx] + year = self[self.ystridx] + return 1 <= value <= monthrange(year, month)[1] + + def append(self, val, label=None): + if hasattr(val, '__len__'): + if val.isdigit() and len(val) > 2: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + elif val > 100: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + + super(self.__class__, self).append(int(val)) + + if label == 'M': + if self.has_month: + raise ValueError('Month is already set') + self.mstridx = len(self) - 1 + elif label == 'D': + if self.has_day: + raise ValueError('Day is already set') + self.dstridx = len(self) - 1 + elif label == 'Y': + if self.has_year: + raise ValueError('Year is already set') + self.ystridx = len(self) - 1 + + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + + def resolve_ymd(self, yearfirst, dayfirst): + len_ymd = len(self) + year, month, day = (None, None, None) + + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + + mstridx = self.mstridx + + if len_ymd > 3: + raise ValueError("More than three YMD values") + elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): + # One member, or two members with a month string + if mstridx is not None: + month = self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] + + if len_ymd > 1 or mstridx is None: + if other > 31: + year = other + else: + day = other + + elif len_ymd == 2: + # Two members with numbers + if self[0] > 31: + # 99-01 + year, month = self + elif self[1] > 31: + # 01-99 + month, year = self + elif dayfirst and self[1] <= 12: + # 13-01 + day, month = self + else: + # 01-13 + month, day = self + + elif len_ymd == 3: + # Three members + if mstridx == 0: + if self[1] > 31: + # Apr-2003-25 + month, year, day = self + else: + month, day, year = self + elif mstridx == 1: + if self[0] > 31 or (yearfirst and self[2] <= 31): + # 99-Jan-01 + year, month, day = self + else: + # 01-Jan-01 + # Give precendence to day-first, since + # two-digit years is usually hand-written. + day, month, year = self + + elif mstridx == 2: + # WTF!? + if self[1] > 31: + # 01-99-Jan + day, year, month = self + else: + # 99-01-Jan + year, day, month = self + + else: + if (self[0] > 31 or + self.ystridx == 0 or + (yearfirst and self[1] <= 12 and self[2] <= 31)): + # 99-01-01 + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self + elif self[0] > 12 or (dayfirst and self[1] <= 12): + # 13-01-01 + day, month, year = self + else: + # 01-13-01 + month, day, year = self + + return year, month, day + + +class parser(object): + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, + ignoretz=False, tzinfos=None, **kwargs): + """ + Parse the date/time string into a :class:`datetime.datetime` object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a + naive :class:`datetime.datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param \\*\\*kwargs: + Keyword arguments as passed to ``_parse()``. + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises TypeError: + Raised for non-string or character stream input. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + if default is None: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + res, skipped_tokens = self._parse(timestr, **kwargs) + + if res is None: + raise ValueError("Unknown string format:", timestr) + + if len(res) == 0: + raise ValueError("String does not contain a date:", timestr) + + ret = self._build_naive(res, default) + + if not ignoretz: + ret = self._build_tzaware(ret, res, tzinfos) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm","any_unused_tokens"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + ``parse()``, which passes on its ``kwargs`` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. If set to ``None``, this value is retrieved from the + current :class:`parserinfo` object (which itself defaults to + ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + If this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + skipped_idxs = [] + + # year/month/day list + ymd = _ymd() + + len_l = len(l) + i = 0 + try: + while i < len_l: + + # Check if it's a number + value_repr = l[i] + try: + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Numeric token + i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) + + # Check weekday + elif info.weekday(l[i]) is not None: + value = info.weekday(l[i]) + res.weekday = value + + # Check month name + elif info.month(l[i]) is not None: + value = info.month(l[i]) + ymd.append(value, 'M') + + if i + 1 < len_l: + if l[i + 1] in ('-', '/'): + # Jan-01[-99] + sep = l[i + 1] + ymd.append(l[i + 2]) + + if i + 3 < len_l and l[i + 3] == sep: + # Jan-01-99 + ymd.append(l[i + 4]) + i += 2 + + i += 2 + + elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and + info.pertain(l[i + 2])): + # Jan of 01 + # In this case, 01 is clearly year + if l[i + 4].isdigit(): + # Convert it here to become unambiguous + value = int(l[i + 4]) + year = str(info.convertyear(value)) + ymd.append(year, 'Y') + else: + # Wrong guess + pass + # TODO: not hit in tests + i += 4 + + # Check am/pm + elif info.ampm(l[i]) is not None: + value = info.ampm(l[i]) + val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) + + if val_is_ampm: + res.hour = self._adjust_ampm(res.hour, value) + res.ampm = value + + elif fuzzy: + skipped_idxs.append(i) + + # Check for a timezone name + elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i + 1 < len_l and l[i + 1] in ('+', '-'): + l[i + 1] = ('+', '-')[l[i + 1] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + # Check for a numbered timezone + elif res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + len_li = len(l[i + 1]) + + # TODO: check that l[i + 1] is integer? + if len_li == 4: + # -0300 + hour_offset = int(l[i + 1][:2]) + min_offset = int(l[i + 1][2:]) + elif i + 2 < len_l and l[i + 2] == ':': + # -03:00 + hour_offset = int(l[i + 1]) + min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? + i += 2 + elif len_li <= 2: + # -[0]3 + hour_offset = int(l[i + 1][:2]) + min_offset = 0 + else: + raise ValueError(timestr) + + res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) + + # Look for a timezone name between parenthesis + if (i + 5 < len_l and + info.jump(l[i + 2]) and l[i + 3] == '(' and + l[i + 5] == ')' and + 3 <= len(l[i + 4]) and + self._could_be_tzname(res.hour, res.tzname, + None, l[i + 4])): + # -0300 (BRST) + res.tzname = l[i + 4] + i += 4 + + i += 1 + + # Check jumps + elif not (info.jump(l[i]) or fuzzy): + raise ValueError(timestr) + + else: + skipped_idxs.append(i) + i += 1 + + # Process year/month/day + year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) + + res.century_specified = ymd.century_specified + res.year = year + res.month = month + res.day = day + + except (IndexError, ValueError): + return None, None + + if not info.validate(res): + return None, None + + if fuzzy_with_tokens: + skipped_tokens = self._recombine_skipped(l, skipped_idxs) + return res, tuple(skipped_tokens) + else: + return res, None + + def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): + # Token is a number + value_repr = tokens[idx] + try: + value = self._to_decimal(value_repr) + except Exception as e: + six.raise_from(ValueError('Unknown numeric token'), e) + + len_li = len(value_repr) + + len_l = len(tokens) + + if (len(ymd) == 3 and len_li in (2, 4) and + res.hour is None and + (idx + 1 >= len_l or + (tokens[idx + 1] != ':' and + info.hms(tokens[idx + 1]) is None))): + # 19990101T23[59] + s = tokens[idx] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = tokens[idx] + + if not ymd and '.' not in tokens[idx]: + ymd.append(s[:2]) + ymd.append(s[2:4]) + ymd.append(s[4:]) + else: + # 19990101T235959[.59] + + # TODO: Check if res attributes already set. + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = self._parsems(s[4:]) + + elif len_li in (8, 12, 14): + # YYYYMMDD + s = tokens[idx] + ymd.append(s[:4], 'Y') + ymd.append(s[4:6]) + ymd.append(s[6:8]) + + if len_li > 8: + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li > 12: + res.second = int(s[12:]) + + elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) + (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) + if hms is not None: + # TODO: checking that hour/minute/second are not + # already set? + self._assign_hms(res, value_repr, hms) + + elif idx + 2 < len_l and tokens[idx + 1] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this? + (res.minute, res.second) = self._parse_min_sec(value) + + if idx + 4 < len_l and tokens[idx + 3] == ':': + res.second, res.microsecond = self._parsems(tokens[idx + 4]) + + idx += 2 + + idx += 2 + + elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): + sep = tokens[idx + 1] + ymd.append(value_repr) + + if idx + 2 < len_l and not info.jump(tokens[idx + 2]): + if tokens[idx + 2].isdigit(): + # 01-01[-01] + ymd.append(tokens[idx + 2]) + else: + # 01-Jan[-01] + value = info.month(tokens[idx + 2]) + + if value is not None: + ymd.append(value, 'M') + else: + raise ValueError() + + if idx + 3 < len_l and tokens[idx + 3] == sep: + # We have three members + value = info.month(tokens[idx + 4]) + + if value is not None: + ymd.append(value, 'M') + else: + ymd.append(tokens[idx + 4]) + idx += 2 + + idx += 1 + idx += 1 + + elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): + if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: + # 12 am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) + idx += 1 + else: + # Year, month or day + ymd.append(value) + idx += 1 + + elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): + # 12am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) + idx += 1 + + elif ymd.could_be_day(value): + ymd.append(value) + + elif not fuzzy: + raise ValueError() + + return idx + + def _find_hms_idx(self, idx, tokens, info, allow_jump): + len_l = len(tokens) + + if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: + # There is an "h", "m", or "s" label following this token. We take + # assign the upcoming label to the current token. + # e.g. the "12" in 12h" + hms_idx = idx + 1 + + elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and + info.hms(tokens[idx+2]) is not None): + # There is a space and then an "h", "m", or "s" label. + # e.g. the "12" in "12 h" + hms_idx = idx + 2 + + elif idx > 0 and info.hms(tokens[idx-1]) is not None: + # There is a "h", "m", or "s" preceeding this token. Since neither + # of the previous cases was hit, there is no label following this + # token, so we use the previous label. + # e.g. the "04" in "12h04" + hms_idx = idx-1 + + elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and + info.hms(tokens[idx-2]) is not None): + # If we are looking at the final token, we allow for a + # backward-looking check to skip over a space. + # TODO: Are we sure this is the right condition here? + hms_idx = idx - 2 + + else: + hms_idx = None + + return hms_idx + + def _assign_hms(self, res, value_repr, hms): + # See GH issue #427, fixing float rounding + value = self._to_decimal(value_repr) + + if hms == 0: + # Hour + res.hour = int(value) + if value % 1: + res.minute = int(60*(value % 1)) + + elif hms == 1: + (res.minute, res.second) = self._parse_min_sec(value) + + elif hms == 2: + (res.second, res.microsecond) = self._parsems(value_repr) + + def _could_be_tzname(self, hour, tzname, tzoffset, token): + return (hour is not None and + tzname is None and + tzoffset is None and + len(token) <= 5 and + (all(x in string.ascii_uppercase for x in token) + or token in self.info.UTCZONE)) + + def _ampm_valid(self, hour, ampm, fuzzy): + """ + For fuzzy parsing, 'a' or 'am' (both valid English words) + may erroneously trigger the AM/PM flag. Deal with that + here. + """ + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with AM or PM flag.') + elif not 0 <= hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for 12-hour clock.') + + return val_is_ampm + + def _adjust_ampm(self, hour, ampm): + if hour < 12 and ampm == 1: + hour += 12 + elif hour == 12 and ampm == 0: + hour = 0 + return hour + + def _parse_min_sec(self, value): + # TODO: Every usage of this function sets res.second to the return + # value. Are there any cases where second will be returned as None and + # we *dont* want to set res.second = None? + minute = int(value) + second = None + + sec_remainder = value % 1 + if sec_remainder: + second = int(60 * sec_remainder) + return (minute, second) + + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _parse_hms(self, idx, tokens, info, hms_idx): + # TODO: Is this going to admit a lot of false-positives for when we + # just happen to have digits and "h", "m" or "s" characters in non-date + # text? I guess hex hashes won't have that problem, but there's plenty + # of random junk out there. + if hms_idx is None: + hms = None + new_idx = idx + elif hms_idx > idx: + hms = info.hms(tokens[hms_idx]) + new_idx = hms_idx + else: + # Looking backwards, increment one. + hms = info.hms(tokens[hms_idx]) + 1 + new_idx = idx + + return (new_idx, hms) + + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens + + def _build_tzinfo(self, tzinfos, tzname, tzoffset): + if callable(tzinfos): + tzdata = tzinfos(tzname, tzoffset) + else: + tzdata = tzinfos.get(tzname) + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(tzname, tzdata) + return tzinfo + + def _build_tzaware(self, naive, res, tzinfos): + if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): + tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) + aware = naive.replace(tzinfo=tzinfo) + aware = self._assign_tzname(aware, res.tzname) + + elif res.tzname and res.tzname in time.tzname: + aware = naive.replace(tzinfo=tz.tzlocal()) + + # Handle ambiguous local datetime + aware = self._assign_tzname(aware, res.tzname) + + # This is mostly relevant for winter GMT zones parsed in the UK + if (aware.tzname() != res.tzname and + res.tzname in self.info.UTCZONE): + aware = aware.replace(tzinfo=tz.tzutc()) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.tzutc()) + + elif res.tzoffset: + aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + elif not res.tzname and not res.tzoffset: + # i.e. no timezone information was found. + aware = naive + + elif res.tzname: + # tz-like string was parsed but we don't know what to do + # with it + warnings.warn("tzname {tzname} identified but not understood. " + "Pass `tzinfos` argument in order to correctly " + "return a timezone-aware datetime. In a future " + "version, this will raise an " + "exception.".format(tzname=res.tzname), + category=UnknownTimezoneWarning) + aware = naive + + return aware + + def _build_naive(self, res, default): + repl = {} + for attr in ("year", "month", "day", "hour", + "minute", "second", "microsecond"): + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + if 'day' not in repl: + # If the default day exceeds the last day of the month, fall back + # to the end of the month. + cyear = default.year if res.year is None else res.year + cmonth = default.month if res.month is None else res.month + cday = default.day if res.day is None else res.day + + if cday > monthrange(cyear, cmonth)[1]: + repl['day'] = monthrange(cyear, cmonth)[1] + + naive = default.replace(**repl) + + if res.weekday is not None and not res.day: + naive = naive + relativedelta.relativedelta(weekday=res.weekday) + + return naive + + def _assign_tzname(self, dt, tzname): + if dt.tzname() != tzname: + new_dt = tz.enfold(dt, fold=1) + if new_dt.tzname() == tzname: + return new_dt + + return dt + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + + Parse a string in one of the supported formats, using the + ``parserinfo`` parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If ``None``, the default arguments to the :class:`parserinfo` + constructor are used. + + The ``**kwargs`` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM and + YMD. If set to ``None``, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + res = self._result() + l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] + used_idxs = list() + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + + for ii in range(j): + used_idxs.append(ii) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + used_idxs.append(i) + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) * signal) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i]) * 3600 + + int(l[i + 2]) * 60) * signal) + used_idxs.append(i) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2]) * 3600 * signal) + else: + return None + used_idxs.append(i) + i += 1 + if res.dstabbr: + break + else: + break + + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789+-"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + used_idxs.append(i) + i += 2 + if l[i] == '-': + value = int(l[i + 1]) * -1 + used_idxs.append(i) + i += 1 + else: + value = int(l[i]) + used_idxs.append(i) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i]) - 1) % 7 + else: + x.day = int(l[i]) + used_idxs.append(i) + i += 2 + x.time = int(l[i]) + used_idxs.append(i) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + used_idxs.append(i) + i += 1 + else: + signal = 1 + used_idxs.append(i) + res.dstoffset = (res.stdoffset + int(l[i]) * signal) + + # This was a made-up format that is not in normal use + warn(('Parsed time zone "%s"' % tzstr) + + 'is in a non-standard dateutil-specific format, which ' + + 'is now deprecated; support for parsing this format ' + + 'will be removed in future versions. It is recommended ' + + 'that you switch to a standard format like the GNU ' + + 'TZ variable format.', tz.DeprecatedTzFormatWarning) + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + used_idxs.append(i) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + used_idxs.append(i) + i += 1 + x.month = int(l[i]) + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.weekday = (int(l[i]) - 1) % 7 + else: + # year day (zero based) + x.yday = int(l[i]) + 1 + + used_idxs.append(i) + i += 1 + + if i < len_l and l[i] == '/': + used_idxs.append(i) + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 + used_idxs.append(i) + i += 2 + if i + 1 < len_l and l[i + 1] == ':': + used_idxs.append(i) + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2]) * 3600) + else: + return None + used_idxs.append(i) + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + unused_idxs = set(range(len_l)).difference(used_idxs) + res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + +class UnknownTimezoneWarning(RuntimeWarning): + """Raised when the parser finds a timezone it cannot parse into a tzinfo""" +# vim:ts=4:sw=4:et diff --git a/src/dateutil/parser/isoparser.py b/src/dateutil/parser/isoparser.py new file mode 100644 index 00000000..e3cf6d8c --- /dev/null +++ b/src/dateutil/parser/isoparser.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- +""" +This module offers a parser for ISO-8601 strings + +It is intended to support all valid date, time and datetime formats per the +ISO-8601 specification. + +..versionadded:: 2.7.0 +""" +from datetime import datetime, timedelta, time, date +import calendar +from dateutil import tz + +from functools import wraps + +import re +import six + +__all__ = ["isoparse", "isoparser"] + + +def _takes_ascii(f): + @wraps(f) + def func(self, str_in, *args, **kwargs): + # If it's a stream, read the whole thing + str_in = getattr(str_in, 'read', lambda: str_in)() + + # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII + if isinstance(str_in, six.text_type): + # ASCII is the same in UTF-8 + try: + str_in = str_in.encode('ascii') + except UnicodeEncodeError as e: + msg = 'ISO-8601 strings should contain only ASCII characters' + six.raise_from(ValueError(msg), e) + + return f(self, str_in, *args, **kwargs) + + return func + + +class isoparser(object): + def __init__(self, sep=None): + """ + :param sep: + A single character that separates date and time portions. If + ``None``, the parser will accept any single character. + For strict ISO-8601 adherence, pass ``'T'``. + """ + if sep is not None: + if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): + raise ValueError('Separator must be a single, non-numeric ' + + 'ASCII character') + + sep = sep.encode('ascii') + + self._sep = sep + + @_takes_ascii + def isoparse(self, dt_str): + """ + Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. + + An ISO-8601 datetime string consists of a date portion, followed + optionally by a time portion - the date and time portions are separated + by a single character separator, which is ``T`` in the official + standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be + combined with a time portion. + + Supported date formats are: + + Common: + + - ``YYYY`` + - ``YYYY-MM`` or ``YYYYMM`` + - ``YYYY-MM-DD`` or ``YYYYMMDD`` + + Uncommon: + + - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) + - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day + + The ISO week and day numbering follows the same logic as + :func:`datetime.date.isocalendar`. + + Supported time formats are: + + - ``hh`` + - ``hh:mm`` or ``hhmm`` + - ``hh:mm:ss`` or ``hhmmss`` + - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) + + Midnight is a special case for `hh`, as the standard supports both + 00:00 and 24:00 as a representation. The decimal separator can be + either a dot or a comma. + + + .. caution:: + + Support for fractional components other than seconds is part of the + ISO-8601 standard, but is not currently implemented in this parser. + + Supported time zone offset formats are: + + - `Z` (UTC) + - `±HH:MM` + - `±HHMM` + - `±HH` + + Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, + with the exception of UTC, which will be represented as + :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such + as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. + + :param dt_str: + A string or stream containing only an ISO-8601 datetime string + + :return: + Returns a :class:`datetime.datetime` representing the string. + Unspecified components default to their lowest value. + + .. warning:: + + As of version 2.7.0, the strictness of the parser should not be + considered a stable part of the contract. Any valid ISO-8601 string + that parses correctly with the default settings will continue to + parse correctly in future versions, but invalid strings that + currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not + guaranteed to continue failing in future versions if they encode + a valid date. + + .. versionadded:: 2.7.0 + """ + components, pos = self._parse_isodate(dt_str) + + if len(dt_str) > pos: + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + components += self._parse_isotime(dt_str[pos + 1:]) + else: + raise ValueError('String contains unknown ISO components') + + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + + return datetime(*components) + + @_takes_ascii + def parse_isodate(self, datestr): + """ + Parse the date portion of an ISO string. + + :param datestr: + The string portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.date` object + """ + components, pos = self._parse_isodate(datestr) + if pos < len(datestr): + raise ValueError('String contains unknown ISO ' + + 'components: {}'.format(datestr)) + return date(*components) + + @_takes_ascii + def parse_isotime(self, timestr): + """ + Parse the time portion of an ISO string. + + :param timestr: + The time portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.time` object + """ + components = self._parse_isotime(timestr) + if components[0] == 24: + components[0] = 0 + return time(*components) + + @_takes_ascii + def parse_tzstr(self, tzstr, zero_as_utc=True): + """ + Parse a valid ISO time zone string. + + See :func:`isoparser.isoparse` for details on supported formats. + + :param tzstr: + A string representing an ISO time zone offset + + :param zero_as_utc: + Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones + + :return: + Returns :class:`dateutil.tz.tzoffset` for offsets and + :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is + specified) offsets equivalent to UTC. + """ + return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + + # Constants + _DATE_SEP = b'-' + _TIME_SEP = b':' + _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') + + def _parse_isodate(self, dt_str): + try: + return self._parse_isodate_common(dt_str) + except ValueError: + return self._parse_isodate_uncommon(dt_str) + + def _parse_isodate_common(self, dt_str): + len_str = len(dt_str) + components = [1, 1, 1] + + if len_str < 4: + raise ValueError('ISO string too short') + + # Year + components[0] = int(dt_str[0:4]) + pos = 4 + if pos >= len_str: + return components, pos + + has_sep = dt_str[pos:pos + 1] == self._DATE_SEP + if has_sep: + pos += 1 + + # Month + if len_str - pos < 2: + raise ValueError('Invalid common month') + + components[1] = int(dt_str[pos:pos + 2]) + pos += 2 + + if pos >= len_str: + if has_sep: + return components, pos + else: + raise ValueError('Invalid ISO format') + + if has_sep: + if dt_str[pos:pos + 1] != self._DATE_SEP: + raise ValueError('Invalid separator in ISO string') + pos += 1 + + # Day + if len_str - pos < 2: + raise ValueError('Invalid common day') + components[2] = int(dt_str[pos:pos + 2]) + return components, pos + 2 + + def _parse_isodate_uncommon(self, dt_str): + if len(dt_str) < 4: + raise ValueError('ISO string too short') + + # All ISO formats start with the year + year = int(dt_str[0:4]) + + has_sep = dt_str[4:5] == self._DATE_SEP + + pos = 4 + has_sep # Skip '-' if it's there + if dt_str[pos:pos + 1] == b'W': + # YYYY-?Www-?D? + pos += 1 + weekno = int(dt_str[pos:pos + 2]) + pos += 2 + + dayno = 1 + if len(dt_str) > pos: + if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: + raise ValueError('Inconsistent use of dash separator') + + pos += has_sep + + dayno = int(dt_str[pos:pos + 1]) + pos += 1 + + base_date = self._calculate_weekdate(year, weekno, dayno) + else: + # YYYYDDD or YYYY-DDD + if len(dt_str) - pos < 3: + raise ValueError('Invalid ordinal day') + + ordinal_day = int(dt_str[pos:pos + 3]) + pos += 3 + + if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): + raise ValueError('Invalid ordinal day' + + ' {} for year {}'.format(ordinal_day, year)) + + base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) + + components = [base_date.year, base_date.month, base_date.day] + return components, pos + + def _calculate_weekdate(self, year, week, day): + """ + Calculate the day of corresponding to the ISO year-week-day calendar. + + This function is effectively the inverse of + :func:`datetime.date.isocalendar`. + + :param year: + The year in the ISO calendar + + :param week: + The week in the ISO calendar - range is [1, 53] + + :param day: + The day in the ISO calendar - range is [1 (MON), 7 (SUN)] + + :return: + Returns a :class:`datetime.date` + """ + if not 0 < week < 54: + raise ValueError('Invalid week: {}'.format(week)) + + if not 0 < day < 8: # Range is 1-7 + raise ValueError('Invalid weekday: {}'.format(day)) + + # Get week 1 for the specific year: + jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it + week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) + + # Now add the specific number of weeks and days to get what we want + week_offset = (week - 1) * 7 + (day - 1) + return week_1 + timedelta(days=week_offset) + + def _parse_isotime(self, timestr): + len_str = len(timestr) + components = [0, 0, 0, 0, None] + pos = 0 + comp = -1 + + if len(timestr) < 2: + raise ValueError('ISO time too short') + + has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP + + while pos < len_str and comp < 5: + comp += 1 + + if timestr[pos:pos + 1] in b'-+Zz': + # Detect time zone boundary + components[-1] = self._parse_tzstr(timestr[pos:]) + pos = len_str + break + + if comp < 3: + # Hour, minute, second + components[comp] = int(timestr[pos:pos + 2]) + pos += 2 + if (has_sep and pos < len_str and + timestr[pos:pos + 1] == self._TIME_SEP): + pos += 1 + + if comp == 3: + # Fraction of a second + frac = self._FRACTION_REGEX.match(timestr[pos:]) + if not frac: + continue + + us_str = frac.group(1)[:6] # Truncate to microseconds + components[comp] = int(us_str) * 10**(6 - len(us_str)) + pos += len(frac.group()) + + if pos < len_str: + raise ValueError('Unused components in ISO string') + + if components[0] == 24: + # Standard supports 00:00 and 24:00 as representations of midnight + if any(component != 0 for component in components[1:4]): + raise ValueError('Hour may only be 24 at 24:00:00.000') + + return components + + def _parse_tzstr(self, tzstr, zero_as_utc=True): + if tzstr == b'Z' or tzstr == b'z': + return tz.tzutc() + + if len(tzstr) not in {3, 5, 6}: + raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') + + if tzstr[0:1] == b'-': + mult = -1 + elif tzstr[0:1] == b'+': + mult = 1 + else: + raise ValueError('Time zone offset requires sign') + + hours = int(tzstr[1:3]) + if len(tzstr) == 3: + minutes = 0 + else: + minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) + + if zero_as_utc and hours == 0 and minutes == 0: + return tz.tzutc() + else: + if minutes > 59: + raise ValueError('Invalid minutes in time zone offset') + + if hours > 23: + raise ValueError('Invalid hours in time zone offset') + + return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) + + +DEFAULT_ISOPARSER = isoparser() +isoparse = DEFAULT_ISOPARSER.isoparse diff --git a/src/dateutil/relativedelta.py b/src/dateutil/relativedelta.py new file mode 100644 index 00000000..c65c66e6 --- /dev/null +++ b/src/dateutil/relativedelta.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +import datetime +import calendar + +import operator +from math import copysign + +from six import integer_types +from warnings import warn + +from ._common import weekday + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + + +class relativedelta(object): + """ + The relativedelta type is designed to be applied to an existing datetime and + can replace specific components of that datetime, or represents an interval + of time. + + It is based on the specification of the excellent work done by M.-A. Lemburg + in his + `mx.DateTime `_ extension. + However, notice that this type does *NOT* implement the same algorithm as + his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + + There are two different ways to build a relativedelta instance. The + first one is passing it two date/datetime classes:: + + relativedelta(datetime1, datetime2) + + The second one is passing it any number of the following keyword arguments:: + + relativedelta(arg1=x,arg2=y,arg3=z...) + + year, month, day, hour, minute, second, microsecond: + Absolute information (argument is singular); adding or subtracting a + relativedelta with absolute information does not perform an arithmetic + operation, but rather REPLACES the corresponding value in the + original datetime with the value(s) in relativedelta. + + years, months, weeks, days, hours, minutes, seconds, microseconds: + Relative information, may be negative (argument is plural); adding + or subtracting a relativedelta with relative information performs + the corresponding aritmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc) available in the + relativedelta module. These instances may receive a parameter N, + specifying the Nth weekday, which could be positive or negative + (like MO(+1) or MO(-2)). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. This argument is always + relative e.g. if the calculated date is already Monday, using MO(1) + or MO(-1) won't change the day. To effectively make it absolute, use + it in combination with the day argument (e.g. day=1, MO(1) for first + Monday of the month). + + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. + + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. + + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). + + The order of attributes considered when this relativedelta is + added to a datetime is: + + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds + + Finally, weekday is applied, using the rule described above. + + For example + + >>> from datetime import datetime + >>> from dateutil.relativedelta import relativedelta, MO + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + >>> dt + delta + datetime.datetime(2018, 4, 2, 14, 37) + + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. + + """ + + def __init__(self, dt1=None, dt2=None, + years=0, months=0, days=0, leapdays=0, weeks=0, + hours=0, minutes=0, seconds=0, microseconds=0, + year=None, month=None, day=None, weekday=None, + yearday=None, nlyearday=None, + hour=None, minute=None, second=None, microsecond=None): + + if dt1 and dt2: + # datetime is a subclass of date. So both must be date + if not (isinstance(dt1, datetime.date) and + isinstance(dt2, datetime.date)): + raise TypeError("relativedelta only diffs datetime/date") + + # We allow two dates, or two datetimes, so we coerce them to be + # of the same type + if (isinstance(dt1, datetime.datetime) != + isinstance(dt2, datetime.datetime)): + if not isinstance(dt1, datetime.datetime): + dt1 = datetime.datetime.fromordinal(dt1.toordinal()) + elif not isinstance(dt2, datetime.datetime): + dt2 = datetime.datetime.fromordinal(dt2.toordinal()) + + self.years = 0 + self.months = 0 + self.days = 0 + self.leapdays = 0 + self.hours = 0 + self.minutes = 0 + self.seconds = 0 + self.microseconds = 0 + self.year = None + self.month = None + self.day = None + self.weekday = None + self.hour = None + self.minute = None + self.second = None + self.microsecond = None + self._has_time = 0 + + # Get year / month delta between the two + months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) + self._set_months(months) + + # Remove the year/month delta so the timedelta is just well-defined + # time units (seconds, days and microseconds) + dtm = self.__radd__(dt2) + + # If we've overshot our target, make an adjustment + if dt1 < dt2: + compare = operator.gt + increment = 1 + else: + compare = operator.lt + increment = -1 + + while compare(dt1, dtm): + months += increment + self._set_months(months) + dtm = self.__radd__(dt2) + + # Get the timedelta between the "months-adjusted" date and dt1 + delta = dt1 - dtm + self.seconds = delta.seconds + delta.days * 86400 + self.microseconds = delta.microseconds + else: + # Check for non-integer values in integer-only quantities + if any(x is not None and x != int(x) for x in (years, months)): + raise ValueError("Non-integer years and months are " + "ambiguous and not currently supported.") + + # Relative information + self.years = int(years) + self.months = int(months) + self.days = days + weeks * 7 + self.leapdays = leapdays + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + + # Absolute information + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + + if any(x is not None and int(x) != x + for x in (year, month, day, hour, + minute, second, microsecond)): + # For now we'll deprecate floats - later it'll be an error. + warn("Non-integer value passed as absolute information. " + + "This is not a well-defined condition and will raise " + + "errors in future versions.", DeprecationWarning) + + if isinstance(weekday, integer_types): + self.weekday = weekdays[weekday] + else: + self.weekday = weekday + + yday = 0 + if nlyearday: + yday = nlyearday + elif yearday: + yday = yearday + if yearday > 59: + self.leapdays = -1 + if yday: + ydayidx = [31, 59, 90, 120, 151, 181, 212, + 243, 273, 304, 334, 366] + for idx, ydays in enumerate(ydayidx): + if yday <= ydays: + self.month = idx+1 + if idx == 0: + self.day = yday + else: + self.day = yday-ydayidx[idx-1] + break + else: + raise ValueError("invalid year day (%d)" % yday) + + self._fix() + + def _fix(self): + if abs(self.microseconds) > 999999: + s = _sign(self.microseconds) + div, mod = divmod(self.microseconds * s, 1000000) + self.microseconds = mod * s + self.seconds += div * s + if abs(self.seconds) > 59: + s = _sign(self.seconds) + div, mod = divmod(self.seconds * s, 60) + self.seconds = mod * s + self.minutes += div * s + if abs(self.minutes) > 59: + s = _sign(self.minutes) + div, mod = divmod(self.minutes * s, 60) + self.minutes = mod * s + self.hours += div * s + if abs(self.hours) > 23: + s = _sign(self.hours) + div, mod = divmod(self.hours * s, 24) + self.hours = mod * s + self.days += div * s + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years += div * s + if (self.hours or self.minutes or self.seconds or self.microseconds + or self.hour is not None or self.minute is not None or + self.second is not None or self.microsecond is not None): + self._has_time = 1 + else: + self._has_time = 0 + + @property + def weeks(self): + return int(self.days / 7.0) + + @weeks.setter + def weeks(self, value): + self.days = self.days - (self.weeks * 7) + value * 7 + + def _set_months(self, months): + self.months = months + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years = div * s + else: + self.years = 0 + + def normalized(self): + """ + Return a version of this object represented entirely using integer + values for the relative attributes. + + >>> relativedelta(days=1.5, hours=2).normalized() + relativedelta(days=+1, hours=+14) + + :return: + Returns a :class:`dateutil.relativedelta.relativedelta` object. + """ + # Cascade remainders down (rounding each to roughly nearest microsecond) + days = int(self.days) + + hours_f = round(self.hours + 24 * (self.days - days), 11) + hours = int(hours_f) + + minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) + minutes = int(minutes_f) + + seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) + seconds = int(seconds_f) + + microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) + + # Constructor carries overflow back up with call to _fix() + return self.__class__(years=self.years, months=self.months, + days=days, hours=hours, minutes=minutes, + seconds=seconds, microseconds=microseconds, + leapdays=self.leapdays, year=self.year, + month=self.month, day=self.day, + weekday=self.weekday, hour=self.hour, + minute=self.minute, second=self.second, + microsecond=self.microsecond) + + def __add__(self, other): + if isinstance(other, relativedelta): + return self.__class__(years=other.years + self.years, + months=other.months + self.months, + days=other.days + self.days, + hours=other.hours + self.hours, + minutes=other.minutes + self.minutes, + seconds=other.seconds + self.seconds, + microseconds=(other.microseconds + + self.microseconds), + leapdays=other.leapdays or self.leapdays, + year=(other.year if other.year is not None + else self.year), + month=(other.month if other.month is not None + else self.month), + day=(other.day if other.day is not None + else self.day), + weekday=(other.weekday if other.weekday is not None + else self.weekday), + hour=(other.hour if other.hour is not None + else self.hour), + minute=(other.minute if other.minute is not None + else self.minute), + second=(other.second if other.second is not None + else self.second), + microsecond=(other.microsecond if other.microsecond + is not None else + self.microsecond)) + if isinstance(other, datetime.timedelta): + return self.__class__(years=self.years, + months=self.months, + days=self.days + other.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds + other.seconds, + microseconds=self.microseconds + other.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + if not isinstance(other, datetime.date): + return NotImplemented + elif self._has_time and not isinstance(other, datetime.datetime): + other = datetime.datetime.fromordinal(other.toordinal()) + year = (self.year or other.year)+self.years + month = self.month or other.month + if self.months: + assert 1 <= abs(self.months) <= 12 + month += self.months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + day = min(calendar.monthrange(year, month)[1], + self.day or other.day) + repl = {"year": year, "month": month, "day": day} + for attr in ["hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + repl[attr] = value + days = self.days + if self.leapdays and month > 2 and calendar.isleap(year): + days += self.leapdays + ret = (other.replace(**repl) + + datetime.timedelta(days=days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds)) + if self.weekday: + weekday, nth = self.weekday.weekday, self.weekday.n or 1 + jumpdays = (abs(nth) - 1) * 7 + if nth > 0: + jumpdays += (7 - ret.weekday() + weekday) % 7 + else: + jumpdays += (ret.weekday() - weekday) % 7 + jumpdays *= -1 + ret += datetime.timedelta(days=jumpdays) + return ret + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__neg__().__radd__(other) + + def __sub__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented # In case the other object defines __rsub__ + return self.__class__(years=self.years - other.years, + months=self.months - other.months, + days=self.days - other.days, + hours=self.hours - other.hours, + minutes=self.minutes - other.minutes, + seconds=self.seconds - other.seconds, + microseconds=self.microseconds - other.microseconds, + leapdays=self.leapdays or other.leapdays, + year=(self.year if self.year is not None + else other.year), + month=(self.month if self.month is not None else + other.month), + day=(self.day if self.day is not None else + other.day), + weekday=(self.weekday if self.weekday is not None else + other.weekday), + hour=(self.hour if self.hour is not None else + other.hour), + minute=(self.minute if self.minute is not None else + other.minute), + second=(self.second if self.second is not None else + other.second), + microsecond=(self.microsecond if self.microsecond + is not None else + other.microsecond)) + + def __abs__(self): + return self.__class__(years=abs(self.years), + months=abs(self.months), + days=abs(self.days), + hours=abs(self.hours), + minutes=abs(self.minutes), + seconds=abs(self.seconds), + microseconds=abs(self.microseconds), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __neg__(self): + return self.__class__(years=-self.years, + months=-self.months, + days=-self.days, + hours=-self.hours, + minutes=-self.minutes, + seconds=-self.seconds, + microseconds=-self.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __bool__(self): + return not (not self.years and + not self.months and + not self.days and + not self.hours and + not self.minutes and + not self.seconds and + not self.microseconds and + not self.leapdays and + self.year is None and + self.month is None and + self.day is None and + self.weekday is None and + self.hour is None and + self.minute is None and + self.second is None and + self.microsecond is None) + # Compatibility with Python 2.x + __nonzero__ = __bool__ + + def __mul__(self, other): + try: + f = float(other) + except TypeError: + return NotImplemented + + return self.__class__(years=int(self.years * f), + months=int(self.months * f), + days=int(self.days * f), + hours=int(self.hours * f), + minutes=int(self.minutes * f), + seconds=int(self.seconds * f), + microseconds=int(self.microseconds * f), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + __rmul__ = __mul__ + + def __eq__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented + if self.weekday or other.weekday: + if not self.weekday or not other.weekday: + return False + if self.weekday.weekday != other.weekday.weekday: + return False + n1, n2 = self.weekday.n, other.weekday.n + if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): + return False + return (self.years == other.years and + self.months == other.months and + self.days == other.days and + self.hours == other.hours and + self.minutes == other.minutes and + self.seconds == other.seconds and + self.microseconds == other.microseconds and + self.leapdays == other.leapdays and + self.year == other.year and + self.month == other.month and + self.day == other.day and + self.hour == other.hour and + self.minute == other.minute and + self.second == other.second and + self.microsecond == other.microsecond) + + def __hash__(self): + return hash(( + self.weekday, + self.years, + self.months, + self.days, + self.hours, + self.minutes, + self.seconds, + self.microseconds, + self.leapdays, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + )) + + def __ne__(self, other): + return not self.__eq__(other) + + def __div__(self, other): + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) + + __truediv__ = __div__ + + def __repr__(self): + l = [] + for attr in ["years", "months", "days", "leapdays", + "hours", "minutes", "seconds", "microseconds"]: + value = getattr(self, attr) + if value: + l.append("{attr}={value:+g}".format(attr=attr, value=value)) + for attr in ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + l.append("{attr}={value}".format(attr=attr, value=repr(value))) + return "{classname}({attrs})".format(classname=self.__class__.__name__, + attrs=", ".join(l)) + + +def _sign(x): + return int(copysign(1, x)) + +# vim:ts=4:sw=4:et diff --git a/src/dateutil/rrule.py b/src/dateutil/rrule.py new file mode 100644 index 00000000..20a0c4ac --- /dev/null +++ b/src/dateutil/rrule.py @@ -0,0 +1,1736 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC `_, +including support for caching of results. +""" +import itertools +import datetime +import calendar +import re +import sys + +try: + from math import gcd +except ImportError: + from fractions import gcd + +from six import advance_iterator, integer_types +from six.moves import _thread, range +import heapq + +from ._common import weekday as weekdaybase +from .tz import tzutc, tzlocal + +# For warning about deprecation of until and count +from warnings import warn + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n==0") + + super(weekday, self).__init__(wkday, n) + + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + + +def _invalidates_cache(f): + """ + Decorator for rruleset methods which may invalidate the + cached length. + """ + def inner_func(self, *args, **kwargs): + rv = f(self, *args, **kwargs) + self._invalidate_cache() + return rv + + return inner_func + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _invalidate_cache(self): + if self._cache is not None: + self._cache = [] + self._cache_complete = False + self._cache_gen = self._iter() + + if self._cache_lock.locked(): + self._cache_lock.release() + + self._len = None + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penality. + def count(self): + """ Returns the number of recurrences in this set. It will have go + trough the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def xafter(self, dt, count=None, inc=False): + """ + Generator which yields up to `count` recurrences after the given + datetime instance, equivalent to `after`. + + :param dt: + The datetime at which to start generating recurrences. + + :param count: + The maximum number of recurrences to generate. If `None` (default), + dates are generated until the recurrence rule is exhausted. + + :param inc: + If `dt` is an instance of the rule and `inc` is `True`, it is + included in the output. + + :yields: Yields a sequence of `datetime` objects. + """ + + if self._cache_complete: + gen = self._cache + else: + gen = self + + # Select the comparison function + if inc: + comp = lambda dc, dtc: dc >= dtc + else: + comp = lambda dc, dtc: dc > dtc + + # Generate dates + n = 0 + for d in gen: + if comp(d, dt): + if count is not None: + n += 1 + if n > count: + break + + yield d + + def between(self, after, before, inc=False, count=1): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + .. note:: + Per RFC section 3.3.10, recurrence instances falling on invalid dates + and times are ignored rather than coerced: + + Recurrence rules may generate recurrence instances with an invalid + date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM + on a day where the local time is moved forward by an hour at 1:00 + AM). Such recurrence instances MUST be ignored and MUST NOT be + counted as part of the recurrence set. + + This can lead to possibly surprising behavior when, for example, the + start date occurs at the end of the month: + + >>> from dateutil.rrule import rrule, MONTHLY + >>> from datetime import datetime + >>> start_date = datetime(2014, 12, 31) + >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) + ... # doctest: +NORMALIZE_WHITESPACE + [datetime.datetime(2014, 12, 31, 0, 0), + datetime.datetime(2015, 1, 31, 0, 0), + datetime.datetime(2015, 3, 31, 0, 0), + datetime.datetime(2015, 5, 31, 0, 0)] + + Additionally, it supports the following keyword arguments: + + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + If given, this determines how many occurrences will be generated. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param until: + If given, this must be a datetime instance specifying the upper-bound + limit of the recurrence. The last recurrence in the rule is the greatest + datetime that is less than or equal to the value specified in the + ``until`` parameter. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + # Cache the original byxxx rules, if they are provided, as the _byxxx + # attributes do not necessarily map to the inputs, and this can be + # a problem in generating the strings. Only store things if they've + # been supplied (the string retrieval will just use .get()) + self._original_rule = {} + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" + " and has been deprecated in dateutil. Future versions will " + "raise an error.", DeprecationWarning) + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if self._bysetpos: + self._original_rule['bysetpos'] = self._bysetpos + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + self._original_rule['bymonth'] = None + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == MONTHLY: + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == WEEKLY: + byweekday = dtstart.weekday() + self._original_rule['byweekday'] = None + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + if 'bymonth' not in self._original_rule: + self._original_rule['bymonth'] = self._bymonth + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + self._original_rule['byyearday'] = self._byyearday + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster + else: + self._byeaster = None + + # bymonthday + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + bymonthday = set(bymonthday) # Ensure it's unique + + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) + + # Storing positive numbers first, then negative numbers + if 'bymonthday' not in self._original_rule: + self._original_rule['bymonthday'] = tuple( + itertools.chain(self._bymonthday, self._bynmonthday)) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + self._original_rule['byweekno'] = self._byweekno + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + orig_byweekday = [weekday(x) for x in self._byweekday] + else: + orig_byweekday = () + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + orig_bynweekday = [weekday(*x) for x in self._bynweekday] + else: + orig_bynweekday = () + + if 'byweekday' not in self._original_rule: + self._original_rule['byweekday'] = tuple(itertools.chain( + orig_byweekday, orig_bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = {dtstart.hour} + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + self._original_rule['byhour'] = self._byhour + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = {dtstart.minute} + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + self._original_rule['byminute'] = self._byminute + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + self._original_rule['bysecond'] = self._bysecond + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def __str__(self): + """ + Output a string that would generate this RRULE if passed to rrulestr. + This is mostly compatible with RFC5545, except for the + dateutil-specific extension BYEASTER. + """ + + output = [] + h, m, s = [None] * 3 + if self._dtstart: + output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) + h, m, s = self._dtstart.timetuple()[3:6] + + parts = ['FREQ=' + FREQNAMES[self._freq]] + if self._interval != 1: + parts.append('INTERVAL=' + str(self._interval)) + + if self._wkst: + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) + + if self._count is not None: + parts.append('COUNT=' + str(self._count)) + + if self._until: + parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) + + if self._original_rule.get('byweekday') is not None: + # The str() method on weekday objects doesn't generate + # RFC5545-compliant strings, so we should modify that. + original_rule = dict(self._original_rule) + wday_strings = [] + for wday in original_rule['byweekday']: + if wday.n: + wday_strings.append('{n:+d}{wday}'.format( + n=wday.n, + wday=repr(wday)[0:2])) + else: + wday_strings.append(repr(wday)) + + original_rule['byweekday'] = wday_strings + else: + original_rule = self._original_rule + + partfmt = '{name}={vals}' + for name, key in [('BYSETPOS', 'bysetpos'), + ('BYMONTH', 'bymonth'), + ('BYMONTHDAY', 'bymonthday'), + ('BYYEARDAY', 'byyearday'), + ('BYWEEKNO', 'byweekno'), + ('BYDAY', 'byweekday'), + ('BYHOUR', 'byhour'), + ('BYMINUTE', 'byminute'), + ('BYSECOND', 'bysecond'), + ('BYEASTER', 'byeaster')]: + value = original_rule.get(key) + if value: + parts.append(partfmt.format(name=name, vals=(','.join(str(v) + for v in value)))) + + output.append('RRULE:' + ';'.join(parts)) + return '\n'.join(output) + + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + total += 1 + yield res + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal + i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + + total += 1 + yield res + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399 - (hour * 3600 + minute * 60 + second)) + // interval) * interval) + + rep_rate = (24 * 3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365 + calendar.isleap(year) + self.nextyearlen = 365 + calendar.isleap(year + 1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None] * self.yearlen + i = datetime.date(year, month, day).toordinal() - self.yearordinal + dset[i] = i + return dset, i, i + 1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + if self.genlist[0] is self: + heapq.heappop(self.genlist) + else: + self.genlist.remove(self) + heapq.heapify(self.genlist) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + @_invalidates_cache + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + @_invalidates_cache + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + @_invalidates_cache + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + @_invalidates_cache + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + lastdt = None + total = 0 + heapq.heapify(rlist) + heapq.heapify(exlist) + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exitem = exlist[0] + advance_iterator(exitem) + if exlist and exlist[0] is exitem: + heapq.heapreplace(exlist, exitem) + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + if rlist and rlist[0] is ritem: + heapq.heapreplace(rlist, ritem) + self._len = total + + + + +class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ + l = [] + for wday in value.split(','): + if '(' in wday: + # If it's of the form TH(+1), etc. + splt = wday.split('(') + w = splt[0] + n = int(splt[1][:-1]) + elif len(wday): + # If it's of the form +1MO + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + else: + raise ValueError("Invalid (empty) BYDAY specification.") + + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzids=None, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P[^:]+):', s) + )) + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError("unsupported RRULE parm: "+parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) + elif name == "DTSTART": + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + rset.exdate(value) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/src/dateutil/tz/__init__.py b/src/dateutil/tz/__init__.py new file mode 100644 index 00000000..5a2d9cd6 --- /dev/null +++ b/src/dateutil/tz/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from .tz import * +from .tz import __doc__ + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", + "enfold", "datetime_ambiguous", "datetime_exists", + "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/src/dateutil/tz/_common.py b/src/dateutil/tz/_common.py new file mode 100644 index 00000000..594e0823 --- /dev/null +++ b/src/dateutil/tz/_common.py @@ -0,0 +1,419 @@ +from six import PY2 + +from functools import wraps + +from datetime import datetime, timedelta, tzinfo + + +ZERO = timedelta(0) + +__all__ = ['tzname_in_python2', 'enfold'] + + +def tzname_in_python2(namefunc): + """Change unicode output into bytestrings in Python 2 + + tzname() API changed in Python 3. It used to return bytes, but was changed + to unicode strings + """ + if PY2: + @wraps(namefunc) + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None: + name = name.encode() + + return name + + return adjust_encoding + else: + return namefunc + + +# The following is adapted from Alexander Belopolsky's tz library +# https://github.com/abalkin/tz +if hasattr(datetime, 'fold'): + # This is the pre-python 3.6 fold situation + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + return dt.replace(fold=fold) + +else: + class _DatetimeWithFold(datetime): + """ + This is a class designed to provide a PEP 495-compliant interface for + Python versions before 3.6. It is used only for dates in a fold, so + the ``fold`` attribute is fixed at ``1``. + + .. versionadded:: 2.6.0 + """ + __slots__ = () + + def replace(self, *args, **kwargs): + """ + Return a datetime with the same attributes, except for those + attributes given new values by whichever keyword arguments are + specified. Note that tzinfo=None can be specified to create a naive + datetime from an aware datetime with no conversion of date and time + data. + + This is reimplemented in ``_DatetimeWithFold`` because pypy3 will + return a ``datetime.datetime`` even if ``fold`` is unchanged. + """ + argnames = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo' + ) + + for arg, argname in zip(args, argnames): + if argname in kwargs: + raise TypeError('Duplicate argument: {}'.format(argname)) + + kwargs[argname] = arg + + for argname in argnames: + if argname not in kwargs: + kwargs[argname] = getattr(self, argname) + + dt_class = self.__class__ if kwargs.get('fold', 1) else datetime + + return dt_class(**kwargs) + + @property + def fold(self): + return 1 + + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + if getattr(dt, 'fold', 0) == fold: + return dt + + args = dt.timetuple()[:6] + args += (dt.microsecond, dt.tzinfo) + + if fold: + return _DatetimeWithFold(*args) + else: + return datetime(*args) + + +def _validate_fromutc_inputs(f): + """ + The CPython version of ``fromutc`` checks that the input is a ``datetime`` + object and that ``self`` is attached as its ``tzinfo``. + """ + @wraps(f) + def fromutc(self, dt): + 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") + + return f(self, dt) + + return fromutc + + +class _tzinfo(tzinfo): + """ + Base class for all ``dateutil`` ``tzinfo`` objects. + """ + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + + dt = dt.replace(tzinfo=self) + + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) + + return same_dt and not same_offset + + def _fold_status(self, dt_utc, dt_wall): + """ + Determine the fold status of a "wall" datetime, given a representation + of the same datetime as a (naive) UTC datetime. This is calculated based + on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all + datetimes, and that this offset is the actual number of hours separating + ``dt_utc`` and ``dt_wall``. + + :param dt_utc: + Representation of the datetime as UTC + + :param dt_wall: + Representation of the datetime as "wall time". This parameter must + either have a `fold` attribute or have a fold-naive + :class:`datetime.tzinfo` attached, otherwise the calculation may + fail. + """ + if self.is_ambiguous(dt_wall): + delta_wall = dt_wall - dt_utc + _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) + else: + _fold = 0 + + return _fold + + def _fold(self, dt): + return getattr(dt, 'fold', 0) + + def _fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + + # Re-implement the algorithm from Python's datetime.py + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # The original datetime.py code assumes that `dst()` defaults to + # zero during ambiguous times. PEP 495 inverts this presumption, so + # for pre-PEP 495 versions of python, we need to tweak the algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurance, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + dt_wall = self._fromutc(dt) + + # Calculate the fold status given the two datetimes. + _fold = self._fold_status(dt, dt_wall) + + # Set the default fold value for ambiguous dates + return enfold(dt_wall, fold=_fold) + + +class tzrangebase(_tzinfo): + """ + This is an abstract base class for time zones represented by an annual + transition into and out of DST. Child classes should implement the following + methods: + + * ``__init__(self, *args, **kwargs)`` + * ``transitions(self, year)`` - this is expected to return a tuple of + datetimes representing the DST on and off transitions in standard + time. + + A fully initialized ``tzrangebase`` subclass should also provide the + following attributes: + * ``hasdst``: Boolean whether or not the zone uses DST. + * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects + representing the respective UTC offsets. + * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short + abbreviations in DST and STD, respectively. + * ``_hasdst``: Whether or not the zone has DST. + + .. versionadded:: 2.6.0 + """ + def __init__(self): + raise NotImplementedError('tzrangebase is an abstract base class') + + def utcoffset(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_base_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def fromutc(self, dt): + """ Given a datetime in UTC, return local time """ + 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") + + # Get transitions - if there are none, fixed offset + transitions = self.transitions(dt.year) + if transitions is None: + return dt + self.utcoffset(dt) + + # Get the transition times in UTC + dston, dstoff = transitions + + dston -= self._std_offset + dstoff -= self._std_offset + + utc_transitions = (dston, dstoff) + dt_utc = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt_utc, utc_transitions) + + if isdst: + dt_wall = dt + self._dst_offset + else: + dt_wall = dt + self._std_offset + + _fold = int(not isdst and self.is_ambiguous(dt_wall)) + + return enfold(dt_wall, fold=_fold) + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if not self.hasdst: + return False + + start, end = self.transitions(dt.year) + + dt = dt.replace(tzinfo=None) + return (end <= dt < end + self._dst_base_offset) + + def _isdst(self, dt): + if not self.hasdst: + return False + elif dt is None: + return None + + transitions = self.transitions(dt.year) + + if transitions is None: + return False + + dt = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt, transitions) + + # Handle ambiguous dates + if not isdst and self.is_ambiguous(dt): + return not self._fold(dt) + else: + return isdst + + def _naive_isdst(self, dt, transitions): + dston, dstoff = transitions + + dt = dt.replace(tzinfo=None) + + if dston < dstoff: + isdst = dston <= dt < dstoff + else: + isdst = not dstoff <= dt < dston + + return isdst + + @property + def _dst_base_offset(self): + return self._dst_offset - self._std_offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ diff --git a/src/dateutil/tz/_factories.py b/src/dateutil/tz/_factories.py new file mode 100644 index 00000000..d2560eb7 --- /dev/null +++ b/src/dateutil/tz/_factories.py @@ -0,0 +1,73 @@ +from datetime import timedelta +import weakref +from collections import OrderedDict + + +class _TzSingleton(type): + def __init__(cls, *args, **kwargs): + cls.__instance = None + super(_TzSingleton, cls).__init__(*args, **kwargs) + + def __call__(cls): + if cls.__instance is None: + cls.__instance = super(_TzSingleton, cls).__call__() + return cls.__instance + + +class _TzFactory(type): + def instance(cls, *args, **kwargs): + """Alternate constructor that returns a fresh instance""" + return type.__call__(cls, *args, **kwargs) + + +class _TzOffsetFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + def __call__(cls, name, offset): + if isinstance(offset, timedelta): + key = (name, offset.total_seconds()) + else: + key = (name, offset) + + instance = cls.__instances.get(key, None) + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(name, offset)) + + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + # TODO: Maybe this should be under a lock? + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + + +class _TzStrFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + def __call__(cls, s, posix_offset=False): + key = (s, posix_offset) + instance = cls.__instances.get(key, None) + + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(s, posix_offset)) + + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + + # Remove an item if the strong cache is overpopulated + # TODO: Maybe this should be under a lock? + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + diff --git a/src/dateutil/tz/tz.py b/src/dateutil/tz/tz.py new file mode 100644 index 00000000..d05414e7 --- /dev/null +++ b/src/dateutil/tz/tz.py @@ -0,0 +1,1836 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" +import datetime +import struct +import time +import sys +import os +import bisect +import weakref +from collections import OrderedDict + +import six +from six import string_types +from six.moves import _thread +from ._common import tzname_in_python2, _tzinfo +from ._common import tzrangebase, enfold +from ._common import _validate_fromutc_inputs + +from ._factories import _TzSingleton, _TzOffsetFactory +from ._factories import _TzStrFactory +try: + from .win import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +# For warning about rounding tzinfo +from warnings import warn + +ZERO = datetime.timedelta(0) +EPOCH = datetime.datetime.utcfromtimestamp(0) +EPOCHORDINAL = EPOCH.toordinal() + + +@six.add_metaclass(_TzSingleton) +class tzutc(datetime.tzinfo): + """ + This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True + """ + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +@six.add_metaclass(_TzOffsetFactory) +class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object). + """ + def __init__(self, name, offset): + self._name = name + + try: + # Allow a timedelta + offset = offset.total_seconds() + except (TypeError, AttributeError): + pass + + self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + def __eq__(self, other): + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + int(self._offset.total_seconds())) + + __reduce__ = object.__reduce__ + + +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ + def __init__(self): + super(tzlocal, self).__init__() + + self._std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + self._dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + self._dst_offset = self._std_offset + + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) + + def utcoffset(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset - self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._tznames[self._isdst(dt)] + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval + + def __eq__(self, other): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: + return NotImplemented + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", + "isstd", "isgmt", "dstoffset"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return NotImplemented + + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt and + self.dstoffset == other.dstoffset) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + + +class _tzfile(object): + """ + Lightweight class for holding the relevant transition and time zone + information read from binary tzfiles. + """ + attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', + 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] + + def __init__(self, **kwargs): + for attr in self.attrs: + setattr(self, attr, kwargs.get(attr, None)) + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. + + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + See `Sources for Time Zone and Daylight Saving Time Data + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files + `_ with the `zic time zone compiler + `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + + """ + + def __init__(self, fileobj, filename=None): + super(tzfile, self).__init__() + + file_opened_here = False + if isinstance(fileobj, string_types): + self._filename = fileobj + fileobj = open(fileobj, 'rb') + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = repr(fileobj) + + if fileobj is not None: + if not file_opened_here: + fileobj = _nullcontext(fileobj) + + with fileobj as file_stream: + tzobj = self._read_tzfile(file_stream) + + self._set_tzdata(tzobj) + + def _set_tzdata(self, tzobj): + """ Set the time zone data of this object from a _tzfile object """ + # Copy the relevant attributes over as private attributes + for attr in _tzfile.attrs: + setattr(self, '_' + attr, getattr(tzobj, attr)) + + def _read_tzfile(self, fileobj): + out = _tzfile() + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) + else: + out.trans_list_utc = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + out.trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + out.trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now (but seek for correct file position) + if leapcnt: + fileobj.seek(leapcnt * 8, os.SEEK_CUR) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # Build ttinfo list + out.ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + gmtoff = _get_supported_offset(gmtoff) + tti = _ttinfo() + tti.offset = gmtoff + tti.dstoffset = datetime.timedelta(0) + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + out.ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + out.ttinfo_std = None + out.ttinfo_dst = None + out.ttinfo_before = None + if out.ttinfo_list: + if not out.trans_list_utc: + out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] + else: + for i in range(timecnt-1, -1, -1): + tti = out.trans_idx[i] + if not out.ttinfo_std and not tti.isdst: + out.ttinfo_std = tti + elif not out.ttinfo_dst and tti.isdst: + out.ttinfo_dst = tti + + if out.ttinfo_std and out.ttinfo_dst: + break + else: + if out.ttinfo_dst and not out.ttinfo_std: + out.ttinfo_std = out.ttinfo_dst + + for tti in out.ttinfo_list: + if not tti.isdst: + out.ttinfo_before = tti + break + else: + out.ttinfo_before = out.ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + lastdst = None + lastoffset = None + lastdstoffset = None + lastbaseoffset = None + out.trans_list = [] + + for i, tti in enumerate(out.trans_idx): + offset = tti.offset + dstoffset = 0 + + if lastdst is not None: + if tti.isdst: + if not lastdst: + dstoffset = offset - lastoffset + + if not dstoffset and lastdstoffset: + dstoffset = lastdstoffset + + tti.dstoffset = datetime.timedelta(seconds=dstoffset) + lastdstoffset = dstoffset + + # If a time zone changes its base offset during a DST transition, + # then you need to adjust by the previous base offset to get the + # transition time in local time. Otherwise you use the current + # base offset. Ideally, I would have some mathematical proof of + # why this is true, but I haven't really thought about it enough. + baseoffset = offset - dstoffset + adjustment = baseoffset + if (lastbaseoffset is not None and baseoffset != lastbaseoffset + and tti.isdst != lastdst): + # The base DST has changed + adjustment = lastbaseoffset + + lastdst = tti.isdst + lastoffset = offset + lastbaseoffset = baseoffset + + out.trans_list.append(out.trans_list_utc[i] + adjustment) + + out.trans_idx = tuple(out.trans_idx) + out.trans_list = tuple(out.trans_list) + out.trans_list_utc = tuple(out.trans_list_utc) + + return out + + def _find_last_transition(self, dt, in_utc=False): + # If there's no list, there are no transitions to find + if not self._trans_list: + return None + + timestamp = _datetime_to_timestamp(dt) + + # Find where the timestamp fits in the transition list - if the + # timestamp is a transition time, it's part of the "after" period. + trans_list = self._trans_list_utc if in_utc else self._trans_list + idx = bisect.bisect_right(trans_list, timestamp) + + # We want to know when the previous transition was, so subtract off 1 + return idx - 1 + + def _get_ttinfo(self, idx): + # For no list or after the last transition, default to _ttinfo_std + if idx is None or (idx + 1) >= len(self._trans_list): + return self._ttinfo_std + + # If there is a list and the time is before it, return _ttinfo_before + if idx < 0: + return self._ttinfo_before + + return self._trans_idx[idx] + + def _find_ttinfo(self, dt): + idx = self._resolve_ambiguous_time(dt) + + return self._get_ttinfo(idx) + + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + # These isinstance checks are in datetime.tzinfo, so we'll preserve + # them, even if we don't care about duck typing. + if not isinstance(dt, datetime.datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # First treat UTC as wall time and get the transition we're in. + idx = self._find_last_transition(dt, in_utc=True) + tti = self._get_ttinfo(idx) + + dt_out = dt + datetime.timedelta(seconds=tti.offset) + + fold = self.is_ambiguous(dt_out, idx=idx) + + return enfold(dt_out, fold=int(fold)) + + def is_ambiguous(self, dt, idx=None): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if idx is None: + idx = self._find_last_transition(dt) + + # Calculate the difference in offsets from current to previous + timestamp = _datetime_to_timestamp(dt) + tti = self._get_ttinfo(idx) + + if idx is None or idx <= 0: + return False + + od = self._get_ttinfo(idx - 1).offset - tti.offset + tt = self._trans_list[idx] # Transition time + + return timestamp < tt + od + + def _resolve_ambiguous_time(self, dt): + idx = self._find_last_transition(dt) + + # If we have no transitions, return the index + _fold = self._fold(dt) + if idx is None or idx == 0: + return idx + + # If it's ambiguous and we're in a fold, shift to a different index. + idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) + + return idx - idx_offset + + def utcoffset(self, dt): + if dt is None: + return None + + if not self._ttinfo_std: + return ZERO + + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if dt is None: + return None + + if not self._ttinfo_dst: + return ZERO + + tti = self._find_ttinfo(dt) + + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.dstoffset + + @tzname_in_python2 + def tzname(self, dt): + if not self._ttinfo_std or dt is None: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return NotImplemented + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + def __reduce__(self): + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + return (self.__class__, (None, self._filename), self.__dict__) + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. + + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + + global relativedelta + from dateutil import relativedelta + + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + + try: + stdoffset = stdoffset.total_seconds() + except (TypeError, AttributeError): + pass + + try: + dstoffset = dstoffset.total_seconds() + except (TypeError, AttributeError): + pass + + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + if not self.hasdst: + return None + + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) + + def __eq__(self, other): + if not isinstance(other, tzrange): + return NotImplemented + + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +@six.add_metaclass(_TzStrFactory) +class tzstr(tzrange): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): + global parser + from dateutil.parser import _parser as parser + + self._s = s + + res = parser._parsetz(s) + if res is None or res.any_unused_tokens: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC") and not posix_offset: + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + self.hasdst = bool(self._start_delta) + + def _delta(self, x, isend=0): + from dateutil import relativedelta + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(_tzinfo): + def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + + dt = dt.replace(tzinfo=None) + + try: + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] + except ValueError: + pass + + lastcompdt = None + lastcomp = None + + for comp in self._comps: + compdt = self._find_compdt(comp, dt) + + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) + + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + + return lastcomp + + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + + def utcoffset(self, dt): + if dt is None: + return None + + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 + """ + def __init__(self, fileobj): + global rrule + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + else: + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _nullcontext(fileobj) + + self._vtz = {} + + with fileobj as fobj: + self._parse_rfc(fobj.read()) + + def keys(self): + """ + Retrieves the available time zones as a list. + """ + return list(self._vtz.keys()) + + def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ + if tzid is None: + if len(self._vtz) == 0: + raise ValueError("no timezones defined") + elif len(self._vtz) > 1: + raise ValueError("more than one timezone available") + tzid = next(iter(self._vtz)) + + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal + elif len(s) == 6: + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal + else: + raise ValueError("invalid offset: " + s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + + +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) + + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache_size = 8 + self.__strong_cache = OrderedDict() + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + rv = self.nocache(name=name) + if not (name is None + or isinstance(rv, tzlocal_classes) + or rv is None): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + # + # We also cannot store weak references to None, so we + # will also not store that. + self.__instances[name] = rv + else: + # No need for strong caching, return immediately + return rv + + self.__strong_cache[name] = self.__strong_cache.pop(name, rv) + + if len(self.__strong_cache) > self.__strong_cache_size: + self.__strong_cache.popitem(last=False) + + return rv + + def set_cache_size(self, size): + with self._cache_lock: + self.__strong_cache_size = size + while len(self.__strong_cache) > size: + self.__strong_cache.popitem(last=False) + + def cache_clear(self): + with self._cache_lock: + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache.clear() + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name == ":": + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + if name.startswith(":"): + name = name[1:] + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except (WindowsError, UnicodeEncodeError): + # UnicodeEncodeError is for Python 2.7 compat + tz = None + + if not tz: + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = tzutc() + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz + + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except Exception: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. + """ + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() + + +if sys.version_info >= (3, 6): + def _get_supported_offset(second_offset): + return second_offset +else: + def _get_supported_offset(second_offset): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + old_offset = second_offset + calculated_offset = 60 * ((second_offset + 30) // 60) + return calculated_offset + + +try: + # Python 3.7 feature + from contextmanager import nullcontext as _nullcontext +except ImportError: + class _nullcontext(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + +# vim:ts=4:sw=4:et diff --git a/src/dateutil/tz/win.py b/src/dateutil/tz/win.py new file mode 100644 index 00000000..cde07ba7 --- /dev/null +++ b/src/dateutil/tz/win.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +""" +This module provides an interface to the native time zone data on Windows, +including :py:class:`datetime.tzinfo` implementations. + +Attempting to import this module on a non-Windows platform will raise an +:py:obj:`ImportError`. +""" +# This code was originally contributed by Jeffrey Harris. +import datetime +import struct + +from six.moves import winreg +from six import text_type + +try: + import ctypes + from ctypes import wintypes +except ValueError: + # ValueError is raised on non-Windows systems for some horrible reason. + raise ImportError("Running tzwin on non-Windows system") + +from ._common import tzrangebase + +__all__ = ["tzwin", "tzwinlocal", "tzres"] + +ONEWEEK = datetime.timedelta(7) + +TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" +TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" +TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + + +def _settzkeyname(): + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + winreg.OpenKey(handle, TZKEYNAMENT).Close() + TZKEYNAME = TZKEYNAMENT + except WindowsError: + TZKEYNAME = TZKEYNAME9X + handle.Close() + return TZKEYNAME + + +TZKEYNAME = _settzkeyname() + + +class tzres(object): + """ + Class for accessing ``tzres.dll``, which contains timezone name related + resources. + + .. versionadded:: 2.5.0 + """ + p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char + + def __init__(self, tzres_loc='tzres.dll'): + # Load the user32 DLL so we can load strings from tzres + user32 = ctypes.WinDLL('user32') + + # Specify the LoadStringW function + user32.LoadStringW.argtypes = (wintypes.HINSTANCE, + wintypes.UINT, + wintypes.LPWSTR, + ctypes.c_int) + + self.LoadStringW = user32.LoadStringW + self._tzres = ctypes.WinDLL(tzres_loc) + self.tzres_loc = tzres_loc + + def load_name(self, offset): + """ + Load a timezone name from a DLL offset (integer). + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.load_name(112)) + 'Eastern Standard Time' + + :param offset: + A positive integer value referring to a string from the tzres dll. + + .. note:: + + Offsets found in the registry are generally of the form + ``@tzres.dll,-114``. The offset in this case is 114, not -114. + + """ + resource = self.p_wchar() + lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) + nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) + return resource[:nchar] + + def name_from_string(self, tzname_str): + """ + Parse strings as returned from the Windows registry into the time zone + name as defined in the registry. + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.name_from_string('@tzres.dll,-251')) + 'Dateline Daylight Time' + >>> print(tzr.name_from_string('Eastern Standard Time')) + 'Eastern Standard Time' + + :param tzname_str: + A timezone name string as returned from a Windows registry key. + + :return: + Returns the localized timezone string from tzres.dll if the string + is of the form `@tzres.dll,-offset`, else returns the input string. + """ + if not tzname_str.startswith('@'): + return tzname_str + + name_splt = tzname_str.split(',-') + try: + offset = int(name_splt[1]) + except: + raise ValueError("Malformed timezone string.") + + return self.load_name(offset) + + +class tzwinbase(tzrangebase): + """tzinfo class based on win32's timezones available in the registry.""" + def __init__(self): + raise NotImplementedError('tzwinbase is an abstract base class') + + def __eq__(self, other): + # Compare on all relevant dimensions, including name. + if not isinstance(other, tzwinbase): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._stddayofweek == other._stddayofweek and + self._dstdayofweek == other._dstdayofweek and + self._stdweeknumber == other._stdweeknumber and + self._dstweeknumber == other._dstweeknumber and + self._stdhour == other._stdhour and + self._dsthour == other._dsthour and + self._stdminute == other._stdminute and + self._dstminute == other._dstminute and + self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr) + + @staticmethod + def list(): + """Return a list of all time zones known to the system.""" + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZKEYNAME) as tzkey: + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] + return result + + def display(self): + """ + Return the display name of the time zone. + """ + return self._display + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + + if not self.hasdst: + return None + + dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, + self._dsthour, self._dstminute, + self._dstweeknumber) + + dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, + self._stdhour, self._stdminute, + self._stdweeknumber) + + # Ambiguous dates default to the STD side + dstoff -= self._dst_base_offset + + return dston, dstoff + + def _get_hasdst(self): + return self._dstmonth != 0 + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +class tzwin(tzwinbase): + """ + Time zone object created from the zone info in the Windows registry + + These are similar to :py:class:`dateutil.tz.tzrange` objects in that + the time zone data is provided in the format of a single offset rule + for either 0 or 2 time zone transitions per year. + + :param: name + The name of a Windows time zone key, e.g. "Eastern Standard Time". + The full list of keys can be retrieved with :func:`tzwin.list`. + """ + + def __init__(self, name): + self._name = name + + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + keydict = valuestodict(tzkey) + + self._std_abbr = keydict["Std"] + self._dst_abbr = keydict["Dlt"] + + self._display = keydict["Display"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=3l16h", keydict["TZI"]) + stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + dstoffset = stdoffset-tup[2] # + DaylightBias * -1 + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[4:9] + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[12:17] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwin(%s)" % repr(self._name) + + def __reduce__(self): + return (self.__class__, (self._name,)) + + +class tzwinlocal(tzwinbase): + """ + Class representing the local time zone information in the Windows registry + + While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` + module) to retrieve time zone information, ``tzwinlocal`` retrieves the + rules directly from the Windows registry and creates an object like + :class:`dateutil.tz.tzwin`. + + Because Windows does not have an equivalent of :func:`time.tzset`, on + Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the + time zone settings *at the time that the process was started*, meaning + changes to the machine's time zone settings during the run of a program + on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. + Because ``tzwinlocal`` reads the registry directly, it is unaffected by + this issue. + """ + def __init__(self): + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: + keydict = valuestodict(tzlocalkey) + + self._std_abbr = keydict["StandardName"] + self._dst_abbr = keydict["DaylightName"] + + try: + tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, + sn=self._std_abbr) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + _keydict = valuestodict(tzkey) + self._display = _keydict["Display"] + except OSError: + self._display = None + + stdoffset = -keydict["Bias"]-keydict["StandardBias"] + dstoffset = stdoffset-keydict["DaylightBias"] + + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # For reasons unclear, in this particular key, the day of week has been + # moved to the END of the SYSTEMTIME structure. + tup = struct.unpack("=8h", keydict["StandardStart"]) + + (self._stdmonth, + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[1:5] + + self._stddayofweek = tup[7] + + tup = struct.unpack("=8h", keydict["DaylightStart"]) + + (self._dstmonth, + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[1:5] + + self._dstdayofweek = tup[7] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwinlocal()" + + def __str__(self): + # str will return the standard name, not the daylight name. + return "tzwinlocal(%s)" % repr(self._std_abbr) + + def __reduce__(self): + return (self.__class__, ()) + + +def picknthweekday(year, month, dayofweek, hour, minute, whichweek): + """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ + first = datetime.datetime(year, month, 1, hour, minute) + + # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), + # Because 7 % 7 = 0 + weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) + wd = weekdayone + ((whichweek - 1) * ONEWEEK) + if (wd.month != month): + wd -= ONEWEEK + + return wd + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dout = {} + size = winreg.QueryInfoKey(key)[1] + tz_res = None + + for i in range(size): + key_name, value, dtype = winreg.EnumValue(key, i) + if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: + # If it's a DWORD (32-bit integer), it's stored as unsigned - convert + # that to a proper signed integer + if value & (1 << 31): + value = value - (1 << 32) + elif dtype == winreg.REG_SZ: + # If it's a reference to the tzres DLL, load the actual string + if value.startswith('@tzres'): + tz_res = tz_res or tzres() + value = tz_res.name_from_string(value) + + value = value.rstrip('\x00') # Remove trailing nulls + + dout[key_name] = value + + return dout diff --git a/src/dateutil/tzwin.py b/src/dateutil/tzwin.py new file mode 100644 index 00000000..cebc673e --- /dev/null +++ b/src/dateutil/tzwin.py @@ -0,0 +1,2 @@ +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/src/dateutil/utils.py b/src/dateutil/utils.py new file mode 100644 index 00000000..ebcce6aa --- /dev/null +++ b/src/dateutil/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +This module offers general convenience and utility functions for dealing with +datetimes. + +.. versionadded:: 2.7.0 +""" +from __future__ import unicode_literals + +from datetime import datetime, time + + +def today(tzinfo=None): + """ + Returns a :py:class:`datetime` representing the current day at midnight + + :param tzinfo: + The time zone to attach (also used to determine the current day). + + :return: + A :py:class:`datetime.datetime` object representing the current day + at midnight. + """ + + dt = datetime.now(tzinfo) + return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) + + +def default_tzinfo(dt, tzinfo): + """ + Sets the the ``tzinfo`` parameter on naive datetimes only + + This is useful for example when you are provided a datetime that may have + either an implicit or explicit time zone, such as when parsing a time zone + string. + + .. doctest:: + + >>> from dateutil.tz import tzoffset + >>> from dateutil.parser import parse + >>> from dateutil.utils import default_tzinfo + >>> dflt_tz = tzoffset("EST", -18000) + >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) + 2014-01-01 12:30:00+00:00 + >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) + 2014-01-01 12:30:00-05:00 + + :param dt: + The datetime on which to replace the time zone + + :param tzinfo: + The :py:class:`datetime.tzinfo` subclass instance to assign to + ``dt`` if (and only if) it is naive. + + :return: + Returns an aware :py:class:`datetime.datetime`. + """ + if dt.tzinfo is not None: + return dt + else: + return dt.replace(tzinfo=tzinfo) + + +def within_delta(dt1, dt2, delta): + """ + Useful for comparing two datetimes that may a negilible difference + to be considered equal. + """ + delta = abs(delta) + difference = dt1 - dt2 + return -delta <= difference <= delta diff --git a/src/dateutil/zoneinfo/__init__.py b/src/dateutil/zoneinfo/__init__.py new file mode 100644 index 00000000..34f11ad6 --- /dev/null +++ b/src/dateutil/zoneinfo/__init__.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +import warnings +import json + +from tarfile import TarFile +from pkgutil import get_data +from io import BytesIO + +from dateutil.tz import tzfile as _tzfile + +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] + +ZONEFILENAME = "dateutil-zoneinfo.tar.gz" +METADATA_FN = 'METADATA' + + +class tzfile(_tzfile): + def __reduce__(self): + return (gettz, (self._filename,)) + + +def getzoneinfofile_stream(): + try: + return BytesIO(get_data(__name__, ZONEFILENAME)) + except IOError as e: # TODO switch to FileNotFoundError? + warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror)) + return None + + +class ZoneInfoFile(object): + def __init__(self, zonefile_stream=None): + if zonefile_stream is not None: + with TarFile.open(fileobj=zonefile_stream) as tf: + self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name) + for zf in tf.getmembers() + if zf.isfile() and zf.name != METADATA_FN} + # deal with links: They'll point to their parent object. Less + # waste of memory + links = {zl.name: self.zones[zl.linkname] + for zl in tf.getmembers() if + zl.islnk() or zl.issym()} + self.zones.update(links) + try: + metadata_json = tf.extractfile(tf.getmember(METADATA_FN)) + metadata_str = metadata_json.read().decode('UTF-8') + self.metadata = json.loads(metadata_str) + except KeyError: + # no metadata in tar file + self.metadata = None + else: + self.zones = {} + self.metadata = None + + def get(self, name, default=None): + """ + Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method + for retrieving zones from the zone dictionary. + + :param name: + The name of the zone to retrieve. (Generally IANA zone names) + + :param default: + The value to return in the event of a missing key. + + .. versionadded:: 2.6.0 + + """ + return self.zones.get(name, default) + + +# The current API has gettz as a module function, although in fact it taps into +# a stateful class. So as a workaround for now, without changing the API, we +# will create a new "global" class instance the first time a user requests a +# timezone. Ugly, but adheres to the api. +# +# TODO: Remove after deprecation period. +_CLASS_ZONE_INSTANCE = [] + + +def get_zonefile_instance(new_instance=False): + """ + This is a convenience function which provides a :class:`ZoneInfoFile` + instance using the data provided by the ``dateutil`` package. By default, it + caches a single instance of the ZoneInfoFile object and returns that. + + :param new_instance: + If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and + used as the cached instance for the next call. Otherwise, new instances + are created only as necessary. + + :return: + Returns a :class:`ZoneInfoFile` object. + + .. versionadded:: 2.6 + """ + if new_instance: + zif = None + else: + zif = getattr(get_zonefile_instance, '_cached_instance', None) + + if zif is None: + zif = ZoneInfoFile(getzoneinfofile_stream()) + + get_zonefile_instance._cached_instance = zif + + return zif + + +def gettz(name): + """ + This retrieves a time zone from the local zoneinfo tarball that is packaged + with dateutil. + + :param name: + An IANA-style time zone name, as found in the zoneinfo file. + + :return: + Returns a :class:`dateutil.tz.tzfile` time zone object. + + .. warning:: + It is generally inadvisable to use this function, and it is only + provided for API compatibility with earlier versions. This is *not* + equivalent to ``dateutil.tz.gettz()``, which selects an appropriate + time zone based on the inputs, favoring system zoneinfo. This is ONLY + for accessing the dateutil-specific zoneinfo (which may be out of + date compared to the system zoneinfo). + + .. deprecated:: 2.6 + If you need to use a specific zoneinfofile over the system zoneinfo, + instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call + :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead. + + Use :func:`get_zonefile_instance` to retrieve an instance of the + dateutil-provided zoneinfo. + """ + warnings.warn("zoneinfo.gettz() will be removed in future versions, " + "to use the dateutil-provided zoneinfo files, instantiate a " + "ZoneInfoFile object and use ZoneInfoFile.zones.get() " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].zones.get(name) + + +def gettz_db_metadata(): + """ Get the zonefile metadata + + See `zonefile_metadata`_ + + :returns: + A dictionary with the database metadata + + .. deprecated:: 2.6 + See deprecation warning in :func:`zoneinfo.gettz`. To get metadata, + query the attribute ``zoneinfo.ZoneInfoFile.metadata``. + """ + warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future " + "versions, to use the dateutil-provided zoneinfo files, " + "ZoneInfoFile object and query the 'metadata' attribute " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].metadata diff --git a/src/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/src/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..124f3e14c68f4e94808400af9a8d4ecdb2178880 GIT binary patch literal 154405 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 R+?^>J*5HKbLj12+fLcm@iV1fvkXsEd) z0wx=3u7H3kg_^4(U>XRRHUg%LfEglSrU=-31k4%%vqiuh5inN-%rn&77Xb?lH4jF> z!b8nJBVf@8SUdujjDV#fV3`Qm4@BVm0 zw!McStx&u2*WHLGgfUC*DXxD5b$&F_pOrE3NEKr9JSgC$R}@X>xxJ6QX(Y4P?IuD} zYpa_CQF4_fC`@C$=e24lyE-$>zsh`4JaXluy1aYx)TahU1qSX5p0 z=sN=&$}41{W5_!nIK!C=s`ul@FdNi%lA2Vas&kD?{3qY~P1>(U_$Fy)BUUPb^@3D> z<5;FyMe(A875iILz?5RT47*~ppAomq$j_jOrR`w-tK=VC zQX^S#*)aA_Y5Y5EXL&QOZE87E^F5OH{2m*xetwtu*_`f%AoLw!TnKD!6w;x@Nm%!d zE!h(BLhOWy@iY2Hgq5j{CBD)Rk{Dm2`$oo;M$VM>j46$RSoRxb&Y&cw`cvZJr^M<6 z#KQ!{>V(9@gv9DZ#KT0y>cqsu#Kh_(#KZAys=tJZ$EAtKp~T~_@qWD{9ycZ)|3Ex$ zPdx5UJnlbGi9^cPLHFm|gD<}octlb<@pvBbcp33{or+$t4{2~@LItJ}JLX&V0KA=% zJJ+1&3X}Kd^jp)e7|4Lq)>4k4naVUp)-?G2%SmEcsG;MvL~U9c{rr0 z{}ElMs@*;87Q?El-8jFh{Xy$}04H}DUk1tH5Z>cMwF>;t?p=5BhrvwcRqg5Ef{7qg zeKW_ke}!^FWqkDiaj`PKTF|2d*-Xk8mYI|@ro*8Du=5PaKlnzwci+`~@4h>yj=Qvy z(SSex2ev!XQDm^smsTbk@P*G89ZXi$?MvBJ?a~!B?f!BW0k<`ScY-$yqH~}Ea99t>D{9r=!XFIA0T@hUeY_f`cp+fVt{ z-Tn-v`3H!19I8mi&#v!iL0-7lJoKT|7f@MT4gm4DJ1Mwf^RU^?>Kl8&TCK$6hlmas zgtUj3^8PkdAu~cl#r%4Jry=!B?qmMc?c{&lnRA^){y6y0TAI7^_TSY6@a(3 zreC+m%Hvr$!3$fn{`_5VueOntlhZ~37LeNWa^wt@1 zl}~bT-SF7?=;H($Xy|_LX(`7%-7i#kc}$wyuN)KFLUj_LHhM?QV5}=h+j7{N0~My2Cz-# zRi`f=PhetHGO!-_U=>#e>BaK=3xv(J26s2Ts!OLg93eDSnQ#sKs~XSe8i=XuqQqvs z)q+t$#OIf9*?e;0pI8h`*4DTdXpS^!w4Rebvf2is1$vB&0L$FhX{hTQ)b-Mo;E>0P zyI!C3FQJpuMn|t#9gSI)qE!X^&!bNWfBQpBHcx2Hy`3uYoiaGY%Bf21o>4N2-REbt z-KL0aKkHLC;$6Klb-e8Ix}u(;NxqykJaBCN_+`oW#>eElN`$#LN?{fZxAOCJ6o1)+ zMXcYr`9-{;e)8LCPAV`3{r9P)-|lAolGU`o-)u_w;PTFgxu-EoLvd6W?pjSzR+d0y z^IlHJPyqt1`CF0Z*4%Si50_zhh=uU$S>#b)*YG`jYT4Bl=gIe+DK2AI_qG@Pp41R#JmVm=8}G>!d{}iIH1jxSeto}9Z00o+ zWqV2f#LUAzceIUlNh|xH*S~=-ywtL}y5-p~{(2p;!08W^ndzQGU18^;FZdST3YEDU z9`_yXBXI`yKNidjF*Wy7MJUggiyREjsEaMK2VB%j-$uqY^L(VGv~E7EX)NMz6`oaz zp&>0uL(R;)Hgq|kH@$G`CskN!s=cyt;TxQky`Pe0eh~2KtysQF?a)m4SZ-+9ahR)h zek5@{f4bsXEB(>9IpE~{nX3p5b=9+%N%TwWOVywMzT(Vsmb$%Ch}L-m3*FXT7Whn7 ziOAPWuz4#lXQ0|T&TXuL9<`iB{(?P7Nyt2G zNF{rmMt{Cxb8FrFS(&}!oJ zaxaQW_;@Sd34Ks4v-b(YQ3rS9#3aEC%j{WGlaBp0=m@NP2Sq~?B0`#~Ycl!NZ$ZY^~Ezx(Tc6Hb{YD4k&7ZqK0NN%Ir@Q zLA$!znq$t|n)(HU`2#E6hLHdC5bnhn;1$6>B{`UAJSTAP9BdF`e~M#hZe^t0H<)av z#Qd_V#)Pk`My7Y*chC#{dDjWgyr;Si8dBVgVS?O?6vt)u<$~Ns8Vk|xU4zL94b0_L zHQRZ|ulUOBC2XW7z`GPDsoUTIUPgKytxZG{FS~>C9ix@q256)Bzc$2-Z%9B-;FPjl zC!Dj{|JM|}gP>{id#fRp#khw52EMWjZC6-luO850)FmZ-%VlDjXaKMtblvgAv4uPX z4%Ltpd(AC4s?wDPEezA9yT7cPn=eR?n_DUU+UjuqkcrNIAzSUu;k}UKs0DwgS=%mS zq=*GNrWq|mC~PA)myrQCm6KR3`n^inx%=C+>d*>Rq2w_Wn_iM;o?(53#(XNZy=Gxc zyycXCH0~A?Zevj@qw;!eGKJ(J-_^J@!w4rjrv9U6<@lRm( z&b#{@C!Bo($ZU7u2Z!L3y$M*x#Y#i-wSAOFA9mlO$g+3aUT>!@%K6igC2;{Et}A4N zyWe9VvQCnW=*`NACKYezBivuOMB_w6!hic!4EA{jHfgV0ULW z3Fx^Z6f4xm{&f@pBFM^pxH^y`ADx3&1ds|N;p(eOcRi?>ev&(g>JU(N42-!ycr*eb zP9E-P^8K%w>yh+nt}}_X4Z!cwJr1%=DkWcg0O9^+vsC>#`_7_k-ap5r3gEqZf$J84 z5bW*6^-U-MN=lG^n!m{WY?3);#{jmsM`8CJ>(bhjfb10DVeHk~b|l%M6&^uuVFK58 z*$9!+3vA^*+7Di~D4l71YSdQd^6mGE-&vYXM?`tj0*|TKMnV<3+ux0k?>)n17CrxL zToOC+tniaQnl>!u75&BFK%BBbvWyVxegz7S5S}Q_56`|p|{5s|m&<~Q(4^q$%($Ehw3g)uV z4-n`FIp_y@=m!Pp2SvjobfpJnd>z^*XwZ(QonzyUrx~?lqo|;1WZd|w-uM#A%+oM> zSXW}dW5^f~P%`}q3{jR&1OLDS+mD(_4Vp#{bAao>(}duiMPt@O@f6K9;O0@y5Z8(< zRvncB^m*^@s~qOY7c!g)bc2&|LTjL-zx`P82v*L+VYo#i1)do^5ip^nSlGgjN41iYaqxQv3mtqtd^A z!v@DD3x~5G+4xR;fO*1DnLAP`T)debnl?>>nl|;~CQTMsxG%G&&6>_e#PdD0aln|C z^`Lr&9jAj)Xvwi{h7NNua~{I%cRs->z~H%b-8N-&$XV?6%c6M5vLz3!YW`~ZoF<;p zA(BkOv+%i$w`N1~xBnS$F4ah7RzD)lElG2a!yTDjTZNMBnU=|!tU@jP z$1kc-uFf8sd(g5D&*l<8*CdsrH)S2xC1o8S^gKodzK;FUhr56vtgPebAd9E?jG3_& zi4<7fr$R82Huf@wp&C`PUUD*^!&a>rk5b*{N|+Nc_@5rx|J&o;j2YI~F$)7Y?;8WS zOkNFY12hRT(5xpjfQW3_#-o-}75zF7Wm=$i7`{4DfKTfFd{g^8K<)6AO8~GGw7Faj zkU&K=pLqLqdaQs44{lR$AJ=MfFJ3#jv|p%|oL4wvROUOq){ZHAsuwM5n$yF~9GzJttjK1se9+MJ`o!}?afa{myS@yGmgWQ=pQU2avl7;Gzr=pY$-L#@c8JV#}{aI#dgg`$-*#gwdTg5kB*4-$BAwVs?=4a<1KUgpw=0gvjvn%1vM{IV4%dV; zZ5rK2K5gp4p=~&jJz9Ewtu@Tx5lpXCzc-^1d~fF}vZu`o_chpM|5Lrl->z?#$fgB1+!rx;N0Bvib+pBm?Ym z9+lQ$8M*KQZVLb(Y0|kB~sIUoC*bFLc0Ts6Fn&f#ro^Hb_kAFAO zUB^~X8Jt~dmzG%{(4Bd=YCTZ9V{K4i;GY9TfgjgjqNAtI;rH4=#LpAQxZvGOt}Dg~}hkwdbtj?B7V6(Xut-wLXV5n6chII}t zdiv%Q$`=I;pkar9{zAx$lu=4{?MLNTTqtqG7(F#st$`fwdFgNizqnLRLagGKa1L3( ze}^iruBsqCPZCktWxOkW+3eesT=EMICf?l;HbCSSUVp^G^%)D7piBMbvitxTRlfj1}Wl`I+)6j6BW}Z^aOAH9pFG zK^S+@X&Mzv(-EFOvBJQt$g%&_DVF$@NIaqjZDfNIZ;Nga8~5S&{3CU)j~K@C2;=Vg zN2k`98XNYXHuoi5Nk3v7sUv85<{vE*G3mL8cAEP}r-M=g4d~Dd}E2UaKd52$7-n zX_q32mZY3|e~+IyYTi#=HGO{*Q7Zk;Iq7nHV$7LFMtJr>OCxx1Y(K{%;J6_KIhR^S znFZPw^dy@~5Hnbn1JIW#e*ij%i3bS80PyK_L+(y@Su!sAq4bteld5O+qx2+RG#Sh% z(tp#>?-~jdO{nSKdGckIA2q>0pZ9)s8C?0pIQTtxBPjrbDXDS8ko$g7?9B=Rq4t4; zuVxwKwSd^GifwzC`)BZziA&7U8S1F*(73eK5$1ZMXSY?*mohlK(yHm$?uP$$mVmM} zrBghB@9TIIzii>JIcb{S?LdCEzAo=2y55Zfeu9WDO8a=`9Y-Cez^CMkzXe*Cn_H{5vR zp|3VVsH(f+8WxcN8Dkom+QWP(%#i1-kCW#tf`jL56K{Mtta-|(7L->?rv7V&w?zk}RI4qD5GIcXh9tC~fjp3dT_rJJq!GI>rJnmpHn zH-2PbOYxbfo?fJO%9N{SYDBDdYNf1ds(q+f#X;TnK544m(?;jZ&ei3eW?@)Ia>$TYZfK%^YyK##=^5Dv>8@}qLdbcpAd&_o; zf4S&ei2zh2*dBIw+pe$&1cDo`T%(Q&LvX~s1$x{%^yRV-ix%V{l5$eOz4m?gXS1^f zMmBGj7foe1TTFI}^RoMWDTc-~nfz4>6)97{)|<40pQ&)Om?tIbJm#rYa2YYL7p^5t zRuj56Z_D)S8T;IJ6&@sF#zTflTux2QOG8{vL(EG{Tuw{OOGjKzN6br4T%PrJx2=La zm{Q(|aZDFixUol5QMJNAsbirr7EIt9+j%PaHt_xH7$>vjss@I#w}G>K)HP3s;IhZa zEj;Zt$qLf7#QAR{%lg=gMQ}k&CDpP?(lq3A>8HPSKKnGr@m_rSKT2Cpgw2~qY)DXS zmr@HKg)fl@_#BIb|11>t`Vey|D&&grnW@|YI+4r0k4@?_mLw&6&ZKWIA$)XRYnd}w zc^tEOS31m3Oz!v?Cm*Aa#h)hJu2L#;UUV3u;9w{q<1o50!esaO4N#ueDtV-OgeFmS zp>?*r@AX~n4=G{8mnfzgDe8!<&rT$+6H8m8&3;`SrJMG?f{g9+Im#KsUXoMKM)|ai zDq8g%>c#ciEIiXnMxR%#cz=Y{duW#}6e#;$7L2Itcm?Em;;B47#=EW0SVA4Lx#FBb zDr6Xyhg;dVur7M8L+pbE{svM4bV1clBwzjBTKq9$=nIg1(%by{c4tUH}O^fpo(5_swN>`nSKNUyY=;P!200X9vubrKZ6y z2O^ZYbP;DZtY6jn`UeO{g9P@qd|ox+T57fX-}ld-kCnxpSIxP&s%m@rcqe%KKzDb} z5vztnxzBrkiIcL#pz5i<@~!q;t^oMGNw)k2{@9xXv;4_NK`vVkl!_Vyq`opZmVGZI zj>B;*{y6uOD$Wd1@t!6Lp=w`^JV>cG(0$|4ysOKS_ED@!uZNKLhB&%j)kg9YOeN%F z5qAP6LHBm(r-z>|Iw;OPIv&l=#ugTzV)SJte-t-EbA)h=av2k(x2t3#w{&k$hGY8O ztYsNT1H4OUh$#JYPD(Wvv6hX_x@;-vrv+Gkx-O%3pvS|gh(C0)7?JI{jGr3rT%v9L zxAZ=3Qxk8Bc|dx5aO!>-ik2_OoaimVgP@0D_?a*7(Gwyr!|oY=x#3NMpW!V%FSl}2+cukoYx*rtaW1KwFTkh1`)mr!C;TY#xc#F!E`i# zdh*5~gip&LWVN&gdsN3Dq_DW`S43&quX~VN>ddSigQO19%63t#W~*r4gK8ZILu2eS zoWQe{Sl6keHpXo%w;s_S_9XfVZ^c<$NbJs4zILjY4N&x#4a}#=XTDb-$)!Fgw>w|7 zn%T+#NhwvthZZDekl63w{Z?fIoJ;ZIQ6jgR=yvC{f_CTEm@`}Scx3}-@1@VvI@2Cp zf<$grLBa=h5}=JT=*3+Efn$&`nrx5|lL%6RL4sF`K|Oj}~CWi#WeW$Q>G z{+TS|llCXgzvfR`Yu2AMX#;EZOs*=nQ?^<*O)~w2$tyLq+ixO1dO;!*?BsHeV1Jn* z@-^7ZmdJ;htz~kRPowydwFdEcpcH7tdws5YgO8fZVL~Qqc7Xhp$IjMc#*DpmHwuEs?ODbT3Gtp zi|^mks)LEMJGVM-1d>dhTDVi%JGb`**JhZtcHEXUOrdTZ{SQLa@Dt~_50zc;h z49)r_Zo$j@=6ag%3yBS)`NzQ91=HH$Y5#ik2N?X7LThVjfM)` zrcZ#A?~q>~ZvnMZz)DyN@2+2!50$?U*So)N)TlD3SyR2qMygjAXm7ylA1YBBz_C{b z_%`DCorYT$ccxO>)>+!*Z-M3XU?FX3iin%|PDg>6&@o@%!oQ?D4=-A0vL`D$-UYJ; z$XbV&$U6k`?kTO*p_o6=Vwvl_RDA|zMs{eF-tDfAOgQ=Oeha*~`*p_~of_R0hKQq!{EJvb<@BY&;!Uec$R z?%Wg|{=SPrc^d;Nv?{>BHDpgSaJk#%V2v1Ac#m@SZ@84|+x<>`nAArg(O&h`*kG|iRfxg2Ak59q7nVh{r> zeK!eob{Y@%0E^j8z??}LC}0fNPk5bzpft- ziXo0mAdXAnyu|^A^$ZhIS(+!jlM(2*|h3Z$g-4@V3v-YmX1E` z7GNMSu$fm%ETACk1}Nb83XC1V^-=){gADgWKMufj#X=OKflUXwDT2v&@PwH34&W z$GWjnxF+A0sP=ldM&$H%L?&un&1}w16Ry)*E1RpbN~7Z|+NE`0AjSB_xC8u0y3YoZ zkGN*x;1iUa9YAMnwgip^#uNh2~I7i z!@kfsbxtG3c=hDx2q~j$UjV#6qal#p>xDR{m(Nk`lE=!SYh&wh4WmOnjl>m$xa8>L zcEtW`l>-(ZtJxwt<7Jb_IYpkDPy_pgsfb;dGh?Rjzs>)SBUK+^ z{l*>MyVUhRsh3!wUDjstl%o2Y={s9mC4VbL=DR9k*Y~98b4}sP;-hRKSFRRyvRFh$ zd@H;IPrUWj&LKX=)12Q4*Mu3FZNTY=E0pHo5t2TB{vK{(eG0GrvjW(rwgDmNe!%GC zBcvHUTKH^|Kn>)%FV$v;NPuB}lz>$aoa!~j0mSlM9vFcgI_qdS)VL6~_tH%SW`~ya_P)x{u!xKd4x=qVt6vBHmCp(+3HDZ-yVp62kTayMZs=5*(>EBodR{refo{q6vC0 z*VHPw6KzS-eb4r3lAEO))WFhYU#56R8L1|c zU&UE^m1s3sm7LA!*!~+!Eh)|~>xD*XS)wwtdquJsdjb~pjsvd<_IG9%iyPCA7%#m= z<(oZ}xr=v$8?-CQgUKM(;ZuI5`1P6_Hk^!?=?jg=@|*(lTK8B^@pp~$rB}cit%rF% zy2n}^ytpx-UXHfzmQiGXOp^0 zG(X$3s8SL)H&Tbs#*vRtfbLa!QDyp$gkPA~Fa{gFP6xT$#pn&@Y@~knj9~3we^)~M zQD*FCnGpt`6u#H|CA+HnD@>&iR#*GoO9S*99DQaaBv z(`nG|iSS2Lx$rd&tkxyMiSNkpN2y5gN87pZN2Nfc<9E+8Wk8MxV!9wTkl<^|zh5^{ ze7`=guw7g+`H$wF5q~rUgkM^%H>tc|Z_=|`zl|R3s{KTK$ALeJ1v-iVog9KrB)ISg zlHHdds!zkOK4o1U{dgIE-*=PsVK^gp6LPj%z+nu05#F6UKNsRLShz9UKZbC^6=qn} z8hRiLkQpNCFK0l-2HC#BPyURGdiZudT*EVseje`rRcjH@%0JG6Fb52Vru!QyO*eLM zvXwyZmA}{Bowol24t}0^FYZV>Njn`EWTXS9q#oN|he?I_nTbpH5MAzjmWZ3L&DJBc z(e);WrH9rA9#{o_Qy+UTYaoj=c@;5p9QJ#Z*|K_@oXc(yB{k}f;b*AJal`%F#^J64 zubI;Cv&wizG>Um$VNb&-o~i4aB|bSR(LEpzOQZLcwM!63$s@AqY1jt#pmPP4wCYy3{2Z?aDVDPhBc~N!`IrL|TcK(=R z{G?)|Oj*UrB*?{7!{woAt_$T2%Wp6);Z3=7xx#YDD_eWDuNf(5f-1EQ0RynlpQj!m^mp)jFdtj)mhsjiN~f|dzrxd=e7TKG@lHWA*z01mErmk`l)LrA!ZrhU(#JJ_?B-l!Lq)3FW_GS><{PDT^ zC*FQBv|ff)O#KjlQgZ?jkO2W~##QJCGm_L$5F>?tAnTfJJ^%7Y0ryCu9P^1{X;P=4 zJ7ds)8Q8hPeGRb0yJY^YaKBudQ|_&3nv>h*jp#zZ_Co&;*6WUfYdDML`SqZ0Ow9vC z(H;*NrOYh{Zo%}%pYezEH3NYA#LK29QgLvwk)TAgg9}mmEF(b|yl1UfjVi`|AIs`q z6sLMK4CVQjSNd$}LXzEI%d7}ZpirHb&7{2W1lhf+*T^-$dDMSN1~{deE^yG}a8;zLV-#jEj$uIo9CA(?4>!M{=nhL5xIkthhm(L~*>hK>|c^B1CZ#M3FLo z!=D6;nG-h$cd{V**%194h<+|aKM$h+g}X(NMLz^HnGv&qaTv>9F>u3_Nd?Uy;sI(- zcTO4<@Tv+8BQ6V<2a00kC%%p7RNA6PXO4s8mi-4L?9khya(3wNV&L8ZQT%DEUKuxV zQ9IuPE7g2^w2DsD>8sp6)TyevM#V9Zf7NKct8ot@B8+KVZs5e{Vv*jR7Wy_&sUj372 zwI4X<2%ZegE|9m5N3P!n&mbyXeL!8DIO=0qv&B29{if$$ zr8Iz$DRMT#`tr8Xue8;xENalNFIXoUu;D#hsb<*U4qBelYYt#;d!EGNQ0gbQ;2GFb zhb)Ymu%kyDM8{l}N)fh}w6{BiG}~Qz+gzXHOvz{#A5aU`Otqz2PKzl&)xExaoV@Z` zsOa7)P$9r?>VjnYlY^jA!1WN>_8MEMp+X$y%G0;gd0sIH1DUy#AcF zXPa5UoCW{C#K$x|yZGo;khKT8!cuV5s0BmWZXnhO^(6oN(gjU~+y(137AD!#Puue@ zy500e7i{#e{GSl}gz7SUQo{|>gt4NFhv_nX(reXYk}NUhChink(SoRycui?i4@ez+ z#A0+MQbsP7`zj;97Vh?|&|FR$U9RlUD1m>Tm4Rqoa;sM})_-_ZVDv)|*q1~ZNn?)r zVzVeK3Tz^{l#xez5(m3FNx6ZdMKqT}rW-|BIC~PVw93d2#h^S>U+c(!T7+@3SwtT) zs0C?d3YOl}IV8V{B@&PGTtMS9O?JeoXyt#5@EY`>X!RPj8HX%9Ml_(is$_NY@6*SB)Jw`qzdv{dNj#L^zMtGo-LZDEMhtt6xVcvJM{10aOLPu2 zF-b{E=zaK>;QCS&7@x10p<-qFnJlp}`=%P%G5U=!lOnJf_G(B>IeNJa%F5^R+^L>> z+{87bAX)U~#TUTB@$30JBf{n%IJlQ+EQKnOqt!qHABL!0`;$4edqH*bE2}Hl74<5-E33!e z4jA?!0jXHlS5`Y;j)p#@&8G|y&8MX9elY9J@N)Z+5J2U%GK{8iWp&4WH017H!?!uo zLbU^Gle|ceKKoNu@m^WkHqA&X>?6lU`cpW)Hm>uT?gei_hoY0K%6_1w#_0Hq`IKtT zG``$hLD``X&PitrNq?qJCg6&$wXR(1mhV^q%RIDya3>?%OcC+~QtLmj8|T>mydItT zXO_PQukEoOzSO$^{$BL}fQ9G+<7QWYy<0}hEI`-#Yz83s@REad1{v<31G9tAo-LR; zH|>fj^2_)RXQ$;b<|MtP-#NGK$5b-4HJl8Hp2jxXYZ}QutWLc?^frC}xfaI#WZ2_b zQVcQI8?jP^syFA~1{@Q$AVOQUAXF_GV#?b<4KA0mK$6{nS=2N z_csj^64D(a4T6MH3kXPvbeABKA|)LQ2)KZBD&5^BAxndFgLEU^y}S3hpYQMaV_tLT z%sKCK?%lm}r_LECDC)2yS@>-E(cTW(1=mgE%&ViwN)2%0i}}}U*-vR(F!RMfDGXG} zZQ8K=eTnT{x2ILeeA^4Q6l1z{N*Q|%FK-PzCWY2A{zRi0`?=0;Z2b+iIVZkXfexB& z%a^Q9f60Drq(?ucd4mXPW!l_mmgYxI`!hO!I0Ia>@?z=mZ}LG0*-^g7z*#iV{O|Hj zNpcUV+qZrf|F5CDzuG&yLu?I0-kS;^p;0P+eq}&&G>)y>%6mg!9z((#yJQ0oFL&Us zei+uG;`_O5!@GuFS zPnk841LztMK>IY1Nwbwng|b8S`(BVYsXJemAyo=?f`$WhN;P@RiIm$6iS7dOX&2n{ z$$hF@Y4wW0eJ#y^o=z#L^R1I|%G}%1raiY3R5d*R{!Mbpg3ob7t4)Iu39{9zvT~yv zx1{B{p?*b0ek~6RL%xmdty<-U&p+Cwma-WZ6@umq#uAk&ISiJ>om%EsCNp-dL4Gi) zpum4!n2$&?NGHN9nc3u+pBm9%Yn>ahAjPNx1-Ln!Q&o#tG$-)6Vkw+>QIThORP}Ru zDh)|mBQqD;`eSYs!knonY&|!H*LnnxTjn*1QdGz%99%x5^7N)0V;aIQ`b%lGtoet-E9mDVgagD!ZwrVP-2IH z0(=K0&sd|(F4{?kd{H?IB5;&W1dh_fzTlRKXdAq437$3yzpbvT4*MQ!@Y*akmROwF zLbH0Ih}A*avwGokTtmKufn(Hw(Ii8;M;4e-l;dQ4Z^-8{AQGqhkHtuc?Th4We1iOJ zydb{ApQmvRR=>K8YRf>@r?tt`;pOaUB$m5R`5rMOHdry-L+3S>Rpo3x^4+xz7@enf z8M!tL7`c{#`L`~k^GAyR2FlrOILq00xy#w-l*`%O{>r|!V7hkp7}-Uw7hEJ6fqe#TtF zYeG;lQL`I>UkTvQa{T#+!NuSnutC3?d7tGD+-Mb56y=X%e=FdRIf`!?U1W71a#ISc zXV==A&#+H){9)hbo}&a`}#<8>(#>i34h zphAYhYs-A!(VR~M*D*lJ{Q#AipS)2V(n+SAzfqFT8iX~tdtRs?tic^VlZujx%Y3BL^33vUuAyotO*-jE$UXoIO6Or~JsBRlZ@ z)^i^a4yK~P`^??Zn%6b$StYgYDY~;aCS=qS)CUhVAY(iC0XPfQ@worK9psmmwe(+A z0h8WFv!(1t^9C)*LdkvbFa_prVEz@v{f~X{KA>vw9>tJ!<&Vzx#bEY^Kxg)bxTIju z_pnE}iGZP5>Eyw|X7DjB zVcE}k^$z$eA^1*l@V8FI*A5NI(~n+S9*0Jb*^_huMS{8|UVkvhx^Ul4-byI_>5d;- zsNKc4x%e5eu0J#t<#lnAUZBE?=dz5~}k?p8;OyNKIg za1*8OVO$!Cqf-EdS!Jx`bLy2Q#F0E9F~Qium}M85Y+J$Ipy}Ls$=EL}FNQ6@Vf}jk-Ai*< z8kh2u&SB-6_ZiPXB&I-D6#IZC!BA`CF7(1xi^FEeO6@EN^vJe+%f1io0X=#BO8_ai zEp>1y4q^Vkb;m5*8rM0q&oMmd3y9jsvg4Wx&!{VcRYIt+T#C~itjGpS4>XqDi{il9 zXp7HW9D3R#oqxOCI5fQIE-fCrJ;)Pek=37asaZcX3_m8sue{HLr5c&OT-T61l6i3x zzCyk|cLBt^fSZs)rqDxO6>KYd-Gp_{uESt3MxLAF+An|?f@uL^n6us z-@4rHvm*3Bo0Fguo0jk;`}E`XGIB^|2RbBCd@FpDc4SCBB-;7X1M=M>22XIt^M{m3 zZsDV$@etXd8Rt?en}D)!r!Esu6`ZFlOgU~<3OF$uDAa!P+*`&KW2l|uM=e44jNBc! ze(w*$_fY;f;=Hb&5jI<;zIQVpW*RuF=j14pI$mGE+vn@C@xd(|Ebn!wACp_<&>}3iG}?Ww5O091(IOhaXs#8Lb>Y43u+jY!7#mGSh5@w zV>vb{Ul{$PYT5;QjO5p8uRDBf8;v&9m~9pEZ#v_>8t)Z+qA3;d^c2LIcq6Y+UU63_ zO5L=IS?e+pNOt_WxOvj2V)p%(<16UrZFxYq92K*R0z%$8REjLdSM32R4#2wM3%DHCw)``q09098Sy<68DO5vh(~uB#;J z)+u@i@3zJ=!x3xV?~-TaPPn^B^n70WbG%NpS>UaL)tn38h{RDmZXsv+!eeGNUaMF= zrP*FiI)9|RW%k?De2B{h3_!1Etr*S6F(P&WFH9xq2L?F#P-7{ZaGQy1!^#W2fqY#1 zcMU1?yDSh6@wsxXm4|z$E0tX+?6x6hbE76;A^r2mzWe4#Z7-|_9pYcNxjPhGV@szH zs;m~~ohmgpIjI{qIL0Q%w?<)i&_~Bio8gyHcv*6IxEXq2FZ<6(D&C4ceH8p(LNey3pV|uEeO!VGPH(ULEi5`+6D^N1U7ezfOKqZdr}`n=R7-8?W76 z9ruA0cP!9rdsz--W;u+EDpjQ~?=2t%nxL5?Gx0|PM@|tUz4mwTvRY)t-rv8{r2rWN zh4vsyU<9}+08R{n3zp70l#RiY2*C92%=%#pTVOHLQa0c(Uq#Z-z1b^<4+8GdYb%Z7 zyt_9mOY+M#2^0Hg(>o?Nbq{GR#NwZ3{O6m0JRZ6hQHs`}qs_>r)b|^iUdwk)>wS1) zQiLAV&RJ`C2% zpp=q6I87wj;hfZH$US7H{S>cMyD6H#IP+MAKAX`qewMe#`=swjrt*>V<)JMJtxtY4 zfhV}<$E`khy!uvhP4NcUSUp2Ypo?v+y&yaO8vnbSR&{5Ee>6Lfa1|g ziX>V!yVm`C_591uLZ!X0rg4+Fi@A8t)m1}Ui1%)&hUkAxq-(Z7O5S+f6XER&t`pZH z^qF7hwar?_=9lWotc0YvuL_iY*??*ZfVXReC-1VI0ThTr$PlBk&ac~`zEq&1-a z<&#xnc8u_@m0|5v{Tcjx5qPW`#^)crS`U3&FIxtfH5>_S&%0WG9DQ4LfBmg7np+bP z5e1jIL1(RkUh$=n1V_5G$`|izMm@y>4n%gLTUW&SJQNR`6jXkHjLa7@GgN*8{arr_ zrbo(Y$Oe;MVDdN+w`G=o+Rv!W70t<~I~f12k}lj(WXs*H-hnTdRHJK@5YAP^Dy8o zeci`)uiY@YkB4kD8^0b>xHsK!A}p;lEoml|)1SW&iC?H_Or4c}r(AWB@Umvb*=6IT zA@O_Ghu&f0G0xeWpAiXWQl)SCC3&p*d}V)F0y@K)sN#6GBTDufjdbz*e9K64?i~-P z>7S|H@H;hqo~aPhje5wjW+9Ku#`hTQq#wRN!|NJ3xILw)@`lYMWZQf-e+libv~Mg8 zLnzD`JEf*67BEdsNvee}?k+a5{piJH@Wi3g`h<8=Q0e9GK1UQBrOo~-G9RXGO$F=_HTd&?n2g3NmU=@leyn6aOwsD`W7#RXGIi7fP3_j&yvCu>*A6}#A9eb9WndK+g5o$Oy24NMs;V8pz4009t|Kz2-ev-Doxtxqc3Er;uvOsIx(_eCSv%U&;|KF*H7Cq0`uR$^PTxn7T3no_ z1yeEksxY&FsQ`cINnt&Lo+vIvIAzIg_?p{H z{+Mo>w7wL=WM?bV?+yf-{2hjSI@LxvmU#M<9 zGyt`w0xm(_K^o;z#9#XWR2MK~x@$2|Gjvd)O}`9itlshKNa|HZJE-&0qwkL0y!^7x z9JrhLRpu2=AZ_Kv^QZS==7-UW=})FU^RNcs%Ojq`ZtZMk0@b%(k?0T*eu0*MOW?oh zK0|vSh~D9RM2zTIS z#^AJ7o|+~t@DK}>H6MF99EuYg-yf`0sKp?6J~iGXNR2>#{QFenv74F9gBn}fU(xmb z{Of?NF5o=+maZRkjFtMgcPrXdF1$Kz_t|f8$8Gq?-f&|vgJOhqm7}K#xcOxB&G&Q6 ziI-1cT|zH)%v)3Y&U_hJub(jAZpOD%=(QO)QEwEzi?7c8eCU}@>DD4|`AE*vm^Yea zdz?aGtN5gF8rA>_c|;iOmXGu-jdi0*p~vYvY?YQ2{))xVSH2Ac=0!nrroy~2Bh$mP z!3gIL&>h@_lmCT!7_~UcwL2CPAFdj>0!4y~k-#|>l4@%Ym}4TXFEVz%kxS_+^*|wR zlU~M13Nh}+O^tAto4M2;tYZ5V_|E=fJ$Vx$;PH~^{54)hh1-7$UmP>&Em387lY{s44{n*?y7bj%(>C@l57g;}VDWG*=FTW8Yl zSnF_xtLWMydD`8_-$~YgfdN!SLV3~^C+QTa?MNKkdp)HU7yd z$b{nRTPEl0`|YDbuTxbgWb0-sXm{diSzFO0%#FdY1O_Au3Yn`U*G6#c@Ak zGb=L}76ge*#wLa(3&cd0JgMuM{w)x>hs258b4UrNIU=;|`zpXSyMLu{u+^WWY!0ZW zOWL-?yXzFa-4izYvx10>*FK z(5NMczi_2+*>QT&YuJ4q;pzWUG2M}u5k=NJk*3WYf;h7cTu!5rVT zp&h-!bA-W;l<*vtvz{y@kSvgsE+&yIP?9dDkStJ>F4B-L(vmLHkuK7cE;5iVGLkMb zkv^cYUSrDrBY@2_;pM0^Tf&(`+Jj)ouO#<)tk+NE{_()(%}Hv;zzW%(_OCD`F6jda z>ouF)KULU#7AV1D31z?*+FeEYQ12a*`IPs$# z0kg^1ZCyyxH#3~nx_P!=j?(FA!hOZjS{2=+_9{q|eKcYC+Q#Cto#+B5zL%4Z@eM$w!~huFBCVQ5I8KSa*53C{&H;@M=OOr#r{TP54da09RT=GV57_~0o1NP&9 z{i}MWxo_8yg4&fl!iN}PYL)MRBG{cTLHZu|eEKKu6e5mzf4CV@cffk}ZQis?T@YSo zqmS3sY(^S6hFecI8pBY4KO|0ojyx~u*mDoDKT=c?$^R%9_4=g5f<-l?`Y(aR+zK+) zgu_4Mi(A)A(hs=&FJcF)W62ohWOv~G4{Vg@@4H9_Nj zh5Fex_`fH6y00Wi(g!=9&U?6m^Xfm&pG~{3#3Gw3jQ?gikd9ac$4E$ZW1YP=r{-5e z(fOexKHRcP7PCJuHhTa3v-Gx8En*ykln!{Aq-zCC@g?vY?#+H;nul`T0jzm~0Q6%3 z=LqWe0_3j{*J|=68!85Ldll;gheqIgVGcWJ(?LOe9H5W(roKJ|F*2O@mYh#MHOw6A z)>+kPr!OrU+mpT0-@h0TA|aZBh4E+5pTMr_gMec%|D~YYB-)J% za@pmd+sGiVQHdDsAo9iwS7tJh)wiu@wLl6@@%7lT7+u4ogTZR3!@`%L;X&^h{9!=B z68R<_ptJrF|6$?~KDh|3W?xMfxGhBHv8k2=KUo`YouEGeH>dt``0?U9*>8tM#Pq5# zjf3T1>k!7fRlU)^Rg=0A_G~$2@fjCojy#D!MmnJ@$ttuhTR>;0#R zZWOnib6~(D)`zAYeZBjX+M`tsOVk*3$sH7B2^_nf1Mk>V=$tA??xzG&~}$1%K`*YY^xLKR!=4;9+hSKscksKJ8UKobUTx{A@mkG5CIY(EoYRiy~**rp~ABeo#?3`!_Q_ zs28>l1I~`SZi}9ew&AM{~w@SQaiRQ`n3Xj@-4fR^3agn`j?KLc+)nfH3gykL(59^ewXyaM1OEcSRMmD|_tW z7OL{OfXfkT^@FL~jm{o+VA9`Z;cG+r_`Q;OBBBS{32gd+D1&zE^_rz+_*uy|ry+qtj<%Y>;H88yDwgJ(|M{-i=oCghq{f8 z>DO@!1)RX}V784Gm*+d?jqdfofaGnyn_1`j!B5SIFp(mLd(>{qs@It8n}Y$$b;oDO zH_(J*6R*X^m6;~pq*GY**a2I*Z}+m@GQVq}SDL(pLY-oB9?E&nHc(=J%(U*8Enw>n z=V{GtVjwYDr6Glju?Bd`BTz6eBD{5R=8XpCkb@xDTnI@e0jn9E+%IXESJc0eb#a!B z2DzStP}m$HX%!u-nS^{>04PFwCeFIi@HFHg95$CoQt3cCWVY@Q`M^aeB_h;rbO%lC z(Nc^9NKH-VJzOZ^iQWC`3@WK5aD=sk;Z8jJJjH50Ls> zCin%w_=0$K#|#*f3dBdRmyK1WNPMVrxg0xnREXHhh3ZaG>RJGzLti4R4k3m4NQVm` zKnkc!o-9FJAb>04M;}qTd;pYU9x{sDGR*e_+tatoNEyzeFy&?B^cUdcc zjy}XlEw0jdB0leq$g=FDIWBHE-R(=b#pI~>#@OHGxJ~o^o@yANB&UbrQa?PyKU;p< z9C4!=To@>xxy$bsBlK&Kn3VWeqe=3~YoUtidBrm5sm0m1+aS`j*K8ZO93jLpFmp6g zI2LOFsobrQ!sQoscq}l@&07mKMs0yR{UWx2i*w+K&;`%YlpTu6bR#bLk8~u!qX5vb zi6D39?wz(NCZX$Q_f7@iz|k5oG@Lbo*8tSu$FK4xEYoJ!A~UJf^LXdeqqxZ&{Wk;O<~&oPU%H0o5u~ZkfGm)>nu=;a7+w-z{r& zeTp&f+eG7bQtqao&06gie%NVvshp%3)w4TBDMD8C3vy=gL`;ETK3mL z@3*bL4!Z-M0wZy|OFV;aj!i=f6#Zq`cbzPF#7EtqkvbNE4sIp8ul7e*!u`*JhPF)t z{CwgvvTH{iC7W2klQG}CX-xb_PoouNZ(Cg z-DVbA4Iw~0;Xl=sIn|cmDGOYyf(_fCcRHbWdZKqi(L2APcgCz^nB!nUaj>$&2oSXV zr;fim;G{ody0HVIy{HKY{++;vch@oC-w6DeNDxdUFVRT6&`5rv*=3xeGdp82pJFgO zV=|v&GJ~g@ono zbz&pH0=-Fxn=|?xSc6OD%*cGoxSXg%@qre$G3j?DF1EUH9qsejyA-1ls z{);JBr{kohkcT2?P(xwv5-+gYS$_p!zJyxqwTp_?;aH;|7XxLN01T${YZNFexCfGE zhCHzn8L8_{ecqpNE_aH(p_Mi%nqUx8i(|~3)7<5^C3L5IP?q`?jT2+OL3BmoPILu1 zdT%>^N%#6|#WBtp!2=-O318G_q*$LWFAtA@KIeY@ zu#4hIB2N48FB|9?ubETTf-gl*zGJXaMQTT@NX*gj5aBIw6TL*#2p4y1*^LLieVtOL zo!_Yn5&9x^Ue00_kj--Gp21T7Qn}RNvRL+&mV*XGFLl?4CxoP!3pA|$NFw{Bp6qtB zRg$Ako+#8tdCsDfSpKpn{qxJ09$K_@I}&z>*PaC~Zr7@tDHU<{wW2D%qYIAzlIk^n z#?}nRQnz%*CO+>UU;Gd#{8wNx7I82%DZR>{Q0OOWzTkNxY`34bSzvdds63T*AaEq6 z&UOS*=GqP+4f}TYr5lZZ^tr|pYwG-1I>tD$7lg?(f`siZf1Yo3*>#UsNedE6Alla_ zOr2=MoFv#8YEZ>rwkB`#^9YR#^4t&d{YJG)JpMhI6!b4tdXamP|09Sk-_UGtvS#E< z*rz6w*VpPXw65<8XS1#04qu;FhF{iLh~+DA ziSC7b<|ycueUs0=a-zZ^I?tQo^)c$ph(4SyCk+`qtMj+TuPp zJd7sBhR&LjNMyS4_EJF7Ve7{gNx}|q=&eow-KI81Pf7ZKH2K z`VphWIGz=I$Gwg29WrdgMXbbO)voF-lNxj$C=3FQ5GLZ53u!Q(LO^ymu;6EzExE=H z7N`mmafg#ZWO7o!jPt;Xkl|e}Vgr={zpV2>E>My5e^R`x&gk3d;__=)gP4pW8jRUs zBKeRl$=oU9Eh7QqqhK9WiMZ2Z8jNc^1(IvLE4U~SKM>TO0AjQ!Xfd8vf;WN$3$sdG z#Pgg5lJA?q+E9@2&keIOM>0@aY>j>pvz90k7kv2v4Mv$wd`Wq4v?!ia(rt9G1yN1J zoe{}HLT(Vb{HiC?l8NCx&%ctxF~(dw^>4Px4w z1nqJXXS-mxgU>buH0KL=|Npg3|5Hh<^`A;VrT;;&?x3qr{lb8Nb~k`#J_F4Jr3JYD zlU~XH5B4_Uf3TW^m^;c`#N%KZ0DqtA`TsM4=YPO;+rBOPKGVShxD#D>YDzUkv6ei` z&srF4cbD+}hR86ko$=~@1I@wc8nko3?+90p?-P`gUdSqqH=+6yK;_kHgCZbS_6`Ze z>>umaGf%0t&~o*QmKj9zi8sj^J=vc3<$|mfH60fxdaZZ@0>eYx)XN-V5CDA_SYH=A z0U~s`OfH}ey+D?$2Wqt??o53fKv4yhrUUR61tTtC?zik6FwP#S7>OzWTaA0%IRRfdUHEhd zUpCZHr;@f4IR!jg2o^L{P!OrcZKK_VC@OG0b>04Ikp9mI*rNvGb90wy|6+Qa96-)k z^ilc*yHN96`6B4r0c81Mvqh*%TjdYm9Ml?j$pdM#E1p;K*xkGA>FJWP|M)~fJU>%= z8S#pw!sJzw$bD=t+tVB%vxt}{I)Srqck+I$)6FGHKSUgN&ZETms#nBR6aJ;54dtd! zB~=NSOen=1Ky1?IHt)wY^^5?T8}SO@Q&Z|JbQY?1u5JwMDIu5a{~%6GYD}QKFIq*M z5ns}+RerK@osE?PypJn*aIv6wZ^{8HX_XSQXVOf!+yy?K&8URC=LJ49#=s+Kvl6r+ zKL-9&&$n068x!IGZc_IJSNnBh?jVIe-!Y%he2|p6Xm^o6_)g3@Q&$C8waJuZ`Xh^I zjfa!KlB8@2V+DZKo=tv2JPQ>W3cRkbvc9Wx_Wn;qjtdmw*rZZ9J3N5U zjA=yJm;--q!?Yyl?$uS!Pyq)J;4xw!eA1tgS;c%m6Auzm5K_&iaiH?f+-4t$;Aj+! z)%gdw72RdkibL|MuabBUbxZf-TgBu5P6aHsmw%oZf6!GHFz4U&st~KvDQOWeaGm*c zak2#{v(wy(xS_3OARwiKcf_Rl*Eak>6~i5rKsgiz8G3Nk=3UMC>HmH_vlsVzXIARg z`duXPT*0j4_+=3#QKP8cM4zK=r09W)5!M~CE$QWcWv_CKquJ8phn@{9crN9iU5+Lt z7p7^}?eSB}kNyTcV|YS0Q|ocY>h;(QsS9{Za75#8qWAJoA7~gzy`An1|2}jeK2;R` zz_UsV#*-IG!?PN*WQ|>Fk!OCz2b0k{&Y@xg zMJdO`7`)ZySGe`P<<^JlF5r*fZyOnrp$SA5 zF97Nk$3}prF#`DOTLPH+@2RE@=&tV+B+e~_<3^|dha-Wlh6rtR{SJON63beX=) z;5vBvS4+nD*E&W`Kgy~w;YdtJ)w#>boJz=?O39o;WG-cYbr9E()ZrcAti?D7skiKZ zuN2QK8~s=M0@bkp9U5I^dexV91vSb_r1goG_y?R0IENk}iWbU%$fKq>XJdr4-Y1F* zF2}2*gjZ$^iu-NXJG=ERk#h}(a{~RlR=B2%-VyYx%f~Gq2~_w;S&JInOwaHbE`8k@ zeEF*HwlleprfWKOvd+y)fY8mxz-;#d^lzRE!L*W^w3qw%#wm5)1)Zv%c&0JbUszhG zLdWO&_qxLNv3qlcGhD5HW^?1ug!YHwlVV!6D4GAr)-#(fsWK(c_5Ycu2zAl<*Ldt; zdc$BG%Ge*B*jN6_GIF`SS@`p)wK1aBMpeXcY{6&L=k*+qvW@D1L9QyHDeF`Ax06@b zup0IUqA7OZ{(Hq((}k+GG*8XdvP8S^)d)*ty*pAB?3Y*{h^ZScK;vVgp4{&-?&O7{ zr^|_HOes?5+gAGKgYku(r-oWO#oEckOm20=E_ouKAD)UX=e3%Vf&CIAaZBW4W1MPJ zaOQ+^}fVXa*rT<*;!f1{&{dPdO|FH zxI<}Ytg=$yfs)eo5DUv+**9`paj)ej8BV?iCB!aisK{xNC@CG!#KoGtXJhd$O$@OL ziVE5C^ zi#;?~k*iIM3dyfc49Sm)3bBHK*tS6-`EqaMYNKT2YG1A3zIn#tZTLp+Ts0_Us|bXr z1anc4Ed`4#{tbxJ?#RZ{+yKh|QIvDF<9hU`-|^|wqQv;v;rh5(dQDJ469{YqRuF=c z&YC>PZLOR`|nmjP3dK9EXpXf{MT}>|KV1RYW_p0l9609JnkRuUhbxd zW;_+MQyPP3qlWkfhH1^8NHL#G11U)Tk3iL61FEj^A6zf~;TOkWnTaGi(zx!lmY-|s8%<5qZ{fdBBbGm)W>QXi& z{s}nM0BkCc4xov|xi8lj01E&%Rd?P`o=i?5d3pw)LB$lql=Kl*>}RWcyU?b4kKW}h zK+3LWi+I=bqIA(s3@hMP>f?#L$AL6Dd44tSk(0eE`wu9EUdJpWgX=vaBw_tlB=hqTEJ ztVVXK3_L6E_glxMzSM>CG&}rrCJTHYcevzUjp_Lcbjus~;O*Q)TpuPL6?Z8TK8)3&@GeXKHU`7tq-Az9l} zv{NbEFfN>|?#=f^15<_VM^5o%X@Vh#BbLFPE6>?!AGSGr{?c)h%Lm{C@_yQ)8?62EK>`W#pvb^V4?w_N4W86-7$S%UdFK24OWZ zH3Vi1+-T=+CHkPQ*0Qr(3xX*7bii{Dz5@~jEk z^VRJ|1VN;I?`;|RdK_s|AAIK-hv}vh0Q+X8###Aew-^T~W8-;%j<6T~kH5Pfnonf^ai>;asaWfeANl@M{rcDH-%+=9leFM;R5)2l4p7bxCV?&9?`cm`0$ z>U0d`Xz`ft!e7>*^!rh!XN{Lnn69i%8c&|ZVQpUeP+=cc6Y7PmUchmBMQ*9qI~Rd6 z=P+e26PN>Sbsqu(ZEN(ZtwWg8=oSD@zl6Yl!PF1bi5(Ee(-mOc*y+1ZWvpkaQBRoq z*?#+~rM%%%#QT89=DV4VdRkkI@6ZDMu*ltEs;ynC;NnKBUva^5FuCf3-7?=f;=SrO zQb#_9DOzKM590G4xnJkT37e6)7-E1-RU#3gjoBO#5p?)1QnM)Fu(4w@(nQ2LSI_>Mx>kn9BTn~ia%544W{cNA3N>M~EwCNM%z3nr%>n-cLzt7NcLIu%7 zT!PHI=Fz%t(YmNvYF#iwYA{01FhT?|LtHRJYA{32Fhc~fLR_#yYOq4iutEf}LtL;! zYOq7jutNlKLR=KBJ`mw;65)M_9_V9e&We&^{tjWDM`NBxXP(Dkp2uuqsCy(+O(0WE zBvVa{s7{SS3(i0bu3#4OMaN2$werBkYsAF6#>5lF!t=nwYsA94#=;ZD#`6gE8~jwp zn>!=-5NuEyO`z$kw*j1q)Sevh-$5kff3Kwd7n|Dw+4SNLzHw<6j`Y~9D3flO<@w1w z>vU=UZhI1M}$XDC9{XtDkL(MfA= zP{VTWf!{;F!aK7|idBnY36p;GR&V&UKt+a-=P9a_(qXq)PC9#Cb+#kyNl!=BgR5-O z>P{;?wF6NAkLuz5s`P9uReZ0;GNr@(;afAoFHyzAha+V^?cLuvaL!2?^e*BuB)uCJ zO{DDyU-(Vm2!v0Vt3PS=Z;L!ZjdIrX)A8J;>PFtq+gP8u)J5Fg79hRV`rfzObP8Bj z)w2q+JbV52P+aNlKFzc1&-s)I62easpqFIYlCSR)G2!9<@RYL>04ewOVU0Db{T6@q zWIN9f*w{n_iYdBavgw_E#rZ_ZWOpVIP`z*Ha(V^8UZ)-O{_;xA`2X^7DD)eyYuLjlj(FO+9GUV1DBV(?3NFs%_dLHGZ8>EXS z3)tn7ad;ect0Nj9RSwgkbN+=}9Kbv*>wM=mVj6Sz&Tb4ftiHOzHJW-$193ZxvHZKx z2|>6P3HUx~H1?lyaf3GrP)ZL!1p_v=R?t6d+T)PK5KhJ_!%T=jcFBbgYq78fH*|e{ zUlaJgA@F@OY}hA`^anPZ5DyNPDh`$d4pt-%Rv8Y~5DwNsAM6h=J=?Q7*YByrlY*8) z4*ej35hVD4gmXd5!pV!TxbJ(c%mPUhyfGA2c8I9Ce!O88s_IbsBZ&!-++hIA-`?k2 znFYtid*g!)d?2BpSyr&B6Xd{UW4~bho;dvVxt5SX7f5)-_VnVJ%qge*jxNY&W07yQ z5()}ZgMuGHLeYPOFpxkG65K(;uEmzc3N9Wa9-bKNn(IWl8Q+(k4Zv}MPh(Ol8Qw#kChQ2h|gS&4^9|Icksi$Tc8DfL<_P* z3vxsYazP7nM+@@SU_lnl_a`h9X?6;<6n^`r7^WCf!s-n6^lhzD&#R1oFeCM|N77BU zc#%a~3c9ZVWtu%BK;QTr$h=Yb%7^K z&f-iOp1-M`@4N+1FSU$g-+dqu7uP+kbDY009leQ8FRQ9;rjApwgR-pO^f zqOamU-|7Ca?bp+-f2Xu6-e%+-tZyzc@}{YoRS|k7y?WHNX?eqNaL3s15h3URjy4wc zVJ>d75obdvcKcdF%P~WN^WX7(;g4v1q$eP1Z&j$fZ?5qu5|CtfTNfpM5xzFk+ZuSQ zH;IYr%hQSK62-Lzz(d&Dz}|jKO_hoi(Shno=@rYvi)ZSy8yKxulu9etu-G=o208nt z8GL01spDTqVQ&_ctu9|a^HQOBhxyT3=v1XN(*gHMR)ss{XH@QZ{=xJYFv)6WtgHuh&XuN$s&~ZTDgaiIF5!%h0QupN8TXjrjtaI zR`5eUYZrc81fGKeH!DIlpEXz(Bt+fpik;4;ge#U|QQh@qbmF0X3G6;ZysN!s&9Pw5pag!jjhYGjG7-l%y?XiQu&EYH91Y=UFjvZ>6@?^Cq`M(18R&?_9Iyo{I~ zb0)7SsYWZ5nea(anz(-ym)oG39}mq`n)r!N@SauYyr>H!6+a09@;v)G;ln{e+3}@# zi%B{3dsA^Wnzj7@*xo7S4CfE#+8({&DZYKv7b@?wk_-^^TfK?|bn!pV+^yoxA*#UM zK@M_JaCJWR7HaW0R&6NoZb#3I^6qOCdfh7p>qkZOcD1Dug+|>Un3@y`sqLaJN9s#; z&-AXYR%UIpNKG!oWgMEyTug2`)Iy5z64R~-u7wm1CVjfo>WW@n$1Ak_tCuYab0f`;xVJG)4@TO%4$e;n} z=HY_E^GG88l@hpdVPQNOSSD@%x>ZPC+xL9CtwbDnOhO{W0%{QN``I#fqX| zNG!lHdqgAfuFb!Gx!{&Z-gwtcu&+`Nda8=Stj;@)c_a{L2A7uWYTHcNXSiuEe3@ zhP_t2ZM-yJ(e5}LI<9{#+PoxZE#n#okPaO;Jjd{tP42SIkyHfG<}P z-y>S`hfrSrryim02T%?rIkSPp5PpL);}`%ZQNNst|afw^2hrbu_(<$XpadG34qXW>&j)+@wIQ8IKH0rH_M z2Ze8Yczk_Vlho@y=hTz+G_*OKgb{nT7hJE1E_~}0#HiSDCs88$t{kz=P$TxLL2gfG z%iDk(@ol6Sz)qp{>TOitKp4?OHU{_QlmBq4X-WM0(kD7dJ&n9GsrumFuitbKH}?+S zG`P$5gk|Dj7V2tVw{EPcbfhf?kt!k;#0RLBhy;-e(gRdg#F|tE`5`;~<)tVu$=^E` zcO!u`%;~?V|MXBfxag#~7^JwETDl825bDdYv9inzJ~!LXPXNNQ%YP7 zff5lQ8V{_VtP?m38hBqwMz*PQ=bYHp({&ByH66N|A7}l1} zv?{|wQuaD#Wy|BOu{>o-8H%w`{A0lum{}et!Tp`VN%-+plurNGydG8kt(o7k;fEU+ zx89ozPQEGofu`BJEDE-R%t%ff6j1${ zllN~w^zLjsoql0;!tM<8FLJue_g_!uZ1B52yNj7)!IEJ=T_1@wavqY4zB)PG@eFC} zybqco<67kFyyz%$_1JuiQK}}WV%P(x;34-~!DFKKorhVx!d$4nc}&?vuI(r(F^&$w zSGaNXCtWcIfLZyT8 zO{5#O)b6|{r|iWCMID}C8J!1&oIE!8-ommsT%}6ZKT2Mw$LE(_7dv5_R)Bm@ewn3Q z)X*y;4lZLE>+Vm1IK9YWuZW0wsfqcFOlJd37e58MAO-~@2E`)=<yqyBBR%^)%VszY{ zq*yr}EdXmx#0HC6OqBuZzgLW45*PV0!u8<`|1+dS$}r)K9?;`$^hD16+3CJY_{S--Y`2vLj@KhASi&KP zaerSllEY{19g?$g<$l|j2wKYvBia39#mJg1a@oZ-J}6WSu}p88Lsfb}LpdK%Qq}yW ztLz-7pJ53pO8I-m%Eof4#l{j2vpVpx3t{lk3S&6guvC5gML+suRync<;#=LCdF5Al zbF_L}?&Mc;dDAQB5bT9^>j~%4dtVv9UA&F+G&0#1yK-)*DaEyo_hBHo#Q#s3VAuNq zGYb{gOtlfkM4tdaW}5uIzirUC)HZC9feQ@o%;2?WrdGTm+!sD$CH1%hV5`78!i7 zg6V~_art%lVn^d^gIZlVaFRHR$%v` z0?zlMgASXB%l9sUUF#8ndMJkN3IkCeF!^g%=K4RaS?p4g$b2E_OZ+2_n1-PKG_iC@XOOZZstxsGWkyJ`cktsJpBAQ z;2GP=RGahbI|HrL`1$Pno*qe;VE@WNEXSoqk?WbWd$Ux|_f)HGxl~`T>5je;tbd~u zgqTtAyX6Xfxxi(W{1RM}4HFS${2G|X?aH0H)Fm|*nLQMfJh4~r7w)>W9<%3G^?A?r z?q+W=`{U?NrQYSknY`g38Rgc?`KDSzYhdi9Ta`YQPfPmyq?;m-psj@WS(ddaLlQVl zVL2O&%WL=^s-7+)M^{_mqtVK;Te>y-w!X!7l5?lnd1T94CFj?qQQFw*Lr5;leZQp~ z)oVdvHCXFIL$1#?s*?WtQWy*C)hnrUOsR8hsdHSZb9||DLaB3NnO**X4dHKNK5#w3 z*y%*@K}_&Lg787|@IiX;K{oI~pEvPIuehYnd5pBW#W2ouCf`^Xqk0v){qE+~c;G&h z*||fb(1bUR9EtOO{aSLjvgA2!F#poRI0^(fK%f%@PzpdG8U%P=OYDx9+^5OY@l+4I zvC4_@`Zi&Ph{e>u_g=|*SMg%CBYUwnX{;+d&0CU1{K?aN0HN^ucqM{_?EAEv1C!@5 zxf>gU+yCWQkZsXTU3lHqlrX(AoH)H=!B8Z>mjYk@ecDHvzR0E~VYGIH21!pxZp4+cKcrI-vVwK(}2$H`p)8>09(BT;L8|-~n9V z30&X>T;L5{-~(JB6fO`RJ`fo`5Dh*M13nN3K9E3zn#&pq$y3Ji6&fmA#K)qy_5tkh zf!y$c{P2Oo@PXp+fp0Xt0S5i+-K%MQGxR4H+v61FtOXuut|$!?=cxXTzm<0K18{!+ zDHZnlCoFXt!hOEvJSyNd`F(Ej;mo_k`CJYgH{o(j z581O>9j946zrw=Yt(vF){rvoD$>j3)XMv&jSr&+@{jL1>B4*u+W#he1T<7$2t``!- zaEguE9av#fn(QkwnkNyHrObj}REmuqAVbs{S7`9KM?Xh-@K%%k*e$_M!%v}%|0%05 z<1|Lj}b$e|2cB#;Tl4EUXPJ=~nOL3RD-&`>1;~5k! zU}i%RZZqQbINbVpVqmBI;Hnm)Ln>@0jN#)IuTo@GquM0^R^o(Fm414F@IoO~P_mcu zw=3g$Fwrwf(WW!WF6#3igBHdpsU`DNEKXfX0zwM(h+N-(-iATcsL)-lyfFJq+;6ja zux*@w5q$n~8p-sy(7f)1ob=nfU60!ISFFmaaazdt9q?+^6TwQOky3l&>O=DPe*tM#F%ctNqvm!{>S!U56b&qzBtm0jj+w z-UmUO9Cj{Pf3laaSsV;VRRlD9Yw=?NTwhgtjK9!dm`k*xi5eW1w!umbB`6-oPp{?K86qzzW9}iQTv;II?p`N8 z0DpM8bAn~ng4}XX?g_H~X1t#52I}*EgE$s(7VE?2^G4tMoz^*{GOyyZe?u3X%iA|!nn zc$xI!Ckab?G{2@NiCy7wU1xlqaymE!Ww>z0IMUPKd3w((EbaKnjc_!Rva}o76dqq% zkXWVqVE2S@wOd+;45>uajqO?T(U^YMND6-K|%i~B))E}N^XSb2#*J^kgXmM27nQKR`b12rU_|QJoSBSiRp}1=}$O4GAyHi zm^ z2PB#i$@g3$O_=aHy>y)>vodBEQ&_6G<-1cA`3B{dzu76VBP@i}4&n3QV>Q@8E*A$K z4DBGY82}z{o&SnHoe;W7!USq;dpW@kue*cMZSm`;eQHxk9P%l`&65z_J4jq7$av}h z_)2w!e0WzoLFo0U$i2UhCvbQZZ&P4QbpSFr)*xdW^jbU`D-MHzRyyM%zKiG0>OSr| z^qABlZk+SbLKhe$Zhv1Lmnl9&Y$7ICJv>Y5gQ+xor=-HgDC0iQ&`ez?jx2chcKSulpF zl$B=yeDV^)3vFvG;&FJZcxNprG&Ami zf=F9WC1d?Iz*K&R;q~6KrU$DDCX93FD_b-5ittTt*&al;rVpZS6)yW;W~(D=Evz) zB_owlCJy0kL$KIHpT`O)k(#?qOq;yhI>^PoeYOuGSlB(+nkFyi&;0F7VYOu{ORsiN zQm$=w21TB^9NQDssP(cjl=8aJrpFw3jnaXJPjwvgF~P*S#8EGUJkdz_A?2i*)sJ3py_MQX6qfh*F~fZGE*lpb(|Y8P zKtxCHv@{qh!driH%A)Gv(I|N+9+X=7B zZ>=eoyB`@6Q+18PQjE@zdVCjj3Mzl= zh`aajaX2h%gRUx_a@U3sJr|t$^OoudR4YuPB`2(ECs+`F*#r;w;j|gYql!=MN^x+@ z=^55vpAO+k(t4*V;`VEHj)|J=xLn`0vf-#bpV&0h(9=D%+M|}f?~T5iS4ZUTqUxu( zgA9P!cXiwQG=jFwJ7^bXYFiP1?ro1PW?TB5&x0RkTbi4C#U3SPLn77pCwi0*V<;H! z?L$v@f^}XTZH$sU|K8oM_+9C{*w4r6&O-H-einUoSiXy+URv*RiZkTX?-(P|MixQF z-HNn}eRXnos>wqbH_C^>`W1)GA1hu}Px89^JQAm~WV-ttIlw6{t2+Vbd;Vec)xJ>u z-iqI{9n1yc1a09&#TYw3?C<-kR$C+X48OI>ISTLNN<4(G3-4n}+yo50(K|oLtk%yX zyC3T}F|eOQzl*eA70oYut``j(@%@I``c#ei`4zXxFN(Kgxt|8HuUP(Y_dTP7(E2|J z@~Ig6id*WOSL&Q!>ReFjT=;(n<(eI2-@^&tBM9Fk2|p!Yi>>G( zd9ITcYz9L|m56fAdSIu%=b*mld~t##Odv$2k&WX^692$EptL*lExhy;r3-`TUXc1; zi27c5{*53uO)$hl{F+DgebBmvc+!K!{M&TE0-ERoH!qo6*v9k;eqNG<$`r~QcH51> zX$nptf`Ftcw4PxOBajK3L=VI+kU`AmM=jS{_aoCbW(626`j5c|8Ok1z!F@#$t#PPuXG1MSK`3q!d zE8^6CzHJXoe1{kPgF?Mq^*xo}Y{z&)6|iwSjv1E;Ebzf?qeiox%}Bb?FX{ImUk z%_VtXY(L<|} zs1E3^4d|{9=xz+?Zf4dX-a|k*qvbh8MuN#cM}sS|5D|zF5vULmm=F;-5fKCn1Yl8D zSK~hMw4=AQvaszt+IZOH)UGf$0Fi|X;!XhC65qB$%3-4tTYsL7BCZA`#lZedkhXbX zKIM6`_iOu9zH`SdxtFn({e7qO{_W}2Z3Y_01ZPkGLxY1&*c7)J$%iJDy|o-vK1MN; zdHW^2d}2%W$gMDO7w?{3j@&FK$*HVnQJg!Iia6bKctxdtXvuoUMBq zel=G3Pd_A#^uiY)_`uzK?1D8SjtJ<+Euby005r-!JVCOC0fGhU$(nViCTq?1DvXKF z*I#npTRPqw{#@KYxoaILZ@*B@5wmp^SqV=~zHSy6ZJ7|HVs^zU@`+;qwZ3Wac(Q9K z-jHE`(iQ3KXUnnYVTXAJb-`TNU3JKL43)0N-X}uul>TADe2pIXHSafTVSMtJP$|f} zeS+!ly~Z@flP+)M{r{Hg?Tcq^7f%n*F4&CZ`w@~AsDHFtty1IjTu-Mya`7$Uwm$i2 zacsC6FV6eQ605sN%cjWd82*(wc;d^0LrH>T8ONsdkskJA=B5 z_<3k_aQh1q6rhX?vv#5Z^GW#K?f(4ez-F%3h+JC6u$&W2L%?pAVQIfV-12!}-E#S0 zoouiV!ZJ$i{X1<5=B=-L%((sjfY73;dMXXN>Nb#dBs=iOFEM6Y?G?{%C6&{dKft}@ z^|qqc^h8SEOwFf&OHlK6v6s6pzqZl}FX_axUs2DJVVu4$!i+rz&(UPmUG*#fVTF%n z;v|sk66sy(aG2K|J(J@o_}rc{K`s_f^frFg`#*ft+hAU^pbcsZOMWmC|yELxl?t9a05Yzz5fqfagp^5 zA9f2Obqf-83o>=f3+fh>*J!)6CG=^TKY~oFf`&sGojtIW?<4dy@ePqoiYedu5NcMNT%P;=v6=!IuFS~7Txa>D&t){-O2md4Fc zX1DDHH>jsKV9T*PAvE4^L|WXMX1L8q;m*ybw4qI0()k zg7beUWO3ZQv)0U=z(jVc;^wp`Iv(_H+bS)0K7n`epr%N-Er%Q$sQD4TxRscHgO8{H zC2Q9lKXAqJzM z!ZszSv5#`q`B5+uFI@C3T5(mSK`lo``_PBsg>cmq^t%eIS$Z|0-TiI zmx!C&Z1u2@IUk{F**$s^D35|GafSoR6BIeNqQD>7VdN{aAjdigthg9b`@z>e?eBDm zz^@(3m)DY#b<+1mA9$hFhjdLQ&h&~^UUPU=S${YmPLu>!<^qhf+?RbdzN|Vpo{!#j z>@9z?UujmBU1$1Q*Cgr{>^vzsIZWjrbN0Qif7xX=DnHkke&Ij^`3x*oMqT0oyC$>m z#tQG3iV_7o;I0@3uaDriOKi#=waY=qLRB$v3H;XTUw2ht`ME{%lqiLfCoC->pP4QmBm*MER0)^D6~3`PaKKeKU=P3Jtv6G>f+ z#4EmHP$fdcdz(YVWXw$H9#b6I)*Bo;yeAi_WgJ8J^D8l@non5_7c?1j>s9u$z}1ov zOs($=Sw_Y7F1*Q|A2K|}q+z_zlH=Bjb+hSel8gv_SS01$sVp^n@!4@yd16oICl8)0 zW~uYnQs-<^=NwY!|D39S8bGHib_WKw80KWGwejaC-a`N0WaEtp_pob~4lGJV8wE%p zz~RMM#bvP;!8(=}K8;>4fKbV0_@kJ>n)O`5i>;DNRGM`>ZEtIYXScKAmjfh3H_fAW zJ($U&F~*yh_UdEO@>scs9q*)^?Pv#C5&PArq^7Y^jb$Enc@Urg0WT0Z0)feY096sY zML^^PO#+Oakd5l8Zz(!>fuQ+*IzDv=&53ZP8}$4tY}El#ZsoDB`dn0|6Hh znEwZ$fB;qWWpp425K4mpYSHO9Ya1~u4Ue^S=@VVH8OEzn&4t?hC~g9!4+FZ)9wZid zvsM+Mnu3?vBcNh%-mFK(zPU=+f0Rf;B|Gq1>S=g95s|{)TC#;F+~HOhA~d}ne&Pn{ z{M+HtG%N;A3(2I8QCkLr8fv9A+lB=6MQnyMR0Boiif;*mVY-^|qWQCq%B-aU3>ud_ zKV^nT7jYTRNdC=7Bz(pK?cB-?w`YKQQTczZR3a0a1uLVGWqx-R3N(tmTY^G9syGGCJVOp2C(lvO{TE;W3O`KWA`(4Y z0EUAz^7;u#im$pcX$%G5yL0|_EwC$H@u~{g)jDTiW$2y7IS+0$XcW1N8gOI)HHJO#{qmQ8U8kcr9`VpRL4FiBg7P zsNR)<`HeqkGPTzObKtL(X~}`L0oCNZryE^66UU)baBH!tnVLtOmpMIbL^Az9CY zb94vT*_Py<@dk4Q>_C43MC4Z^Y$g5Kq!v(b0llNAgA0U$j`y9zN5{o-XWJMIj+mI> zF6p-!EeKX?&ZFP#loljz=F8#TMHeVlEP&D-{p^`VcE}e$F`7JMDsNoSO~nB+W{UxD zl5@%ijj3p8z8v37wUl-}Ev@cSW;SP<>jIPH?Ov@cPPZ?2@3%-8Rd?OpCf{_F>TP{= zla}GsGB3{ArC*=AlsBjn#JXjk#ezL3UVZ1qI%0JUZ9U@roD%ZsUX_BTy#9fcf$d>qD>@m<41Np<}CPSM{@_{!ML#Re@+UaX`QciuPZtHQ$B1IC`HDK%H7sJDB`oGr zEiBW`?TVFlwwfl+_K~+5!v&E0)!7rfm)>_2`K5!kCV#o@t41xF{W;SE_t>x z+BbLXRj8|WCg=|1@ie95LidIeaZ}R2_*jl6LVuzM3CmvKGMu|!12Eh^HZQK$ob$iuojEcI5k0|B%i{@z@Ayxif^T3>fnz73(QAbF?L-&NN*eRynO?pMO4X7V z25&0*;#7NtoxR2vHM2UM#5AKjJ=u`z7qC#z;IY?GCTt}G?zV<0%P>4l+X3}?bOzap zLx93~=ehb^p~xQU>{tr4f^`zs*X-^zkA4G79M0Y>;Sb?5JV&DU;%-P_UuNYKgi165 zk?!+$ZX)6Yo)QCfIm~t+7LG4w>PoTt<4o3T0z)2;>r_wvx*Eesj$h~R4S0cGn8hQf zm2|uS>-rABFAghR&cFjc_X3+ZI(Pmp*SY@^A8IRNS5&0{{c7Kzd9+d#5i)e)Lj`a)1?2E**HbFV&X050%Tx3XUm)2Uc*pR8sf{%aU6&2Njw&>^ z{T$+2$9qyp4^g1lanK)oz?hcg{-hTuO6EMifpmk?f!Iy(SL0V=owJ06GmAm7Y6PWB z*9!4pqz9vB@!EnF#Phbjbm9f6C|URDD7!}FALlpcM({cwmu)55#tFC|qwXG@AQVLH zXYau>vQ1ji`e1U@YL|3$X1#8-$t|Zwq7}B!#FzN9WIi>Oq2E^#Fk5rYU&LL_@+E1< zsY6V;>N{G0M6X!FrSH3n3w;lN{`6gblgtiX7YtdU$h$|0ITLq+@yZ!TU9=opSyR_l z>ZVZ|IS1A;Lfyp>7}OrgyvT>uN-ftnj}3KJcXnq1)t4VeGgW7# z7cQ-5{u@Sqn-|#?cFxT1#qKPk`qFqbQ*9<_;qvE9!$xDrfjhZ6fuD2E>ql1^E{?MO zg*QlaZ!N>Z0>6=v>0Gaaf5+XytC`f2^)FT+fQr-VTI6`2a55V$eN$7`WJF6A!UCR4M4sYRD_hmPu7$OW zQEB8$jWQ$d=bk=*)=^F^<^&Gn2CSD|0q6@4lM~SZ0nvg$!}_hC`9R~`Z}}?)m#ZSC z`KY6#YoMBTcaGleI8sV0H-}MHsn=3#LBy~9z@3BL>A8(%}2=JVkU zIRE-xjchuExr4KCj?~vV=Qt3>&$@0EVESumACN&r#2k(~Yq9ra8cp zl~XNubwatetAc(3Fz`O!?)vVf8|@yC6F=vLZimk6Ct&^kxb(FS+}Y_t&7BmIJJmkr zrk&KMW|&FvsY%QORihHl=02d;`}c1KU`FMF`Q*!FBWepXA?WE_8{Y#;=!~AvWMIw? zzyNv@)H5o1P#vhTn)69WfkzD0HNNVbpKfiM*IV1_oNyxQh|4BgA&(~V^(-W@m3&tc ze~UOA-9WXU4>A<|(_ep7$}MAE*GAtWcCunk?+n|j|90b9#^ckM^9c$Y{iyz~;?*4z z0Ryp%`foUYx>GdG|A#GK{pbFfAV&L`!o7-qtEdc7~+y~$j{ zm9RWCZt4E3YnIIVFAUM-!hq=TV-2NeAeyfD?@23b1NNgu3tVsyczJ=6C3$C#Ur=!e zx%Q6lmN;$cUTZi_05y=}-LO}V0)vH!8@xl1(!dJxYm!`|QcH5qcf&&CAU^ow=exd3 z3>BV#2cj5l9#02T5xc^KS_{M(UAp9G3bM41@n0TZ{QSJ5%`-vAwlZ{`6|FQ78>5nF z^-tN7l%oJtrqikb(c2i6G0aj+hxMVpb5DKQZf%ak5Sf zhG$u^R`YeWSj{PVj|NCvuvX8ptyL#9c3q}-U4A_$cUy_+9qSr>%2;9FH*=D#EZ5ID zvp?m^k7%_x#HcsH%52p-SFDFj;<)XFon;^%8oxs_A|wbbhD;+J?Aiv!b`|W`?P18C z@6JV5m8UXG6He-YIgo{@tP>i{JmD-!bT*kfvIXOtwHD(zh+i`3{GQV0UQ5{x1tU74);hEPUgFax zG!`*C7o-9;^s~68lD#y=O$^?|Uq|+gp$ok#=+))v_&J=k?@?Gf$gDNJ1bA6G5#y-p^9(#Yh>t}UgZRZ=PAqzo8vklTu2?7T0k8tsPYEg zxhWl=(vb$mo-S10aH!`{VeONZkId3>)sUR6PybrXIvF&p@%@)* zF?DFO`kRr{nxLdrTV9s-1^mL}HNnI>AGtRpu}w9>HwkqG?8m14V8jDjI)7r_kqx8O z-M93l4>>_eACl7XS4q4Z39@G@o~u2!TpgnUhGpNA{f2}KX~n7FH~`%v88(Tt3I2?z z{=EWLs0C}*2cRZyHskt@;_bOpTizM#zyJxXd#0dB{xQ0_@ynx4>2^2U;wvK)S`u+$ znDfcdC41MB>!%wBVIlu2wP|uZeol9vKN9>k4y?{|?Ardjp>Gv3^S)Xl=G6UKgF9t6 zb4I{kL#6H@`9nUihRuO*>Wu!D%Ew}-9vf?aT)d@o#1oht8m?@$Xgd81XlUpx03q$q z>;JZb#mC>?t{7K~=})$Zi!RmJ+XZ}GV4!lk8;W^c%}s0hybbym3?-YonEVxhwduz{ z#?6+)Ys0IRPihoNzFfYk9zYJ+BO-Td3!*w!a)9CLp=oy8r%#v^JtJA+!a5NM^V33@ z2sO@)NlRD@ZxcVfH(AG7-LUksK z@AQ!Jdir)u*IkgkGzf=Eh0KHTB=p^OQC>QlS6egQ#qNH)bd22-$SoXaPZ1jZmsd|k zAJ_?-;pHvw2LB@*0S=a5Ip+20Z&#yZ>FF*31P5FGHkbbW;y1%!(%kM}OcO8a);`aT zt*;jwb0>eoselQp$4Sy7oD=*_iw~ur8Wvca)HMNCUxO^`C$_IYPOfTXR`!Y@XoIBw zwB%3EFE{U71w%QTI@z;?t~*aglLsA499PZpfW}_W%cVUBlRK=s>)pYfAiO8w)_%YMb z57M0*)=_(s{|3iP5e#<9nh`sxtd(vr5W;fqMSbyPo5e zuZDPjoNYRCzB2-z7{{B)I3Fuh*^XPr9m576MUXw3TjQQR{Z_A?o4n~4Fe-?4C_g~1 z1Osnu`sD;mnmKEFIDHDlAxqec=QF7VWStOdur~H;ekRdwHPbK8cB|LNx@T1o`UDqQ ze?H_bMmI6c=Dh-bGQg9$ zM(a5&*51I8eQ^-)4x^f!vtoopwGJ3$<;+Vs+Y3-36Hu9kIERN?4F(u z7gP!t)Cw0g3>UNn7j$4M&txs(mBYzPBlX`25i-`A0j9d&#u{q)5qLQrV!Bmc8Paj_ z_PPb8mzvolSvxI~2BwoY$(dS*SK_1{{DZ#!gQ!n+ZLGh65Q{ko#V+CorqffWF*M1T z{)`wJ!vk&L_0OY@slfMW!}l1$Pg!mv)Q3i;bHs}Ann-b(NpV?7aal=m+2Bi$6;5HN z>C^Gd47f+63)1qs;G>U?)fx<`P+*TyV5?Bpq-1n5JvAOB75l^j3~Et8^R$1whSiln zKbxA-BbLOLgX7PpZpH2DvNk}{h+xM6FfrT(aLIXP$U5f0Sh)EpcEtP^U1RT*`DvH= zc|F~a9TQzc-c+LIwr;=Uu9ot!mz|Em9dT!l2Ez@GhTJdu-}ka{L|y^f`)O@@=SQ`w z`}ogw8)x+@`+q)lVZZxa&c1OeLY9M4dBlk(@)%M%n#0nxG49<|RW83MO<24lZ8Ny^ zE3(%iQl(W@m`C*;0W#`lZJu;rzzgZZYL$1a{@Ak%9OzRLH;z*#e90lovE;ct- z{tsy~5F=x7!D&lZgdnJ)YrM9lJ5KXLAHG*5F`(g!Ff5Z1{)&wcO=vh7o?-fe($? zx^NgL2HcfBp_YCylyWoCLr&C36>VGm#5kebM zNo0gKsRd12gBB&MK+`t=PO9-kQ7Rj>MuZODL`u)f3tcHg}=HT$w7wbY&UDQlE#u2U~5f=5XwCvX3 zqY`kje!i>mw}5?wth(;>#}AxTN^nzh-5+iq%cNl@Dxk-UEl$=w_>BA^zu9*m6j4d3 z#84!etHHIDNh=QFRzJ~hK z89hnbMFHT8Fy&w=ZgrINXx+958`J1n*W7Q(V52*L zAtichx9u6iI0!LulB4Ne=7rgX8uScEmKK)08!B5$-6QoEfQ90{yi)p!Z#k!e_d_%X z(DQ(rge=d2g)=UIl)(YV^8`a!`VqinKTX7_e>xLyi)E2~d`cv{?n&d`_yV`w*MaMU zG89l$1`CPpA&2>zlzrj19jhXMVu``jhbR({Dxm&4^Zv3WMn^z4DIk8bJ|uO!QkOi~ zDJnj6Pxb60DYsqDM-xS4FKsh96+)ms%gj^HKN26sR+fK}a=vJ;+!p{#QCROGY*ez< zFo|oDdAgpF$^B??;Hi^$(6yaWG^vtyr_AQO|@=aQ~C0NAO-)9B_-)CP`|agM2xwFG<#6x^(55^)J(jMW0;NOacd@i08tZ!H7AC{h&68DGv4%7&^51Vv0YhMn+|qIW#TsO&oH+HRBOtE@g+jMu=OthK$cPY z|G5ClGV#;d!o+?bbZ9;+d$VxJGv=Sa6z)nGCP!O&pL$!5bs~yBwJi&U!m71qZRpgdfav&qz!p!|-->aethVp3qCXzn};=jA7c1+3h;I$$+Sa2eoM6*eUt`><}=pzXVXhXj!t3+z0c~!!V=&nLRAr4`xVL*AtDe4I=(^ z#8l&XI!<$RY_IJ*Z1=5bTn(a`znyt}zw@wKK_TZ}>#1^8``FJ{=ZW6sbn|~?mx3vT z>LxqE?&bGC3QSx-+-7YA3sP;hSFQDjvk6?~)$3iF^+$Qh?y?yRyC#;r{v!PqR0remakBQNaU5c54unoN zeEgoA?XnkFIRhbt#}Ze_a*hH5_>Fs+EOv%}lM8Mc@MKS-tRt7JRYvM0&lkOhZ6I8{mhtre8WyhPDU*Z>ET;hj}b6n0AIj>39t5vrxZG>Xk zz6BA89%W%Bm&_P_J>(+y44Hexvhr%NfU0vkzVS3Rij6yvLT)qrif&g<_SWAsFRdN24hIgG7uEcItWUcu8gz0`FKl^NWVTm;tOUnNM{QVVlJiMGC0)H3_ zH7R@aPnvE7dckj<1sFzZIMlP?As5=O$4^i-4DW9wJo+!+_OQeT`d+Ykz#rbb ztn#z=^9tisSd4@x;U2T3VDBYwk#WkG*|L5Yaa$Ud=qiNP^fHD!ap^yIe6#%|q}7?E z$w@Cq;U`EYPyf`4TJ$6$c5IjmcdJvdp$xeuF<%|+!L2XUlmGZI=^8+G!-mpg1TsLe zjc~XAmlQ;{S#WBu$29;;=so7S2FfVB+NT-gt@VNudv(^JYOQ%57B%izEAoAq;X`8N@ynO20Z5kX+WEsUiJj$mD8^RemI1UG0 zPZJ6|U6kT0CG2i-HEL21z51Wy)=Ox>lP1P%0JlHkh=ML#_fdN zNCvez0FvM}P6Iy8G2h&B1+z{>8cs@Th6%)={+Zu02-#&FXRS1zP?!c}1a{5U{5U*; z6Wj%hL8sci2t1?eRLSix&g(rl-o8XClPRB?>+XC;zMoWSK%Cl^4!@D5N=v{vVma3e zyCT9$I`r_gmnSm#-}Hx-SnatI;IN)VzQ`|XKcY5H5&rH2s~Za}-gCx_x56~QrQLbI z8-S<3`-9t0aypI&Me5B6IUELy8=Q9G82s;e9+Z_*^puaoJSfUSDZ;2@DW?jKGKHUA z$SMBdr#?Nyz*4FUABP2Yi;(*OdD+sDDj;bOfWHCOqF!gVPCoFfQpMeVfB~X@OudG` z8np`N$~P;zCSjSQ!Nzp!`Vj!Xa)l(NpAX~wsz)tX-GaIlkX*oO6YnhelkJ!oxk!I17k2V&ZbSB6n?b@h-*5AIlBVuQUkz_=QsTsZI9dBE?~iAjzrcj z_AMEbp?~rr5R9AB3`=N+wROJdut9L10*Z<2#}2wyEDCV1Bx8Q$J>-%DPx7#b^rZuD ztDU;{d@n2BLUJr9P7GpNTrt! zh)2aymU|f9$zM+(J~<}s_U-6&GfmUCYGI$5MAE#ap23rNQ9_~2YQM21bCR$tJo&v#8e;0u27>IfTeDi1g>Lv+A%UjD$p1pb7 z>2&#%#H(KauF!l*bszP+tcbGlqvuTNHwQZ$gW+~0BNbjLD$VfI+_^h3pEb17vj;NI zScq(U`gFB+uRyi6V=^J0AycP#h;T({2s6)ajd25SxW{`_%p+`JRD2p?Hf%b#_oRWs z6@w>ZX6&DM!f(Ai=gJW;q^BWEMH$w`}1GvW6GWL8=Y$ zm=4lsEH7{Qu5YxFyh$Iwybh{z+CN&_3Fp6NZ*Pw)j<2@)Or=g|GI=VIBiEI)>L(WC zE8hM;3OnzpCZ2fhE24BEfPyG3h;#vIA|(i+OK;MC7Vk?d<7T1Bn!a8Wt z#|MpoXJyaU2gD98EBLvN1@BSXY;i1|oU&dJxQK6Tbt_LJP=8n6@dv&|>>)s8#CyNT z=?>U=kI?M~V!j!rB=c)H{Z>9zB)^L2M~okliytGt@-L)hp=xkbE*OH7QGU8p&8YTW zynwuO*q{B4(ksSypIk6BomZnR^c{o#BcqZY%+)3bTAIDro_&RE0iHr)DNlc#FE7FC zms{EgW_*GFa{aRP%f4k`u7e%S8Frd>{M|k4B6(uCClPGv(AMwzzQU6+y5gLHjWgaS zU8l`AeD}F$TFISTjjryZ&zh~A&(;3t)j2Ly zH?wcrQ1+pL_d`9fG5-mDNcyGAXpFe?eF80+6KyokNuxuxGi9gOL?3E;Kh(K2Pt#~E z_Ictj8IcgG-L@?hkrb+ZxJ=AsL7Xb}oBBmyF^4vuvsN)av`C$(S2c$cnJLXm0ZSFk zz7?t@b%!|d6EQXCZF+WXJS8S54@DeZh!i=o_a~^eG{ed`K1))Hq_IUeo;`5->_XsS zir<>L=pgA>&Eb;VIIZ4K{~EM@Sgu-T$?Tu)wa2EoQm@VK14=tw>*< z%$ds5txKbL_?zL=n!`h7;d0LBUsfw6J!uq$@}V5dr;_bGc#*W8L;38U)d}f=)@22p zPhFIVg8t|$eI)PK4Epshi83J)PhaT>WlW8%wc+SGFp=n+;JABAZrBz^+F6SskE?V+0v{1-T`2AR&?Y9cBVuorH+ z7+lZAxz|lTK5g=w%Ja%e`ERBUwm#iEwjO+g6isc!;z zaJOQzM%}3*Fw{qtfaE-vlzVEAB}r9-vsaTt$xL>KdPBe3EyAVp?JL>q2=L{X^!Qim zjXiOa^Jhl|T8XaU0uxH_{wirEDjMx*vwomc|5}P)n5d+KlbP>aHsMZYX$BuaVHq_S zdbi#1wudEn>$U)>jR5!=fT_pHp0Dx=Ozma*)VBkBmuoVa-s^+SLbm5G)9Lpeq8H=X zg-jO=?W-Hhrd*aYCNc!P72~L&b zOMd+|Ez}u>)?Bart0FoaE>AH_>03t`da7G3;sU9tS9=WYhOeQ#`RAK?&#aV`l)vTg zO1#yKv1PL}a7(Hkr2Bdi)2#W%%20AML3uZz;MIy{zedScc;%G|FJkm^8ON})bjCSzbw?rvnSF<+Oo>j!58(OkrV$#{jiiMNZY~$Hjjo1_Jh3*!-4wF|79%V0J z-fK|~CkQ)MRFhTWkhuV?lbq2fUz>Q}O4W<0G;YnEsw#N>TKOo{2K?}vd93pTFu;dQ3@q;fmd_xH^ZpIYCZ+&RLjnLQcXj~23B-W4A^O0*lNiMS zzM!2vXg7QE8|e56!sjiCMDML}2r_V64sJ6+ySJIwfH-I=9e$0FkTDZD1(d+tAy`2V zSd;~9N6GD@4?kU6gXi1Sk`I5}RMrBcB;b&+Am;~XTz`N1Hx7XG@#O`UQSE||8490yl&#+YAe$t=aqb7DQD|MgtM?eJ2@n0%*e$iKYj&SDY zJ>d_HJ7P@JOmD7If+8Bm&-=Dp8$R|KKG|>b($Z^RcT)OrFqjuVMI*94tH~AC@pqZ= zfFuN3)Jf4BfF+~w_z?ho-BNQz{JGB`j$;rbVF}eYyJ`x)`}y%1H{arcS>QiE2@+C0 z(YI!eBVBksqNIVV+*C|2!n$Lzl|Iv5Pz^82n~pr!>oyeFnFmG+zYtoWJWkB>2<2_dEPw@}CB~$KFhV zAASUD8S^>*%d{C4vt9j;OYe0_6Egm=%?VZa466!ka1Y~SB0JrFKTY;E%==8{-RAhp z3xSySuHWL#)4#>he)VYt3Vq^D>sFcK3%|sh`hJOfQ_3*Wovz1|{f>9Y_#Mwim%0Tsil zo8VdcLJjvshUxj~`u;eX9Pg35cTX}s@91#0*A0l>?$C_A z>A0JLLo~hZ$EF=|05;ua*$A>w=24*VF*-}AyQ(p=oROGj_4_9)g0fFmEuU9KCo65$ zk_t7^F|(hLv#&31eGi=r{w%oZrShAh9Fii@Xv;eTC>gD+v}J`ffb&1nPKePlwQq(8 ze<@z(;mUHsXAU6l44@fbVk=j1$J@}%S&Uh~_HpgJ+0_rd2jdlLeeKkjPEf3KC zXs^p_`r_EgXf-MtlzL;i+IwL?+RR_Z-dih+sC0%^+#ivo8`n}5ZiA)xH8~y%nM~U* zot>6I5+P5TvKhM(ok{tK>3}GRgWHWv`EkcPiwd8th&mI%nDhqsq55K?(CA9OsOc5C z@xNWi8rSnfY{;7F+mUHhO?->xJooq4L)Jz;{FlyTfZrE~Cjzk!l&`)-+9p1e`xd^M z7D`HyUXl4!E?`FEJr3UbXZ3)u+FFj_cw|e&Iii|8yrQ?qVlN=FQCNTF;|D|7K;2_E zq=Tt7bT()5p04N7k_~kF$&-0j&Ec-*WZ6>Dt4k^ntiKD_cIC4duE5iC5U@LMs6RF{ zIH$)t*yy@ep<5M%2*gm7C7Wh;jbM`8n4)C!uL(h!=yr|b{)&4j?aIj`T~~EWmgJAl zq9R8_z}h|kmywTKD`Ox3IF}stV-;L~mTaiAuhG`rAW`sOwoV*X=NUkuYdYVQ`T;O{aw)_C^{m%nZ$=h|@t1aKo)=pfA2aC9NJk;5xI)y8k{; z8SiIYV`(FrY0S zTj9lIc`P-v8u+{K&RitHixi5tZ-_k1Ua-!{=zDX%NIN23*rG^PA>1#-7vuXzx!>uc zRCQ;PqwD_rA`oO!?px}s+)e4DP9^5D>bc>)fC%J04 zSXqJ8a4x*1C<#yS3*|v37!*{h^|j}aT4NtSEGf7(Y#IJPc1+5(|JhM-jNhME2=^FM z`2QB~C{3(fWXTHPH(_tS7ncaG{TW>Mxv?$33`hl6?!{h9>f3m(;ZWdtQ<5w610c@^ zcq=7*ueOK_US7c+jGdJ|F6C*(CvAlTFAP3wsWCD!FT!vR0xRr>_v2 znmMe)>j*4w5#U*C@RwRcrvDcj1-ssh5^dUP->z{3`(^9j;N&YN2ClQD<`F1*_4uVN zOeJsmT7X2ult;1WlO+c{>M@OJfs;$;}DYTZEqa)AozpO zLRz^>2z@aA*hIT_ zbVP@T^VK0?iN3bm&qtKeTPaqSD6fDaBU_r+WH8Pv=lD)4Ry^Ghx`eL;J`{Me6mic& zq#nTYNuZjapLt0=PWyY?-8P8eEQg3elaM?cqB5piHS;p9rt$SvC8{YXroF@t7+A6h*>&Te zfkE{IFsKEpbwJ=&0D>eC7=pn4R}iw?T~sp#%v%bZqhlo*civDv%NA5g_%tJ^foDGn zC)!paq%IuL^z3`5E4iQx`DYTRfT6%v8T*)QJP&iu} zSuBSvRzeo5!e6O#yfDtzMHU-`WSbz1Ei(Tc3#->unH;4{mR5bb0U(7zZ?Xh$w`yRD zrizEA05c5R4UpXktch0Y7b(#QGm{)|-#TjHTx%%p&XQ{qyy{)GTNZ)|RneWqqyntz z#3V1+c2z`o0%ic0UD)&=z&I1Q*27JzT;Am0-H$rxDEy6P1iMOjxA%eM$NdRErrz_2 z4E5?P;wFs)u6oN35OZpMzcWkKXy$HT=>l5Zh2Os20{=DZ3j=xDgW%b?+i{6*!5y9+ z1-Mf-2HYuCbheB013XNXj-3%lcS;YEDd+tL*^FNHlN0-=GrCIkbI}0%2Du6C*)2?MGfjbA^b+Qbgi%@Gec2Vk{SM|~zhnBH zu5(^Raurh!3CvUvr9t^vEN26ny}f4K2%=f{vaQ?u(24z@yje}31Dn-g;sce%*CruM z0%@JWi`y|$9)ivU*UY)3cB}VYp5p6I^wO^@622%P!aS&%*udbl=X97$ zPJ<>qD&L=!ofB1@)|u4VemE`4p_DB!*F>-g#GL5d`K#0m*zW_}H^AQrU^FI`D=B*F zIG}Y))z5krf$9Bm3`}QnqIM5kEY(d6lK3mr)jIN4&$E;gUnzYD&hjwhWb0i$UxlXE zsjf{Ma9o_tztQBc!Ef;sI6YUuYm>%n`QutBRfki6O^)RXCM2eP?_!RCSx36-T|?%^ zuepy#>Cv=z>P?5Htx;wl5PsYO;U)rgirU)2I+L0H zAGid7Aty{$%_14>9Ay$rPl|R*TNk)QSYl`Ght2@4|GKcxV80E4{)}$ZrTCq3?Fyyh zBkPC<3xn(@gh^G?#2 z?z3-atx^LWtylkbfS6)DyHk90aEx1)+**|-+L*Ac?GM-?K)dP3=oUK5>i3(=IZtgP zKu6;QjruzYh}vzv^A^-O7sGMNZG}%xB~BnPa3?G;ptW&W888NXt1U0txxPIPUVZ>` zRrWo^E%12Z^EB_Gnh$((UV_~&To?)QmdFzgyZFWMxd?V>VN#Jx z9hZcU%4k#DUz`J+qzX8ibbljyz+oNZRJDz8j|m#_Z=5HAOt(+Ueyb&XUl~$F-zS@t zotIYm_Kab5d9q-gA*z{jMXn+E@8e??jIQ+6JBts_lfL!-Dt+IjquB<>ft|u2JVuq< zoN<808k$iQSQEL9Q2wQYe9f-ikv z>F?S5aGmxP{3>y7=(Fz{zaK`-Jx5?ISIUuy#ek{;bxtIzs~di z41t9{T}?tWvND@WkvMTnV~i%xP61ZAfR!A^%46*Mr9o9td1zB7IjJ89tr+mtG?*)- z0#2$jFloDW1wycn!ODe>bkh!ley6H;%Jpmpr;oQr{)wdCC{*g}Dy5$j+m*f0n%6~@ z*H&ydo~@uRmBf~@pAfDK47iE{as@Hdx5EoG)Bjd7g_99%uJ<||-UxTJIC0sjy`k)A z=y$-h^P`k*B-^0}`&} zkFTl{il+4Ha&~0(2}_c5bn;3<^q2R&jH#ZCp4!*6o@d7e6V=sO$Bd+}1{qPfj6+0* z>Yn6#bo*hRfya`3vdFHaWQURGZbxXdny~&sQSxX5qWP~z%jvk^jZKo^%~lD^fsGt# zRn`H`2S9pmJm9;#)=hHUadV};gS)BmPPQ1J5^}f~U+1>cdYOK>aImu7Zs@AyY5tFS z=@Rf!-b$-}ec5AGcG#~-un%KJRRK>LMsSh#;n-PWwFO>bii2e7qCdRY1pCUGVk*sskJ7EXhxtX$iSlv3MCF8%R(SA=ZKCY|if z(M81l>Cc8@n2FK(W0J(W$>aK&mJ7ij%Wc`N~#bMvw z-#YOlRqs3?STDvadbZXpe3rB5cpk>EX$j4WvykLJS*m=K=+pYB|5993Znw@pcj_jJ zK{IQvT60Bqn03kjL;fHACGVz@+WZm0e4oUzbVq8#wbCqbB7dpIX>&}~{J(aePH{k) z#>4+Qm&M}^x~mV(@ei|d^CWAWHuUI9QZDA|%|%Jh~`$yqa2J7w#t z1f(qwWZVLpI}L{qDLxhqI)92!5`omEhVXDe4Yc63#kEQd<(yChU3l&HT6+d=Zm5Ai zytX#P>;q(c6)YwTABsfI&_H3fm3QIK&cORkO_eCP{uCRKMYIP@7lR}?ZS zjiOu#YVaOjJ0D`!1R3|C%CuMu)cr=mEebVom9E-G+D;R7{b0#;XTv9nLF#5wa7#c9 ze7pNoTgP@Mqp8iVA>(M4T!L)e`ro@0<5UzuB2w^}Pe_y75Q#enwzrqTEk4wT2!2LA zb@H(FF4TtXRj(vHVnS}q5_ZL_lhJ!o!Oz%{+d35EHR_!7tt3z%TKL%u z(nel)euq2+X@ZFQklu(~%@H189b`Hh3 zGIMSb^!%<-K4S1QFXZ+)#rP?6?j;L8ITC3?0g+&a`pCe~kjU)^kZ}QMt|uH$t{4dQQiPu+ zAh*GH$`YD83x|(}BbVbT#My_VyCNxr*ks{O5lDsykTy=Jk1qV|J966`GCl;&Eh>0) z%gYenr2J`-^^*&~i80>#Ku>ZU!9`uu})1^7N{aN}t1L1B@%0?@(VR#UZV|{xm@5s(Cw5(U@p_m+} ziyqYHfl_WSdJHS`P%>|=du>+f;hn!Y*5Bg^QE zD3gyKja*CP|3+uYGP)_BWT?WN8u#{8qgz(e9Jl@6e`+84>Pu>Dx>t<4P(Xie9zIMKeb-@;$|=m8hkOcJa9>%V4XH{mIZah-q@v^dMjE5sSaesk+SIzyUge z%am5m?%|%iKVFI4rL)9tvVWtkkL^X=C2gX9i`e@$ z5&QaG+3Z-Fgh0$acKp!RY-;M0APeo3o!%?*`>k1pQDSAR(4v=4B@X zMK3|o$$tP20tg5cK=48X1hOD_0D`w5aM36xA(kUlk^`OSnAsnL_#=oXKwX49sAB}d zClFA6{eUl<5AH);XzR*^gSOJc6NbXs;&0sRb>j}&nqV#;C1Y*PjPmh`QW}yr2{*|* z`4KxST?sx|W*{Bhr0Nfdg-S9eriHftI-^TW2ix{hTHS@%{q;D}>p708yP<6O1z^jm z|7&#sv3J)kaNp%VNt^KDgVpnc%_qeRZ8f6?k429ZGcPT{DlaXeG#@3{@*F07OYlh0 zl1lT@f!cqd_6q^1l>)Ua(x8?Sv`Yc)?uCMOoS?)!2b7S260`p#;1Q#xljai!t-6@; z^JG9P25CM?P7S`v8mD5%vkd2;*I#YMAlF5Guy@SXs+df!?f@lmy$>ghKXd#7AfZH-)>5XeQa;LDmA8!Ep*Q~R^Jx!p^cW|p^ zbWc3B#pR3m*3H82YYdj1vl*jA)W(!#A);Yd(xE5VNB2`NBzgOOZ2HMl)y&>s@r~JO zy?5)Pny~L2B%=Vo2RNNvoK~5J1;C5IFLJ?>KVI$n*&43+6YPJyrE7l8b+;g5T}>?B zO<8cm`i+{1fu_H+`wvViCjN`vj((CAbb$zuNJ0U3U2t2**rV{9*3bcS?5u^| zcq~D$EP^5VA#Xdhy%y2lxNfdt)clUzCL@LOx$nu>M)3<}XW8Z8)j{F7#i1Ty7l%?T z^Y`m(R>vhA&mwwS$uZbH$ksQPxxx96)G=P4!n8)5&2eUh*-uLqn z5H7a7G}PAgf$MT+!pM1xgxBRFRCaM;lHu?2uiyVXqEb5#4i}^rsV0*b7S^mCS5gjE z+haPYF2l|qUIJq}ekkyfoPo1Zi-Are&G#cLjS18~tlyV-+)qC*G~@Tj!j*p6Rl%>L zivAKeFP&`JiW%}2jhhcYpdELorCCZJ-syItqvlpc1dU1tIam2P^Sh2)URQ)>ZAET)qgjLIsDl# z*4^asG=wl!pUDfY6Q6}qXXOta#ZMP1KQL4qzc;HkCeE;GIn#=$7|B)mei;AHs7-$y zYC5J-o;9Q0%EBsbPvI%c4J_}c>w0GfJRX_!Z^siWf5%f9hDtI_K4s^0aqrdU5uePL z4B`2H|5Ea@^-i_8+Gy=|nu7uQ(Mmrp;#M6Q1$&#X$Wno#Dn}c!SV)-{RQ|h#5#>0C_JQ3Fc-%DN;NM%U9%q}J@~_>Fc4~d>Y%sJ8h**D% zot2ZW%`C_5u|`iLK7gX+ua@VgRB&BjIu=X_ywn9)kziO9^jKp4fv$80%b^)%s_H@W zbkO|51x#@D?{>Jz>H_D`HaV`}yB!RF!P@SF@Y%<5Tv;F}LxZwC>;LMY0W0?eO?HRD zyX7(?2-XuTI7i>N_YR=?8)tX-?ol`^2xz8_3T)06=QofO>SWk6{c zm>++x3+QwnIX~mAMT12}z{(^afV(eac5doK0um~jqkCYTj{>I=)`Va_Flrhh@eXX$ zU141y(Gg7Ky)Vacb^YA!umBTf@nBr226UbW;}3YM(3R1oUGd~S?U80J22Wh4FUhpkE>XbKWm9!z@tWC{O`cvd`c)9?^JNi7fzDXKv4J{=Q|XP zR-c@T0qYwC*ztrDd3y%y&8sFE;h`l|Li>K%y3fUFC#|Uz3PTpCeLpof{|H5$tlMBk zHaGHb@^9_mLsx2-B!I)Y8(0`#DIl~66CC%<5`hB%0?T$WFZBMKe}l00pUH}3f2Ta& z5PyiJ`%Q%Odz8s;$pQlX<)AIbVEcY+UBAaihq%Mo3rLO~7Ur%Jb5JF8W8MIDQAr!a z3~x=ZkfAVOYn0$DGq3){E!$I?{TPwy0;rkPt}8Mez>ThrvwpJ#9;yg{e5X zj6&Z+6<1bOnkBa?9<-I1@$Ze1`a`FwvGc2|=klZ1C{f?G_-vGfbw$nSi7L8%?c%Xp z%*;&xh1c}h=v9W~_qh%-tmVq=B`|D7J7%93&>8;kbD(+HlRKUpuL%6xy7;~N__ks% zH^nF!BV9DlH0>V|t2lGD`xF?|<>q2r`0;~mJR$54LNp&!z87>^44j$NdXo0$^;hD6 zslG1F4Y+dGAF6Xtrd%XP4v9r!$a7>GWiykb(ss}QM;wU-8~!*;?iCoK&JKA#_KC7d z>aNmXJbHF#l7ws`!LcCLGY@q<$j)mbiEKjFvro?w#_p7=OOtO)gMbtS!XViF56FOk z0t9j(;M4#CoG>95&M+21Q7j9FXuyyK7)k>}#d08c3<4Jr%*SoUn2}Y*27qZ|O=)sl z1uzH&gV7;Uitv0^X7zYVA1O7Z?c1^(IRt_QAq?t?AkYJWH3(||1IZvTeX%2D4#Fr9 zY8QqujHObrtAJKQ%<7LJJO7QOfb1J6bO%LCpeTX&{(NxE#Y;o=bB$l)J;T?)4Yz*$ zmnAHEuz7Tkv1fl@#6Y^$;#p` zUc>c)cz@EE?rPG;(7EAg^m@I7$Kvry8(ZVSWt3~{u@b$re76=_Wb3Dkle^zSwW~{@ zZRL3+?x1&Qbzx;>47EESy5Me`VW}n&+-QTfLAQOkY4q2W>2K*zbGhN+eYL#sjh#V3 zG9PwTK0rV5F`C%Jvg>2w56XH9T}R5F!ub>l(#~IVErR8hUz^>sD+x>EdDIn{U%!G2muX*ASEfNlZjdls)BqTIJ^AKNhzbZ^mO&oO zQ9?|KaH#mKlRrRhZ8;{qy5#57t#!nZKEU%yIrb89l8149@#8xHoaO*}cHh4PI(E}1 zI7HwXAhZNa&~wfb>A|e{1qb}e4YB(!%jhA@yUf3W`RbqT@M{q^tX$#QRQ@^Hc&PW| z?8$e}M%~{EZWUAFs|HOK&E8HL?gAB~k{Uy39G(#Bil(2wT5vPw{s#w~nF84xaWg|C?F7C7Ep3~{V(Q5+zC!TMj1VkDuv;KTsebYW(6>g+3t$7_> zI$hS4XYcOSa4&PJ*dSxA^jmHVxkkZY=dM>~I+yhB+KrLrg<uGTJnm4*4Amq46Cx+>c!2zL5woX{s z+^&2AK@`(Khy`6N2^qt{J@N!{=pZ3^0u^+S7_t$Hd?7CQ=C;8F3^u!6 zLu3ZdYMTP|F92Ku!bklVOo?M?CFTTW^IPa$dXOA$-Zn~e1i040c|Qu(zx@^V@W$i; zP$pzAiX%6}m>&Mfuf!@E&R zhr?m_X&mP_5|L+(yZb1v8UI~**EY)@+4de{*P4p5#w5tI+0H+%tDPbS=>!dSKgn>H z&Zn_j9{kSTG@JQb4dF7=r$#*w4J*)TPH)dONFJcSOwariV63|jY=3fx*Rjq(M6YMo zw;#={pI4OsD!Rw6Nhxn2FZ{@fUUagA_S#-&uXtp9MuX<&tvnmdoE{uJ?@N*6h~Q`B zEVq0BGwhF6tdGXZmeDrUMYu2a^+ov3&sAJLiBbOodtyTm_!h1%%L?zc-NbN_Ch-#o zyB7-8p%`R$u^%4TMY)#SVYj#@Ivk1{d0+fYtHwfPf0C=p&S0Wv$}*K7Y~I9iA8=RWG2i!OB=@GaQ${Arcv7b=K$-;EcYvGZ*E zo+7+yxp$^q@j#$iBG>EhQNZ4v4pVjUs}t(J;Qq`y$@iiaUS29rvZBfdX$O1he3Pd9n5Ypv^o{#i(+Et zViIU6DRc3Ea|xJ>$)Tkb&{7Drl=2_>J`lz!OK2a8ES0_DZVAxlmsS^mYYR!Mi@>!- z;o4$wZ3$_07+m{x_wfEJjR!E#P`oid%v(L1djHVZMD z{@7oG-n^vcTBVbSZ#Ifd){IMBc$IS}HAWMb>&LMd?hy*HnA(?A)Sv1$F_|WL(5jhl zWyS@DuJ;8@D}~rN!_`=xG~LZz#EXkJWYTp!{n`=NWm%TBrZi9HlSbwdzqh_|1EU4f zPrl4a&g0rN&sPj8T5yUa*h#D7ohPekqL?q@m<3||hk%(c;$K6rq&;?Wl| z{w89U{&?*3Jyx!gp=HWu4#_WPM}Y?Ix7gTaiCT-_lxntLbFTQEpAYMqZqM;>8N7YV zcMaT5Iy=Lg#NIvzHu?Zgl>~9%G7m%brQ}&82d{*}dgb)s9s3s`+NFx}`H8X5C_@kED=T(s;QH=|)}^Kqt8E2pw#d#k*URjj+QexzzNb8OZ_ z*Yh*#{_O1Bbz;rokU#x7PHVVxbYylm>$Kos^cS7zd#N{MWCSY*wt`1GGHWetyXkJ2 z24<*VV4j^ye9n%i@A+_m*Tkb8+FYlAr?YbwYckQa{3gylp*3c=49a_zd3CV{UL!L~ zy*al#r6;fcBq)*HasSzu(|PA}@H;=gr^uJw$d`g=xB0}8FX2{y%u*{e^0{b-6&j7YAbH2@?=!0F8`(&t5t9alvrag%P=ZUJ4Q#_9Z6`|&?SIn z1EeiY4Ad)t=>arK@4D#WW+C0FKuwS|ZhWxVRul&oIE|X?$^#4%4Zz7sS~$g@Pb}?^ z_JN%6=d1|uCy^$&&F&zkc|i67J?mbF=>|E_)}0-PsMXwg09?B{r*R{`!VWRJc^e&x z&A?s;5VwKyy-t7e#ZLb^HS{$U!FTHfivS1YM=jFQ9;9G8l#B}jfpbJM3b?^FhKD-k z9G-rJWR_F8J`8IYdCr@!WUDdGYg3I!oLSa=iie6a`1-d9>@|w;CtKEJ6tR489?@5s zXR3?g+E_B@%m!qt0pHS?&9{lifI$-wX1b9jJeRT;k3kRrL0HeB5fzJoMC`?iZ}Rg| z?~J=!y|rS8=@yP-xLW3XCBpvc578Yq{-4vi-e#FEc00Q%M#;)bW*y)?EBu#!C4qc! z{oH$Y06dQ$7I(zVcj#~e{TGOYQv~j7N6A=<1jnSdItBGf%#57;&?N%P4w#4jB(+Q{ z;p@?+#Xr%Le#11hl65kh&V9G=xr~aI%Ih4!AnH8;UKFhI>?|TGXKpR=;bLZzqJRyY zDfk<`HV69B=1~ry=I^4`YU69yt7FeL=1^^-&=s9oOS#R}{I4B9XGHeJ$1~#1Z<@74 zc4H&_*+z&C56;6nH}9$aWBmxunrUz$bMYunz!h0-6h3@MLwyep3=IPVd*EjYp`0|A zKqYt$yVVM0r6j&u+FrXIyPULj&LEq1qLc27P6ZOy?v6p6EqhJgB7HP^9s>7-nCV2&P1$pI&-AYIM4{HOHG5 z)Dx8R>(6bn0J+|OITH8Hk$)J%lx>Tco^ZmSJcmDdv1@&&N3QL?**2m16`?s{Ndp-v z%XxiZ4!rjtyqCE^g3A2uvBgJQ4jA!&7lJRyo;NfkGEvB$D5Oj@vL_lT6NBuDK|-bA z{C_p#GCpFM80xdapR{AGFA#3JCI*EZ5@Jl?{j`2P&Xi*XAbNOH34UvOt@U>aDDyv` z9(jIx@u>No&{m6tS0>dx+%I`+ZK_22j8^;3T4l3K;yK>j-we}W^o2-p>{o7 zvAB#HDq!$cu^7Fs$v1ht>D*Sd$ae1iDrQgYkl(~+!nImr;yR!w!N{lvza7!=*$7y9 zb$n+RH%RtUBVDc0E$rn-Jp#M2Fj!=;-qhF6@3C1aSKA_#=P9ZZPsv&kImM`k*L045 z2BT~xOqFKZ1`4K3Mq{+aJEJgitMaZ?jpGE|7+M=s$Ne%6KLy#eb{_^aJv*0Q=tv>G zkLKEa408Tvd*fnci@uJf_%qI=Hd)RErGL1DFUleFv39feA#!q0)&r3*Uc;N1a^E9! znHb>-5a=Kg#5xptlFNHH6%w>aq2Uq21cN6qLkA%c>u}@=9GICA6w5q$lwh4ZNG-~XLs z*LCMQ2>Ffq`j!(#oLI<8)xlY;ObEdr51#>6alB!+Cf*>wfw2Gk4+C?|OYOOpLU!x_ E0mcVuvH$=8 literal 0 HcmV?d00001 diff --git a/src/dateutil/zoneinfo/rebuild.py b/src/dateutil/zoneinfo/rebuild.py new file mode 100644 index 00000000..78f0d1a0 --- /dev/null +++ b/src/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,53 @@ +import logging +import os +import tempfile +import shutil +import json +from subprocess import check_call +from tarfile import TarFile + +from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME + + +def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): + """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* + + filename is the timezone tarball from ``ftp.iana.org/tz``. + + """ + tmpdir = tempfile.mkdtemp() + zonedir = os.path.join(tmpdir, "zoneinfo") + moduledir = os.path.dirname(__file__) + try: + with TarFile.open(filename) as tf: + for name in zonegroups: + tf.extract(name, tmpdir) + filepaths = [os.path.join(tmpdir, n) for n in zonegroups] + try: + check_call(["zic", "-d", zonedir] + filepaths) + except OSError as e: + _print_on_nosuchfile(e) + raise + # write metadata file + with open(os.path.join(zonedir, METADATA_FN), 'w') as f: + json.dump(metadata, f, indent=4, sort_keys=True) + target = os.path.join(moduledir, ZONEFILENAME) + with TarFile.open(target, "w:%s" % format) as tf: + for entry in os.listdir(zonedir): + entrypath = os.path.join(zonedir, entry) + tf.add(entrypath, entry) + finally: + shutil.rmtree(tmpdir) + + +def _print_on_nosuchfile(e): + """Print helpful troubleshooting message + + e is an exception raised by subprocess.check_call() + + """ + if e.errno == 2: + logging.error( + "Could not find zic. Perhaps you need to install " + "libc-bin or some other package that provides it, " + "or it's not in your PATH?") diff --git a/src/gam.py b/src/gam.py index a24fb13f..69eed83b 100755 --- a/src/gam.py +++ b/src/gam.py @@ -51,6 +51,7 @@ import shlex from multiprocessing import Pool from multiprocessing import freeze_support +import dateutil.parser import dns.resolver import googleapiclient @@ -601,6 +602,8 @@ def SetGlobalVariables(): _getOldEnvVar(GC_DRIVE_MAX_RESULTS, u'GAM_DRIVE_MAX_RESULTS') _getOldEnvVar(GC_MEMBER_MAX_RESULTS, u'GAM_MEMBER_MAX_RESULTS') _getOldEnvVar(GC_USER_MAX_RESULTS, u'GAM_USER_MAX_RESULTS') + _getOldEnvVar(GC_CSV_HEADER_FILTER, u'GAM_CSV_HEADER_FILTER') + _getOldEnvVar(GC_CSV_ROW_FILTER, u'GAM_CSV_ROW_FILTER') _getOldSignalFile(GC_DEBUG_LEVEL, u'debug.gam', filePresentValue=4, fileAbsentValue=0) _getOldSignalFile(GC_NO_VERIFY_SSL, u'noverifyssl.txt') _getOldSignalFile(GC_NO_BROWSER, u'nobrowser.txt') @@ -10652,12 +10655,46 @@ def sortCSVTitles(firstTitle, titles): titles.insert(0, title) def writeCSVfile(csvRows, titles, list_type, todrive): + if GC_Values[GC_CSV_ROW_FILTER]: + row_dict = json.loads(GC_Values[GC_CSV_ROW_FILTER]) + for match_column, filter_str in row_dict.items(): + if filter_str.lower()[:4] == u'date': + new_csvRows = [] + direction = filter_str[4] + if direction not in [u'<', u'>']: + systemErrorExit(3, u'%s is not a valid filter date direction' % direction) + try: + date_str = filter_str[5:] + filter_date = dateutil.parser.parse(date_str, ignoretz=True) + except ValueError: + systemErrorExit(3, u'%s is not a date GAM understands' % filter_str) + for row in csvRows: + try: + row_date = dateutil.parser.parse(row[match_column], ignoretz=True) + except ValueError: + row_date = dateutil.parser.parse(u'1/1/1970') + if direction == u'<' and row_date < filter_date: + new_csvRows.append(row) + elif direction == u'>' and row_date > filter_date: + new_csvRows.append(row) + csvRows = new_csvRows + else: + if filter_str.lower()[:6] == u'regex': + filter_str = filter_str[6:] + if match_column not in titles: + sys.stderr.write(u'WARNING: Row filter %s is not in output columns\n' % match_column) + continue + regex = re.compile(filter_str) + csvRows = [row for row in csvRows if regex.search(row.get(match_column, u''))] + if GC_Values[GC_CSV_HEADER_FILTER]: + titles_filter = GC_Values[GC_CSV_HEADER_FILTER].lower().split(u',') + titles = [t for t in titles if t.lower() in titles_filter] csv.register_dialect(u'nixstdout', lineterminator=u'\n') if todrive: - string_file = StringIO.StringIO() - writer = csv.DictWriter(string_file, fieldnames=titles, dialect=u'nixstdout', quoting=csv.QUOTE_MINIMAL) + write_to = StringIO.StringIO() else: - writer = csv.DictWriter(sys.stdout, fieldnames=titles, dialect=u'nixstdout', quoting=csv.QUOTE_MINIMAL) + write_to = sys.stdout + writer = csv.DictWriter(write_to, fieldnames=titles, dialect=u'nixstdout', extrasaction=u'ignore', quoting=csv.QUOTE_MINIMAL) try: writer.writerow(dict((item, item) for item in writer.fieldnames)) writer.writerows(csvRows) diff --git a/src/var.py b/src/var.py index e85b4ed8..2d1b53e7 100644 --- a/src/var.py +++ b/src/var.py @@ -721,6 +721,10 @@ GC_SHOW_GETTINGS = u'show_gettings' GC_SITE_DIR = u'site_dir' # When retrieving lists of Users from API, how many should be retrieved in each chunk GC_USER_MAX_RESULTS = u'user_max_results' +# CSV Columns GAM should show on CSV output +GC_CSV_HEADER_FILTER = u'csv_header_filter' +# CSV Rows GAM should filter +GC_CSV_ROW_FILTER = u'csv_row_filter' GC_Defaults = { GC_ACTIVITY_MAX_RESULTS: 100, @@ -750,6 +754,8 @@ GC_Defaults = { GC_SHOW_GETTINGS: True, GC_SITE_DIR: u'', GC_USER_MAX_RESULTS: 500, + GC_CSV_HEADER_FILTER: u'', + GC_CSV_ROW_FILTER: u'', } GC_Values = {} @@ -794,6 +800,8 @@ GC_VAR_INFO = { GC_SHOW_GETTINGS: {GC_VAR_TYPE: GC_TYPE_BOOLEAN}, GC_SITE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY}, GC_USER_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 500)}, + GC_CSV_HEADER_FILTER: {GC_VAR_TYPE: GC_TYPE_STRING}, + GC_CSV_ROW_FILTER: {GC_VAR_TYPE: GC_TYPE_STRING}, } # Google API constants