mirror of
https://github.com/SickGear/SickGear.git
synced 2025-01-07 10:33:38 +00:00
Merge branch 'feature/UpdateDateutil' into develop
This commit is contained in:
commit
54129c519c
16 changed files with 1407 additions and 549 deletions
|
@ -3,6 +3,7 @@
|
||||||
* Update backports/ssl_match_hostname 3.5.0.1 (r18) to 3.7.0.1 (r28)
|
* Update backports/ssl_match_hostname 3.5.0.1 (r18) to 3.7.0.1 (r28)
|
||||||
* Update cachecontrol library 0.12.3 (db54c40) to 0.12.4 (bd94f7e)
|
* Update cachecontrol library 0.12.3 (db54c40) to 0.12.4 (bd94f7e)
|
||||||
* Update chardet packages 3.0.4 (9b8c5c2) to 4.0.0 (b3d867a)
|
* Update chardet packages 3.0.4 (9b8c5c2) to 4.0.0 (b3d867a)
|
||||||
|
* Update dateutil library 2.6.1 (2f3a160) to 2.7.2 (ff03c0f)
|
||||||
|
|
||||||
[develop changelog]
|
[develop changelog]
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from ._version import VERSION as __version__
|
try:
|
||||||
|
from ._version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = 'unknown'
|
||||||
|
|
||||||
|
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||||
|
'utils', 'zoneinfo']
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
"""
|
|
||||||
Contains information about the dateutil version.
|
|
||||||
"""
|
|
||||||
|
|
||||||
VERSION_MAJOR = 2
|
|
||||||
VERSION_MINOR = 6
|
|
||||||
VERSION_PATCH = 1
|
|
||||||
|
|
||||||
VERSION_TUPLE = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
|
|
||||||
VERSION = '.'.join(map(str, VERSION_TUPLE))
|
|
|
@ -41,11 +41,11 @@ def easter(year, method=EASTER_WESTERN):
|
||||||
|
|
||||||
More about the algorithm may be found at:
|
More about the algorithm may be found at:
|
||||||
|
|
||||||
http://users.chariot.net.au/~gmarts/eastalg.htm
|
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||||
|
|
||||||
and
|
and
|
||||||
|
|
||||||
http://www.tondering.dk/claus/calendar.html
|
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
60
lib/dateutil/parser/__init__.py
Normal file
60
lib/dateutil/parser/__init__.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from ._parser import parse, parser, parserinfo
|
||||||
|
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||||
|
from ._parser import UnknownTimezoneWarning
|
||||||
|
|
||||||
|
from ._parser import __doc__
|
||||||
|
|
||||||
|
from .isoparser import isoparser, isoparse
|
||||||
|
|
||||||
|
__all__ = ['parse', 'parser', 'parserinfo',
|
||||||
|
'isoparse', 'isoparser',
|
||||||
|
'UnknownTimezoneWarning']
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Deprecate portions of the private interface so that downstream code that
|
||||||
|
# is improperly relying on it is given *some* notice.
|
||||||
|
|
||||||
|
|
||||||
|
def __deprecated_private_func(f):
|
||||||
|
from functools import wraps
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private function and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=f.__name__)
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def deprecated_func(*args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return deprecated_func
|
||||||
|
|
||||||
|
def __deprecate_private_class(c):
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private class and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=c.__name__)
|
||||||
|
|
||||||
|
class private_class(c):
|
||||||
|
__doc__ = c.__doc__
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
super(private_class, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
private_class.__name__ = c.__name__
|
||||||
|
|
||||||
|
return private_class
|
||||||
|
|
||||||
|
|
||||||
|
from ._parser import _timelex, _resultbase
|
||||||
|
from ._parser import _tzparser, _parsetz
|
||||||
|
|
||||||
|
_timelex = __deprecate_private_class(_timelex)
|
||||||
|
_tzparser = __deprecate_private_class(_tzparser)
|
||||||
|
_resultbase = __deprecate_private_class(_resultbase)
|
||||||
|
_parsetz = __deprecated_private_func(_parsetz)
|
File diff suppressed because it is too large
Load diff
402
lib/dateutil/parser/isoparser.py
Normal file
402
lib/dateutil/parser/isoparser.py
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
# -*- 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.
|
||||||
|
"""
|
||||||
|
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.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits)
|
||||||
|
|
||||||
|
Midnight is a special case for `hh`, as the standard supports both
|
||||||
|
00:00 and 24:00 as a representation.
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
"""
|
||||||
|
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')
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
return time(*self._parse_isotime(timestr))
|
||||||
|
|
||||||
|
@_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
|
||||||
|
_MICROSECOND_END_REGEX = re.compile(b'[-+Z]+')
|
||||||
|
_DATE_SEP = b'-'
|
||||||
|
_TIME_SEP = b':'
|
||||||
|
_MICRO_SEP = b'.'
|
||||||
|
|
||||||
|
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'-+Z':
|
||||||
|
# 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:
|
||||||
|
# Microsecond
|
||||||
|
if timestr[pos:pos + 1] != self._MICRO_SEP:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pos += 1
|
||||||
|
us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6],
|
||||||
|
1)[0]
|
||||||
|
|
||||||
|
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||||
|
pos += len(us_str)
|
||||||
|
|
||||||
|
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')
|
||||||
|
components[0] = 0
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
if tzstr == b'Z':
|
||||||
|
return tz.tzutc()
|
||||||
|
|
||||||
|
if len(tzstr) not in {3, 5, 6}:
|
||||||
|
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
||||||
|
|
||||||
|
if tzstr[0:1] == b'-':
|
||||||
|
mult = -1
|
||||||
|
elif tzstr[0:1] == b'+':
|
||||||
|
mult = 1
|
||||||
|
else:
|
||||||
|
raise ValueError('Time zone offset requires sign')
|
||||||
|
|
||||||
|
hours = int(tzstr[1:3])
|
||||||
|
if len(tzstr) == 3:
|
||||||
|
minutes = 0
|
||||||
|
else:
|
||||||
|
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
||||||
|
|
||||||
|
if zero_as_utc and hours == 0 and minutes == 0:
|
||||||
|
return tz.tzutc()
|
||||||
|
else:
|
||||||
|
if minutes > 59:
|
||||||
|
raise ValueError('Invalid minutes in time zone offset')
|
||||||
|
|
||||||
|
if hours > 23:
|
||||||
|
raise ValueError('Invalid hours in time zone offset')
|
||||||
|
|
||||||
|
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ISOPARSER = isoparser()
|
||||||
|
isoparse = DEFAULT_ISOPARSER.isoparse
|
|
@ -19,7 +19,7 @@ class relativedelta(object):
|
||||||
"""
|
"""
|
||||||
The relativedelta type is based on the specification of the excellent
|
The relativedelta type is based on the specification of the excellent
|
||||||
work done by M.-A. Lemburg in his
|
work done by M.-A. Lemburg in his
|
||||||
`mx.DateTime <http://www.egenix.com/files/python/mxDateTime.html>`_ extension.
|
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
||||||
However, notice that this type does *NOT* implement the same algorithm as
|
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.
|
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class relativedelta(object):
|
||||||
|
|
||||||
year, month, day, hour, minute, second, microsecond:
|
year, month, day, hour, minute, second, microsecond:
|
||||||
Absolute information (argument is singular); adding or subtracting a
|
Absolute information (argument is singular); adding or subtracting a
|
||||||
relativedelta with absolute information does not perform an aritmetic
|
relativedelta with absolute information does not perform an arithmetic
|
||||||
operation, but rather REPLACES the corresponding value in the
|
operation, but rather REPLACES the corresponding value in the
|
||||||
original datetime with the value(s) in relativedelta.
|
original datetime with the value(s) in relativedelta.
|
||||||
|
|
||||||
|
@ -95,11 +95,6 @@ class relativedelta(object):
|
||||||
yearday=None, nlyearday=None,
|
yearday=None, nlyearday=None,
|
||||||
hour=None, minute=None, second=None, microsecond=None):
|
hour=None, minute=None, second=None, microsecond=None):
|
||||||
|
|
||||||
# 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.")
|
|
||||||
|
|
||||||
if dt1 and dt2:
|
if dt1 and dt2:
|
||||||
# datetime is a subclass of date. So both must be date
|
# datetime is a subclass of date. So both must be date
|
||||||
if not (isinstance(dt1, datetime.date) and
|
if not (isinstance(dt1, datetime.date) and
|
||||||
|
@ -159,9 +154,14 @@ class relativedelta(object):
|
||||||
self.seconds = delta.seconds + delta.days * 86400
|
self.seconds = delta.seconds + delta.days * 86400
|
||||||
self.microseconds = delta.microseconds
|
self.microseconds = delta.microseconds
|
||||||
else:
|
else:
|
||||||
|
# Check for non-integer values in integer-only quantities
|
||||||
|
if any(x is not None and x != int(x) for x in (years, months)):
|
||||||
|
raise ValueError("Non-integer years and months are "
|
||||||
|
"ambiguous and not currently supported.")
|
||||||
|
|
||||||
# Relative information
|
# Relative information
|
||||||
self.years = years
|
self.years = int(years)
|
||||||
self.months = months
|
self.months = int(months)
|
||||||
self.days = days + weeks * 7
|
self.days = days + weeks * 7
|
||||||
self.leapdays = leapdays
|
self.leapdays = leapdays
|
||||||
self.hours = hours
|
self.hours = hours
|
||||||
|
@ -249,7 +249,7 @@ class relativedelta(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def weeks(self):
|
def weeks(self):
|
||||||
return self.days // 7
|
return int(self.days / 7.0)
|
||||||
|
|
||||||
@weeks.setter
|
@weeks.setter
|
||||||
def weeks(self, value):
|
def weeks(self, value):
|
||||||
|
@ -422,6 +422,24 @@ class relativedelta(object):
|
||||||
is not None else
|
is not None else
|
||||||
other.microsecond))
|
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):
|
def __neg__(self):
|
||||||
return self.__class__(years=-self.years,
|
return self.__class__(years=-self.years,
|
||||||
months=-self.months,
|
months=-self.months,
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
"""
|
"""
|
||||||
The rrule module offers a small, complete, and very fast, implementation of
|
The rrule module offers a small, complete, and very fast, implementation of
|
||||||
the recurrence rules documented in the
|
the recurrence rules documented in the
|
||||||
`iCalendar RFC <http://www.ietf.org/rfc/rfc2445.txt>`_,
|
`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
|
||||||
including support for caching of results.
|
including support for caching of results.
|
||||||
"""
|
"""
|
||||||
import itertools
|
import itertools
|
||||||
import datetime
|
import datetime
|
||||||
import calendar
|
import calendar
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -20,6 +21,7 @@ from six.moves import _thread, range
|
||||||
import heapq
|
import heapq
|
||||||
|
|
||||||
from ._common import weekday as weekdaybase
|
from ._common import weekday as weekdaybase
|
||||||
|
from .tz import tzutc, tzlocal
|
||||||
|
|
||||||
# For warning about deprecation of until and count
|
# For warning about deprecation of until and count
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
@ -359,7 +361,7 @@ class rrule(rrulebase):
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
As of version 2.5.0, the use of the ``until`` keyword together
|
As of version 2.5.0, the use of the ``until`` keyword together
|
||||||
with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10.
|
with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
|
||||||
:param until:
|
:param until:
|
||||||
If given, this must be a datetime instance, that will specify the
|
If given, this must be a datetime instance, that will specify the
|
||||||
limit of the recurrence. The last recurrence in the rule is the greatest
|
limit of the recurrence. The last recurrence in the rule is the greatest
|
||||||
|
@ -368,7 +370,7 @@ class rrule(rrulebase):
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
As of version 2.5.0, the use of the ``until`` keyword together
|
As of version 2.5.0, the use of the ``until`` keyword together
|
||||||
with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10.
|
with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
|
||||||
:param bysetpos:
|
:param bysetpos:
|
||||||
If given, it must be either an integer, or a sequence of integers,
|
If given, it must be either an integer, or a sequence of integers,
|
||||||
positive or negative. Each given integer will specify an occurrence
|
positive or negative. Each given integer will specify an occurrence
|
||||||
|
@ -446,8 +448,22 @@ class rrule(rrulebase):
|
||||||
until = datetime.datetime.fromordinal(until.toordinal())
|
until = datetime.datetime.fromordinal(until.toordinal())
|
||||||
self._until = until
|
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:
|
if count is not None and until:
|
||||||
warn("Using both 'count' and 'until' is inconsistent with RFC 2445"
|
warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
|
||||||
" and has been deprecated in dateutil. Future versions will "
|
" and has been deprecated in dateutil. Future versions will "
|
||||||
"raise an error.", DeprecationWarning)
|
"raise an error.", DeprecationWarning)
|
||||||
|
|
||||||
|
@ -674,7 +690,7 @@ class rrule(rrulebase):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
Output a string that would generate this RRULE if passed to rrulestr.
|
Output a string that would generate this RRULE if passed to rrulestr.
|
||||||
This is mostly compatible with RFC2445, except for the
|
This is mostly compatible with RFC5545, except for the
|
||||||
dateutil-specific extension BYEASTER.
|
dateutil-specific extension BYEASTER.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -699,7 +715,7 @@ class rrule(rrulebase):
|
||||||
|
|
||||||
if self._original_rule.get('byweekday') is not None:
|
if self._original_rule.get('byweekday') is not None:
|
||||||
# The str() method on weekday objects doesn't generate
|
# The str() method on weekday objects doesn't generate
|
||||||
# RFC2445-compliant strings, so we should modify that.
|
# RFC5545-compliant strings, so we should modify that.
|
||||||
original_rule = dict(self._original_rule)
|
original_rule = dict(self._original_rule)
|
||||||
wday_strings = []
|
wday_strings = []
|
||||||
for wday in original_rule['byweekday']:
|
for wday in original_rule['byweekday']:
|
||||||
|
@ -1496,11 +1512,17 @@ class _rrulestr(object):
|
||||||
forceset=False,
|
forceset=False,
|
||||||
compatible=False,
|
compatible=False,
|
||||||
ignoretz=False,
|
ignoretz=False,
|
||||||
|
tzids=None,
|
||||||
tzinfos=None):
|
tzinfos=None):
|
||||||
global parser
|
global parser
|
||||||
if compatible:
|
if compatible:
|
||||||
forceset = True
|
forceset = True
|
||||||
unfold = True
|
unfold = True
|
||||||
|
|
||||||
|
TZID_NAMES = dict(map(
|
||||||
|
lambda x: (x.upper(), x),
|
||||||
|
re.findall('TZID=(?P<name>[^:]+):', s)
|
||||||
|
))
|
||||||
s = s.upper()
|
s = s.upper()
|
||||||
if not s.strip():
|
if not s.strip():
|
||||||
raise ValueError("empty string")
|
raise ValueError("empty string")
|
||||||
|
@ -1563,8 +1585,29 @@ class _rrulestr(object):
|
||||||
# RFC 5445 3.8.2.4: The VALUE parameter is optional, but
|
# RFC 5445 3.8.2.4: The VALUE parameter is optional, but
|
||||||
# may be found only once.
|
# may be found only once.
|
||||||
value_found = False
|
value_found = False
|
||||||
|
TZID = None
|
||||||
valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
|
valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
|
||||||
for parm in parms:
|
for parm in parms:
|
||||||
|
if parm.startswith("TZID="):
|
||||||
|
try:
|
||||||
|
tzkey = TZID_NAMES[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
|
||||||
if parm not in valid_values:
|
if parm not in valid_values:
|
||||||
raise ValueError("unsupported DTSTART parm: "+parm)
|
raise ValueError("unsupported DTSTART parm: "+parm)
|
||||||
else:
|
else:
|
||||||
|
@ -1577,6 +1620,11 @@ class _rrulestr(object):
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
dtstart = parser.parse(value, ignoretz=ignoretz,
|
dtstart = parser.parse(value, ignoretz=ignoretz,
|
||||||
tzinfos=tzinfos)
|
tzinfos=tzinfos)
|
||||||
|
if TZID is not None:
|
||||||
|
if dtstart.tzinfo is None:
|
||||||
|
dtstart = dtstart.replace(tzinfo=TZID)
|
||||||
|
else:
|
||||||
|
raise ValueError('DTSTART specifies multiple timezones')
|
||||||
else:
|
else:
|
||||||
raise ValueError("unsupported property: "+name)
|
raise ValueError("unsupported property: "+name)
|
||||||
if (forceset or len(rrulevals) > 1 or rdatevals
|
if (forceset or len(rrulevals) > 1 or rdatevals
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
from .tz import *
|
from .tz import *
|
||||||
|
|
||||||
|
#: Convenience constant providing a :class:`tzutc()` instance
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.7.0
|
||||||
|
UTC = tzutc()
|
||||||
|
|
||||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||||
"enfold", "datetime_ambiguous", "datetime_exists"]
|
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||||
|
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedTzFormatWarning(Warning):
|
||||||
|
"""Warning raised when time zones are parsed from deprecated formats."""
|
||||||
|
|
49
lib/dateutil/tz/_factories.py
Normal file
49
lib/dateutil/tz/_factories.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
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))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class _TzStrFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = {}
|
||||||
|
|
||||||
|
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))
|
||||||
|
return instance
|
||||||
|
|
|
@ -14,12 +14,15 @@ import sys
|
||||||
import os
|
import os
|
||||||
import bisect
|
import bisect
|
||||||
|
|
||||||
|
import six
|
||||||
from six import string_types
|
from six import string_types
|
||||||
from six.moves import _thread
|
from six.moves import _thread
|
||||||
from ._common import tzname_in_python2, _tzinfo
|
from ._common import tzname_in_python2, _tzinfo
|
||||||
from ._common import tzrangebase, enfold
|
from ._common import tzrangebase, enfold
|
||||||
from ._common import _validate_fromutc_inputs
|
from ._common import _validate_fromutc_inputs
|
||||||
|
|
||||||
|
from ._factories import _TzSingleton, _TzOffsetFactory
|
||||||
|
from ._factories import _TzStrFactory
|
||||||
try:
|
try:
|
||||||
from .win import tzwin, tzwinlocal
|
from .win import tzwin, tzwinlocal
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -30,9 +33,38 @@ EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||||
EPOCHORDINAL = EPOCH.toordinal()
|
EPOCHORDINAL = EPOCH.toordinal()
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(_TzSingleton)
|
||||||
class tzutc(datetime.tzinfo):
|
class tzutc(datetime.tzinfo):
|
||||||
"""
|
"""
|
||||||
This is a tzinfo object that represents the UTC time zone.
|
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):
|
def utcoffset(self, dt):
|
||||||
return ZERO
|
return ZERO
|
||||||
|
@ -86,16 +118,16 @@ class tzutc(datetime.tzinfo):
|
||||||
__reduce__ = object.__reduce__
|
__reduce__ = object.__reduce__
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(_TzOffsetFactory)
|
||||||
class tzoffset(datetime.tzinfo):
|
class tzoffset(datetime.tzinfo):
|
||||||
"""
|
"""
|
||||||
A simple class for representing a fixed offset from UTC.
|
A simple class for representing a fixed offset from UTC.
|
||||||
|
|
||||||
:param name:
|
:param name:
|
||||||
The timezone name, to be returned when ``tzname()`` is called.
|
The timezone name, to be returned when ``tzname()`` is called.
|
||||||
|
|
||||||
:param offset:
|
:param offset:
|
||||||
The time zone offset in seconds, or (since version 2.6.0, represented
|
The time zone offset in seconds, or (since version 2.6.0, represented
|
||||||
as a :py:class:`datetime.timedelta` object.
|
as a :py:class:`datetime.timedelta` object).
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, offset):
|
def __init__(self, name, offset):
|
||||||
self._name = name
|
self._name = name
|
||||||
|
@ -128,8 +160,6 @@ class tzoffset(datetime.tzinfo):
|
||||||
|
|
||||||
:param dt:
|
:param dt:
|
||||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
:return:
|
:return:
|
||||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
@ -171,6 +201,7 @@ class tzlocal(_tzinfo):
|
||||||
|
|
||||||
self._dst_saved = self._dst_offset - self._std_offset
|
self._dst_saved = self._dst_offset - self._std_offset
|
||||||
self._hasdst = bool(self._dst_saved)
|
self._hasdst = bool(self._dst_saved)
|
||||||
|
self._tznames = tuple(time.tzname)
|
||||||
|
|
||||||
def utcoffset(self, dt):
|
def utcoffset(self, dt):
|
||||||
if dt is None and self._hasdst:
|
if dt is None and self._hasdst:
|
||||||
|
@ -192,7 +223,7 @@ class tzlocal(_tzinfo):
|
||||||
|
|
||||||
@tzname_in_python2
|
@tzname_in_python2
|
||||||
def tzname(self, dt):
|
def tzname(self, dt):
|
||||||
return time.tzname[self._isdst(dt)]
|
return self._tznames[self._isdst(dt)]
|
||||||
|
|
||||||
def is_ambiguous(self, dt):
|
def is_ambiguous(self, dt):
|
||||||
"""
|
"""
|
||||||
|
@ -257,12 +288,20 @@ class tzlocal(_tzinfo):
|
||||||
return dstval
|
return dstval
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, tzlocal):
|
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
|
return NotImplemented
|
||||||
|
|
||||||
return (self._std_offset == other._std_offset and
|
|
||||||
self._dst_offset == other._dst_offset)
|
|
||||||
|
|
||||||
__hash__ = None
|
__hash__ = None
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
|
@ -348,8 +387,8 @@ class tzfile(_tzinfo):
|
||||||
``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
|
``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
|
||||||
|
|
||||||
See `Sources for Time Zone and Daylight Saving Time Data
|
See `Sources for Time Zone and Daylight Saving Time Data
|
||||||
<http://www.twinsun.com/tz/tz-link.htm>`_ for more information. Time zone
|
<https://data.iana.org/time-zones/tz-link.html>`_ for more information. Time
|
||||||
files can be compiled from the `IANA Time Zone database files
|
zone files can be compiled from the `IANA Time Zone database files
|
||||||
<https://www.iana.org/time-zones>`_ with the `zic time zone compiler
|
<https://www.iana.org/time-zones>`_ with the `zic time zone compiler
|
||||||
<https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
|
<https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
|
||||||
"""
|
"""
|
||||||
|
@ -927,6 +966,7 @@ class tzrange(tzrangebase):
|
||||||
return self._dst_base_offset_
|
return self._dst_base_offset_
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(_TzStrFactory)
|
||||||
class tzstr(tzrange):
|
class tzstr(tzrange):
|
||||||
"""
|
"""
|
||||||
``tzstr`` objects are time zone objects specified by a time-zone string as
|
``tzstr`` objects are time zone objects specified by a time-zone string as
|
||||||
|
@ -953,17 +993,29 @@ class tzstr(tzrange):
|
||||||
``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
|
``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
|
||||||
POSIX standard.
|
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`:
|
.. _`GNU C Library: TZ Variable`:
|
||||||
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
|
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
|
||||||
"""
|
"""
|
||||||
def __init__(self, s, posix_offset=False):
|
def __init__(self, s, posix_offset=False):
|
||||||
global parser
|
global parser
|
||||||
from dateutil import parser
|
from dateutil.parser import _parser as parser
|
||||||
|
|
||||||
self._s = s
|
self._s = s
|
||||||
|
|
||||||
res = parser._parsetz(s)
|
res = parser._parsetz(s)
|
||||||
if res is None:
|
if res is None or res.any_unused_tokens:
|
||||||
raise ValueError("unknown string format")
|
raise ValueError("unknown string format")
|
||||||
|
|
||||||
# Here we break the compatibility with the TZ variable handling.
|
# Here we break the compatibility with the TZ variable handling.
|
||||||
|
@ -1133,13 +1185,13 @@ class _tzicalvtz(_tzinfo):
|
||||||
class tzical(object):
|
class tzical(object):
|
||||||
"""
|
"""
|
||||||
This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
|
This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
|
||||||
as set out in `RFC 2445`_ Section 4.6.5 into one or more `tzinfo` objects.
|
as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
|
||||||
|
|
||||||
:param `fileobj`:
|
:param `fileobj`:
|
||||||
A file or stream in iCalendar format, which should be UTF-8 encoded
|
A file or stream in iCalendar format, which should be UTF-8 encoded
|
||||||
with CRLF endings.
|
with CRLF endings.
|
||||||
|
|
||||||
.. _`RFC 2445`: https://www.ietf.org/rfc/rfc2445.txt
|
.. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
|
||||||
"""
|
"""
|
||||||
def __init__(self, fileobj):
|
def __init__(self, fileobj):
|
||||||
global rrule
|
global rrule
|
||||||
|
@ -1346,84 +1398,118 @@ else:
|
||||||
TZFILES = []
|
TZFILES = []
|
||||||
TZPATHS = []
|
TZPATHS = []
|
||||||
|
|
||||||
|
def __get_gettz(name, zoneinfo_priority=False):
|
||||||
|
tzlocal_classes = (tzlocal,)
|
||||||
|
if tzwinlocal is not None:
|
||||||
|
tzlocal_classes += (tzwinlocal,)
|
||||||
|
|
||||||
def gettz(name=None, zoneinfo_priority=False):
|
class GettzFunc(object):
|
||||||
tz = None
|
def __init__(self, name, zoneinfo_priority=False):
|
||||||
if not name:
|
|
||||||
try:
|
self.__instances = {}
|
||||||
name = os.environ["TZ"]
|
self._cache_lock = _thread.allocate_lock()
|
||||||
except KeyError:
|
|
||||||
pass
|
def __call__(self, name=None, zoneinfo_priority=False):
|
||||||
if name is None or name == ":":
|
with self._cache_lock:
|
||||||
for filepath in TZFILES:
|
rv = self.__instances.get(name, None)
|
||||||
if not os.path.isabs(filepath):
|
|
||||||
filename = filepath
|
if rv is None:
|
||||||
for path in TZPATHS:
|
rv = self.nocache(name=name, zoneinfo_priority=zoneinfo_priority)
|
||||||
filepath = os.path.join(path, filename)
|
if not (name is None or isinstance(rv, tzlocal_classes)):
|
||||||
if os.path.isfile(filepath):
|
# tzlocal is slightly more complicated than the other
|
||||||
break
|
# time zone providers because it depends on environment
|
||||||
else:
|
# at construction time, so don't cache that.
|
||||||
continue
|
self.__instances[name] = rv
|
||||||
if os.path.isfile(filepath):
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def cache_clear(self):
|
||||||
|
with self._cache_lock:
|
||||||
|
self.__instances = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def nocache(name=None, zoneinfo_priority=False):
|
||||||
|
"""A non-cached version of gettz"""
|
||||||
|
tz = None
|
||||||
|
if not name:
|
||||||
try:
|
try:
|
||||||
tz = tzfile(filepath)
|
name = os.environ["TZ"]
|
||||||
break
|
except KeyError:
|
||||||
except (IOError, OSError, ValueError):
|
|
||||||
pass
|
pass
|
||||||
else:
|
if name is None or name == ":":
|
||||||
tz = tzlocal()
|
for filepath in TZFILES:
|
||||||
else:
|
if not os.path.isabs(filepath):
|
||||||
if name.startswith(":"):
|
filename = filepath
|
||||||
name = name[:-1]
|
for path in TZPATHS:
|
||||||
if os.path.isabs(name):
|
filepath = os.path.join(path, filename)
|
||||||
if os.path.isfile(name):
|
if os.path.isfile(filepath):
|
||||||
tz = tzfile(name)
|
|
||||||
else:
|
|
||||||
tz = None
|
|
||||||
else:
|
|
||||||
if zoneinfo_priority:
|
|
||||||
from dateutil.zoneinfo import get_zonefile_instance
|
|
||||||
tz = get_zonefile_instance().get(name)
|
|
||||||
if not tz:
|
|
||||||
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:
|
|
||||||
tz = None
|
|
||||||
|
|
||||||
if not zoneinfo_priority and not tz:
|
|
||||||
from dateutil.zoneinfo import get_zonefile_instance
|
|
||||||
tz = get_zonefile_instance().get(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
|
break
|
||||||
else:
|
else:
|
||||||
if name in ("GMT", "UTC"):
|
continue
|
||||||
tz = tzutc()
|
if os.path.isfile(filepath):
|
||||||
elif name in time.tzname:
|
try:
|
||||||
tz = tzlocal()
|
tz = tzfile(filepath)
|
||||||
return tz
|
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:
|
||||||
|
if zoneinfo_priority:
|
||||||
|
from dateutil.zoneinfo import get_zonefile_instance
|
||||||
|
tz = get_zonefile_instance().get(name)
|
||||||
|
if not tz:
|
||||||
|
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:
|
||||||
|
tz = None
|
||||||
|
|
||||||
|
if not zoneinfo_priority and not tz:
|
||||||
|
from dateutil.zoneinfo import get_zonefile_instance
|
||||||
|
tz = get_zonefile_instance().get(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
|
||||||
|
|
||||||
|
return GettzFunc(name, zoneinfo_priority)
|
||||||
|
|
||||||
|
gettz = __get_gettz(name=None, zoneinfo_priority=False)
|
||||||
|
del __get_gettz
|
||||||
|
|
||||||
def datetime_exists(dt, tz=None):
|
def datetime_exists(dt, tz=None):
|
||||||
"""
|
"""
|
||||||
|
@ -1440,6 +1526,8 @@ def datetime_exists(dt, tz=None):
|
||||||
|
|
||||||
:return:
|
:return:
|
||||||
Returns a boolean value whether or not the "wall time" exists in ``tz``.
|
Returns a boolean value whether or not the "wall time" exists in ``tz``.
|
||||||
|
|
||||||
|
..versionadded:: 2.7.0
|
||||||
"""
|
"""
|
||||||
if tz is None:
|
if tz is None:
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
|
@ -1502,6 +1590,51 @@ def datetime_ambiguous(dt, tz=None):
|
||||||
return not (same_offset and same_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):
|
def _datetime_to_timestamp(dt):
|
||||||
"""
|
"""
|
||||||
Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds
|
Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds
|
||||||
|
|
|
@ -1,6 +1,59 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
|
||||||
|
def today(tzinfo=None):
|
||||||
|
"""
|
||||||
|
Returns a :py:class:`datetime` representing the current day at midnight
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The time zone to attach (also used to determine the current day).
|
||||||
|
|
||||||
|
:return:
|
||||||
|
A :py:class:`datetime.datetime` object representing the current day
|
||||||
|
at midnight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = datetime.now(tzinfo)
|
||||||
|
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||||
|
|
||||||
|
|
||||||
|
def default_tzinfo(dt, tzinfo):
|
||||||
|
"""
|
||||||
|
Sets the the ``tzinfo`` parameter on naive datetimes only
|
||||||
|
|
||||||
|
This is useful for example when you are provided a datetime that may have
|
||||||
|
either an implicit or explicit time zone, such as when parsing a time zone
|
||||||
|
string.
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from dateutil.tz import tzoffset
|
||||||
|
>>> from dateutil.parser import parse
|
||||||
|
>>> from dateutil.utils import default_tzinfo
|
||||||
|
>>> dflt_tz = tzoffset("EST", -18000)
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00+00:00
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00-05:00
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
The datetime on which to replace the time zone
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||||
|
``dt`` if (and only if) it is naive.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an aware :py:class:`datetime.datetime`.
|
||||||
|
"""
|
||||||
|
if dt.tzinfo is not None:
|
||||||
|
return dt
|
||||||
|
else:
|
||||||
|
return dt.replace(tzinfo=tzinfo)
|
||||||
|
|
||||||
|
|
||||||
def within_delta(dt1, dt2, delta):
|
def within_delta(dt1, dt2, delta):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,20 +6,19 @@ import os
|
||||||
from tarfile import TarFile
|
from tarfile import TarFile
|
||||||
from pkgutil import get_data
|
from pkgutil import get_data
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from contextlib import closing
|
|
||||||
|
|
||||||
from dateutil.tz import tzfile
|
from dateutil.tz import tzfile as _tzfile
|
||||||
|
|
||||||
from sickbeard import encodingKludge as ek
|
from sickbeard import encodingKludge as ek
|
||||||
import sickbeard
|
import sickbeard
|
||||||
|
|
||||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata", "rebuild"]
|
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||||
|
|
||||||
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||||
METADATA_FN = 'METADATA'
|
METADATA_FN = 'METADATA'
|
||||||
|
|
||||||
|
|
||||||
class tzfile(tzfile):
|
class tzfile(_tzfile):
|
||||||
def __reduce__(self):
|
def __reduce__(self):
|
||||||
return (gettz, (self._filename,))
|
return (gettz, (self._filename,))
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||||
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||||
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||||
|
|
||||||
filename is the timezone tarball from ftp.iana.org/tz.
|
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
tmpdir = tempfile.mkdtemp()
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
|
|
@ -189,6 +189,7 @@ def _update_zoneinfo():
|
||||||
from dateutil.zoneinfo import gettz
|
from dateutil.zoneinfo import gettz
|
||||||
if '_CLASS_ZONE_INSTANCE' in gettz.func_globals:
|
if '_CLASS_ZONE_INSTANCE' in gettz.func_globals:
|
||||||
gettz.func_globals.__setitem__('_CLASS_ZONE_INSTANCE', list())
|
gettz.func_globals.__setitem__('_CLASS_ZONE_INSTANCE', list())
|
||||||
|
tz.gettz.cache_clear()
|
||||||
|
|
||||||
sb_timezone = get_tz()
|
sb_timezone = get_tz()
|
||||||
except:
|
except:
|
||||||
|
|
Loading…
Reference in a new issue