From bfb80f68894336d5afe2d22f98d6672b4be32a98 Mon Sep 17 00:00:00 2001 From: marco Date: Sun, 16 Aug 2020 12:55:12 +0200 Subject: [PATCH] alcuni fix per kodi 19 --- core/downloader.py | 2 +- core/scrapertools.py | 8 +- core/support.py | 4 +- core/tvdb.py | 4 +- core/videolibrarytools.py | 4 +- lib/babelfish/converters/__init__.py | 1 - lib/babelfish/country.py | 6 +- lib/babelfish/language.py | 6 +- lib/babelfish/script.py | 6 +- lib/dateutil/__init__.py | 15 +- lib/dateutil/_common.py | 43 + lib/dateutil/_version.py | 4 + lib/dateutil/easter.py | 51 +- lib/dateutil/parser.py | 886 -------- lib/dateutil/parser/__init__.py | 61 + lib/dateutil/parser/_parser.py | 1609 ++++++++++++++ lib/dateutil/parser/isoparser.py | 411 ++++ lib/dateutil/relativedelta.py | 559 +++-- lib/dateutil/rrule.py | 1217 ++++++++--- lib/dateutil/tz.py | 958 --------- lib/dateutil/tz/__init__.py | 12 + lib/dateutil/tz/_common.py | 419 ++++ lib/dateutil/tz/_factories.py | 80 + lib/dateutil/tz/tz.py | 1849 +++++++++++++++++ lib/dateutil/tz/win.py | 370 ++++ lib/dateutil/tzwin.py | 182 +- lib/dateutil/utils.py | 71 + lib/dateutil/zoneinfo/__init__.py | 220 +- .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 153315 bytes lib/dateutil/zoneinfo/rebuild.py | 53 + lib/dateutil/zoneinfo/zoneinfo-2012c.tar.gz | Bin 86841 -> 0 bytes platformcode/download_and_play.py | 2 +- platformcode/updater.py | 2 +- platformcode/xbmc_videolibrary.py | 4 +- servers/hdplayer.json | 41 - servers/hdplayer.py | 18 - specials/downloads.py | 14 +- specials/kodfavorites.py | 2 +- specials/news.py | 6 +- specials/resolverdns.py | 37 +- specials/search.py | 4 +- specials/videolibrary.py | 4 +- 42 files changed, 6510 insertions(+), 2735 deletions(-) create mode 100644 lib/dateutil/_common.py create mode 100644 lib/dateutil/_version.py delete mode 100644 lib/dateutil/parser.py create mode 100644 lib/dateutil/parser/__init__.py create mode 100644 lib/dateutil/parser/_parser.py create mode 100644 lib/dateutil/parser/isoparser.py delete mode 100644 lib/dateutil/tz.py create mode 100644 lib/dateutil/tz/__init__.py create mode 100644 lib/dateutil/tz/_common.py create mode 100644 lib/dateutil/tz/_factories.py create mode 100644 lib/dateutil/tz/tz.py create mode 100644 lib/dateutil/tz/win.py create mode 100644 lib/dateutil/utils.py create mode 100644 lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 lib/dateutil/zoneinfo/rebuild.py delete mode 100644 lib/dateutil/zoneinfo/zoneinfo-2012c.tar.gz delete mode 100644 servers/hdplayer.json delete mode 100644 servers/hdplayer.py diff --git a/core/downloader.py b/core/downloader.py index 3cb52c33..b714b474 100644 --- a/core/downloader.py +++ b/core/downloader.py @@ -113,7 +113,7 @@ class Downloader(object): line2 = config.get_localized_string(59983) % ( self.downloaded[1], self.downloaded[2], self.size[1], self.size[2], self.speed[1], self.speed[2], self.connections[0], self.connections[1]) line3 = config.get_localized_string(60202) % (self.remaining_time) - progreso.update(int(self.progress), line1, line2 + " " + line3) + progreso.update(int(self.progress), line1 + '\n' + line2 + " " + line3) self.__update_json() finally: progreso.close() diff --git a/core/scrapertools.py b/core/scrapertools.py index 43631578..c31fae9e 100644 --- a/core/scrapertools.py +++ b/core/scrapertools.py @@ -85,7 +85,7 @@ def decodeHtmlentities(data): if match.group(1) == "#" and ent.replace(";", "").isdigit(): ent = unichr(int(ent.replace(";", ""))) - return ent.encode('utf-8') + return ent if PY3 else ent.encode('utf-8') else: cp = html5.get(ent) if cp: @@ -112,9 +112,11 @@ def unescape(text): # character reference try: if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)).encode("utf-8") + ret = unichr(int(text[3:-1], 16)) + return ret if PY3 else ret.encode("utf-8") else: - return unichr(int(text[2:-1])).encode("utf-8") + ret = unichr(int(text[2:-1])) + return ret if PY3 else ret.encode("utf-8") except ValueError: logger.error("error de valor") diff --git a/core/support.py b/core/support.py index 41388039..28135a6d 100755 --- a/core/support.py +++ b/core/support.py @@ -21,8 +21,10 @@ from core.item import Item from lib import unshortenit from platformcode import config from platformcode.logger import log +from platformcode import logger from specials import autoplay + def hdpass_get_servers(item): def get_hosts(url, quality): ret = [] @@ -466,7 +468,7 @@ def scrape(func): # if url may be changed and channel has findhost to update if 'findhost' in func.__globals__ and not itemlist: - logger.info('running findhost ' + func.__module__) + log('running findhost ' + func.__module__) host = func.__globals__['findhost']() parse = list(urlparse.urlparse(item.url)) from core import jsontools diff --git a/core/tvdb.py b/core/tvdb.py index fd56e864..c6d9f56d 100644 --- a/core/tvdb.py +++ b/core/tvdb.py @@ -100,12 +100,12 @@ def find_and_set_infoLabels(item): otvdb_global = Tvdb(tvdb_id=item.infoLabels['tvdb_id']) if not item.contentSeason: - p_dialog.update(50, config.get_localized_string(60296), config.get_localized_string(60295)) + p_dialog.update(50, config.get_localized_string(60296) + '\n' + config.get_localized_string(60295)) results, info_load = otvdb_global.get_list_results() logger.debug("results: %s" % results) if not item.contentSeason: - p_dialog.update(100, config.get_localized_string(60296), config.get_localized_string(60297) % len(results)) + p_dialog.update(100, config.get_localized_string(60296) + '\n' + config.get_localized_string(60297) % len(results)) p_dialog.close() if len(results) > 1: diff --git a/core/videolibrarytools.py b/core/videolibrarytools.py index a3771bdc..fe1ba427 100644 --- a/core/videolibrarytools.py +++ b/core/videolibrarytools.py @@ -205,7 +205,7 @@ def save_movie(item, silent=False): logger.error(traceback.format_exc()) if filetools.write(json_path, item.tojson()): - if not silent: p_dialog.update(100, config.get_localized_string(60062), item.contentTitle) + if not silent: p_dialog.update(100, config.get_localized_string(60062) + '\n' + item.contentTitle) item_nfo.library_urls[item.channel] = item.url if filetools.write(nfo_path, head_nfo + item_nfo.tojson()): @@ -221,7 +221,7 @@ def save_movie(item, silent=False): # If we get to this point it is because something has gone wrong logger.error("Could not save %s in the video library" % item.contentTitle) if not silent: - p_dialog.update(100, config.get_localized_string(60063), item.contentTitle) + p_dialog.update(100, config.get_localized_string(60063) + '\n' + item.contentTitle) p_dialog.close() return 0, 0, -1, path diff --git a/lib/babelfish/converters/__init__.py b/lib/babelfish/converters/__init__.py index 5dc98ddf..210be98d 100755 --- a/lib/babelfish/converters/__init__.py +++ b/lib/babelfish/converters/__init__.py @@ -245,7 +245,6 @@ class ConverterManager(object): def parse(str): import re match = re.match('(?P\w+) = (?P[a-z0-9.]+):(?P\w+)', str) - print match.groupdict() return match.groupdict() for ep in (parse(c) for c in self.registered_converters + self.internal_converters): if ep.get('name') == name: diff --git a/lib/babelfish/country.py b/lib/babelfish/country.py index 8e5bf80f..a9ebb66c 100755 --- a/lib/babelfish/country.py +++ b/lib/babelfish/country.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from collections import namedtuple from functools import partial # from pkg_resources import resource_stream # @UnresolvedImport -import os +import os, io from .converters import ConverterManager from . import basestr @@ -19,10 +19,10 @@ COUNTRY_MATRIX = [] #: The namedtuple used in the :data:`COUNTRY_MATRIX` IsoCountry = namedtuple('IsoCountry', ['name', 'alpha2']) -f = open(os.path.join(os.path.dirname(__file__), 'data/iso-3166-1.txt')) +f = io.open(os.path.join(os.path.dirname(__file__), 'data/iso-3166-1.txt'), encoding='utf-8') f.readline() for l in f: - iso_country = IsoCountry(*l.decode('utf-8').strip().split(';')) + iso_country = IsoCountry(*l.strip().split(';')) COUNTRIES[iso_country.alpha2] = iso_country.name COUNTRY_MATRIX.append(iso_country) f.close() diff --git a/lib/babelfish/language.py b/lib/babelfish/language.py index 9b5e7825..fd1b7232 100755 --- a/lib/babelfish/language.py +++ b/lib/babelfish/language.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from collections import namedtuple from functools import partial -import os +import os, io # from pkg_resources import resource_stream # @UnresolvedImport from .converters import ConverterManager from .country import Country @@ -22,10 +22,10 @@ LANGUAGE_MATRIX = [] #: The namedtuple used in the :data:`LANGUAGE_MATRIX` IsoLanguage = namedtuple('IsoLanguage', ['alpha3', 'alpha3b', 'alpha3t', 'alpha2', 'scope', 'type', 'name', 'comment']) -f = open(os.path.join(os.path.dirname(__file__), 'data/iso-639-3.tab')) +f = io.open(os.path.join(os.path.dirname(__file__), 'data/iso-639-3.tab'), encoding='utf-8') f.readline() for l in f: - iso_language = IsoLanguage(*l.decode('utf-8').split('\t')) + iso_language = IsoLanguage(*l.split('\t')) LANGUAGES.add(iso_language.alpha3) LANGUAGE_MATRIX.append(iso_language) f.close() diff --git a/lib/babelfish/script.py b/lib/babelfish/script.py index 5b3970a2..ce418906 100755 --- a/lib/babelfish/script.py +++ b/lib/babelfish/script.py @@ -6,7 +6,7 @@ # from __future__ import unicode_literals -import os +import os, io from collections import namedtuple # from pkg_resources import resource_stream # @UnresolvedImport from . import basestr @@ -20,10 +20,10 @@ SCRIPT_MATRIX = [] #: The namedtuple used in the :data:`SCRIPT_MATRIX` IsoScript = namedtuple('IsoScript', ['code', 'number', 'name', 'french_name', 'pva', 'date']) -f = open(os.path.join(os.path.dirname(__file__), 'data/iso15924-utf8-20131012.txt')) +f = io.open(os.path.join(os.path.dirname(__file__), 'data/iso15924-utf8-20131012.txt'), encoding='utf-8') f.readline() for l in f: - l = l.decode('utf-8').strip() + l = l.strip() if not l or l.startswith('#'): continue script = IsoScript._make(l.split(';')) diff --git a/lib/dateutil/__init__.py b/lib/dateutil/__init__.py index 9a42dcea..0defb82e 100644 --- a/lib/dateutil/__init__.py +++ b/lib/dateutil/__init__.py @@ -1,9 +1,8 @@ -""" -Copyright (c) 2003-2010 Gustavo Niemeyer +# -*- coding: utf-8 -*- +try: + from ._version import version as __version__ +except ImportError: + __version__ = 'unknown' -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" -__version__ = "1.5.0.1" +__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', + 'utils', 'zoneinfo'] diff --git a/lib/dateutil/_common.py b/lib/dateutil/_common.py new file mode 100644 index 00000000..4eb2659b --- /dev/null +++ b/lib/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/lib/dateutil/_version.py b/lib/dateutil/_version.py new file mode 100644 index 00000000..eac12096 --- /dev/null +++ b/lib/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.1' diff --git a/lib/dateutil/easter.py b/lib/dateutil/easter.py index d7944104..53b7c789 100644 --- a/lib/dateutil/easter.py +++ b/lib/dateutil/easter.py @@ -1,19 +1,17 @@ +# -*- coding: utf-8 -*- """ -Copyright (c) 2003-2007 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. +This module offers a generic easter computing method for any given year, using +Western, Orthodox or Julian algorithms. """ -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" import datetime __all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] -EASTER_JULIAN = 1 +EASTER_JULIAN = 1 EASTER_ORTHODOX = 2 -EASTER_WESTERN = 3 +EASTER_WESTERN = 3 + def easter(year, method=EASTER_WESTERN): """ @@ -25,7 +23,7 @@ def easter(year, method=EASTER_WESTERN): 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 @@ -35,24 +33,24 @@ def easter(year, method=EASTER_WESTERN): These methods are represented by the constants: - EASTER_JULIAN = 1 - EASTER_ORTHODOX = 2 - EASTER_WESTERN = 3 + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` The default method is method 3. - + More about the algorithm may be found at: - http://users.chariot.net.au/~gmarts/eastalg.htm + `GM Arts: Easter Algorithms `_ and - http://www.tondering.dk/claus/calendar.html + `The Calendar FAQ: Easter `_ """ if not (1 <= method <= 3): - raise ValueError, "invalid method" + raise ValueError("invalid method") # g - Golden year - 1 # c - Century @@ -69,24 +67,23 @@ def easter(year, method=EASTER_WESTERN): e = 0 if method < 3: # Old method - i = (19*g+15)%30 - j = (y+y//4+i)%7 + 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 + 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 + 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)) - + 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/lib/dateutil/parser.py b/lib/dateutil/parser.py deleted file mode 100644 index 5d824e41..00000000 --- a/lib/dateutil/parser.py +++ /dev/null @@ -1,886 +0,0 @@ -# -*- coding:iso-8859-1 -*- -""" -Copyright (c) 2003-2007 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime -import string -import time -import sys -import os - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -import relativedelta -import tz - - -__all__ = ["parse", "parserinfo"] - - -# Some pointers: -# -# http://www.cl.cam.ac.uk/~mgk25/iso-time.html -# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html -# http://www.w3.org/TR/NOTE-datetime -# http://ringmaster.arc.nasa.gov/tools/time_formats.html -# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm -# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html - - -class _timelex(object): - - def __init__(self, instream): - if isinstance(instream, basestring): - instream = StringIO(instream) - self.instream = instream - self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_' - 'ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' - 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ') - self.numchars = '0123456789' - self.whitespace = ' \t\r\n' - self.charstack = [] - self.tokenstack = [] - self.eof = False - - def get_token(self): - if self.tokenstack: - return self.tokenstack.pop(0) - seenletters = False - token = None - state = None - wordchars = self.wordchars - numchars = self.numchars - whitespace = self.whitespace - while not self.eof: - 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: - token = nextchar - if nextchar in wordchars: - state = 'a' - elif nextchar in numchars: - state = '0' - elif nextchar in whitespace: - token = ' ' - break # emit token - else: - break # emit token - elif state == 'a': - seenletters = True - if nextchar in wordchars: - token += nextchar - elif nextchar == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0': - if nextchar in numchars: - token += nextchar - elif nextchar == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == 'a.': - seenletters = True - if nextchar == '.' or nextchar in wordchars: - token += nextchar - elif nextchar in numchars and token[-1] == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0.': - if nextchar == '.' or nextchar in numchars: - token += nextchar - elif nextchar in wordchars 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] == '.')): - l = token.split('.') - token = l[0] - for tok in l[1:]: - self.tokenstack.append('.') - if tok: - self.tokenstack.append(tok) - return token - - def __iter__(self): - return self - - def next(self): - token = self.get_token() - if token is None: - raise StopIteration - return token - - def split(cls, s): - return list(cls(s)) - split = classmethod(split) - - -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, `value`)) - return "%s(%s)" % (classname, ", ".join(l)) - - def __repr__(self): - return self._repr(self.__class__.__name__) - - -class parserinfo(object): - - # 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"), - ("Wed", "Wednesday"), - ("Thu", "Thursday"), - ("Fri", "Friday"), - ("Sat", "Saturday"), - ("Sun", "Sunday")] - MONTHS = [("Jan", "January"), - ("Feb", "February"), - ("Mar", "March"), - ("Apr", "April"), - ("May", "May"), - ("Jun", "June"), - ("Jul", "July"), - ("Aug", "August"), - ("Sep", "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"] - PERTAIN = ["of"] - TZOFFSET = {} - - 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 in range(len(lst)): - v = lst[i] - 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): - if len(name) >= 3: - try: - return self._weekdays[name.lower()] - except KeyError: - pass - return None - - def month(self, name): - if len(name) >= 3: - 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): - if year < 100: - year += self._century - if abs(year-self._year) >= 50: - if year < self._year: - year += 100 - else: - year -= 100 - return year - - def validate(self, res): - # move to info - if res.year is not None: - res.year = self.convertyear(res.year) - if res.tzoffset == 0 and not res.tzname 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 parser(object): - - def __init__(self, info=None): - self.info = info or parserinfo() - - def parse(self, timestr, default=None, - ignoretz=False, tzinfos=None, - **kwargs): - if not default: - default = datetime.datetime.now().replace(hour=0, minute=0, - second=0, microsecond=0) - res = self._parse(timestr, **kwargs) - if res is None: - raise ValueError, "unknown string format" - repl = {} - for attr in ["year", "month", "day", "hour", - "minute", "second", "microsecond"]: - value = getattr(res, attr) - if value is not None: - repl[attr] = value - ret = default.replace(**repl) - if res.weekday is not None and not res.day: - ret = ret+relativedelta.relativedelta(weekday=res.weekday) - if not ignoretz: - if callable(tzinfos) or tzinfos and res.tzname in tzinfos: - if callable(tzinfos): - tzdata = tzinfos(res.tzname, res.tzoffset) - else: - tzdata = tzinfos.get(res.tzname) - if isinstance(tzdata, datetime.tzinfo): - tzinfo = tzdata - elif isinstance(tzdata, basestring): - tzinfo = tz.tzstr(tzdata) - elif isinstance(tzdata, int): - tzinfo = tz.tzoffset(res.tzname, tzdata) - else: - raise ValueError, "offset must be tzinfo subclass, " \ - "tz string, or int offset" - ret = ret.replace(tzinfo=tzinfo) - elif res.tzname and res.tzname in time.tzname: - ret = ret.replace(tzinfo=tz.tzlocal()) - elif res.tzoffset == 0: - ret = ret.replace(tzinfo=tz.tzutc()) - elif res.tzoffset: - ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) - return ret - - class _result(_resultbase): - __slots__ = ["year", "month", "day", "weekday", - "hour", "minute", "second", "microsecond", - "tzname", "tzoffset"] - - def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False): - info = self.info - if dayfirst is None: - dayfirst = info.dayfirst - if yearfirst is None: - yearfirst = info.yearfirst - res = self._result() - l = _timelex.split(timestr) - try: - - # year/month/day list - ymd = [] - - # Index of the month string in ymd - mstridx = -1 - - len_l = len(l) - i = 0 - while i < len_l: - - # Check if it's a number - try: - value_repr = l[i] - value = float(value_repr) - except ValueError: - value = None - - if value is not None: - # Token is a number - len_li = len(l[i]) - i += 1 - if (len(ymd) == 3 and len_li in (2, 4) - and (i >= len_l or (l[i] != ':' and - info.hms(l[i]) is None))): - # 19990101T23[59] - s = l[i-1] - res.hour = int(s[:2]) - if len_li == 4: - res.minute = int(s[2:]) - elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): - # YYMMDD or HHMMSS[.ss] - s = l[i-1] - if not ymd and l[i-1].find('.') == -1: - ymd.append(info.convertyear(int(s[:2]))) - ymd.append(int(s[2:4])) - ymd.append(int(s[4:])) - else: - # 19990101T235959[.59] - res.hour = int(s[:2]) - res.minute = int(s[2:4]) - res.second, res.microsecond = _parsems(s[4:]) - elif len_li == 8: - # YYYYMMDD - s = l[i-1] - ymd.append(int(s[:4])) - ymd.append(int(s[4:6])) - ymd.append(int(s[6:])) - elif len_li in (12, 14): - # YYYYMMDDhhmm[ss] - s = l[i-1] - ymd.append(int(s[:4])) - ymd.append(int(s[4:6])) - ymd.append(int(s[6:8])) - res.hour = int(s[8:10]) - res.minute = int(s[10:12]) - if len_li == 14: - res.second = int(s[12:]) - elif ((i < len_l and info.hms(l[i]) is not None) or - (i+1 < len_l and l[i] == ' ' and - info.hms(l[i+1]) is not None)): - # HH[ ]h or MM[ ]m or SS[.ss][ ]s - if l[i] == ' ': - i += 1 - idx = info.hms(l[i]) - while True: - if idx == 0: - res.hour = int(value) - if value%1: - res.minute = int(60*(value%1)) - elif idx == 1: - res.minute = int(value) - if value%1: - res.second = int(60*(value%1)) - elif idx == 2: - res.second, res.microsecond = \ - _parsems(value_repr) - i += 1 - if i >= len_l or idx == 2: - break - # 12h00 - try: - value_repr = l[i] - value = float(value_repr) - except ValueError: - break - else: - i += 1 - idx += 1 - if i < len_l: - newidx = info.hms(l[i]) - if newidx is not None: - idx = newidx - elif i+1 < len_l and l[i] == ':': - # HH:MM[:SS[.ss]] - res.hour = int(value) - i += 1 - value = float(l[i]) - res.minute = int(value) - if value%1: - res.second = int(60*(value%1)) - i += 1 - if i < len_l and l[i] == ':': - res.second, res.microsecond = _parsems(l[i+1]) - i += 2 - elif i < len_l and l[i] in ('-', '/', '.'): - sep = l[i] - ymd.append(int(value)) - i += 1 - if i < len_l and not info.jump(l[i]): - try: - # 01-01[-01] - ymd.append(int(l[i])) - except ValueError: - # 01-Jan[-01] - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - else: - return None - i += 1 - if i < len_l and l[i] == sep: - # We have three members - i += 1 - value = info.month(l[i]) - if value is not None: - ymd.append(value) - mstridx = len(ymd)-1 - assert mstridx == -1 - else: - ymd.append(int(l[i])) - i += 1 - elif i >= len_l or info.jump(l[i]): - if i+1 < len_l and info.ampm(l[i+1]) is not None: - # 12 am - res.hour = int(value) - if res.hour < 12 and info.ampm(l[i+1]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i+1]) == 0: - res.hour = 0 - i += 1 - else: - # Year, month or day - ymd.append(int(value)) - i += 1 - elif info.ampm(l[i]) is not None: - # 12am - res.hour = int(value) - if res.hour < 12 and info.ampm(l[i]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i]) == 0: - res.hour = 0 - i += 1 - elif not fuzzy: - return None - else: - i += 1 - continue - - # Check weekday - value = info.weekday(l[i]) - if value is not None: - res.weekday = value - i += 1 - continue - - # Check month name - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - i += 1 - if i < len_l: - if l[i] in ('-', '/'): - # Jan-01[-99] - sep = l[i] - i += 1 - ymd.append(int(l[i])) - i += 1 - if i < len_l and l[i] == sep: - # Jan-01-99 - i += 1 - ymd.append(int(l[i])) - i += 1 - elif (i+3 < len_l and l[i] == l[i+2] == ' ' - and info.pertain(l[i+1])): - # Jan of 01 - # In this case, 01 is clearly year - try: - value = int(l[i+3]) - except ValueError: - # Wrong guess - pass - else: - # Convert it here to become unambiguous - ymd.append(info.convertyear(value)) - i += 4 - continue - - # Check am/pm - value = info.ampm(l[i]) - if value is not None: - if value == 1 and res.hour < 12: - res.hour += 12 - elif value == 0 and res.hour == 12: - res.hour = 0 - i += 1 - continue - - # Check for a timezone name - if (res.hour is not None and len(l[i]) <= 5 and - res.tzname is None and res.tzoffset is None and - not [x for x in l[i] if x not in string.ascii_uppercase]): - res.tzname = l[i] - res.tzoffset = info.tzoffset(res.tzname) - i += 1 - - # 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 < len_l and l[i] in ('+', '-'): - l[i] = ('+', '-')[l[i] == '+'] - res.tzoffset = None - if info.utczone(res.tzname): - # With something like GMT+3, the timezone - # is *not* GMT. - res.tzname = None - - continue - - # Check for a numbered timezone - if res.hour is not None and l[i] in ('+', '-'): - signal = (-1,1)[l[i] == '+'] - i += 1 - len_li = len(l[i]) - if len_li == 4: - # -0300 - res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - res.tzoffset = int(l[i])*3600+int(l[i+2])*60 - i += 2 - elif len_li <= 2: - # -[0]3 - res.tzoffset = int(l[i][:2])*3600 - else: - return None - i += 1 - res.tzoffset *= signal - - # Look for a timezone name between parenthesis - if (i+3 < len_l and - info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and - 3 <= len(l[i+2]) <= 5 and - not [x for x in l[i+2] - if x not in string.ascii_uppercase]): - # -0300 (BRST) - res.tzname = l[i+2] - i += 4 - continue - - # Check jumps - if not (info.jump(l[i]) or fuzzy): - return None - - i += 1 - - # Process year/month/day - len_ymd = len(ymd) - if len_ymd > 3: - # More than three members!? - return None - elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): - # One member, or two members with a month string - if mstridx != -1: - res.month = ymd[mstridx] - del ymd[mstridx] - if len_ymd > 1 or mstridx == -1: - if ymd[0] > 31: - res.year = ymd[0] - else: - res.day = ymd[0] - elif len_ymd == 2: - # Two members with numbers - if ymd[0] > 31: - # 99-01 - res.year, res.month = ymd - elif ymd[1] > 31: - # 01-99 - res.month, res.year = ymd - elif dayfirst and ymd[1] <= 12: - # 13-01 - res.day, res.month = ymd - else: - # 01-13 - res.month, res.day = ymd - if len_ymd == 3: - # Three members - if mstridx == 0: - res.month, res.day, res.year = ymd - elif mstridx == 1: - if ymd[0] > 31 or (yearfirst and ymd[2] <= 31): - # 99-Jan-01 - res.year, res.month, res.day = ymd - else: - # 01-Jan-01 - # Give precendence to day-first, since - # two-digit years is usually hand-written. - res.day, res.month, res.year = ymd - elif mstridx == 2: - # WTF!? - if ymd[1] > 31: - # 01-99-Jan - res.day, res.year, res.month = ymd - else: - # 99-01-Jan - res.year, res.day, res.month = ymd - else: - if ymd[0] > 31 or \ - (yearfirst and ymd[1] <= 12 and ymd[2] <= 31): - # 99-01-01 - res.year, res.month, res.day = ymd - elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12): - # 13-01-01 - res.day, res.month, res.year = ymd - else: - # 01-13-01 - res.month, res.day, res.year = ymd - - except (IndexError, ValueError, AssertionError): - return None - - if not info.validate(res): - return None - return res - -DEFAULTPARSER = parser() -def parse(timestr, parserinfo=None, **kwargs): - 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 = _timelex.split(tzstr) - 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]) - 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] == '+'] - 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) - i += 2 - elif len_li <= 2: - # -[0]3 - setattr(res, offattr, - int(l[i][:2])*3600*signal) - else: - return None - 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]) - i += 2 - if l[i] == '-': - value = int(l[i+1])*-1 - i += 1 - else: - value = int(l[i]) - i += 2 - if value: - x.week = value - x.weekday = (int(l[i])-1)%7 - else: - x.day = int(l[i]) - i += 2 - x.time = int(l[i]) - i += 2 - if i < len_l: - if l[i] in ('-','+'): - signal = (-1,1)[l[i] == "+"] - i += 1 - else: - signal = 1 - res.dstoffset = (res.stdoffset+int(l[i]))*signal - 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) - i += 1 - x.jyday = int(l[i]) - elif l[i] == 'M': - # month[-.]week[-.]weekday - i += 1 - x.month = int(l[i]) - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.week = int(l[i]) - if x.week == 5: - x.week = -1 - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.weekday = (int(l[i])-1)%7 - else: - # year day (zero based) - x.yday = int(l[i])+1 - - i += 1 - - if i < len_l and l[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 - i += 2 - if i+1 < len_l and l[i+1] == ':': - i += 2 - x.time += int(l[i]) - elif len_li <= 2: - # -[0]3 - x.time = (int(l[i][:2])*3600) - else: - return None - i += 1 - - assert i == len_l or l[i] == ',' - - i += 1 - - assert i >= len_l - - except (IndexError, ValueError, AssertionError): - return None - - return res - - -DEFAULTTZPARSER = _tzparser() -def _parsetz(tzstr): - return DEFAULTTZPARSER.parse(tzstr) - - -def _parsems(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]) - - -# vim:ts=4:sw=4:et diff --git a/lib/dateutil/parser/__init__.py b/lib/dateutil/parser/__init__.py new file mode 100644 index 00000000..d174b0e4 --- /dev/null +++ b/lib/dateutil/parser/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo, ParserError +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + 'ParserError', + '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/lib/dateutil/parser/_parser.py b/lib/dateutil/parser/_parser.py new file mode 100644 index 00000000..458aa6a3 --- /dev/null +++ b/lib/dateutil/parser/_parser.py @@ -0,0 +1,1609 @@ +# -*- 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", "ParserError"] + + +# 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 leap year + 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 precedence 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 ParserError: + 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 ParserError("Unknown string format: %s", timestr) + + if len(res) == 0: + raise ParserError("String does not contain a date: %s", timestr) + + try: + ret = self._build_naive(res, default) + except ValueError as e: + six.raise_from(ParserError(e.args[0] + ": %s", timestr), e) + + 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" preceding 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 *don't* 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 _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) + + # ------------------------------------------------------------------ + # Handling for individual tokens. These are kept as methods instead + # of functions for the sake of customizability via subclassing. + + 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 _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 + + # ------------------------------------------------------------------ + # Post-Parsing construction of datetime output. These are kept as + # methods instead of functions for the sake of customizability via + # subclassing. + + 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) + else: + raise TypeError("Offset must be tzinfo subclass, tz string, " + "or int offset.") + 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.UTC) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.UTC) + + 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 _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 + + +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 ParserError(ValueError): + """Error class for representing failure to parse a datetime string.""" + def __str__(self): + try: + return self.args[0] % self.args[1:] + except (TypeError, IndexError): + return super(ParserError, self).__str__() + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, str(self)) + + +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/lib/dateutil/parser/isoparser.py b/lib/dateutil/parser/isoparser.py new file mode 100644 index 00000000..48f86a33 --- /dev/null +++ b/lib/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.UTC + + 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.UTC + 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/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py index 9735819b..a9e85f7e 100644 --- a/lib/dateutil/relativedelta.py +++ b/lib/dateutil/relativedelta.py @@ -1,109 +1,105 @@ -""" -Copyright (c) 2003-2010 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - +# -*- 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 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 __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) - -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) - -class relativedelta: +class relativedelta(object): """ -The relativedelta type is based on the specification of the excelent -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. + 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. -There's two different ways to build a relativedelta instance. The -first one is passing it two date/datetime classes: + 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. - relativedelta(datetime1, datetime2) + There are two different ways to build a relativedelta instance. The + first one is passing it two date/datetime classes:: -And the other way is to use the following keyword arguments: + relativedelta(datetime1, datetime2) - year, month, day, hour, minute, second, microsecond: - Absolute information. + The second one is passing it any number of the following keyword arguments:: - years, months, weeks, days, hours, minutes, seconds, microseconds: - Relative information, may be negative. + relativedelta(arg1=x,arg2=y,arg3=z...) - weekday: - One of the weekday instances (MO, TU, etc). 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. + 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. - leapdays: - Will add given days to the date found, if year is a leap - year, and the date found is post 28 of february. + 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 arithmetic operation on the original datetime value + with the information in the relativedelta. - yearday, nlyearday: - Set the yearday or the non-leap year day (jump leap days). - These are converted to day/month/leapdays information. + 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). -Here is the behavior of operations with relativedelta: + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. -1) Calculate the absolute year, using the 'year' argument, or the - original datetime year, if the argument is not present. + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. -2) Add the relative 'years' argument to the absolute year. + 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). -3) Do steps 1 and 2 for month/months. + The order of attributes considered when this relativedelta is + added to a datetime is: -4) Calculate the absolute day, using the 'day' argument, or the - original datetime day, if the argument is not present. Then, - subtract from the day until it fits in the year and month - found after their operations. + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds -5) Add the relative 'days' argument to the absolute day. Notice - that the 'weeks' argument is multiplied by 7 and added to - 'days'. + Finally, weekday is applied, using the rule described above. -6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, - microsecond/microseconds. + 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. -7) If the 'weekday' argument is present, calculate the weekday, - with the given (wday, nth) tuple. wday is the index of the - weekday (0-6, 0=Mon), and nth is the number of weeks to add - forward or backward, depending on its signal. Notice that if - the calculated date is already Monday, for example, using - (0, 1) or (0, -1) won't change the day. """ def __init__(self, dt1=None, dt2=None, @@ -112,15 +108,22 @@ Here is the behavior of operations with relativedelta: year=None, month=None, day=None, weekday=None, yearday=None, nlyearday=None, hour=None, minute=None, second=None, microsecond=None): + if dt1 and dt2: - if not isinstance(dt1, datetime.date) or \ - not isinstance(dt2, datetime.date): - raise TypeError, "relativedelta only diffs datetime/date" - if type(dt1) is not type(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 @@ -139,31 +142,48 @@ Here is the behavior of operations with relativedelta: self.microsecond = None self._has_time = 0 - months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) + # 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: - while dt1 > dtm: - months += 1 - self._set_months(months) - dtm = self.__radd__(dt2) + compare = operator.gt + increment = 1 else: - while dt1 < dtm: - months -= 1 - self._set_months(months) - dtm = self.__radd__(dt2) + 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.seconds = delta.seconds + delta.days * 86400 self.microseconds = delta.microseconds else: - self.years = years - self.months = months - self.days = days+weeks*7 + # 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 @@ -172,7 +192,15 @@ Here is the behavior of operations with relativedelta: self.second = second self.microsecond = microsecond - if type(weekday) is int: + 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 @@ -185,7 +213,8 @@ Here is the behavior of operations with relativedelta: if yearday > 59: self.leapdays = -1 if yday: - ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366] + 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 @@ -195,56 +224,143 @@ Here is the behavior of operations with relativedelta: self.day = yday-ydayidx[idx-1] break else: - raise ValueError, "invalid year day (%d)" % yday + raise ValueError("invalid year day (%d)" % yday) self._fix() def _fix(self): if abs(self.microseconds) > 999999: - s = self.microseconds//abs(self.microseconds) - div, mod = divmod(self.microseconds*s, 1000000) - self.microseconds = mod*s - self.seconds += div*s + 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 = self.seconds//abs(self.seconds) - div, mod = divmod(self.seconds*s, 60) - self.seconds = mod*s - self.minutes += div*s + 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 = self.minutes//abs(self.minutes) - div, mod = divmod(self.minutes*s, 60) - self.minutes = mod*s - self.hours += div*s + 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 = self.hours//abs(self.hours) - div, mod = divmod(self.hours*s, 24) - self.hours = mod*s - self.days += div*s + 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 = self.months//abs(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): + 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 = self.months//abs(self.months) - div, mod = divmod(self.months*s, 12) - self.months = mod*s - self.years = div*s + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years = div * s else: self.years = 0 - def __radd__(self, other): + 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): - raise TypeError, "unsupported type for add operation" + 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 @@ -276,60 +392,70 @@ Here is the behavior of operations with relativedelta: microseconds=self.microseconds)) if self.weekday: weekday, nth = self.weekday.weekday, self.weekday.n or 1 - jumpdays = (abs(nth)-1)*7 + jumpdays = (abs(nth) - 1) * 7 if nth > 0: - jumpdays += (7-ret.weekday()+weekday)%7 + jumpdays += (7 - ret.weekday() + weekday) % 7 else: - jumpdays += (ret.weekday()-weekday)%7 + 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 __add__(self, other): - if not isinstance(other, relativedelta): - raise TypeError, "unsupported type for add operation" - return relativedelta(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 or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=other.second or self.microsecond) - def __sub__(self, other): if not isinstance(other, relativedelta): - raise TypeError, "unsupported type for sub operation" - return relativedelta(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 or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=other.second or self.microsecond) + 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 relativedelta(years=-self.years, + return self.__class__(years=-self.years, months=-self.months, days=-self.days, hours=-self.hours, @@ -346,7 +472,7 @@ Here is the behavior of operations with relativedelta: second=self.second, microsecond=self.microsecond) - def __nonzero__(self): + def __bool__(self): return not (not self.years and not self.months and not self.days and @@ -363,29 +489,37 @@ Here is the behavior of operations with relativedelta: 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): - f = float(other) - return relativedelta(years = int(round(self.years*f)), - months = int(round(self.months*f)), - days = int(round(self.days*f)), - hours = int(round(self.hours*f)), - minutes = int(round(self.minutes*f)), - seconds = int(round(self.seconds*f)), - microseconds = 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) + 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 False + return NotImplemented if self.weekday or other.weekday: if not self.weekday or not other.weekday: return False @@ -400,6 +534,7 @@ Here is the behavior of operations with relativedelta: 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 @@ -409,11 +544,38 @@ Here is the behavior of operations with relativedelta: 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): - return self.__mul__(1/float(other)) + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) + + __truediv__ = __div__ def __repr__(self): l = [] @@ -421,12 +583,17 @@ Here is the behavior of operations with relativedelta: "hours", "minutes", "seconds", "microseconds"]: value = getattr(self, attr) if value: - l.append("%s=%+d" % (attr, 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("%s=%s" % (attr, `value`)) - return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + 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/lib/dateutil/rrule.py b/lib/dateutil/rrule.py index 34722dff..6bf0ea9c 100644 --- a/lib/dateutil/rrule.py +++ b/lib/dateutil/rrule.py @@ -1,96 +1,104 @@ +# -*- coding: utf-8 -*- """ -Copyright (c) 2003-2010 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. +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. """ -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - import itertools import datetime import calendar -import thread -import heapq +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 + +# 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+ +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 = range(1,30), range(1,31), range(1,32) +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 = range(-29,0), range(-30,0), range(-31,0) +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 +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) = range(7) + SECONDLY) = list(range(7)) # Imported on demand. easter = None parser = None -class weekday(object): - __slots__ = ["weekday", "n"] - def __init__(self, weekday, n=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" - self.weekday = weekday - self.n = n + raise ValueError("Can't create weekday with n==0") - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) + super(weekday, self).__init__(wkday, 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 __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) +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) -class rrulebase: +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._cache_gen = self._iter() - self._cache_complete = False + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() else: self._cache = None self._cache_complete = False - self._len = None + self._len = None def __iter__(self): if self._cache_complete: @@ -100,6 +108,17 @@ class rrulebase: 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 @@ -113,7 +132,7 @@ class rrulebase: break try: for j in range(10): - cache.append(gen.next()) + cache.append(advance_iterator(gen)) except StopIteration: self._cache_gen = gen = None self._cache_complete = True @@ -134,13 +153,13 @@ class rrulebase: else: return list(itertools.islice(self, item.start or 0, - item.stop or sys.maxint, + item.stop or sys.maxsize, item.step or 1)) elif item >= 0: gen = iter(self) try: for i in range(item+1): - res = gen.next() + res = advance_iterator(gen) except StopIteration: raise IndexError return res @@ -158,13 +177,19 @@ class rrulebase: return False return False - # __len__() introduces a large performance penality. + # __len__() introduces a large performance penalty. 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 + 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: @@ -183,6 +208,9 @@ class rrulebase: 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: @@ -197,7 +225,52 @@ class rrulebase: return i return None - def between(self, after, before, inc=False): + 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: @@ -226,17 +299,143 @@ class rrulebase: 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): - rrulebase.__init__(self, cache) + super(rrule, self).__init__(cache) global easter if not dtstart: - dtstart = datetime.datetime.now().replace(microsecond=0) + 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: @@ -246,18 +445,46 @@ class rrule(rrulebase): 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 type(wkst) is int: + elif isinstance(wkst, integer_types): self._wkst = wkst else: self._wkst = wkst.weekday + if bysetpos is None: self._bysetpos = None - elif type(bysetpos) is int: + 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") @@ -268,121 +495,192 @@ class rrule(rrulebase): if pos == 0 or not (-366 <= pos <= 366): raise ValueError("bysetpos must be between 1 and 366, " "or between -366 and -1") - if not (byweekno or byyearday or bymonthday or - byweekday is not None or byeaster is not None): + + 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 not bymonth: + 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 not bymonth: + if bymonth is None: self._bymonth = None - elif type(bymonth) is int: - self._bymonth = (bymonth,) else: - self._bymonth = tuple(bymonth) + 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 not byyearday: + if byyearday is None: self._byyearday = None - elif type(byyearday) is int: - self._byyearday = (byyearday,) else: - self._byyearday = tuple(byyearday) + 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 type(byeaster) is int: + if isinstance(byeaster, integer_types): self._byeaster = (byeaster,) else: - self._byeaster = tuple(byeaster) + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster else: self._byeaster = None - # bymonthay - if not bymonthday: + + # bymonthday + if bymonthday is None: self._bymonthday = () self._bynmonthday = () - elif type(bymonthday) is int: - if bymonthday < 0: - self._bynmonthday = (bymonthday,) - self._bymonthday = () - else: - self._bymonthday = (bymonthday,) - self._bynmonthday = () else: - self._bymonthday = tuple([x for x in bymonthday if x > 0]) - self._bynmonthday = tuple([x for x in bymonthday if x < 0]) + 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 - elif type(byweekno) is int: - self._byweekno = (byweekno,) else: - self._byweekno = tuple(byweekno) + 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 - elif type(byweekday) is int: - self._byweekday = (byweekday,) - self._bynweekday = None - elif hasattr(byweekday, "n"): - if not byweekday.n or freq > MONTHLY: - self._byweekday = (byweekday.weekday,) - self._bynweekday = None - else: - self._bynweekday = ((byweekday.weekday, byweekday.n),) - self._byweekday = None else: - self._byweekday = [] - self._bynweekday = [] + # 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 type(wday) is int: - self._byweekday.append(wday) + if isinstance(wday, integer_types): + self._byweekday.add(wday) elif not wday.n or freq > MONTHLY: - self._byweekday.append(wday.weekday) + self._byweekday.add(wday.weekday) else: - self._bynweekday.append((wday.weekday, wday.n)) - self._byweekday = tuple(self._byweekday) - self._bynweekday = tuple(self._bynweekday) + 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,) + self._byhour = {dtstart.hour} else: self._byhour = None - elif type(byhour) is int: - self._byhour = (byhour,) else: - self._byhour = tuple(byhour) + 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,) + self._byminute = {dtstart.minute} else: self._byminute = None - elif type(byminute) is int: - self._byminute = (byminute,) else: - self._byminute = tuple(byminute) + 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,) + self._bysecond = ((dtstart.second,)) else: self._bysecond = None - elif type(bysecond) is int: - self._bysecond = (bysecond,) else: - self._bysecond = tuple(bysecond) + 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 @@ -392,11 +690,87 @@ class rrule(rrulebase): for minute in self._byminute: for second in self._bysecond: self._timeset.append( - datetime.time(hour, minute, second, - tzinfo=self._tzinfo)) + 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() @@ -421,20 +795,20 @@ class rrule(rrulebase): 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] - + 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] + 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 @@ -463,11 +837,10 @@ class rrule(rrulebase): 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)))): + ((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 @@ -481,7 +854,7 @@ class rrule(rrulebase): daypos, timepos = divmod(pos-1, len(timeset)) try: i = [x for x in dayset[start:end] - if x is not None][daypos] + if x is not None][daypos] time = timeset[timepos] except IndexError: pass @@ -496,31 +869,32 @@ class rrule(rrulebase): self._len = total return elif res >= self._dtstart: - total += 1 - yield res - if count: + if count is not None: count -= 1 - if not count: + 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) + 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: - total += 1 - yield res - if count: + if count is not None: count -= 1 - if not count: + if count < 0: self._len = total return + total += 1 + yield res + # Handle frequency and interval fixday = False if freq == YEARLY: @@ -556,60 +930,86 @@ class rrule(rrulebase): if filtered: # Jump to one iteration before next day hour += ((23-hour)//interval)*interval - while True: - hour += interval - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - if not byhour or hour in byhour: - break + + 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 - while True: - minute += interval - div, mod = divmod(minute, 60) + + 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: - minute = mod - hour += div - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - filtered = False - if ((not byhour or hour in byhour) and - (not byminute or minute in byminute)): + 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) - while True: - second += self._interval - div, mod = divmod(second, 60) + 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: - second = mod - minute += div - div, mod = divmod(minute, 60) + hour += div + div, hour = divmod(hour, 24) if div: - minute = mod - hour += div - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True + 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)): + (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: @@ -627,6 +1027,86 @@ class rrule(rrulebase): 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", @@ -642,8 +1122,8 @@ class _iterinfo(object): # 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) + 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() @@ -666,13 +1146,13 @@ class _iterinfo(object): 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 + # 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 + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 else: # Number of days in the year, minus the days we # left in last year. @@ -717,23 +1197,23 @@ class _iterinfo(object): # 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 + 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 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 else: - lnumweeks = 52+(self.yearlen-no1wkst)%7//4 + 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)): + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): ranges = [] if rr._freq == YEARLY: if rr._bymonth: @@ -752,10 +1232,10 @@ class _iterinfo(object): for wday, n in rr._bynweekday: if n < 0: i = last+(n+1)*7 - i -= (self.wdaymask[i]-wday)%7 + i -= (self.wdaymask[i]-wday) % 7 else: i = first+(n-1)*7 - i += (7-self.wdaymask[i]+wday)%7 + i += (7-self.wdaymask[i]+wday) % 7 if first <= i <= last: self.nwdaymask[i] = 1 @@ -769,53 +1249,53 @@ class _iterinfo(object): self.lastmonth = month def ydayset(self, year, month, day): - return range(self.yearlen), 0, self.yearlen + return list(range(self.yearlen)), 0, self.yearlen def mdayset(self, year, month, day): - set = [None]*self.yearlen + dset = [None]*self.yearlen start, end = self.mrange[month-1:month+1] for i in range(start, end): - set[i] = i - return set, start, end + dset[i] = i + return dset, start, end def wdayset(self, year, month, day): # We need to handle cross-year weeks here. - set = [None]*(self.yearlen+7) + dset = [None]*(self.yearlen+7) i = datetime.date(year, month, day).toordinal()-self.yearordinal start = i for j in range(7): - set[i] = i + dset[i] = i i += 1 - #if (not (0 <= i < self.yearlen) or + # 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 set, start, i + return dset, start, i def ddayset(self, year, month, day): - set = [None]*self.yearlen - i = datetime.date(year, month, day).toordinal()-self.yearordinal - set[i] = i - return set, i, i+1 + 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): - set = [] + tset = [] rr = self.rrule for minute in rr._byminute: for second in rr._bysecond: - set.append(datetime.time(hour, minute, second, - tzinfo=rr._tzinfo)) - set.sort() - return set + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset def mtimeset(self, hour, minute, second): - set = [] + tset = [] rr = self.rrule for second in rr._bysecond: - set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) - set.sort() - return set + 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, @@ -823,20 +1303,26 @@ class _iterinfo(object): 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: - class _genitem: + :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 = gen() + self.dt = advance_iterator(gen) genlist.append(self) except StopIteration: pass self.genlist = genlist self.gen = gen - def next(self): + def __next__(self): try: - self.dt = self.gen() + self.dt = advance_iterator(self.gen) except StopIteration: if self.genlist[0] is self: heapq.heappop(self.genlist) @@ -844,61 +1330,133 @@ class rruleset(rrulebase): self.genlist.remove(self) heapq.heapify(self.genlist) - def __cmp__(self, other): - return cmp(self.dt, other.dt) + 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): - rrulebase.__init__(self, cache) + 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).next) - for gen in [iter(x).next for x in self._rrule]: + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: self._genitem(rlist, gen) - heapq.heapify(rlist) exlist = [] self._exdate.sort() - self._genitem(exlist, iter(self._exdate).next) - for gen in [iter(x).next for x in self._exrule]: + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: self._genitem(exlist, gen) - heapq.heapify(exlist) 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] - exitem.next() + 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 - ritem.next() + advance_iterator(ritem) if rlist and rlist[0] is ritem: heapq.heapreplace(rlist, ritem) self._len = total -class _rrulestr: + + + +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, @@ -908,7 +1466,8 @@ class _rrulestr: "MINUTELY": MINUTELY, "SECONDLY": SECONDLY} - _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6} + _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) @@ -916,17 +1475,17 @@ class _rrulestr: 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_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 + _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] @@ -937,23 +1496,37 @@ class _rrulestr: from dateutil import parser try: rrkwargs["until"] = parser.parse(value, - ignoretz=kwargs.get("ignoretz"), - tzinfos=kwargs.get("tzinfos")) + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) except ValueError: - raise ValueError, "invalid until date" + 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, **kwarsg): + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ l = [] for wday in value.split(','): - 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) + 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 @@ -967,7 +1540,7 @@ class _rrulestr: if line.find(':') != -1: name, value = line.split(':') if name != "RRULE": - raise ValueError, "unknown parameter name" + raise ValueError("unknown parameter name") else: value = line rrkwargs = {} @@ -980,11 +1553,63 @@ class _rrulestr: ignoretz=ignoretz, tzinfos=tzinfos) except AttributeError: - raise ValueError, "unknown parameter '%s'" % name + raise ValueError("unknown parameter '%s'" % name) except (KeyError, ValueError): - raise ValueError, "invalid '%s': %s" % (name, value) + 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, @@ -992,14 +1617,20 @@ class _rrulestr: 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" + raise ValueError("empty string") if unfold: lines = s.splitlines() i = 0 @@ -1007,15 +1638,15 @@ class _rrulestr: line = lines[i].rstrip() if not line: del lines[i] - elif i > 0 and line[0] in (" ", "\t"): + 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:'))): + 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) @@ -1034,65 +1665,60 @@ class _rrulestr: name, value = line.split(':', 1) parms = name.split(';') if not parms: - raise ValueError, "empty property name" + raise ValueError("empty property name") name = parms[0] parms = parms[1:] if name == "RRULE": for parm in parms: - raise ValueError, "unsupported RRULE parm: "+parm + 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 + raise ValueError("unsupported RDATE parm: "+parm) rdatevals.append(value) elif name == "EXRULE": for parm in parms: - raise ValueError, "unsupported EXRULE parm: "+parm + raise ValueError("unsupported EXRULE parm: "+parm) exrulevals.append(value) elif name == "EXDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError, "unsupported RDATE parm: "+parm - exdatevals.append(value) + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) elif name == "DTSTART": - for parm in parms: - raise ValueError, "unsupported DTSTART parm: "+parm - if not parser: - from dateutil import parser - dtstart = parser.parse(value, ignoretz=ignoretz, - tzinfos=tzinfos) - elif name.upper().startswith('X-'): - # Ignore experimental properties. - pass + 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): + 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 - set = rruleset(cache=cache) + rset = rruleset(cache=cache) for value in rrulevals: - set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in rdatevals: - for datestr in value.split(','): - set.rdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in exrulevals: - set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, ignoretz=ignoretz, tzinfos=tzinfos)) - for value in exdatevals: + for value in rdatevals: for datestr in value.split(','): - set.exdate(parser.parse(datestr, + 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: - set.rdate(dtstart) - return set + rset.rdate(dtstart) + return rset else: return self._parse_rfc_rrule(rrulevals[0], dtstart=dtstart, @@ -1103,6 +1729,7 @@ class _rrulestr: def __call__(self, s, **kwargs): return self._parse_rfc(s, **kwargs) + rrulestr = _rrulestr() # vim:ts=4:sw=4:et diff --git a/lib/dateutil/tz.py b/lib/dateutil/tz.py deleted file mode 100644 index 2c15a78e..00000000 --- a/lib/dateutil/tz.py +++ /dev/null @@ -1,958 +0,0 @@ -""" -Copyright (c) 2003-2007 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime -import struct -import time -import sys -import os - -relativedelta = None -parser = None -rrule = None - -__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", - "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] - -try: - from dateutil.tzwin import tzwin, tzwinlocal -except (ImportError, OSError): - tzwin, tzwinlocal = None, None - -ZERO = datetime.timedelta(0) -EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() - -class tzutc(datetime.tzinfo): - - def utcoffset(self, dt): - return ZERO - - def dst(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def __eq__(self, other): - return (isinstance(other, tzutc) or - (isinstance(other, tzoffset) and other._offset == ZERO)) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class tzoffset(datetime.tzinfo): - - def __init__(self, name, offset): - self._name = name - self._offset = datetime.timedelta(seconds=offset) - - def utcoffset(self, dt): - return self._offset - - def dst(self, dt): - return ZERO - - def tzname(self, dt): - return self._name - - def __eq__(self, other): - return (isinstance(other, tzoffset) and - self._offset == other._offset) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(%s, %s)" % (self.__class__.__name__, - `self._name`, - self._offset.days*86400+self._offset.seconds) - - __reduce__ = object.__reduce__ - -class tzlocal(datetime.tzinfo): - - _std_offset = datetime.timedelta(seconds=-time.timezone) - if time.daylight: - _dst_offset = datetime.timedelta(seconds=-time.altzone) - else: - _dst_offset = _std_offset - - def utcoffset(self, dt): - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset - - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO - - def tzname(self, dt): - return time.tzname[self._isdst(dt)] - - def _isdst(self, dt): - # 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: - # - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - return time.localtime(timestamp+time.timezone).tm_isdst - - def __eq__(self, other): - if not isinstance(other, tzlocal): - return False - return (self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset) - return True - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class _ttinfo(object): - __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] - - 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, `value`)) - return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) - - def __eq__(self, other): - if not isinstance(other, _ttinfo): - return False - 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) - - def __ne__(self, other): - return not self.__eq__(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(datetime.tzinfo): - - # http://www.twinsun.com/tz/tz-link.htm - # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz - - def __init__(self, fileobj): - if isinstance(fileobj, basestring): - self._filename = fileobj - fileobj = open(fileobj) - elif hasattr(fileobj, "name"): - self._filename = fileobj.name - else: - self._filename = `fileobj` - - # 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) != "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: - self._trans_list = struct.unpack(">%dl" % timecnt, - fileobj.read(timecnt*4)) - else: - self._trans_list = [] - - # 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: - self._trans_idx = struct.unpack(">%dB" % timecnt, - fileobj.read(timecnt)) - else: - self._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) - - # 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 - if leapcnt: - leap = struct.unpack(">%dl" % (leapcnt*2), - fileobj.read(leapcnt*8)) - - # 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)) - - # ** Everything has been read ** - - # Build ttinfo list - self._ttinfo_list = [] - for i in range(typecnt): - gmtoff, isdst, abbrind = ttinfo[i] - # 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 for some information. - gmtoff = (gmtoff+30)//60*60 - tti = _ttinfo() - tti.offset = gmtoff - 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) - self._ttinfo_list.append(tti) - - # Replace ttinfo indexes for ttinfo objects. - trans_idx = [] - for idx in self._trans_idx: - trans_idx.append(self._ttinfo_list[idx]) - self._trans_idx = tuple(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. - self._ttinfo_std = None - self._ttinfo_dst = None - self._ttinfo_before = None - if self._ttinfo_list: - if not self._trans_list: - self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] - else: - for i in range(timecnt-1,-1,-1): - tti = self._trans_idx[i] - if not self._ttinfo_std and not tti.isdst: - self._ttinfo_std = tti - elif not self._ttinfo_dst and tti.isdst: - self._ttinfo_dst = tti - if self._ttinfo_std and self._ttinfo_dst: - break - else: - if self._ttinfo_dst and not self._ttinfo_std: - self._ttinfo_std = self._ttinfo_dst - - for tti in self._ttinfo_list: - if not tti.isdst: - self._ttinfo_before = tti - break - else: - self._ttinfo_before = self._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. - laststdoffset = 0 - self._trans_list = list(self._trans_list) - for i in range(len(self._trans_list)): - tti = self._trans_idx[i] - if not tti.isdst: - # This is std time. - self._trans_list[i] += tti.offset - laststdoffset = tti.offset - else: - # This is dst time. Convert to std. - self._trans_list[i] += laststdoffset - self._trans_list = tuple(self._trans_list) - - def _find_ttinfo(self, dt, laststd=0): - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - idx = 0 - for trans in self._trans_list: - if timestamp < trans: - break - idx += 1 - else: - return self._ttinfo_std - if idx == 0: - return self._ttinfo_before - if laststd: - while idx > 0: - tti = self._trans_idx[idx-1] - if not tti.isdst: - return tti - idx -= 1 - else: - return self._ttinfo_std - else: - return self._trans_idx[idx-1] - - def utcoffset(self, dt): - if not self._ttinfo_std: - return ZERO - return self._find_ttinfo(dt).delta - - def dst(self, dt): - 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.delta-self._find_ttinfo(dt, laststd=1).delta - - # An alternative for that would be: - # - # return self._ttinfo_dst.offset-self._ttinfo_std.offset - # - # However, this class stores historical changes in the - # dst offset, so I belive that this wouldn't be the right - # way to implement this. - - def tzname(self, dt): - if not self._ttinfo_std: - return None - return self._find_ttinfo(dt).abbr - - def __eq__(self, other): - if not isinstance(other, tzfile): - return False - return (self._trans_list == other._trans_list and - self._trans_idx == other._trans_idx and - self._ttinfo_list == other._ttinfo_list) - - def __ne__(self, other): - return not self.__eq__(other) - - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, `self._filename`) - - def __reduce__(self): - if not os.path.isfile(self._filename): - raise ValueError, "Unpickable %s class" % self.__class__.__name__ - return (self.__class__, (self._filename,)) - -class tzrange(datetime.tzinfo): - - def __init__(self, stdabbr, stdoffset=None, - dstabbr=None, dstoffset=None, - start=None, end=None): - global relativedelta - if not relativedelta: - from dateutil import relativedelta - self._std_abbr = stdabbr - self._dst_abbr = dstabbr - 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 - - def utcoffset(self, dt): - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset - - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO - - def tzname(self, dt): - if self._isdst(dt): - return self._dst_abbr - else: - return self._std_abbr - - def _isdst(self, dt): - if not self._start_delta: - return False - year = datetime.datetime(dt.year,1,1) - start = year+self._start_delta - end = year+self._end_delta - dt = dt.replace(tzinfo=None) - if start < end: - return dt >= start and dt < end - else: - return dt >= start or dt < end - - def __eq__(self, other): - if not isinstance(other, tzrange): - return False - 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) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(...)" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class tzstr(tzrange): - - def __init__(self, s): - global parser - if not parser: - from dateutil import parser - self._s = s - - res = parser._parsetz(s) - if res is None: - 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"): - 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) - - def _delta(self, x, isend=0): - 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__, `self._s`) - -class _tzicalvtzcomp: - 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(datetime.tzinfo): - def __init__(self, tzid, comps=[]): - self._tzid = tzid - self._comps = comps - self._cachedate = [] - self._cachecomp = [] - - def _find_comp(self, dt): - if len(self._comps) == 1: - return self._comps[0] - dt = dt.replace(tzinfo=None) - try: - return self._cachecomp[self._cachedate.index(dt)] - except ValueError: - pass - lastcomp = None - lastcompdt = None - for comp in self._comps: - if not comp.isdst: - # Handle the extra hour in DST -> STD - compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) - else: - compdt = comp.rrule.before(dt, inc=True) - 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] - self._cachedate.insert(0, dt) - self._cachecomp.insert(0, lastcomp) - if len(self._cachedate) > 10: - self._cachedate.pop() - self._cachecomp.pop() - return lastcomp - - def utcoffset(self, dt): - return self._find_comp(dt).tzoffsetto - - def dst(self, dt): - comp = self._find_comp(dt) - if comp.isdst: - return comp.tzoffsetdiff - else: - return ZERO - - def tzname(self, dt): - return self._find_comp(dt).tzname - - def __repr__(self): - return "" % `self._tzid` - - __reduce__ = object.__reduce__ - -class tzical: - def __init__(self, fileobj): - global rrule - if not rrule: - from dateutil import rrule - - if isinstance(fileobj, basestring): - self._s = fileobj - fileobj = open(fileobj) - elif hasattr(fileobj, "name"): - self._s = fileobj.name - else: - self._s = `fileobj` - - self._vtz = {} - - self._parse_rfc(fileobj.read()) - - def keys(self): - return self._vtz.keys() - - def get(self, tzid=None): - if tzid is None: - keys = self._vtz.keys() - if len(keys) == 0: - raise ValueError, "no timezones defined" - elif len(keys) > 1: - raise ValueError, "more than one timezone available" - tzid = keys[0] - 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] in (" ", "\t"): - 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": - 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 - elif name.upper().startswith('X-'): - # Ignore experimental properties. - pass - else: - raise ValueError, "unsupported property: "+name - else: - if name == "TZID": - for p in parms: - if not p.upper().startswith('X-'): - raise ValueError, \ - "unsupported TZID parm: "+p - tzid = value - elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): - pass - elif name.upper().startswith('X-'): - # Ignore experimental properties. - 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__, `self._s`) - -if sys.platform != "win32": - TZFILES = ["/etc/localtime", "localtime"] - TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] -else: - TZFILES = [] - TZPATHS = [] - -def gettz(name=None): - 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: - try: - tz = tzwin(name) - except OSError: - pass - if not tz: - from dateutil.zoneinfo import gettz - tz = gettz(name) - if not tz: - for c in name: - # name must have at least one offset to be a tzstr - 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 - -# vim:ts=4:sw=4:et diff --git a/lib/dateutil/tz/__init__.py b/lib/dateutil/tz/__init__.py new file mode 100644 index 00000000..af1352c4 --- /dev/null +++ b/lib/dateutil/tz/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from .tz import * +from .tz import __doc__ + +__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/lib/dateutil/tz/_common.py b/lib/dateutil/tz/_common.py new file mode 100644 index 00000000..e6ac1183 --- /dev/null +++ b/lib/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 + occurrence, 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 + occurrence, 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/lib/dateutil/tz/_factories.py b/lib/dateutil/tz/_factories.py new file mode 100644 index 00000000..f8a65891 --- /dev/null +++ b/lib/dateutil/tz/_factories.py @@ -0,0 +1,80 @@ +from datetime import timedelta +import weakref +from collections import OrderedDict + +from six.moves import _thread + + +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 + + cls._cache_lock = _thread.allocate_lock() + + 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)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls._cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + 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 + + cls.__cache_lock = _thread.allocate_lock() + + 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)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls.__cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + diff --git a/lib/dateutil/tz/tz.py b/lib/dateutil/tz/tz.py new file mode 100644 index 00000000..af81e88e --- /dev/null +++ b/lib/dateutil/tz/tz.py @@ -0,0 +1,1849 @@ +# -*- 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__ + + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + + +@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 that 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: + try: + if name.startswith(":"): + name = name[1:] + except TypeError as e: + if isinstance(name, bytes): + new_msg = "gettz argument should be str, not bytes" + six.raise_from(TypeError(new_msg), e) + else: + raise + 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 = UTC + 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(UTC).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 contextlib 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/lib/dateutil/tz/win.py b/lib/dateutil/tz/win.py new file mode 100644 index 00000000..cde07ba7 --- /dev/null +++ b/lib/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/lib/dateutil/tzwin.py b/lib/dateutil/tzwin.py index 073e0ff6..cebc673e 100644 --- a/lib/dateutil/tzwin.py +++ b/lib/dateutil/tzwin.py @@ -1,180 +1,2 @@ -# This code was originally contributed by Jeffrey Harris. -import datetime -import struct -import _winreg - -__author__ = "Jeffrey Harris & Gustavo Niemeyer " - -__all__ = ["tzwin", "tzwinlocal"] - -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(): - global TZKEYNAME - handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) - try: - _winreg.OpenKey(handle, TZKEYNAMENT).Close() - TZKEYNAME = TZKEYNAMENT - except WindowsError: - TZKEYNAME = TZKEYNAME9X - handle.Close() - -_settzkeyname() - -class tzwinbase(datetime.tzinfo): - """tzinfo class based on win32's timezones available in the registry.""" - - def utcoffset(self, dt): - if self._isdst(dt): - return datetime.timedelta(minutes=self._dstoffset) - else: - return datetime.timedelta(minutes=self._stdoffset) - - def dst(self, dt): - if self._isdst(dt): - minutes = self._dstoffset - self._stdoffset - return datetime.timedelta(minutes=minutes) - else: - return datetime.timedelta(0) - - def tzname(self, dt): - if self._isdst(dt): - return self._dstname - else: - return self._stdname - - def list(): - """Return a list of all time zones known to the system.""" - handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) - tzkey = _winreg.OpenKey(handle, TZKEYNAME) - result = [_winreg.EnumKey(tzkey, i) - for i in range(_winreg.QueryInfoKey(tzkey)[0])] - tzkey.Close() - handle.Close() - return result - list = staticmethod(list) - - def display(self): - return self._display - - def _isdst(self, dt): - dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, - self._dsthour, self._dstminute, - self._dstweeknumber) - dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, - self._stdhour, self._stdminute, - self._stdweeknumber) - if dston < dstoff: - return dston <= dt.replace(tzinfo=None) < dstoff - else: - return not dstoff <= dt.replace(tzinfo=None) < dston - - -class tzwin(tzwinbase): - - def __init__(self, name): - self._name = name - - handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) - tzkey = _winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name)) - keydict = valuestodict(tzkey) - tzkey.Close() - handle.Close() - - self._stdname = keydict["Std"].encode("iso-8859-1") - self._dstname = keydict["Dlt"].encode("iso-8859-1") - - self._display = keydict["Display"] - - # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack("=3l16h", keydict["TZI"]) - self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 - self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 - - (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] - - def __repr__(self): - return "tzwin(%s)" % repr(self._name) - - def __reduce__(self): - return (self.__class__, (self._name,)) - - -class tzwinlocal(tzwinbase): - - def __init__(self): - - handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) - - tzlocalkey = _winreg.OpenKey(handle, TZLOCALKEYNAME) - keydict = valuestodict(tzlocalkey) - tzlocalkey.Close() - - self._stdname = keydict["StandardName"].encode("iso-8859-1") - self._dstname = keydict["DaylightName"].encode("iso-8859-1") - - try: - tzkey = _winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname)) - _keydict = valuestodict(tzkey) - self._display = _keydict["Display"] - tzkey.Close() - except OSError: - self._display = None - - handle.Close() - - self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] - self._dstoffset = self._stdoffset-keydict["DaylightBias"] - - - # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack("=8h", keydict["StandardStart"]) - - (self._stdmonth, - self._stddayofweek, # Sunday = 0 - self._stdweeknumber, # Last = 5 - self._stdhour, - self._stdminute) = tup[1:6] - - tup = struct.unpack("=8h", keydict["DaylightStart"]) - - (self._dstmonth, - self._dstdayofweek, # Sunday = 0 - self._dstweeknumber, # Last = 5 - self._dsthour, - self._dstminute) = tup[1:6] - - 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) - weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1)) - for n in xrange(whichweek): - dt = weekdayone+(whichweek-n)*ONEWEEK - if dt.month == month: - return dt - -def valuestodict(key): - """Convert a registry key's values to a dictionary.""" - dict = {} - size = _winreg.QueryInfoKey(key)[1] - for i in range(size): - data = _winreg.EnumValue(key, i) - dict[data[0]] = data[1] - return dict +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/lib/dateutil/utils.py b/lib/dateutil/utils.py new file mode 100644 index 00000000..44d9c994 --- /dev/null +++ b/lib/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 ``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/lib/dateutil/zoneinfo/__init__.py b/lib/dateutil/zoneinfo/__init__.py index cc326064..34f11ad6 100644 --- a/lib/dateutil/zoneinfo/__init__.py +++ b/lib/dateutil/zoneinfo/__init__.py @@ -1,85 +1,167 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer +# -*- coding: utf-8 -*- +import warnings +import json -This module offers extensions to the standard python 2.3+ -datetime module. -""" -from dateutil.tz import tzfile from tarfile import TarFile -import os +from pkgutil import get_data +from io import BytesIO -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" +from dateutil.tz import tzfile as _tzfile -__all__ = ["setcachesize", "gettz", "rebuild"] +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] -CACHE = {} +ZONEFILENAME = "dateutil-zoneinfo.tar.gz" +METADATA_FN = 'METADATA' -class tzfile(tzfile): + +class tzfile(_tzfile): def __reduce__(self): return (gettz, (self._filename,)) -def getzoneinfofile(): - filenames = os.listdir(os.path.join(os.path.dirname(__file__))) - filenames.sort() - filenames.reverse() - for entry in filenames: - if entry.startswith("zoneinfo") and ".tar." in entry: - return os.path.join(os.path.dirname(__file__), entry) - return None -def buildcache(): - global CACHE - zoneinfofile = getzoneinfofile() - if zoneinfofile: - tf = TarFile.open(zoneinfofile) - try: - for tarinfo in tf.getmembers(): - if tarinfo.islnk() or tarinfo.isfile(): - zonefile = tf.extractfile(tarinfo) - CACHE[tarinfo.name] = tzfile(zonefile) - finally: - tf.close() +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 -buildcache() -del getzoneinfofile -del buildcache +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 setcachesize(_): - # Since the cache now eagerly initialized at - # import time, there's no point in controlling - # its size. - pass def gettz(name): - return CACHE.get(name) + """ + This retrieves a time zone from the local zoneinfo tarball that is packaged + with dateutil. -def rebuild(filename, tag=None, format="gz"): - import tempfile, shutil - tmpdir = tempfile.mkdtemp() - zonedir = os.path.join(tmpdir, "zoneinfo") - moduledir = os.path.dirname(__file__) - if tag: tag = "-"+tag - targetname = "zoneinfo%s.tar.%s" % (tag, format) - try: - tf = TarFile.open(filename) - for name in tf.getnames(): - if not (name.endswith(".sh") or - name.endswith(".tab") or - name == "leapseconds"): - tf.extract(name, tmpdir) - filepath = os.path.join(tmpdir, name) - os.system("zic -d %s %s" % (zonedir, filepath)) - tf.close() - target = os.path.join(moduledir, targetname) - for entry in os.listdir(moduledir): - if entry.startswith("zoneinfo") and ".tar." in entry: - os.unlink(os.path.join(moduledir, entry)) - tf = TarFile.open(target, "w:%s" % format) - for entry in os.listdir(zonedir): - entrypath = os.path.join(zonedir, entry) - tf.add(entrypath, entry) - tf.close() - finally: - shutil.rmtree(tmpdir) + :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/lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..89e83517b562451622cea7cbe1dbfac5c3d2513e GIT binary patch literal 153315 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%0D!KtMVr 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

