mirror of
https://github.com/SickGear/SickGear.git
synced 2025-01-05 17:43:37 +00:00
Update dateutil library 2.6.1 (2f3a160) → 2.7.2 (ff03c0f)
This commit is contained in:
parent
2b199a4eae
commit
929c6a71ae
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 cachecontrol library 0.12.3 (db54c40) to 0.12.4 (bd94f7e)
|
||||
* 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]
|
||||
|
||||
|
|
|
@ -1,2 +1,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:
|
||||
|
||||
http://users.chariot.net.au/~gmarts/eastalg.htm
|
||||
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||
|
||||
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
|
||||
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
|
||||
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:
|
||||
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
|
||||
original datetime with the value(s) in relativedelta.
|
||||
|
||||
|
@ -95,11 +95,6 @@ class relativedelta(object):
|
|||
yearday=None, nlyearday=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:
|
||||
# datetime is a subclass of date. So both must be date
|
||||
if not (isinstance(dt1, datetime.date) and
|
||||
|
@ -159,9 +154,14 @@ class relativedelta(object):
|
|||
self.seconds = delta.seconds + delta.days * 86400
|
||||
self.microseconds = delta.microseconds
|
||||
else:
|
||||
# Check for non-integer values in integer-only quantities
|
||||
if any(x is not None and x != int(x) for x in (years, months)):
|
||||
raise ValueError("Non-integer years and months are "
|
||||
"ambiguous and not currently supported.")
|
||||
|
||||
# Relative information
|
||||
self.years = years
|
||||
self.months = months
|
||||
self.years = int(years)
|
||||
self.months = int(months)
|
||||
self.days = days + weeks * 7
|
||||
self.leapdays = leapdays
|
||||
self.hours = hours
|
||||
|
@ -249,7 +249,7 @@ class relativedelta(object):
|
|||
|
||||
@property
|
||||
def weeks(self):
|
||||
return self.days // 7
|
||||
return int(self.days / 7.0)
|
||||
|
||||
@weeks.setter
|
||||
def weeks(self, value):
|
||||
|
@ -422,6 +422,24 @@ class relativedelta(object):
|
|||
is not None else
|
||||
other.microsecond))
|
||||
|
||||
def __abs__(self):
|
||||
return self.__class__(years=abs(self.years),
|
||||
months=abs(self.months),
|
||||
days=abs(self.days),
|
||||
hours=abs(self.hours),
|
||||
minutes=abs(self.minutes),
|
||||
seconds=abs(self.seconds),
|
||||
microseconds=abs(self.microseconds),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return self.__class__(years=-self.years,
|
||||
months=-self.months,
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
"""
|
||||
The rrule module offers a small, complete, and very fast, implementation of
|
||||
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.
|
||||
"""
|
||||
import itertools
|
||||
import datetime
|
||||
import calendar
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
|
@ -20,6 +21,7 @@ from six.moves import _thread, range
|
|||
import heapq
|
||||
|
||||
from ._common import weekday as weekdaybase
|
||||
from .tz import tzutc, tzlocal
|
||||
|
||||
# For warning about deprecation of until and count
|
||||
from warnings import warn
|
||||
|
@ -359,7 +361,7 @@ class rrule(rrulebase):
|
|||
|
||||
.. note::
|
||||
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:
|
||||
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
|
||||
|
@ -368,7 +370,7 @@ class rrule(rrulebase):
|
|||
|
||||
.. note::
|
||||
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:
|
||||
If given, it must be either an integer, or a sequence of integers,
|
||||
positive or negative. Each given integer will specify an occurrence
|
||||
|
@ -446,8 +448,22 @@ class rrule(rrulebase):
|
|||
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 2445"
|
||||
warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
|
||||
" and has been deprecated in dateutil. Future versions will "
|
||||
"raise an error.", DeprecationWarning)
|
||||
|
||||
|
@ -674,7 +690,7 @@ class rrule(rrulebase):
|
|||
def __str__(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
@ -699,7 +715,7 @@ class rrule(rrulebase):
|
|||
|
||||
if self._original_rule.get('byweekday') is not None:
|
||||
# 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)
|
||||
wday_strings = []
|
||||
for wday in original_rule['byweekday']:
|
||||
|
@ -1496,11 +1512,17 @@ class _rrulestr(object):
|
|||
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<name>[^:]+):', s)
|
||||
))
|
||||
s = s.upper()
|
||||
if not s.strip():
|
||||
raise ValueError("empty string")
|
||||
|
@ -1563,8 +1585,29 @@ class _rrulestr(object):
|
|||
# RFC 5445 3.8.2.4: The VALUE parameter is optional, but
|
||||
# may be found only once.
|
||||
value_found = False
|
||||
TZID = None
|
||||
valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
|
||||
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:
|
||||
raise ValueError("unsupported DTSTART parm: "+parm)
|
||||
else:
|
||||
|
@ -1577,6 +1620,11 @@ class _rrulestr(object):
|
|||
from dateutil import parser
|
||||
dtstart = parser.parse(value, ignoretz=ignoretz,
|
||||
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:
|
||||
raise ValueError("unsupported property: "+name)
|
||||
if (forceset or len(rrulevals) > 1 or rdatevals
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
from .tz import *
|
||||
|
||||
#: Convenience constant providing a :class:`tzutc()` instance
|
||||
#:
|
||||
#: .. versionadded:: 2.7.0
|
||||
UTC = tzutc()
|
||||
|
||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||
"enfold", "datetime_ambiguous", "datetime_exists"]
|
||||
"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 bisect
|
||||
|
||||
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:
|
||||
|
@ -30,9 +33,38 @@ 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
|
||||
|
@ -86,16 +118,16 @@ class tzutc(datetime.tzinfo):
|
|||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
@six.add_metaclass(_TzOffsetFactory)
|
||||
class tzoffset(datetime.tzinfo):
|
||||
"""
|
||||
A simple class for representing a fixed offset from UTC.
|
||||
|
||||
:param name:
|
||||
The timezone name, to be returned when ``tzname()`` is called.
|
||||
|
||||
:param offset:
|
||||
The time zone offset in seconds, or (since version 2.6.0, represented
|
||||
as a :py:class:`datetime.timedelta` object.
|
||||
as a :py:class:`datetime.timedelta` object).
|
||||
"""
|
||||
def __init__(self, name, offset):
|
||||
self._name = name
|
||||
|
@ -128,8 +160,6 @@ class tzoffset(datetime.tzinfo):
|
|||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
|
@ -171,6 +201,7 @@ class tzlocal(_tzinfo):
|
|||
|
||||
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:
|
||||
|
@ -192,7 +223,7 @@ class tzlocal(_tzinfo):
|
|||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return time.tzname[self._isdst(dt)]
|
||||
return self._tznames[self._isdst(dt)]
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
|
@ -257,12 +288,20 @@ class tzlocal(_tzinfo):
|
|||
return dstval
|
||||
|
||||
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 (self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset)
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
|
@ -348,8 +387,8 @@ class tzfile(_tzinfo):
|
|||
``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
|
||||
|
||||
See `Sources for Time Zone and Daylight Saving Time Data
|
||||
<http://www.twinsun.com/tz/tz-link.htm>`_ for more information. Time zone
|
||||
files can be compiled from the `IANA Time Zone database files
|
||||
<https://data.iana.org/time-zones/tz-link.html>`_ for more information. Time
|
||||
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.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
|
||||
"""
|
||||
|
@ -927,6 +966,7 @@ class tzrange(tzrangebase):
|
|||
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
|
||||
|
@ -953,17 +993,29 @@ class tzstr(tzrange):
|
|||
``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 import parser
|
||||
from dateutil.parser import _parser as parser
|
||||
|
||||
self._s = s
|
||||
|
||||
res = parser._parsetz(s)
|
||||
if res is None:
|
||||
if res is None or res.any_unused_tokens:
|
||||
raise ValueError("unknown string format")
|
||||
|
||||
# Here we break the compatibility with the TZ variable handling.
|
||||
|
@ -1133,13 +1185,13 @@ class _tzicalvtz(_tzinfo):
|
|||
class tzical(object):
|
||||
"""
|
||||
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`:
|
||||
A file or stream in iCalendar format, which should be UTF-8 encoded
|
||||
with CRLF endings.
|
||||
|
||||
.. _`RFC 2445`: https://www.ietf.org/rfc/rfc2445.txt
|
||||
.. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
|
||||
"""
|
||||
def __init__(self, fileobj):
|
||||
global rrule
|
||||
|
@ -1346,84 +1398,118 @@ else:
|
|||
TZFILES = []
|
||||
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):
|
||||
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):
|
||||
class GettzFunc(object):
|
||||
def __init__(self, name, zoneinfo_priority=False):
|
||||
|
||||
self.__instances = {}
|
||||
self._cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(self, name=None, zoneinfo_priority=False):
|
||||
with self._cache_lock:
|
||||
rv = self.__instances.get(name, None)
|
||||
|
||||
if rv is None:
|
||||
rv = self.nocache(name=name, zoneinfo_priority=zoneinfo_priority)
|
||||
if not (name is None or isinstance(rv, tzlocal_classes)):
|
||||
# tzlocal is slightly more complicated than the other
|
||||
# time zone providers because it depends on environment
|
||||
# at construction time, so don't cache that.
|
||||
self.__instances[name] = rv
|
||||
|
||||
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:
|
||||
tz = tzfile(filepath)
|
||||
break
|
||||
except (IOError, OSError, ValueError):
|
||||
name = os.environ["TZ"]
|
||||
except KeyError:
|
||||
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
|
||||
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:
|
||||
if name in ("GMT", "UTC"):
|
||||
tz = tzutc()
|
||||
elif name in time.tzname:
|
||||
tz = tzlocal()
|
||||
return tz
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
|
@ -1440,6 +1526,8 @@ def datetime_exists(dt, tz=None):
|
|||
|
||||
: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:
|
||||
|
@ -1502,6 +1590,51 @@ def datetime_ambiguous(dt, tz=None):
|
|||
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
|
||||
|
|
|
@ -1,6 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -6,20 +6,19 @@ import os
|
|||
from tarfile import TarFile
|
||||
from pkgutil import get_data
|
||||
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
|
||||
import sickbeard
|
||||
|
||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata", "rebuild"]
|
||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||
|
||||
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||
METADATA_FN = 'METADATA'
|
||||
|
||||
|
||||
class tzfile(tzfile):
|
||||
class tzfile(_tzfile):
|
||||
def __reduce__(self):
|
||||
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):
|
||||
"""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()
|
||||
|
|
|
@ -189,6 +189,7 @@ def _update_zoneinfo():
|
|||
from dateutil.zoneinfo import gettz
|
||||
if '_CLASS_ZONE_INSTANCE' in gettz.func_globals:
|
||||
gettz.func_globals.__setitem__('_CLASS_ZONE_INSTANCE', list())
|
||||
tz.gettz.cache_clear()
|
||||
|
||||
sb_timezone = get_tz()
|
||||
except:
|
||||
|
|
Loading…
Reference in a new issue