yja~WxFc`yfrj%BE^ zgT@-#KyX5_A7&&ff8<&bkbgToHR_9n62`GWGwRDXOZXVQZ%ttQWOz(DyDUVt6~{kO zTO;5$fof{IioMIfHEgCF20xs(mb*8>s9d6j?ar=5VxOmsJyIEvc@@&X-GYd8{Yb_c!>+W$Ctey={ zrn^rhvqb~i2C;Y3Cpd3k5*Xm=II2UsGy{;`kqHb4v@k5}(V0Ry=R)u$(jL1baz{c; zeyQyCcLp*^3@;P&Hk-3zvbfkJb$E9BsIzA_^AD{F6|2H`wLLISv6!2Ls@88h1CNl< zPqA5C0;b7a($dn+u-sH!Mh+~kW71gFTt?!-0x>;UsQ9n&0W9o-g$130-PGg@B7^rl zFx${I)QZG(vzB#^CKcO=!DR*>*z!TnZt6#F>zMn(pmei}*@dl(?vq*JwS z*%erZjDAY+S)U^%@>hB4Qr};#k!#bhOuK&71^6yqNoNnKX_toHTO3Vh%yxYSob{at zIjZQHf>?S!#pSA3ifU%-g-!@#^gJl1B!rZy!~K1zyA4hk;P z;`ma(nySAiYS9|5FG_59qvJ|roa0K3Oy6ssY*A?nb7p9`w$iH27}m;Z&dI;iToKC9 zxV{GkKP%%(-Du-W?cG8&u3JC>tG28r8(72l60OAgnz`hV92AH`e`s7EG8L3gjVF|@ zW+|2mf=LmFd^K{K-?Qj6pEY<(4#njPO6koLOR?lYbb8QSJ_yG4wV?DdvBh$Vxa3ew zZO&GYMsuZ8yws~DO~ch+w$!Ttq`vVQuG(BBhig)5GozygrPv!B3ebX5Q7YHbLU9n{ zzvk2>hh+aXr`3E;2-3L50(;p2QtN1qYX;PkLu8T?>qL+zwN#s-0b!KWY5~z<4p8GUy;D4)0R*Q*Pzv05vS2qOinUsq0?;Z5idZrqrIJ~-(LOcZESpF3*<B!fY`%-VM73 z*T+(3OD^$k(=IO9B=F*tQn*ud_=db6;l|;a>ymRM*UQrh1c~G>19X9kQNl9^H3X+qAQ2 zS#A8Zuxh^auE*~fm~GL{TYP}A2f1Gf1~!_7funBQVZK{U{nQQm3BIy^qeut(iuc_@ zk9YfSSn+L*aE-15$K_nwP4QuPO!2d&_xRLg%>MPtNt;`}`#(R=x{~pX)0os_`@;4s zsM9~v$-lHp6?8Y)W!5BAS%j#(_mg-`{Jryk#?h{LOEucJaNqhGE+np&B{t7}nF$PB z;aD%6|EMWvZhAIs7-D+%#-PUHY>nLcYZ!WC7Bcb|rBEI=c~i23SeWAtpH{<|&xGQO7li z-{TE=_t|TPQ7EL{7S{h$`x7l#1mV!M+ESTm4V50RX-#I^k!ltLaF%?4-BgOSNiTkC{?P#cHFz72>T4(vzW1n zK6oZJ;V^_x5mlOfW(YJpeOdgLzeIiitGTqPo9Jijb(^UlJ8gA7ACZa>= zcl`vG3xW>W&w5^;tZQ80Y!-0X82e0u(VJ7~tH<@nZj!HU~(F9e&HK(};80=2J)DIY1csNsKM{8}y;kPxtSc;r9&SobweBsEy_5|SO2&v zR#sfe4~cD7y>{{1lIIzXW^0jCjLGKmJ{4x-dA?(+ooh9gAJuBN;Wc;mx>{gM)yELb zZOp9FvawjMXgs~noGt7kZErwxJWkG%0qQ!|%r61be>yJlFM z)SHGEU0dDS>-~G6ZJTTUp?8)-DC>)rKEknp`>UN)KlL#>EWWo-bL6?_sQ{6wsFi?^y#oW)*t=v`%|}8Y72rHL|wX@P1N$-4P7>~*a!7av}>=)*}is{Ank@n zemsyfHk^2G;dMA@b8EvGbm{ta!+X&@zp$u~dGarbPhQb&M(M9T%0x~H!VE4K(H}KJ zH6yzEMu~++B$inXrWM@JDyahAmU-MXuwgJmtd84;@k_2 zuM86mMbyTiW{YHp+e&bUdpm=%Xrok80F#8mxDxXwJk6O*q%a+V5tFoao-QeXbv2T- z#jc%4_FbE2$VoPd0cH{-Er6wb2uhV`EaG$2V;_=WT0TVJ_oH4RV#dCvuS~=ww(#w-3jgpVGjMYNl6%$bn|+reyj;k)jBsQv_!{vt!|W@=I&a>|*(b}UQU67xX5 zl^4JY)^@D@KgRv@|1quq(}2vR$CH1H3Ez!n)VE#ABpg6FmD9^g{;(G<4?qT}4gtf` z#kE18at&H$uE?DIOeqeBH=Y6ha0f7#!TzDr8eD8~g)V9a!0-jwWGj8nYdzHXbSn{`(NqdI99b=^u%r zpr8m|b$~H|IR>9D3C5>olsn)Q`3+Vnz>v;*Mq04Dr$B(d8pY%E88w1c89C#F1##a{ zBIX$=Q&y#On5?D|3}eq`-eB)iYHb1vm2rcr#GW9iy2rM zhksmUw^QtRXAI8+?gplz8Hc*Y1V`9EEv#{b)ote|O}(aJ%wBb=CJq|;t@BNPt-QBx zuJ8K^Tgw!c>KZ&xZGQ1S_s;CrSEMi6?kMw0SjyzyAFnhJI4K7Iz*QpEBd$a%_x827 znp#g?Upci4a`($R+`9SQMSokocp7DH)nQS#XaaEYuHiGyz-H-}o-9R5(AdIw0m%%U zI06WIl-H+MBsy!B|6&LOk^CA%nr?7D2_F%Eg4!~D8+ja^I8f)?xNzv;2rjXk9?Re( zZef_2sTfWnRbc!wZSS($2+n$ikWh4t5NtF`M@OaFM?PIQ;q0_dCZ=-%3U(AgeGihT0i+}maUcw@n3 zX3Wd=A925F{+Pk?oj~)krvHkR&E1g-*cT`8g;DzL7BbUaeGmgZ2R#c8}o6d0|4XiKZodk+W?F?VmXxbKnVw5Sa-A2Pum#9M56Rs25flRhEh3az@;(Yu^X^C5+J^N2o4 z&OYV@g|vV*)B*Z5PB6a-DbC;^=Jl8L*D4k44*t#|o-dP_MwsFZ?gyovf8~ogTkuf? zrBF3HFN!(ul!BKF(qOV#(t5uT>R_^vyrUUK@b-{u@^*wdRdbCdUGrsB^!AW^^0s^p zNE1H{;!jFD_k{w`b8cGgtWL9Yep>BK2>I>q4A8ac73X= zCeVOZZ4-THamVbuaIyD!(#iOCIVAgoO55z;Eue&PaEnP6PzG?Q+2gqG+?DGUYTw^W z(I#zz!Ds+)1^9Q7Ps0q(8ngKQ0`o?q`c0Ri`3TR@Xz<%b%H2{KmNx+U9P2_+}9J=2~UseI1$@JDsOS3}8m_j={N zGXa4gRwxiWjp`NeYT)w;iYBDh{s$p}1kbL+L&b1+t^C|hEzuTS?SS`7^Kx5uX)tU&j^-&#`yGw+aD2z!&m$@{Kat=9K=kT6vBlc%UX!XVP5zpN2@xs~ z3ojF8TYbczcRL6*ClwJyR2Jh1l1(#3vB#q+mL$FG;!^qjpZ0ssFU7?;?5sdjWYD}2 zN*@{s;DHr^@6)CFd!1jo`o(|R;QwjS{--5~z@HdEz?P_fAy=r=Wf1Z|CD#9x6reKH z=Ul9=P7bt!tNMRdWb%HEit(HGOgTnO z>wOGb=YKx^PaAEDtRsvrOxlMg?S#jq)2aU3|C84G0fQF%r$U{8+BRkuI}x1|3rc%7 zIJ;|l8x4T1>~E^FGX(s+hbqC1;ZJ}RF-eESl)Kd(*63>PWwey4$-RpJ81?{I-LCV_iE zG!zhp7(M0g0ae7dIdqIBYRwnWFVCp-05oh10KF{*A=3qH_cEWrpCLcNgVq0&o^1mb zctB;9(UZwG@cdpUktKX50U*S2CIzrDUOU}D8I$9KJo>j&t0QT5{&P(ZOSJBFL@#pc2Nlm?>$?t#fjMlW1 zEBAUvv>)t`-`$d$X!iD@G(&Y9EW|A7JadXF-+tWp7PyforyFZhk9o{R674;ry}GHt z&%OVcnlg94(Oj11z6Oh3uJQN0#(dhx$4%pNXZUe{wZqYVlvEwAC9vunpMW!?uf(5H zLhJQ76z(0$pfx90bNy3kC|I3vJ3JEgN-kz`gvot7aqe)0lyBo#$YUyD`%qD#EoAkU zD0|g`%-Jl!mi6<)Z1RIiq%x%`(DIR) zqll{5tvkC1#D}HF+xmDS2W{qMC5&pF97?u%ItL_0rXol%J6}nO^hh)6@v6noRZV*B z{RzECyGUo{ydWw2^rxQ+xAGtw#-1KNclV*!rk&oeN1FE{{b#Sk{407RmxMb^(t}=` zu=UO`dZXHO8l&1+koto(fZj*~*&pF`ug&gougxrDug!1Hi*zXvd9~Mp=u_C--P;Zm zS!yHo?{}CI7#Hd2ptkGI*PmjOhum|Mho1~buGi+qsnL}kI3 zD$;*;i$DFbd~-n>%D>5z20Hqe+FxXor$7QBq}S#x-X;(Jn`rxEY-$d=9?^(h=wyOz znug#M@H`Dgd&RM;0`M(!x+e7L0?pj_l&wJc;Oz9jsO{dGc0j!QP*puGiHKKS1cxM*gNF8r;yz40{@w6&_~6aB7drdiSL zGeaoc<>31UB_N9Jq4P1Nl`7Mzvuh?4Cp4_QG-6Q$fA4buPZyzCEZ@z}K2{jeG^P^0DHv-*h0*%|I|Dzp zvdM}q|EKSl^Dmq6kD7gD8uxx+w2u;vozRa+e1*=?I)~q}z!7}q*Qq2cYTtRXouCWgF^D~r_FFtFr}Dg!|}>J-o%<9sMeWA9PC<2 zsY)H>{lB>D=i^J%XXN9@+VDv;K_F`XsQyY$2ANFvBw_wdP7i!lg6byr{kMEw}X`Xqi5kCYLfj zCbQiBp<T)2hs|= z&Jp;gdM3d^aU5-wd4+_Az89AE^;n2eqMYrt)Bg%ga>O7QBW3yMUgZe4THrxIpF?Ce zK(BHX?(F74M4v;>2&}OQQWRgf?h5(j7VVfDXGmXnthb?r<<>S)V-xyOaNf0tocwr^ zI@xj#TRGHFqTuhb;Jak3g2UdqV(!*L@N5KNr{&W&bMfam7pZlVaKM7L^Cf$yV8h6B z2M&>s{!_NE^8?`g570z2=JVU7hSxscB6zF9u@lu;k&&~T)aZ#%7Zd2bE0Nn@q)?c= zc6OYU&;MNAj{US$r_N^I-HUe9*7#m)dHlAuT2!!Y_UCzarakcr3lH@|f>!?>Huvfe z;0xXp_zPes8~{^kgs!^B=K-f&oEfwGFv?!-8s%eyLB)o${K8`&clj?J+0L)(fwn=7FHeF2+3uZ2jeGAl;+@Wxj_OD84sUfV9aL^T>rFBa zZbAwFYQ|dAoKSdgO-mQGERuGRmh#?P2M=B~_ghcZ8!8z6#jjuZ6g0?QRx=^=E1^UR zCX{QjcBWWPLXNNZ-T7t~YLCBY@W=dCyv9hz^x)|D&V=Pvb2ch@(k0S;vw!p3zn)81Oq|BSyx zJZY-sAF^^vJ*&y>ds0u~J)1;K*qq_0pOGn@T-npBk0@#9R^A1-tqt2~R6m+{1GZA+ z6L9yb&Nn62wxbe-UDQRvT{h#3--E~1zb;Jws`;EOFQtrOsadX7s;oU17#O?jKLy`M zIt{%sGbqn{Z$1)mB?T2&U$3HG5TnhOrp*@ngr5Ip&ejQqv+s9?5h6MgxQy9)*>n_z z*S_2`h?E(_v5I{FoGw5`4NOi|{D*g#utV`Vzf{5;qh@R86FeP@vfgpre;=CTGcvpXJ8M=ATje}WzMHv zb=JS$6?3!;6nJBh0f~7z_db zEZ?%QCPW%TofAw&>aNGy4R$OK5_p&Ck;?S~9qToVi+s~;7nypzn6Fi)dfzA)<0ce| zd3L)*0dDaFvNGR$jP>+p*w>Byb!pRzfZ=7C%GAt*seZnv@6s@t@mdzjR(Q~ zNfJnJD~=1<60EJ+2I{fOT3UrAD%uV_+rYIqE>AVz+D-$%oyM0qc?sK)X+y34Z}pAM ze04RvSlMhWUn*?6_yNDd>6f(HRwRXU;+AK^-s-UPRBWug(U~7TpMJ9@r}^wg`nVBe zJ<+!S8-Wd)wqz&Kxt2L5uMCtkr`41Hza=;;x;?eW)^{3V{S9T`{jXLFT9#{mCA8(2 zv3@wf5lL->!{BRIOKRYJBj+^$SI>j51&!7b(i*onGbm#k?4T4=U2T5Fej$cZuH=n_ zAUM!TVqqAYIrQwDIHOChh~Y&K#f;g4bMB*p;-mF*sl=l=Vq zF(8p%FHuH@o}R8C#q)`eVZY(75}a|;Rhulh}O$YCN&=7WB1Q`IbE6@QwX_c0l7C6ywHy7W$jvsglK=1>oYK zFH)-e;WA^s^o?5fj*4%^k@j1(nYAZd1 zbC+CxZ(BBzx67(OO#Nr^GT7XT+*#G``MM}HZq2b64?#hxu2- zNyU8>p9L>Pk6|}G&xcx-qy-|!%8bNH!#6`jiX*vElUC6LUW&X!M3`?G%(z#?i1Te2 zT#^h~hKPhK)cW^TkSwBjaX`%+s9}4Z#6c1UbEIQ5`<4{IDZPa}M08!F+HZvlGVVkS zX5T2`kB-TVW_9UcZ7$mZm3e%b(C!LBPCU1r_}r>8WsJTEfb8@I$kR{Mv<6^=th}i^ zuAt(AxT6pwwtgIVHBX)wG9q}l*7Ng|bmyto)qg_K`}v6<5$l!URupgCtfGBYC*AClT6&+%E@;G-tmvGUQqG6OPV%3aor%Uid! zpObf}9B^#-nzft)BPsxe-9cYX189^Eit><5>?TSY{~xqPz<)btaSFb(NZ@JiMeSNS zg@yqq*T&IfcTlM`;E14KHA4)t8)UXJno@S_W?%%w=gZX|piQCm^d25nPog zFRKW|k8%aPn7w5fJ7ZhEF(jvVaHHT4uW=Da-$TOYr^gUeT0=$| z!({m=TICpHwea8S8mZwiLgq{^J-1cJ#xkSG12u~D=Pi;aHXg-_Em4a)t3g@$5*)ZF z5n7#bBp|8x_b<3Tm<1rpOUFZPH!@upwSC9g4lb&TTe<|Ef8HDKfb%uz#zcSFo<`VG z9zCWb<#=bX|37rSV|1L|_dgsoO{2zYY+DT{c4J%9pivt(n%HP;+qTs-wr$&*`Op3P zzIa|eYt5d0ZGHAR*Q}X2>zwQCV*_-Ux>45e_t<2J(b~utdO5`#+}+bFoc2kdabI)(a#>jz8SR0@2!trYHj=4}*mzbxAh ze{>L&UnSFH5<&dJc<$Q5c+O{_CeQ(n-)TPzy;;gw4>x`Z3CDm$?*MMt$^NwYp`Uf6 zqJODL;p2VhBP0GZh-jQS7{aSt5J=j1MRLoTs6-ZY8v_!QMDjFmnECBQS2)^X51p#a z3b!^;C|u2m_If1MOuTxc^^!a#OZ<0L?iEv(&7jH>bFF%)d_A2AILPl_8+;Ua{<7&g z4tf$Xw$(Jt@(#XyeUL9S{>8~Aa$7J1x|$vFE%*R5k&VMzrIRPTvofZs3W&ja49P*Y zo(dql8K)YV+FZHm=LKsM`t(GRnU;^ntT`r;@tkV=+~u+TzCD8P0RANeDlR@g*HU$> z?4+~oVhdEO{?!C2%h{IEhZQH&?^YKvE*PpOBFUI1hBRWkZG@=Q5wAX%9-=eN&hb4R8{2Eu zC41+M>D)R159SsVbtfm(ko7G!+SM8M)V%A$E)K5~W)b2y5J(u^aJ`G-SXdj6-aV?4 z0gI%$`#Wkn2HVvr-D^KTnCIg2hHuBP$MyDEeMF)9!W?N+!(KOCH;slT{Z)rx-^8>a zAZYqCp0v=21Mi8dPkn66O?s41!X_##eK#SpiVqjHF9NqOf}}5khA)DJFM@Lp@DnTw zJuHeWEQ%E@N+>K!2`tJ#Sd=4JluzO^gDM~J{(Qij`hbUlfTx0h_Xh!QDkzBpl{p8M zMdAww(aAeXC_G9DJjy?Klp}bQPaja|KcL7C5L$h{2O~&QUWXTNbo(mS_$t--s?_+Z zHK3N7z*brjn)Slj^uqn>h4biz^Y4WV?}dx&g-h#&%j<xHYEmf&;_KnM&#NXQ4u zBBEHyyrSJ_e4GnF*b6|o3qU}J!Rvt0x%yzzOXQ0X0~P+;>}=92=!<|3i`Vf%=L*N9 zS3d_x3eW70Rs*PN^rHA7D1X4~z|^^tB?vY%Dy1jBH}Rwm4SaUHMmL8P zpj03ckzv1?XzKJRgY9v{AyLipr3~hDT9HpN92hr{w^uz*7HqYQw`%5#ZXRzlYZu)% z{r7D0hv%esvI(%m{wTzOIlUQr&KgI^>NuhH_%(a;2guVP+45DDp_YJ8c$V&|^1(D>3^ zJ;~R#WN7y*uW{VTn5prYu8OzXWq$5E6R3`mQxJLF^0KKh^ao`_hU3cAh`XlM`l*vd z`p2OUm#z*(9k;g{WX+anBCRs>u9nmiuPplWpI@_UpR06AMVdsG5BP57G~LA-p6ONs zeH&o(P3Ex0r+Qv^^W8cyh_BjN1{y;!PIMn5>GVQ4ShAJy+1 z?D=D6zS60KqEVa?utfW~@O&do9~~34QTOPp38tnwYx04&ObiThL_~G=<*Px{~FraerfAYr!{W1QWHM7rfKs+ZbtJHka)WHmd6$3qCgknFa z{WH}5`#<}KeLN>h`sNg>-rs)w*idaQRI7vl)iOi1PGYTt#HhoSo7V)yXd}!Awo{xT z62DIQggy>I8IjOOM9E)2ut@``C|eteV|poK*F4A|kgXJ1dzOB4M-?bSy!a?FE;O(df`%A80VVWb0rC`A8+d#t=>h#Ym-lnK`l&Ce#@DU)!NZVrnQd zwW>XbOc)edq2aJ3n&IoW2G9SPa>JzxiMnGR)RjsKio*bPRe`z|heKVppp9tFgWexJ zC|VtT^Oyfzo2*j%rJ(nbRqwzZ`hgurB=MU-{gCq4^u$~gdkOQ`(&V6=A%E?Ql#3!t zrcp z56>W)q>tU&BTst`Gf&a4{~(`uUyL^)%Y3FIEeX)U5w*f_^*8=YNaO3~YFd_&>P0sw zbZY=axCt$xZ(f-c{tE&x4&9#|c?mjuN{~xx2>%IzhmV|iHU9eu_$<&(SF9cS>VT60 z?zX*z;GDMHO>YyHmeK$|Tr@!>K6Tl>)AelXLh6NfA@eP#l^bL%Z)Y5rCuT9Y{67-4 zdD}_^m~sVkDlsGo?S(A7Q?`DpxGYHJJtvSxpg+<1uVeIsaqt3XwV^_Sd!{ijM}&Q{?p2K*C<7e5lVCm8;r z6|d{I1;1J32uwhGZJyE>W4Z#6F0gA|obOVoRl@c!cP!Ib^{(`9Twi8T*Jw)m<)pCy zKPulx1A~$bJBn7L0E@?psMA7ivR&sJq!5r~NLu3<*3$=sLrI42rf02j9EJ+;D9N9h z4Mf6kl3r*A*2gCdNzkp|=y)k&ry-IGaT+)EXNIpJ=oBL%j;-?pgh?3!+J)yltXWw< z_4bf?13grIATKL>1z=c6PW_ZoA>tWaHw`vrP{+aiHYN8{*_(>T7h;32N>Cf%bIKgo@S);JSi!nGfNfbLof z^EwFgdJyv(t)>yh9UHa$MH+{d$r6%^u*{=rk&E@ZD0Mn|0V(C9e+79&y(>GX?fv@s z7qHzu87JV)v$64fx2~4#_OjG|kv8lm$ltRB-4oO`#FQB+e7M&8gAAQp+9ER6If*Rt zWni`NpQ%h#@YRA^f_(%>+XW2+?OBcR!Rchd5=#16^EpVr?Y&}t<466#W^KRO<@=IR z*W?Y3r-41krrQO#d3WyZ^Han2U$8LOb0GMmd7s0iY}oo`9bHxi3mWpmDq<(6SljK; z_lI7li|zQ0MZ$Q?n8T2cg}JYH7a|?k!w{~G`EnLO#g@eGm6pp>0;U6jI;+=U40i>4 z^xKHcm-X-I-gjMW1swReGKSAhbM;&p!Ny@|TFt zg>6Tj>xE#}zxe7n%r5E}KzmKN?Gi~S<8gJ+W=uFvH2;=RjZRRXbwUdyd+if(@qlNr zycK*{!>pqgYMKxkcNDMNV{JI(S!LWuAFn$!7Mf6wVc=AP?BRB?TZGp-F2=u&0pcU; zwI;SMpKC&XK4fsbb*|3+<;S)^ywrawul>vSVOhBO?fa$oYq18;uuUG5gSkvEU2Q$N zS%}zxQa~sPBBmfZr7OAQnPlivEJiIC$EyG5pwLWYOhG(KS60b0lhCDMjM{t(GfRnq z&qEjjB$Tc~l4o9_OR$)=*py~6k^|5xtRz^Nf;3d$j#5IG_%Uk(>aGz57${vef~Ryb zYx_9^l_Ek(k|~^xB+q(6m%=b>si@2>r3XGog=T)i6y&9JwU<2Gf~HATm5{Oglu3dq zC?s)o7rJy!D1Im-2`?&1B`PT)Drqh%86YZ|ZxV$T>=*jiFZ82-sI-4*uz%=Z|Im;9 z7?B?_(LyK)&?&!QNU&2%a#Krk(@JvFOL8+xax+VEvr2NaOLB8ca&t>^^Gb4m4K{HO zwQ@)8eZrWAA@s+j%%h;p9Sl??1Zz|u+o%KW5wlSwh`5*X1z z2C3e1NN`U)V9CvDOF`dTK=!-w>gwWF{`6uMh0{SNVO8T@HFp(+2)v(-FO)`^-T%vl zDF1CN?AI)ZVZrwRfzfgEfC9YC(@~0Zfm;?$z({V_{qvMDd{E7#&z?RBM9k`afun0& zK_*CFt9#%_k<+i$SF*#^s3!w^0I=S<+%$V_dty*+==g3*Qz%HMT==NB?vdc5NY~~RUEmWp z_Gb5?Ges^)DEn&FgTMAwlHqz!;wERSYbHm#ZeI{&%Z6rDt0L%i&As_7~ZVJ0np`ZI0VagkfDR>sw@ofN_g74I~*3hLX4>M(v_n}&GX?H4- zU0JqXgGauNj@l^p{nJ^m1@-3P}2vIO#TpW%<7R8aK3Wn~84+N~*@&qD_4_J*C^6F>Gxq(_$B zrxA&8oAVCm<3XG>7^n5A#Sq>*p#Jkq4HCOEDo3q=Db8ACf|cPo|3HZ0AqERTcq;0g z{WGn^tJsMD>Ka8FI;n~H87|%*0Wo!PNhdQWPZPCJE3Hr~y-+KoP%AT3pc<8xO{kT9 zsMW7fE7wpfk5DV0P%HmXtKd+p@KCGhP^-95t0dbz97Sx*acs*?lxeJ#X>7L_y@9E2=-jC^!d2&>Fx>R}ATrNll;~0t=r3&m$!6qk) zK>ybG5d-o+BZ}K{z%&H4R8^tUv0k{+F^nvY@4kv2jr<52mDZ38_HQ92pIe| z4hYrKMR5x_Nf;oagaw@1@&%kAELmVUR;gmUD5F!HBxC#of|LM7qTH)LW#yld45dO9 zKO!!A)l$!C)KXEPnSVkvGy7?L7ldZw^o(?tWUMb1WxV|X z6{#45knbcJZ~35{TZ*xsB2nB!kFK8=>};`a+W^ z_Ym$Z&*+5V=@l?zL;g1PC+Cdrc>5V}n*gVdW(V^V_Y|U60m+#IRSZ5%HtrqldCI*n zhSEzEba8{RPrD2Bf7u+MsH|)*gP&WMH_Ra)X~4x=t03wmFl$Pd{dL^KqIA+q_jH0y zmaUTJSvT$KrtL8MBF3N*0z^jY80WQm{DCaddO~?}tZ}cNyOzK$DqC(^7bBSksLZm4$+a zcd0AP7b*#MSv@gXE2DjNJ7)D@Uy%ekQ!YS4?vA65u* zb2G?lWx-m1XsxXg7{nx)WZxSb{4LC?LxR6C$!bJMmOkgsz2ziQ7aky1Qvc372ytN1 zwxE>mT0H!6U8m1ql-@NVQ}_Z978G=o){6dp)0hoYQUT`s2d;Yo=;7xcfzVGti#bWq z5+X_MZGnzezd@ioVy3rc>#Vhbj3RphTFl^Q*pq|WOjGyEg3s2;L!R+Gm?iVLZ7INsjGix4E!&%YOD2C1HAZI8+eIyc?JRv46v9 zq2TcnFW!=^=JW5$=C615_47{&(jDcCCZ{#WT{~A=>y->tT{!AsYbz4#CAR3dvUCsU zbT*SGn;cT7YZ80g+6vOgVF7|FrIMjby-)pfTK5Bqy6i&Bl|=|B19I!eO-0{R1{=2M zDvrYf2Jgs<^||`mBM4=r^~Yh+q;CJNR;YJ@rN%sL5(HK^V1iS1TqeE~1}C#OUM(Tn zr)?WI(zr39(q)(3hKLOA8+VvwlHS&ReUFc+45CmD4QhAQw*>PWW;Fh4yw5_HiKssm zvg&kOwcE)+I$n59YQ#F-TJn8Wn|qNXnMf`F^jNWn{V~yDQ@k}CI zY#T0Rn-)oqB;mPF7052n=!6wXjc8%WEl>RC^S!{9s{|TL#IV|`Aw)bDop5d;bA1Jn zQfq=I8SI}7EwF9>8Xf}ajjcobg@X9){JR|I(x2kOQ+N=@x@^zRAj#{)|#R2D7OGZ$VV3dO!%i@(BPi zG|sQGfF1hEsN;{M;|pp1e8hl3>b$r;JK=47NAubx=`o*RUUztJ zT+p=kW*n#n>8*c>aUztB$bdh-$-#TOnYG-*C9|s&(SIv**>^2>nO`eh2)HlOMF(-| zc<%+_E#&{$nIB4$EIUVcRv~NV*3Krtveb0m9nT~3K6UR$;#7{}UYD2n5cT+^$T`D} zxdFtr2qMXzZ}6R%#6X3?jDo>L^V1B6H#bS#=^@9MrpK6O$C&<#F)fNQEsHU&f-$YL z6iG!8nWuw0SVq44t1%LtfE{w*YL*P-nR z{p;ILmAu^H=P`QX9BGDYvhXymxrV%=&g?9>Z9mrpg(sOnbxF_kKqdN`J>+BI9|jO9 zBGma3Nbamm0GNyZrMhXQ`%;6sHu-zn3GBNAd%`M&Mh(Ao0OY9#K@_xiw{F+Y_T7K4-pH|Lf9I%cgA-Ha}zm- zf|2?StZ$W4r*vMmM?t=g5k^newo}0l2P_^OZ?`%4=oZW?PrG|L-%dZWta#lWOX=e| z8{W+**x_|DqoR5Mhih^w|FVizAB^kYrSZ}q-6x!jt$qRHGzeEYPCsOyiwTMsv8-Hg zqer-c?+vEXSEE^1Tn_rB+LP^c8xF^>BQ_Wf>Z~r?BqAQNh(M34`L{vdkLTDMVT26- z?dV2tX#9ux+HI?`z=Q(`u}jezLpGzBLlcr{_1{M?d!ed z-q@X8`n@SS9#k{jrFHT6Bt$!n4tqX>UsJACKHKO0VQtrdJRQe^%U>kNd#QV7w~kaN zXu$9EOIOt8NRdkdDPT%q11YF7AEuO?vX-2(m7KDdoN~BEc*oZY zBi0rp))6DtS!WoBoK%vUELc=DR8~AxRua`O5+n8xMr;B`Yzjtf21aZSMr;8_Yzane z1x9QQMr;E{Yzszg2S#iU0bCdbWh}=BP6S;tV>U!k(jsD5V{ zu~!(ecNnox7_n~{u`tUoIDc~LL<-&K!euJm=SD=I0H}eBj~=PBkWsnOMje}?H@Cu5 zz)R?_SCI6N#3wf87Z5m_$eex1h3XEXbT2&003LU#i&l5hf@CY3^qtK}5#(s#=0nd; zkoF)+{G%4ERJ|vou>`9Mnh5jQN0K}X4gaj+oRbG%y*)B=FG?WqrS|QpyTdDjfoV2j z7xuk)R*GO`9CDiY0?L{LWpFvd$si9kdPvBMgUbi+qsIHPH)e(QN}7UBWDO#^y_*8P zhR&^jFXW&J3ppq+P0*;7I+0yiJU7b_0~?FVH7FO<))JjT|U@5Y*uRvHGy2c9zP zCh9a6HfzN=<8rDkew|+7G zI_s}i%DnF%?M5(G5}}IK{}jf&Xoz--vyKYXamMKK)ZI5UamL43!_~1+k__9^j+DG9 zua~@G(x80SG^TvU{l^$)noa(U%lhXHId@z*7gSS?_VQ`C%Bi80?hZH3u-!^s`G||U z-S$73B+gKfB;8}?AZ{r})^$8V&NVFhoA3~(csJ#;RJwv0S)5|zSQnn!0F{_|28?PI5~W)_~uYxq-_r#!YDn1Tnt|WrhfMCQVhA!>qGEY z!&fb{lbbF#Z9$KV8pp!-9(oF!f2hd}u_;MB*Ox>9Fw&dxvcYh$p^ZOeP!cItGox(hO!Ac&i@%5(SQK zY&upEbjtD_z!Yf@%rgr~tT zB{%2Qnh&7s63Ag2WEXYFg&ld|(F;v2sCKp-zjn!b(6U|0n-?&Ykpp76>|de|Zl$mp zV$31KMq+&Q^djB&i=d>B>Tj#K)HEfNCbJf2`%QjZnZu^d^a1as<;SOron3m7=$+lU zxELpr+ju@1=7b}FLKA;+uu=ZZGsMcU^%>NO1+AU;u008Sf8rQ&G3SMVaCu;2x*u@d z%sUq6JDq`n=8mN*)*71 zUHN`hWp<-)ECL^WV9Qnj{H!IRa@uI}CWqd~w*%MxnUdLOs0vJ(scjVm zl~Xw?iU(C3@4D&t2-5_qp$GvMpQOV=gNJu)dcR^$ixT?Ba(ID__8 z+w}Yi1UnvXj&4v3+99P9W#KMOrqOB=T@-3=N`W`B79 zGPuGGAtn%F1uRjwMe@x5dDj|p;Yt{``^aKi2Y6j_Yh;8>cy-^p^dPyQQiEV`{-EgiJjN56n{A!0i> zIiJO)M0(2dK{j@8PB$m~f`L53Uq*je(hLsHsEJnn*3P|VoP3hyN)&f)S~UP zT`X(b&xgGa@iIv;7MjxDc)AeyJ=^wh^L*NgFY0 zzN1KU)oAfsv)zxUa=X<+HrrK@2+(u)J%mS4Os{dO$#c0pO?b4Q z+X0t#{%o9irbF1H=(}-tR+F3AU+ag>hopm^qenZ$iMe``j{3r)jLA5z-jfP=_g_CV zz7$I&die4wbfW%}*OfT)<_BrguFW_qyraISy)HHZ5+gdA6YaCbhAa_&|CKo&8cPK>KRx6IP? zWEVkjGiX!0KTKnGw2B^XD#+I_YagKM#SyuM-gA_RNagk=a)rudfD|U*g*$RC(*;z~g8p8?}u473nAFLu(nCpKB?>ul4|U`Z?xuLL)J+!Ho++>-=N+|y6g=^oh~ zXlb9|zXiesMDZ&oj*2z`)3I|^4s@k)4R;SB&F~8Ntp;g7P-5LTxB-n0f+|mK`w8_{x#+mdAI}=QURPJegMOMQ zeeWS`Vq{_3S%JZhmi=xWQq{^%0dIMIQ2a$)XYkf+< zg&VZHo=J!!O6Ydj!>Gf0&$gTxmYuu~1TSjiKoF4ipnndtkgC<{F-HUS_iQ}%mPe87 zza%eIVS*iQUyF|}iKIb`2Nz_8?Y>hVJOW`oF_b+t@rW@2< z4(rp-yddR|xpX+v?_u}nls{4I zVBFrNM}kOCb(695kGtz$90$AG>=VH7=1yzh>vJXOVR~+1Zf8fd4_VdtugnhQf4|w& zK4h*Yo+e*~5M<+_nlH+jiN8&*K_ke{3aqt8h*V$;8;+Uq zyFU8))9N$&u8N=gV#)GO&6(=X)e(}JyjsQAJgJPYb^0k^Z(>|`j-b!p{Cx^D_dyS; z-T(1(trTRUSPH*8Ovj$&g|PdNltAKh&(lw(jJZPu0zTI;>t-R;-Jow76?$~v8+=5^ zo?LTNo*mMxL{ey!p51*@8bx%+HnNe;#gsV?pFa@5e+lb*bKC-x_PpNv-p=erebhObYL=g|$ zC!r0OgjhV{i06>Ut=~$;t?yNllf<4C!J=A+Sv(q24qw<)4%>IiLLWPaQYw^~UO}?D z)a|-WH2JJ?>i{R}cDGRK_82G!?8&-zi+@Uxu6vb>ws=H_k^&TfpLqCU2#S1mYT_(Y z=h-A%=a~Q{f7v=gE$U?wy128?R38|rm&fI~IpwLBzb&)NMk%_&3&)*R^~aup1F4qZ zab@b#UAUxEsoN*RsN0RVWL+<@hbhkE&MtbPy+ekR?WJz-rjvrxP^=RarC#<9_cG1_#;sTXq;5xt z!X-mFUpRGpGnB}%p{Z!%&a|xBksAzIv-aCEa9vM8Eu+KS^JA!vz!v~q)#%ZGgY0dd zYqHz59l#3nPIC4J5I_KsZ?Wyn)}LR9=Q=D^4zg>=%BoTIeo>cu7u#uHKzF^=Jn`7@ zz5Q#y8;FSaeDcfSy&q^9Yj_w}&7E(cn-Kit&!g8Oxx?+7x&Cs+uc^scW_ft#fIJ@8 zT9S?_@v1U$bRG1FE*KQEX?znY<`+2TFbW-Ps3OAbla|ypraZWSGoo_L%=4VNY4tbVR*H3byG$Z#a6YJ)I%XbHZ3SUl!_vW4-AxBEylhT` z!w%%|)Oo$INv%K<4HR`=;OaW?E(>_mXNcz9<&cvRSRWS(yI){Sman>quuuq{KJ- zcGywz`&zW=`))fOclrX_v~AOPgPm^9d`H88eY@Iv@p+Vt`g-v#@w~TyVEBnhr}K2@ z#cTzr^kUX*M%HEA!NP;KIa`+ z*jTQXip3o`r#CgtzJnX{UVK3RBGMk&7HYkhc2&niK*FW`v!YWO&tSyyMID(^bnPzoyCh8y;iSQ?pmjL7P{3e2dqY=%_S@K7k)yc4=nn zURy-uu4Q_bnQ`%?npc88jpvfaZFhX>bPhQW3=D~?Y7}J_ms%a)f1>x7Zkct1`wbt3 zZ%PlkY-!f~v(8F({d~p(K1Q2<66(Jp_5DGAxosExkDvJx);(lj80N92196K?3lsTk zsAeD>sTpprZ-kB%{T5Z(r@4 zdURy?2Hh&DsnV=)ZCpLtW@uk%VZ+wfg<$U5^4PAXn(B8U1(c^cE1z{yZ9)g-@BTXY z44t;WKy@}hm=M)2LYN&XGhw(fihT_4sP1gYK8=p6>i(kr`JIoRP0-uyVlQmCC_`@% zgnJAM5Gy`{=-ENM)F%r3ZL6;#c~m*?>d4HxJbUqs=a&m$x4w|_T89_L1xP+6E~J}x z6;#8pnqfXM44p`@>3nyYZu@l)K#d&%S7+TpuC0zl^LNvLs|MrX>LJU?KhXgGWWe>5 z^zJZ6wcH|Y_iW=vGgb3#gZx?DC0p(3!sUSdpyl9&GBKX#n78^_YPZ~6Q6(1DUotq$ z!`~Cwt(gpRh6sw%K4qRCLs8?KU#pg%x^ws2 z8nh$yZ!SrWP2ET?V@P$`3})NVVyp_77JsNLz}b999}sYPe|taqFjZQn?eXztMwi63 z;sq&kh@R*|_ZJ+^^REV+s_&62bHC9umS>*T4;z(#914StEhc)0mAZ(}vc}W5>Kx)% z?R&Kiv`eO_OGx|uxbav&7WMYE_Xz1M;iUXaY(P$nbhD1(yhrc(u+A;hcuGQliZ zqJTVVy@yM0```P6t~Mn;%sg+R7&d2Nx8aguwgGAtjyT-vr?@t0 zt!D91Fn8TX1V+_#K|}KVzm^o2r}Pr?ouS9nfar!fOoPP7hTiv=P|Sl?H_qrceVUS| zPM_Vkqe<)nH*N(#c_%rEukFF)Uao2V>JGdW_d1bk1%*h6i+50?Uv1J;H*GS}f$yq> zN|b7aj*dWkg54=>=o@#T8P4vNf8MOBGUDday3s$RJE^WLvP3k8mv;ET!jebnAds3k zZoR-t8>mHxQSlCW9&!;t!ny-&qm_KLD`D+pMQ5V#^s`TzNk(IQAE^BFHf8&6kA+4)f)Yut2L(hD=%%@ZC%Ar-U)$+Ua5hHR8WvcD2PESv|`0W z=Wp{(v@-<2`MTjS_G9!lU^s)1VbSxZ8%+Sd3i8E?q` za!ytmwO|hJHg^iYvo%Oj(q8F4SOM_SjYpgTIOVGcKyx;)_&7z%nF;zP!zJ$ze3E|k z+C}eFXqe%ULorCT-)`{GY4quV)1vK~+&yICA5)h;ov{8}1N1aB9h!4&+y9%QGG1P| zvtq8!1Zv?ET_k6xPYu0zAqI4?LQ5icMZ~;`_i&xy`-J20dd2s@ANKR~MIc75{dHR$ zPG2}~8E?w%OG5b8nvC~-m<>hm8do7!MHMa7F1ah`3&tftk%#=Vj}i~L7K?K=Y^<&% z5BbX-vo`#W381pbphNxwugv%>^@Im>qUmLB>tW&lXVq4#FnSxU74mKHcN%P2_{0kV zQ;#^3LB#gL#qdw?YZZ>?l<2v{53Q)o$2-)w29DQpljPTM8|vu32-U*X%crM=O#ZI7 zaNQtke{2j{>I)idl4*b_BhLG6gNo`MdK@a#6O3d(Ew`_&PA?)kvDOu=YzXiP=DNlo zT~tKls+U~7Hw29s*XznOCN`1)j-nPx8N~MG?7Kf8MGMMkV9uG zEod-kBZ}Z=psThYXXT0F=uSV0<}l-+%Hm*#;b0CYxG0coNEQYb)`^!@{PFnNivaZP zXoH>cL;(uCbz7*X4Q6ObRI8>!ilZ3YA@LhI5gKm*^cT=Q37^mc^ryCph-uSZ#t0a( zZ@a+$`~H1-=OZLLl}&piBz-XP#W8BI^Ab$nE$kuG>|jFndvlKcICIGhEV}x0$8VD? z5&a#E1bP~_zKLx{yp^#l4l@7Ie(QSevi%f*_Kf{D+czxNeO0`3fhl6cCHivo_Ei_x zOH%FKEllNdXk2Zp)GHSpt)(^v>f77e(dp`yfQ*D!yPCh+_~6bxTvs}pAgd$}I4_?3 zKAr(Yq+Gcy+?O5_OUYL)%l0?6tnoy1&`6Aye0XFWqru!vd^N%S!fA8IuUgHyiGY^& ztYtDc)(qGGy$hFs?>4Nb$Iv@}#5-5lV6aQY{DC0M*DL;-_?&S*L|9tQLqfM5e+uC`gjuoMN3UYRl{F5 zC9lbbb>2FQjPlhWY;Cshe4Qflx{%uf$e~{TGs1)mq&fuVn_m_{$}uU#x&UXqq4XDZ<;=pL9+ zj8t9CZuHJ|wQ}$C>Zv+@#}$_3;?DN1njU<}<*5(itFb!3@z;!3Uy}c2?6~^RCt7O) zZ>?u$xlfgsrN+>8`-n5KHx?7%VEZm;qbsbvpx#SI@jyo3p&)-w?S9Hy>61v>&| z9fE`js-y*)q{U~+zohQu0b8)leO+QESdtdlk`_3U7PyiYc#;~xqq!4&Lh z>LxwBW4ddjvT+g%rW2Ix=uUWk-YH)2AI&BxYcQZfSW5jT+kp%z6Y2wII3^BD%oMqw zc(|6TKk+(;bURw52?kL*MwS&DeE0&YGyiO5XnJsHWr)8NfEhYeI|(_?ax-Mr{*{@X?4 zYfrcT2&~_Ix$8#+KS#^`3O&+}7fKi!NI6OL!LT+n+*S#(CRX*1dZv*mjq=8+B#kof zKue;KpOI=IjQA15SoL+?4BbSedg0g#MC<$c-`YqbVdZ&#@ZmvrsJFAKv1_27>v>v>=}WQ*RisZ(!2D<}z& zdIKXsTmF@JPbcR-L@@z!>!<2-Eoz)}3D5GDL^Yf5PsG7<4>h)w=`NQ%1C z?HFxLL$`>Yb`v;6M6dh|vLgrC-27>taM|+N$6h+!j`{Fpm*(LGR4gaf{y@ZVc*5n` z-)h>?W>V6rdeZH+yB$|6_e-I))Tg>u_qF-y;9GbU%Spw){u}QmbkRYmYkyeL{AiWX zYiXj@wG5YkS*ei{aivtt0$*@G9JQ`qFV+;WEVuwsMSiSyK!phZxxCwt`8&E8^#83a zwCxSLzLRJ%)OlvDPpwcg_*_GD$npi+D_;#~u>)U3X^R_iT18b+qt|(KYQ+}c36_sM zbvGOOy2ibvk2aapMpIwl-!q;$rxsJ$;NLU72rW-0EnRPbeM{zNkg;S~B9kuECD><9 z7MdI2gWCGH8l)RwxOF4EZGczmv-Az{;CgS=;>&RI@YZm0>JK{Ozq)Qtb^pPvkNZJk zu%KaWOT)=O7uHeh;@Ngf+L2rby$EYqZh$4M^eY6nz@CW|HudwqO9l5EAadm1sI3`@ z>^#iAi8z)7Sirt=G^)tays&#A2W41(bY&aQ(l+@m^C0;-(XA{dIrL!lA&t&Km5N_wSrIyQ<@6X0e&1@|#HW&H$ zc2e*MOP_@uf9~>id;M~B`U2S~a5is!PCyhWIbR4`v@*U*B5#2gvn3k1+7t}eoIq39 z7&f%`kxa!eJ;~a#-KO#_OnfapVh$@~WUBQiGFZhX?&29c*AIw0Ur1(9iOBapm{SZ@ zMOF#D6`_rG9UROSsPOti>WVH1_&O zK;QCFr?UwBzts+ZtYJEc46v(hF{r6#vYFeZ+ouqY&a$ys*aWnOF&tJ{p+^O-a#TXG53L7rVez zFYr9N5RSv26gfVA74j|rkEO4Ui{ktK20=nWO1irwC8fK&q!lD3q@-CwUw}@AX^;mLYfPxKY4-h%0Z!xDkY#WKmcbnDW6!;D0?( z{!4m!eCbMDj?sD%(!=b-kYhD`Wrj`sI%M+ave60>wTY25FnuZ+z8c_EOrwgb`(3P=lS0GRz-XrIYv|08>F59sNJ%h>t<)4=-OQY-cwUM2LQ(v zAoUMuoj>_|&|+M@YtwHMdqABXDbrc(Kd9&WbS|-l407*+d;4{H=Db3VI>2u&wTGV6l6&VYT(yIa5b&l#2+uZZJj~6Km%~{1A zUD*C<3l63nwH;k=O^J7|SiwT)B2|I628fLKpJ3sh+Jmcmlq#4u)pHZD)*a!v9ysCN zbK}2jBF&5B>pEG%(uaMj4+<)KadmRamPZJYr&@=_=(xa))E}Ipg-XT^rt^PJsQZTK zap13c=cWAr;q_sOvoTvCEu}H(dZ7QXFDpcbVVw@JOHz$}M?(_585BCjUUJwNKYS~Z zLM=bBe!?JSn^RY{q}oEcc+4aHRZS$X-1#-zzwaKUDfFyiZ<%L)_2}{N(9nE;jOqBn zOliIg##8Kz>x5Ygro>rqw$dF97U3Kc+e_*3+izUj2MB@B zvqmOdiLzjtB(5j;=I;#GGf_{jux_xsK=1PlE+VBlC)Jbbd(!mHu5W96b2sp(1t$}@ z1t;-11t-4XQ557|AN{-U2V0KdzxzV4rGhO}@L!-FTd>akBU^0dU~7(qt`olGuHwQ+ zhaU<~0zt*C$I~~inA113m|6O}V0%WW3U5wQgXfW}!mU%(;Pk|*@ELvdkGj)0fp*h3 zC&L9NvAExRle+RiI}pVxRk$s?y+QY6qQ+%c^GACn_`)^99n3VLt@5mw;zy}a;@KU* zdcMa2bTokFmI<~*)$)eqNK{%s)ymOE0-Sm08W3yZQf@Q8hj<4SL|f>6sXh+8Sq>b{ zAzye2snW|%BO(`AvoA)(fFD?c*$KYE8EhrpLC`j@lsSQq zhqY%=Y5nBwyX&H;CjELhnWb#eKg>TmQmnFwU?}kC}v@nUrFp0Dt6OBcdn(Irq3^WIdjFi?iEn$df^6 z(D+7Kb1s}Sv21lTv8>Ufz?s~uz?ptsb&lBm`{%^6!XNvN&IQgt#q}F=#Pl1}HRj5{ z*VVkKiWxQvm+0j7iD$(^+|8J}UC+RqFTKyis;2&Jch-#Rt6(njL2L7}JS^+=u^(4! zccQA6GCq{+ZJz&YkA>&)z=!gO5$(?8Ww0KkIJcI0(}}2$`H8K!+U?pj)^CQf=Glty))|Eroqc zeS{Z`IL1e`UbXBP{p&<(j4VJ#WLW$guBl_6F5+yxCW?C-?@@K9I~9@oS55~0N*d*Z z@!`6;DrS5njW_&dOBm2tW`QZsoitQAI8s>d>C%<{e)qv?$j0U6P5yUIBwm7sf`#uT8JK+@&XBy3ZPPlK1oI zxiDR62eI=8lc*)9BHded=;d9=G=#XDpG zO{$rv4i3+R_O=3nds?dh)EHX!xUJ=$?h9qm4>}!T^v81{sov);keBMroqzspl>*j3V9&09;Ncrc+*fmazi8$hv ziRE;a8}FHkjPPVvyc!EuFpP}g?I&y_trX)hVjlcgVl{_zc&)i@1+%m+<@6{#gBaT@ zSFwtYy}W=;I@|pQpg1{#sK6nF_1_k^2+e?+HKF&dV)1j_gqZ@7oqMz}t+g`{$=be-hBg1xUtj{OrCr&|JxNQxKI-~hN<+)V*+(gy7Hemg+r$??2Koy5e8nC9_> z%owq?S@cXEhU;knMm*1u8qnYIYfI}KDxm<@!h+;~W1*%zY2skZ3 zxgWNOo5-AI?R==B2#EE#>X1mUyj9|^&aoJz4?4>@UCZFzaqd}e^!3v_Zf;LA5xTcq z03P-cv4V;nhT!3G~B)e!akg#(8S%2!zb=yP=n3r&-|Rq@(Ne z;?MlM_Sv4(DQB?VWk>~QZ<6%T`z0AJeej)$Rr?X&wwb4c-MD5o@;ZK2Iei599A|tE zkt_uwXhlWzHe0$#i&sS&EXw;`CE&}aUzWcCQ=)M4DpOSad#h8@n+JUxUuQ>JwC350 zH!5QRWwJ|#GtK8nq3T^sNZ3(#X|04v7w)@9b?DiZ&ZG8KSrR95R#D=R{f_I_e*btN z9kW=wxbnO!yXxZgTrFM}9I{;7dSq64u-oLwX_7UdGICaRd#|ovULA1H$2zT?c>;6W zy(h&o+&y!r10XJ8{ok_wNrB(|a)Wb-^A>-W{#f!@snq*(;nRDl2u0Zxs!eqT7|TaM z8s|~LkK>Pk%2+6rgBmD~)t?6>J5hU*0Ujv}gfS?<27t7@On$IQGLCdtvsyBu@;upl zNPy5c;pY&&pk%jBIo@nP?Zr}^K5b!;F5I$m>f@0)V?H3$VCMUCt7kott_-;ol(B~t z34OdFd1x8CK(sHoC;pa6>(SvbW6~O>?YqdlO>%NH37PIr)OVhEBVB0_m$J6*5aRih z=wVytK!Xydc#%o}z>yi>yj|Q8qB*?e88%^cmn1ZICy@7Y%Y)NoaqQ^>S;$gGeG;o1 zZXWr4QA+jD-cz%xq#`fo+kcQ%zOe)w#IJb$!puPVThnEdfAoj~+;;TkHTAVHlHAuue^7s=j+PyjBZ*V)jIai?pN5T?it-(n;UbBv? z%uJVNEd%D53&QdQ&4bR3_yI}cz8xcsHq?wZ5&{I6OELZolUAb`VIsCTN53VB93_4~ zo(vf!*(fCzXkxxl+$;f)>=%k|-_`pTtMK^rs&Jb4E)=B;L3VtUB&^9k7pFn=LNR|V zll{@rxMU)enMU8WL#`@YC993ho7_QJkVkb(CW6blLXqMAOp~G%D7F5cns|O4%sX6A zvHCyaDM(EGN8p3R$$ta^NDv6c294Q;-&naQccB~5`)~kX7<9w7%#b231 zwawWUow@AX#)p<~F|KGf+DE?O;kBk(&FLl-fwh<&EvolQcK)n?@Oc3HQ@bGc(~$kz z8fgRr3p-o;R~lRh7YlC6V-Ac=T3qwAs% zXp_It@;fQ9(Tb7m8RzFSJZpjgJ#E-6jOZ+L_t1O5oI1tilVE4=}=d4 zrvu=~7vhHxh(?>!zT0MnovbdM_t9TZb~;>t%E@$bZ?xabogV=v7I(u^1KnEp6n?V3 zSFu?3aHm+t4-u(C9&yJtf9&L|Qdp$19k?rM`AY6*i?})S$m#m#dJ+U!dRUGVH24bt z+e)bAUf~}~-^#Va>&+35CKVy)fIePPUvo zGl=ge)HIqkxoSo)Ly-sgJDW0BSW19@Eb_kf4)~&Scl7TceS81+hJA}RA&~^nHFd40P}muDnytxR zQi7-h=7yaE7{?WTXFuvdY(TF8U<9O(YJyK|*vNk1hM(OEY#E5B#g{ps`dDB)^7|P# zqg&_CPICJb^|w`LBD0G#-tP-@#R6#UeD8sP8isz<`yteo2`W{rI8%CNyHHB6xjEs7 zG$K2`RX8c$A4wzAq9^KfS`-yDy>^0lr(x|hnAQ6*IT@UE7N*s4cCuIa*7QWJcYL(2 zxFD#vxI;jRit?6M<~)h zzpRm8|KbjSE>y5@mXt;484V~exD62zNqJX@F^6_JxKs!(u7z$v?c>$ds;mr&nG zhX+6^iHgH^leu`w6>aU=4Uoh<@G8YYu?0GxlD5 zU+4Ch=hyZ{_Mj=&Jdu^cv!8ZuPNL%uvSI5IKhaH!w>TJOUk^W~k>sI-VAtPcdj`-`h zKA_T99m*Jl4@DU9f2zhHf)Oh|m=XRfFWovY*}e1)5q;WZd_n!B?>+{@BIjC$UzA<6 zouR#(60ZtmbRNKz7VB&?)AKnd# zDQZ^z&RDF~is*6}e`=uBQ0y+}&oJFelV|V54_28|SgyDiA#S<-h6=c7sefQ{^p?ip z>N{YHocSRvS3y!#$;)E?ubPCtb?4R3p5syUb|INjimMOG-(J!$E({Q`UlJ#Od)BG3 zYeZ#0V<=FxwxP8}-z$17 z&)pT`6ND5vOjtwK^_pBzR{i`OOMIXAHp^yMTXfH_8^v7$VBw+NRkOiUYyh{TJeMr} zH$zu=fQoWzHt_b&dIM6u4oS5|70aV!KC1i(HQfJ&luZ>mJ8$q$EwDc4g&tK3 zA6pEfAc5(kHj?V4Nb@4ng1t;hBH;T=is3y7_;cd9KCx)T4f>TKG}&qv=#F=1RWGPv z{Qhdm8OdXYu1)pYCN=#w+Xer1^$~4gwpDqHyRaj$+dx^ADSSZ%ejwywDEe^#UsP*M znDg3t>qw^0@D1m>Zt1GMVOay?k!|*6TjmX(3vr9>KDRYCbAY)e@cSoX=r48G&nuy zq_IDwYjX{a;Ugyhj|4B^{` z?tQY@fHGtNDaBvJ`OL9$5-)5BhHi6whadBZ4$mA^rW6BL8IoHa9ONNFTDiB!EdKM? z8|PAdaQ0CvW}F*d0bDOS3bqx8&*U@@>n1zrixXs5QL-1T z4I%M5B?_9lL$p%a&uwmqG+%#c$Z_@Q(#slg*6VxytjqL)F{B-DgIc-IostOVr1;PT zZZW3P=T1um^H+ShCaQf*SW8M+%Su=)OjxT#Az#j5Gs0kVz+m%~(T1DRMuXAjJ)=!5 zqfI%Z%?P8-0eAyrvf*a3(GXM!pI7W7v%{&3f%3#c!I2kF9JDeH%JT(U`31`J6u>tde1K`R(}!nN*wJ=18bJJ`p@OnWE~PXRRgiulBuYmY^s5W{&SDK*{to z^%l0|3b-T}?*iU>;L0E$RKaIqi3i}xIlyPRe~md~WvqgxId=|xdkiyt-s1Hl`_g@u zps;PMd0R)6=l+rGucgJc)zqMFs#=IwNZF9fnd(Ec^bZT<1dbgbT3rh%o)eM17ri_w z=Bk~&l@mi68$xH?<3Bg7y7;={X}3DrmR%J)wN+zlp!cPJVE)7n!>b}3xcsX3dIM4q ztc=nycG?s3a9sg-Q($!7V8-%)KMEBrW1#s50zO^BMTmve-T*l14UP0FTu6Jn_z|zQ zlHLZ2CgEj~ms1@a2(o^v?d%%&MJTGz!T0dLK`opCakd_XGl29Mh&zKd%4vi7nTQyk zTasiio`3$y_VXX_4}SRUTtf`5A*c30{1zGNvyA7edNGwJUxtV;b-Ab8@h;QNH%OZ| zwrI2EvfKVxfb%3OI`c*AZL2t?u)u=xpF8(YB0nr1s%-!(TcA=b9YJsv`!V3pbbXMR~VeNNHi4KEXXX|G~D>CE91Y zb|lC?*uwe3Of0bBNk)T_l^8Rv|Agz=8KR-vF zI09Mb4((Y;YY%|k4q1J@B-5YbG%gxgg!D-SXOE!M+_%LW)? zndnz7F?V^)O{pbGPkFu>N&#`rmrAr>%YF2l=1P6du~7F+#tv7-paUr4n&^y~|5Lo4y!OKGPrp5@NY-n2 z&cU+Mg_g3ryebqV$X@PCr$j$sYkkIHkT!5G!#xQJs061Q%z(-ktUJCwEud2OGEe15 zMq>Hfp71rjo$fr)v4pawLBf_{&QiYpu#!c{(I0?9z37q*Nlsa5aB1yI6TER@uKCB_ zK&MK1S!U@?iSqqHV+LTWi-3_W!`y>n91dmli}~_^LNL4N>MEQBfR^H@Ws1I+1jyD(oG%>>s=w+ zU8rmSPn-6Tk=EG?@tG(7rqvLdt(-e+FpyjfY?@WS8Z^pyXRq<-pT$^ z;w)pa^HiyJU}UB*_a{ARqefIL(bhoBt5~^N_>@K*&Ahx&gB!E5(gUk9a^$nX+p*RR zue{jWgYnqrqZRhGkX^imgVu=_iM7R{tK1urg}uyF3yV813ph5M@SgR>`lf*i6d71S zO0u~J#dyH>`h4Usa27AT((O*ShZW49-hLsAE{eDsvY2kuvpAm)TS<6ayD~{>DfW|J zz9W{N;n_>&*xs#IpE;M(HgUA_#p*U5du$id_vy(%UVKGemWb^xx_KNP^K7=b?zOZp z&~NtV z0_G^~Mp)c2r#Bi<8kx~)#;V$pKezre)yp96)}p>(ccApn9f_6d-SL&~BW+k2 z(yDGw%tkG!yZyc$X!#Yd=(gKF(_*^J?s3!c02K=yqd8k+ZQcBa?RO?UVretfwdM2B zvA*4+a{abB#6C8jxqJ4Cs?h=d3FRj$8EJo9X3NCka*+jJcuJ%d$(ZqU*#Sp0iZKJf z+zG2F0=gRkW$TNX52xIqSNh-LqkLFw{e+`Wh_qrEGsu~;C=~c#DhdgU$UWJO!p@?h zbNuL+|8FB~HyS&OfzC0?FQ0QWY!`~1#iGEkAvq~@CVQc%NMeU$9|J8KCLARt(jp@o zBO}rxCmJIs(xM;`vQ-qaQxtNLoGdt#t^bXomCcwTO6RzV7hmiD*nS_cXXQ*bc>rUK zj!281XpEkS6OG6zRN$2^I@2^dll~*7=|@cZ7);X`O!}A#`M4r`c!oWMe;*NXMvO^e zxYo2fM_*=95)5 zUkOdN$ycm)++9qz0Z4GnqlOSA^pj?uvW3%w9|6^KoKGHZ-{>KYsEj6~txLmCE{ud1 zKg!O1_?6CeRYz_J{=b+A^9H@1D;Ntfnwc^%(&CdYaHe}{IQNjf7c0MHPZ~+X!S&=y zjrWPAXd%a8eXdd^84GD+NV%R~cINw7H`6xDLduubC6#_{C6+MZvg=)IqvodU@_qil zDP=!T-WmDrEf{VLXqUZOFfZB<;;1Ycxiu}T8u?1@BXMS^cM~l^dga$zQ{oU3uHOF6 zMX%4#AaQ)UTos}cq_HzC$5+BVyNJO;5E!sFO>n3` zY(1XqcF)zrzgvGhmxFCzjs=r8N^Nip=koujJD2d-qwM=jCPU?o;~(va?H_M{uPOCz z1&|dw3M(-SeTh*n3yxK`x~p`YovA2FwXXslkQ%D6nq@DZp zW5JN*=|U-D)Q)TBZ)BMohC`{oZJnW0&arV}G)p;~DA$YOQvq_j2f-$ei~1g=-L1}; zqK>&vj)OuGr9b7=WLlP^NSQ#BN90DGNItGtKmIuKGW^(j0$q2(D|zuV6dJlVYoUMf zRbG~1h0xog%)eFd)h04c4{o z7NElRzDTxW(aaq7uNQEg{7hDV#GdMn{{UZ&IBhxTiUs%3UREt?F5%OsIh}qIgCRe{ z`E0QwUu>Q3Wk0&@2*)%2EY4>cMfqa+$(fsIwv_$oIi%0Ay}5phF%Yq4Q1qkIf-;CS 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 a$1xo%1ox9%9ceNF__ldZ2ky8&yz@WAsp(Gu literal 0 HcmV?d00001 diff --git a/lib/dateutil/zoneinfo/rebuild.py b/lib/dateutil/zoneinfo/rebuild.py new file mode 100644 index 00000000..78f0d1a0 --- /dev/null +++ b/lib/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/lib/dateutil/zoneinfo/zoneinfo-2012c.tar.gz b/lib/dateutil/zoneinfo/zoneinfo-2012c.tar.gz deleted file mode 100644 index 641f903a51d4ad8072b6bf0b63d668d2f0192fba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86841 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πq9r+Lfjw1{Y0D?1VqmkRB0F`G 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�-#nwoa8Y8QrDK_aRbmoBnL?D^60Mx82xX_83dMUV_-Zs~p*WyBo2)O)_G4 zUwDhn;yq;hPXMF=!N=KwDL14+w2kcGz%prw!3=h&{VHkbll9VYJ&`oL>Q`xxn}ekh z8-_|FD<`v~rpHSE3jZcVH{Wj$QvmRm5^t!od(Fox75y*!))4c@pHX(TY5CX5}PP6yIohNLBU2`y)o;m|&9Dyb7eyGeNCPU`gS}0>O zqGZhXaWZe4!5r@^OqtKyNKThMma?vwI&*wid2;-AgPd-YEI8d4KIgD_uQ~n`n#clz zFL44>a9I#-4<|VAlq|$xDJRr^t1R@%ZdthATv>S4Vp)%y`Lc)&V`Pz)b2w4cxw7bW z!#ICgD45G=XL$8luw9R{xjx^b@R{H@}E>lfx5NA zItY$YOmh4a{jC)3t-eDd?gPH?g9{M(E-rOVK4`rlU*O zo(8cb7+qG_7+voF5?vvfh8Ej9gDZRP1J^b-LD&BAJGlNx8Mt9J3*3lhpyJ8xz)e#` z;AXfRxI04n{g!VtxxPsjWtLK0S<AEHVT4=T89- zqz?xVCS-tR-P@vvyraOwUABPb2^DDhg}3MtN(6f3nJ;?u#u)I}wk+`2BMDH6qyv?g z_kqXf^a76`Ohc-K`+%xLm(UZr?%;`aazij?86aJ`2R+%lC3v!M5O9hy9ym2|DSA5b zI(R1h6j<%^n040qD(jrp3D$Wd3G4jpO{@#|m$NSK%w=8Kj7hF8pCP$6bDTsrE?aV4 zm@2uE+*@)pCPZ>8z)NzQ;Uu}!&PsCECYW{4gvq-1){%App(X3VB_r0uqiOd>q?>4Me8BE!X;3fk-tJvCIYqXKNM;gnGD(c#zGDqgCLr%C)D1w zJ>+Q63v+xKfH~cE$DFGjFqcCuu?|~n(2m9T(N423qMaukLtS%9QF`in)NQ~8kRjX( zy33WWN6cK%v-M(-$;bzp@5g}NHnUOhD_qd$Z64ZXPaxR!QUdC`Dhc)5-39G7$sg^$ z$QES{vPby?SdMTVliEuzu*j?QTOD>|CcG6YOG z*9=X3=>+!O{s-Ex+8FGQH3bK3eFP31^8!qoT?KNIszL6k2WWEl9bn3UN;I|20+8po z0Ubo$hNiVf(X@N>(De60lwVPR4!#nHW~}LlW*#024iWjGf>p6#R(cy0p5zG*?d1y! z2U&pGZgyZ!@M|!)DFEiVwnX#r8(@BuS7^Sp3@mtZ0Ua)01&*lNjgDM!03EeqF*>@S z7#%Zx4BAN12=n`f!58;;4QgY%0 z$|{!oj6>N0JtuJjQ)AgduIZfMAWwGii-oe#PQ9d|r$@@d&D^EoTL#K{JZ>qCC>p_u zJj-B5jU2#<-rj=UvwwFEd(NNiUcPNPF}dg2v9@}gxZbA=(}vZN&}DJNh4eAM#Y|W~=o3<7fO|LCvF>KnwRbXiNKtXshOzQ477}sKtxD zsO8O#sMToj<_twFDgv0#V189;nl88tPo#3UxVTh?0Ir!}uA>zMTy{ z*NRra{?ST{zK^Oij>@1XP5FOW{=>Yac%Cqo*U<8xDEIxc{#X8xl>b_k|6k*OnoSwg ztFUrT5p*hNBxqvsiY2$k^lEGG|3RQw?zJmCatc+6Xa#cPiJN3{|4-mQgv%3VX@&s) zP5ZCj`5zt(IvM}h75rz{;eXnY;sny>`)O>xh90%Guh)F~c*)H>WrKNzs!+4#V*Od7Gk^Wy^bVdDV11Kjl|F3+DdvFCwQQQKaKnM1pLHBS|?7uLD_|+n_1Zf34 zt}s~>za6>%%KeYxrtXM< z!)gYw5gUMrC$|7LO&JbshUv%_;ap@ZyBV^B(HAK(KZ}&qmLof#mLR1Shk-pMJAl1Q zhamgrF90M9Yk~dwBY*?xnZUt>0YF*z=Ex!M?!e(LD}nNaT}b(b$H)s@tt-i;=49p^m`^w^m}iu==UEQ z(;r-XOMiIu5&hAwEA+=}tLRS_?xR1QDyHMZm(iaMo=Jb6vPb+Pc7ynjz@_4s?x^@r z`?2Cz&4uC`y%cfHi#V~_A{(H|G*iHQf)@hjI3Z0_t&nDMmT2?9x5(GyvU@PnMko0n z-{5~388_glt;L;`;-!ws@%dWr9+)d;1=~Y$28G&8JP%Qv8Wj|)m=#l$%7^%Z{_#<$ zkzU2$=>HgALB?Qi9zUbO^*^0iFaGP{Me;v&CjZA$t(#8cjGtXJ!wy({QiLuM_Cl6c z>w$-(tby`D23Yw;3-OWmf!L9Q?V)3hY1pxK0a)ebK;16^zaLl-X$gD%~+z%K7hgsxP1 zKv$RNV%Ijbf@I_RVAp@Cfo>%GU^j|JRv^pS+{-BS6pVNhH9F!M8+XY}P7FpE+2 z(T!C$(+!PMPk3n-|FK!QIq6BEyewfFpWCqbr+d}!|LRVj|DsL&Q&hM5*Gg_C5K1O= zen$Nto0AmA%j5|(5CQmR{@1Nu{Rh*X?Ej%n{Hve;NKvh+mjf0hP-<)6e2n7vi2A6b z$m@R||6D;PPa_1tZ}!+z@zL|GS;zJs5?Ai1WgTaZrCT-qz_L1BDz<)B2HC7lrQ4od1+|_U zLbqGfPuylOlLd+V#BF<6vf8D$5!*YzVL60R#5AMJtoCkp^!E4nvYeU%bf;4nCC>N_ zy36KLNe5{ez2nR!l1}1P^v*&=;);ss^q6FcTS2jy(RB>VJ#muQ!-mW9^hp^5X|x!@x0I^(F!_dqDyZ|FK%x8=_4?tPDNSmR9C{#}0O1aKd- z1Fe7I1o@v}2ffMSgtXnn4!PKi6J~r|8g_7qEd0SnX^#~>Wf4bbNF&F)$vz_|E6n_V z&S{oA21m*W5>2&~a0bdHfDvJ8Q55`UVlAKbiDqgcM2UY$#J?$U{pFv3AvcrH)nEbe z57hrMi1UBQ`rihS{~!0?Ut^v6zQz9`g#DNOAG}EWPn+^zo&WPo-Pc9YtBQ(BA^cd} z7T{QUAGmTyJaT-s4_t+111BcizY z{B%EJveXJNJ%1H28~qk&QX&D&2VMcd`OAT(-Sz>^@+Sbz?Un&85(WV+K8y!id4~ZO zrc)5hwyB6^`B=o75`tK-PC;z0J0Z4{gOS$zjS;(oBBaf{%>a}*8flAEA?*Sd0rvcd zfJ3`c04@ADKzsTCaCF`XIG(5koQ-Ax&KoxXF88y64)c#99Z&s=bjn|cblyA^aZQ+x z&}YUX-_>7k^?l^K`^$A+fqd3q?$vT2?CvZe{M-bfM|my~v11Srxw;P!H7^W^p6mnk z9OeSB;oe9uP6!gi_CjJ=PDmWx3h50QBk|^Mk@(t2NS~)ykc4YhNaBfoNZ);8q+iKp zp#PfVz<~LCfq|lpKvF&iaMEW0+=OvJa#%Ky;++blI`sy4mLb5PrbCf5N-~o6EEY+> z9)$2udLo1Ow?{HISsWRti^7UYrNZ-t!YQSzMGWfH*$dIltGCbQ>WMqCvWVBsRWc;*3 zWU>^Znx4N+H5=22+N7kMYCiB56`cPYwQ0A*)Mok9sLk!xP+KGnp|6OL{VF$TGTFJIn|#3 zoa)eS0+kkihT6W-AgZJDcBJwSpA>aGHCNPWnv&N2%QxuB5Vf>!|+Y_lp98r&9w{R)~T+uN4IcP85Y0#85-+(?p@K zexrunT}TZ-H;vk(d?Ynu#}I1d>Ved#c|EDolUdZB!`!HBxWA|uCrT8v&CoS_bwe~3Czw4IuiznaQP zpHJl`Ora)+4X37fXHZj}`crw9QPe?A^F?VCz9{WkUs3w?2oe9JuW0c8PNIxWcB0H> z=At1p0FeNBCdz_uh~U9Zs6!KLslsran(cF)n&W(mnrn5CnrF0?n)iAob=dv6)cgyR zsRc*!sl#{jsUtSe7L8m!Q8a31o@n&AG|?Dgf=I{Y|Ex8Hke@i8_*{Ql2a@ui>lz^B zXD4f<2mH6sf2ha*Ct5<2@?SUPKlo4W|G%=kzmx@}Ii_3q&&uEllEV0znGJ6LeaZjy zbZ3(F-@1bT>DsjaEKJe=m;Fb3`s{Q2Z{s%VXZBzI7Nz}{Fh}%V_Fv}4^P~;fz&0R2 z8iAUZ|AAaVGEXSfLjM12=f8O}i29EP2u_pn|GNFp6$<$psj>Qj_>TvJh zKGNXvf4!c7BL08V4ftyOKh5!P-it}aGuS68*%ltAK zQJgO`gs+VM$4>$j@qcDIposss>H!e(f3H6~ejfkd=YlBW|4U5~Mf`vC6GRdJA1GDE z|GRBg#{cbRE93u$n}GjX{9mQ|0Ue3|sJoh^!}*!~58?`k@da9t|3tf&dh*|m z{;TuI};M|lJO1nGUcdhz8UNK4`>&a5ZBP_lLN6+1 zOgI)-Hv3pPU%zrkdW++$BlL~TpS3XB-lT;!C834&>f9F2w3rso8~4o)9=dgQh(Z6^ zq4vTSp-&20gzLq%2(RkbqJA_00H)Xy5@Up-=lrL2U>`CR+)(%r%Sz@7bF||BdDZv- z+{pTWUBUmH@Ad!Q9{2@#w|O-1zC025kS9iJS9b+~_?ZZ0a%-Sb_l*GQACmr|PWy-d zK|k@6{ErL^*U)Qxv;Uu)*O&ZXCY|K}>Wcgqs@6s=J`9qz@@Cbvx;r)9vh9F;%ks>0 zYfATg>($ZeHrLzc+fJsZx8ARpZ&xrKZZmIf4U`DOZISZ)b^(RBJ^yBnLpuRZ3qMpt zdwL0XbY5NKcw!IkY&50DdE-*t<$gv@hxw&&$5W#GPWemV&YK74yCxtoeP(37+ki-z zA?%#*?$;Uihyn9GTZ1su?Rh@)-E-L6rb&(WKpl8O1HyZFP^ILU3m)k+qJc( z+r+JS_l0w7SiHHo|M)HW0bIRhRFvHtFH9pXA|MT-0#Zte)PP90bhm(XH$$UzBV9^& zOAXQ?C7nY`cMcQxndg7b`{n&KYwb1nUVG2X+Ow};UB6*IW}`WXf(iGHjUICt3tqQR zS)fJGex<(8;+mb{=APRHk?jfA_JBUE!Q(u$Z5tT=-?6AI`kFD zGKDq+65~(eV=;{+5C<{_0UzU&iqanijv6- z?52W>DD?SF9yETQTaR`yrd-PLL~UyD&s@nR#Dokz^4|ehSUQ(!u&HOHsb7EF4?b+N z4@Bo&MkcXfsK=@KEK&u&h@ynH}0|0U$*IqwC;Xz`$d*Pgd~2Txtz;(N<^ z{!3*Q!ZF(Ub0id!40yAliCg+ubmND%8%!B0wUBZXo(sxoUcy{V#NO?3Q?Zai{%uHo6(AS*~ zFvUGtrk4Fmjc9r`1CT@kCcnh;m_<|2TwgoFW-8P491zsaQ5*%&8CKb1RC3-jrF;e2 z1wXnIx&cSG5N7RyTol+;6c~zAT?UhM0IsXYEJm27)Q6B~{db@h@OaY9FjBW`-~y4F z!&Ml63}KrF7OGi7XLeBNk4>WUn0~x-kZd2^4&S?9$Z=OZg&cCL?ht-zh`hiBg7DA5 z@!(e8D{z**>ID-Nz+mwf2F+*}*IrD*A$(%r?s=Dh=HItakj(JRJ_?YA=N}Xb1FnzM zhSidk04{S?o#f9!a&EvkB-xgX4M8K$fzjF`m=|vvKyIV#&%IgiOzVN)Mk+L5P$XFG zUCAqp>Z6*+hhQWvEY3U$W$~>Ki4Fh zC_0SsnT{aY-`aBAPtIj;_vjUC>a}Gz@O_6RhRVh@Xcl`c_I3t4HNH56Euy+dbDC?m zN*LGi|Bro7JaJ^C#A$H>hqW|;zqWh{qG_HIjW_6Aj4@bW{c4aqsD2d2ub%x^yx^pr z#O$c(XMr&bVGGTBs&KvtvlSGFS;w&O)cU(;9{a;Mb}jK{{+dct)69=O0}et&)=CN| z=Bm{*q$6#kss(L_w&1pM?|-t3dx{?lZD0*QcEVjldWA8*b(fl)kCA%C z%4F5egzdNTN4BfhMJm75*ET~u#B4!{pzN&T#jLY8b<@fVa1;EwVXELT<4yhsHeUF% zf{=0{v&SbqwDk9k#BYT|BbQCFrSn%Y`)+Q-Hnl0u60$jazLMDg9v%&}oxT3>#&3>p@w@lF!$+O7|5lHBIzw%U9WLQIX;cbR^M-vBz&VjravFeo`!Y!HB3xC{+BGefO)jx1l_eo|GZpz?gr?9F=}ANXxG4+7GffF?UYP-m_Grt*tpcVo`W ziYCgs*ZgOSlZ*7^5YWNc!<(zPwwB8_oW~|qnr=(UvQ;kiCeD!`nGTtS_UBXPp1q-< z%Svq^t`(=$1PR#CoQQG=bzdf1H~b@5^O$^}o++>B+~C7DCKkxgkfX(IM=O-Q?l48F z;9e@`7n6vaoUURMZP?7jVt2zO=dk1x+<$4+xp=7R{q5$JbN%=?6xRyP{MI~ONB!}J z_m&Xk1D9e8XPWjBr?p@MZ2&J?8rCa?j-rarYq8!5N!*@pVFg)7)3E zhRPrNAJ>_sR{JE9G?~YqX{v+qZOXlB^$&Qp@9P60AMCD<{U70!{#wWR-7-%$x>f0) z3CTT~4QLl$bmZr}$K@h*6v?K1Xd)Ls)@S7G6o^^hw!-2p!g(y`bl4j$;vk>!IU(hm zYnk~ozER@!;X^dz%!0uad-o8=y(`tfhQLqZ^}~u6sQLG<`3p}JJ}jxaZpCp{bq~b! zBT0`fZ}Q(LikZRq=6hI(4ivTr8-+Dk4_2)hIe*Pa`YB4XbI!%e?0L#3oinx;muBK9 z{f$4GnPC!9ykB9t*szpMj{OzapJ~se7`e0lgy^%TzJ3>3%;qtERbifefA6BAj@MZE z7X2MbRSsF@f$zA6&iWk3wK;Ei6{3sgI&^I2Wnad}X}Q$iYt z@?5#))KTS|&zA64G+(9cyuI_bTuBsb*|w@m8=Ipf~VwmSxOwFaaFI z2+7?SZ1dt9P7KQcA&2ML5#LQ7Z+}iU`Y|$**#>jw$8?b*UH4EVovLDH zbe>4SH+bQ<&E3}(q9ZZi7SSkZwlqQ1Qq+xIMW_FJev+F!`^edRCB->$9$wqymCeKI zn0^vgDCS;pvS{tnu|A0b|LyOBKRJU@J@lzFaLh5dcua5gqEDfJNf_6=yB*in_i?QE zq9{I^$)P!IvU>K{SLyMwhzNP%C=a?H4eIMZ=?C}nx=2p$%A0+mqTdn0 zBp^M+#fL0nhI`S5M&+4cglFIM_qE1i_=$P-u}1%6NwO*4EK9%GjUch(n;paYZzPC@x{8@E@?N?i97UObpAzS}Ji>Saau|YBK`|bTa})0J)$xcI zMd_Kow-d@6{AJqv$ROF4LbS1?Iv}rT01}>ac+yLB6Xu&RlO!-Cy`LMQn7oI#!H6HX>2&N<-d0!X}Qeo!Kx$?2>e+O zOzpSXPePN0r(V@1AU<=3zmo9;CP#r)tC|wi?R+?xF;97N@*eDKgucBdaq8i30mNjy ze5hK(2U8@XcC(_HoMs59?Z0Ri!k`u@RB>eKVgCMgo?`3U2bSWStQ5cw9YyH13oKlN z$e*Mh0aAoFOvAfGjqPW3a55K6fWGtLf3gwDh%X;qPH!CZV) zp&$=Kp&*%kU2~6}gQKU?{vBMnsYZ;#0KD_G$=p+*iJY^iKjmPmB=eI4Km9jHa;L$T z22snEUigQV1Ll^nq^Pv-pdl|Ir5-N{@vxJ2GRclfP?#d`O~`F(PUNlAL-0v9gd~}( zA>?*%GK`cMOva$h9z5ztX&KcCmx}5*jvA$yu-K)JtK;7+?fKHtvbePWa^qLL>J)5Z zrtqqEPm(&jXKElSp+KpXyKZc#qJY!7w!YGk$?Sjdcq3>1QSX~$Sy@sK{z2Rh$|#Fg znvz8Z$jy{7oz1>UjPUWgA;mrlUN{vNlTa9?Yf&iIp(>fs+W9+=YZXVFAv$$h?>cxcbeeYGhlBlYi;o@FkKWWM;R}?*Y`0_Ki1swfX)iAxV zX=YHC<3ymQ@~`ReQQBvai8shwnb`C5r_|Rc~eg{R>YZ$@@a}qz!3iV$|sYnuPvwT;k zv`0=Zs7OkhB7N1h=pXa6zQEq`woOfj7d~1JXyO27=B-_-=8<5Sw4XQt9SfTU3}yk- zNWpK-&z~;8$$xhFMH1nUcVvc2Ku0Z7UxB^O{~D9MUwnaK(sC}m_x({6B@8rSxRjlR zgE;A`Y4G0b*f`@f7?Vg{+-k10x9;gA6Slte)Gs#YGa^IvOz(9@ku375TFwhDCI$st z-p5cn;83~8&QA`;T`15~EPY(O?sdz|d_m&W;2!!1(Rv}=W|mlW@RV`AidlI+hbg*M z-Xp1**n7|r_I;EqN5y5#Yl5Eh&~mjsiD2mA*INg7b4UOv2Tj}}k-PfzrFAe|QZ!#Y{tQ!6c@O*iE*`qXXerDCS zeITq_`ZA#Hit5#nrw5L5zV(Q!l2RW{?>qXtDVJ5&e|$qT_1F_u$Gbi>S)1!Z)Yj|X zd0P_BO4i1t`TZRx2G6h7?$Jq0al`G%*AX4Ois|(YLVYNTK^H8)iAZeCUpdSRV#31g zi4+J662@f?1u8$k*Njg8c!{9N-|l=Icw^YJu;lVy+S1dwT;F=-~)+KVDArsm|(r7kwX=vTDbZlT0}vWQdXF0gStn%- zD9uWF+e(z@yOrr6pU2I!=wpyR2gfD_+Es*RH6w`{|BW;lGWNLpQ4PCE7=>MR?zvso zoJL)7U%iL<>-OLZ2EOmhP!5;Nx!aAU&&5l4UB4a6_>43mlg!0K0MO-Oxdgj;me%TN zQZ{n1uv2P`NL<4Ah5_8YA8lVA1cQQs%6&lc5={3QNOMZ!SwbJX?V{y?=Qv82M-XYp z!ppq@!8XpmuVn+)GM(HI9fIL5&vzjfz*^I5faV4cB*U1DQ-C;o`}|EpSnaeLk*^&t z;NiFhMJgfqNA43QHlcMbLfOT=pxloWYLVb~41J&j;D8zB>p+Q8z}~Rw14$2^tNjCe zJ_Z8yUL*MBcMwMR3pz-y?{+0q@!jtAFbVg49tCciCR=1 zO$j1WZXu*w5Tl}g2tGkhJ~5Ej9tmzh8RB24okRY&2gi;4a{GH5w3}f@h=wgbWYHUHXasFE~lW`xqMxB#FBt)R! z$P#kpz6nX0m;?JH-!PH<0z`BHDfIePQJh&v7+I*M#0;JDTKgL&E0pfSx$`58i0_1e z6>?}Y5^P)vFkeALz5)>z|8Mhexde%pF}UD0VRij0Ww$u3Mupf zSjwhpasC2R8E}1FkaCkx(gJ*7RC^PfpIpNaKiiZ!2Mr^gza!5W%EI=*$roVb8KB|{ z^xs!ii#G%b&L5)q?9T6r2b`kCptTcQAfCkV@yQ4<$=N=r6%aePZb$KEQK|kw?k&H2 z$FDnd3tbzJTLtiz03&m%KkjqFs4g*a;5WJ-Y^ECkku22O3v{?^#2bJAnvOV+_(-)iNzvk(`3 zleh!7#cmfFgv^XZi`1s#=kkNs29VylcsS7lai{)$_GWZ1H1Ar_*x%b?Q)Gy){W;=e9aVw^SansW#&X2!^ScgK@OCCF{fN&kCY4DffcwwXrnatz!NJ1bcF^k->}+!~${22F2u zK?l@niuE(k{*vF@VXK|6KX;ar&e=<4ca%K)S3+bb4Q!8({WvghjMKs&PEJF;vUd`^ zOs!%X@Ro{E6ahWKkFf>b%;6Cit;vL8L&I3`a$DtBWN|m6AGe=NPp?x=+Xl$l2%sn#RBUOm zUgg+FzUubCf;$1&C%TTrw?kQ>*ew!R`0>7&uf}Vz-Yd*8d(UDE{DPCdv6g(9wfq@# z_-ZAXw8%+%#-P}Cxy#q~!JlQcmR%Cx^qVyQ{IxVc85wTWSA_cS3cWX+T_l8RmpDB-Q+`%m_6I^r zY3&*~4!BTF{-&_w62z9vHaxG27#f%6Ifldn9Vs^uv~)|hBD<}mY`&U`#W5+D^=!cw5gY~$QPGs!7`K?;8kZh)HE3fpOOcqHEcB+;pw+m(xxU!}1Z}IH^{2?b z9xBI-LR=jx1^5~7KaJ3NF45~&ZB;U*t^xw1>{NuMe}{JyqKtg!VA7ym>L1AfX6GNN z)noeVVQ^$rsi+-rl#Y0(g$8kqnuQE!gT!0*t1z`ZLgTr4B))QU7{qet+QgYtcuS34 zuA7hf%1lL*Q|h^T5VSPF4Q(mou&ceqh^t)>9##A5g;nFKTH03{etSMKB9nd%| z1Y#@)qG+`I%E_8acLbWeXYd`M2**ULB$6f&n9;-R-hbch1Ev-_yH0dxjZ9?K8l;uy zLd6NO4QFtiV-y$RB^USlOIl4oPbuC_AXzC)ut9K*_nMOU<2T`rkyhgKrvSEx$N-Hf z1y5MT$+E>9=GpY2J>R**waET4zW8y#-oX@X!Mz|x#cY48Pj2ex3f1DSrS?=>!@}ZR zHCdfKoq<%tQre=Y`7G~C7&Et5e-+oOTzelYMz1LYafd_z8-~T-@ zG}XshKUHkZRxo6^Zl*@%6q*;W(s%x|xWH!BrZXrWUK{w=R?mwIAzJXjfNhR2SWa=xukvyDEI9$m?7RX6#$qvU&~b;Y0~azD{Q% znvl#yjRE}OX9t`hSx`B7q1dkkjXCYy~DN>=?^ajTN)byqIT z?;_*8Zv44R`!lE}vto8~L6`I93dNy_?BtRz&mWTIxZz*WF3

SU`w8{;N=!pBp!x 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 3: PY3 = True; unicode = str; unichr = chr; long = int +if PY3: import urllib.parse as urlparse + import _ssl + DEFAULT_CIPHERS = _ssl._DEFAULT_CIPHERS +else: + DEFAULT_CIPHERS = ssl._DEFAULT_CIPHERS from lib.requests_toolbelt.adapters import host_header_ssl from lib import doh @@ -25,24 +28,20 @@ elif 'PROTOCOL_SSLv23' in ssl.__dict__: else: protocol = ssl.PROTOCOL_SSLv3 -class CustomSocket(ssl.SSLSocket): - def __init__(self, *args, **kwargs): - super(CustomSocket, self).__init__(*args, **kwargs) class CustomContext(ssl.SSLContext): def __init__(self, protocol, hostname, *args, **kwargs): self.hostname = hostname - super(CustomContext, self).__init__(protocol) + if PY3: + super(CustomContext, self).__init__() + else: + super(CustomContext, self).__init__(protocol) + self.verify_mode = ssl.CERT_NONE - def wrap_socket(self, sock, server_side=False, - do_handshake_on_connect=True, - suppress_ragged_eofs=True, - server_hostname=None): - return CustomSocket(sock=sock, server_side=server_side, - do_handshake_on_connect=do_handshake_on_connect, - suppress_ragged_eofs=suppress_ragged_eofs, - server_hostname=self.hostname, - _context=self) + def wrap_socket(self, *args, **kwargs): + kwargs['server_hostname'] = self.hostname + self.verify_mode = ssl.CERT_NONE + return super(CustomContext, self).wrap_socket(*args, **kwargs) class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter): @@ -52,7 +51,7 @@ class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter): self.cur = self.conn.cursor() self.ssl_context = CustomContext(protocol, domain) self.CF = CF # if cloudscrape is in action - self.cipherSuite = kwargs.pop('cipherSuite', ssl._DEFAULT_CIPHERS) + self.cipherSuite = kwargs.pop('cipherSuite', DEFAULT_CIPHERS) super(CipherSuiteAdapter, self).__init__(**kwargs) diff --git a/specials/search.py b/specials/search.py index 821f28f9..052bbbe3 100644 --- a/specials/search.py +++ b/specials/search.py @@ -249,7 +249,7 @@ def channel_search(item): cnt += 1 searching_titles.remove(searching_titles[searching.index(channel)]) searching.remove(channel) - progress.update(old_div(((total_search_actions - len(search_action_list)) * 100), total_search_actions), config.get_localized_string(70744) % str(len(channel_list) - cnt), ', '.join(searching_titles)) + progress.update(old_div(((total_search_actions - len(search_action_list)) * 100), total_search_actions), config.get_localized_string(70744) % str(len(channel_list) - cnt) + '\n' + ', '.join(searching_titles)) progress.close() @@ -262,7 +262,7 @@ def channel_search(item): ch_name = channel_titles[channel_list.index(key)] grouped = list() cnt += 1 - progress.update(old_div((cnt * 100), len(ch_list)), config.get_localized_string(60295), config.get_localized_string(60293)) + progress.update(old_div((cnt * 100), len(ch_list)), config.get_localized_string(60295) + '\n' + config.get_localized_string(60293)) if len(value) <= max_results and item.mode != 'all': if len(value) == 1: if not value[0].action or config.get_localized_string(70006).lower() in value[0].title.lower(): diff --git a/specials/videolibrary.py b/specials/videolibrary.py index 124484ef..543834ad 100644 --- a/specials/videolibrary.py +++ b/specials/videolibrary.py @@ -212,7 +212,7 @@ def get_results(nfo_path, root, Type, local=False): else: update_text = config.get_localized_string(60023) value = 1 - item.title += " [B]" + u"\u2022".encode('utf-8') + "[/B]" + item.title += " [B]" + u"\u2022" + "[/B]" # Context menu: Delete series / channel channels_num = len(item.library_urls) @@ -645,7 +645,7 @@ def move_videolibrary(current_path, new_path, current_movies_folder, new_movies_ return config.verify_directories_created() - progress.update(10, config.get_localized_string(20000), config.get_localized_string(80012)) + progress.update(10, config.get_localized_string(20000) + '\n' + config.get_localized_string(80012)) if current_movies_path != new_movies_path: if filetools.listdir(current_movies_path): dir_util.copy_tree(current_movies_path, new_movies_path)