mirror of https://github.com/sgoudham/Enso-Bot.git
Trying to use environment variables
parent
5c7e1f13a8
commit
e61cdbd61e
@ -0,0 +1 @@
|
|||||||
|
DISCORD_TOKEN=NzE2NzAxNjk5MTQ1NzI4MDk0.XtgR5w.QI2bzKWTC4wFuQnDQuJanx1gZns
|
@ -1,2 +1,2 @@
|
|||||||
#Hiding Token
|
#Hiding Token
|
||||||
*token.txt
|
*.env
|
Binary file not shown.
@ -0,0 +1,8 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
try:
|
||||||
|
from ._version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = 'unknown'
|
||||||
|
|
||||||
|
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||||
|
'utils', 'zoneinfo']
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Common code used in multiple modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class weekday(object):
|
||||||
|
__slots__ = ["weekday", "n"]
|
||||||
|
|
||||||
|
def __init__(self, weekday, n=None):
|
||||||
|
self.weekday = weekday
|
||||||
|
self.n = n
|
||||||
|
|
||||||
|
def __call__(self, n):
|
||||||
|
if n == self.n:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return self.__class__(self.weekday, n)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
try:
|
||||||
|
if self.weekday != other.weekday or self.n != other.n:
|
||||||
|
return False
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.weekday,
|
||||||
|
self.n,
|
||||||
|
))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||||
|
if not self.n:
|
||||||
|
return s
|
||||||
|
else:
|
||||||
|
return "%s(%+d)" % (s, self.n)
|
||||||
|
|
||||||
|
# vim:ts=4:sw=4:et
|
@ -0,0 +1,4 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# file generated by setuptools_scm
|
||||||
|
# don't change, don't track in version control
|
||||||
|
version = '2.8.1'
|
@ -0,0 +1,89 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers a generic easter computing method for any given year, using
|
||||||
|
Western, Orthodox or Julian algorithms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||||
|
|
||||||
|
EASTER_JULIAN = 1
|
||||||
|
EASTER_ORTHODOX = 2
|
||||||
|
EASTER_WESTERN = 3
|
||||||
|
|
||||||
|
|
||||||
|
def easter(year, method=EASTER_WESTERN):
|
||||||
|
"""
|
||||||
|
This method was ported from the work done by GM Arts,
|
||||||
|
on top of the algorithm by Claus Tondering, which was
|
||||||
|
based in part on the algorithm of Ouding (1940), as
|
||||||
|
quoted in "Explanatory Supplement to the Astronomical
|
||||||
|
Almanac", P. Kenneth Seidelmann, editor.
|
||||||
|
|
||||||
|
This algorithm implements three different easter
|
||||||
|
calculation methods:
|
||||||
|
|
||||||
|
1 - Original calculation in Julian calendar, valid in
|
||||||
|
dates after 326 AD
|
||||||
|
2 - Original method, with date converted to Gregorian
|
||||||
|
calendar, valid in years 1583 to 4099
|
||||||
|
3 - Revised method, in Gregorian calendar, valid in
|
||||||
|
years 1583 to 4099 as well
|
||||||
|
|
||||||
|
These methods are represented by the constants:
|
||||||
|
|
||||||
|
* ``EASTER_JULIAN = 1``
|
||||||
|
* ``EASTER_ORTHODOX = 2``
|
||||||
|
* ``EASTER_WESTERN = 3``
|
||||||
|
|
||||||
|
The default method is method 3.
|
||||||
|
|
||||||
|
More about the algorithm may be found at:
|
||||||
|
|
||||||
|
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (1 <= method <= 3):
|
||||||
|
raise ValueError("invalid method")
|
||||||
|
|
||||||
|
# g - Golden year - 1
|
||||||
|
# c - Century
|
||||||
|
# h - (23 - Epact) mod 30
|
||||||
|
# i - Number of days from March 21 to Paschal Full Moon
|
||||||
|
# j - Weekday for PFM (0=Sunday, etc)
|
||||||
|
# p - Number of days from March 21 to Sunday on or before PFM
|
||||||
|
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||||
|
# e - Extra days to add for method 2 (converting Julian
|
||||||
|
# date to Gregorian date)
|
||||||
|
|
||||||
|
y = year
|
||||||
|
g = y % 19
|
||||||
|
e = 0
|
||||||
|
if method < 3:
|
||||||
|
# Old method
|
||||||
|
i = (19*g + 15) % 30
|
||||||
|
j = (y + y//4 + i) % 7
|
||||||
|
if method == 2:
|
||||||
|
# Extra dates to convert Julian to Gregorian date
|
||||||
|
e = 10
|
||||||
|
if y > 1600:
|
||||||
|
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||||
|
else:
|
||||||
|
# New method
|
||||||
|
c = y//100
|
||||||
|
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||||
|
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||||
|
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||||
|
|
||||||
|
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||||
|
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||||
|
p = i - j + e
|
||||||
|
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||||
|
m = 3 + (p + 26)//30
|
||||||
|
return datetime.date(int(y), int(m), int(d))
|
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from ._parser import parse, parser, parserinfo, ParserError
|
||||||
|
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||||
|
from ._parser import UnknownTimezoneWarning
|
||||||
|
|
||||||
|
from ._parser import __doc__
|
||||||
|
|
||||||
|
from .isoparser import isoparser, isoparse
|
||||||
|
|
||||||
|
__all__ = ['parse', 'parser', 'parserinfo',
|
||||||
|
'isoparse', 'isoparser',
|
||||||
|
'ParserError',
|
||||||
|
'UnknownTimezoneWarning']
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Deprecate portions of the private interface so that downstream code that
|
||||||
|
# is improperly relying on it is given *some* notice.
|
||||||
|
|
||||||
|
|
||||||
|
def __deprecated_private_func(f):
|
||||||
|
from functools import wraps
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private function and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=f.__name__)
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def deprecated_func(*args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return deprecated_func
|
||||||
|
|
||||||
|
def __deprecate_private_class(c):
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private class and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=c.__name__)
|
||||||
|
|
||||||
|
class private_class(c):
|
||||||
|
__doc__ = c.__doc__
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
super(private_class, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
private_class.__name__ = c.__name__
|
||||||
|
|
||||||
|
return private_class
|
||||||
|
|
||||||
|
|
||||||
|
from ._parser import _timelex, _resultbase
|
||||||
|
from ._parser import _tzparser, _parsetz
|
||||||
|
|
||||||
|
_timelex = __deprecate_private_class(_timelex)
|
||||||
|
_tzparser = __deprecate_private_class(_tzparser)
|
||||||
|
_resultbase = __deprecate_private_class(_resultbase)
|
||||||
|
_parsetz = __deprecated_private_func(_parsetz)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,411 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers a parser for ISO-8601 strings
|
||||||
|
|
||||||
|
It is intended to support all valid date, time and datetime formats per the
|
||||||
|
ISO-8601 specification.
|
||||||
|
|
||||||
|
..versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta, time, date
|
||||||
|
import calendar
|
||||||
|
from dateutil import tz
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = ["isoparse", "isoparser"]
|
||||||
|
|
||||||
|
|
||||||
|
def _takes_ascii(f):
|
||||||
|
@wraps(f)
|
||||||
|
def func(self, str_in, *args, **kwargs):
|
||||||
|
# If it's a stream, read the whole thing
|
||||||
|
str_in = getattr(str_in, 'read', lambda: str_in)()
|
||||||
|
|
||||||
|
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
||||||
|
if isinstance(str_in, six.text_type):
|
||||||
|
# ASCII is the same in UTF-8
|
||||||
|
try:
|
||||||
|
str_in = str_in.encode('ascii')
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
msg = 'ISO-8601 strings should contain only ASCII characters'
|
||||||
|
six.raise_from(ValueError(msg), e)
|
||||||
|
|
||||||
|
return f(self, str_in, *args, **kwargs)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class isoparser(object):
|
||||||
|
def __init__(self, sep=None):
|
||||||
|
"""
|
||||||
|
:param sep:
|
||||||
|
A single character that separates date and time portions. If
|
||||||
|
``None``, the parser will accept any single character.
|
||||||
|
For strict ISO-8601 adherence, pass ``'T'``.
|
||||||
|
"""
|
||||||
|
if sep is not None:
|
||||||
|
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
||||||
|
raise ValueError('Separator must be a single, non-numeric ' +
|
||||||
|
'ASCII character')
|
||||||
|
|
||||||
|
sep = sep.encode('ascii')
|
||||||
|
|
||||||
|
self._sep = sep
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def isoparse(self, dt_str):
|
||||||
|
"""
|
||||||
|
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
||||||
|
|
||||||
|
An ISO-8601 datetime string consists of a date portion, followed
|
||||||
|
optionally by a time portion - the date and time portions are separated
|
||||||
|
by a single character separator, which is ``T`` in the official
|
||||||
|
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
||||||
|
combined with a time portion.
|
||||||
|
|
||||||
|
Supported date formats are:
|
||||||
|
|
||||||
|
Common:
|
||||||
|
|
||||||
|
- ``YYYY``
|
||||||
|
- ``YYYY-MM`` or ``YYYYMM``
|
||||||
|
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||||
|
|
||||||
|
Uncommon:
|
||||||
|
|
||||||
|
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
||||||
|
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
||||||
|
|
||||||
|
The ISO week and day numbering follows the same logic as
|
||||||
|
:func:`datetime.date.isocalendar`.
|
||||||
|
|
||||||
|
Supported time formats are:
|
||||||
|
|
||||||
|
- ``hh``
|
||||||
|
- ``hh:mm`` or ``hhmm``
|
||||||
|
- ``hh:mm:ss`` or ``hhmmss``
|
||||||
|
- ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
|
||||||
|
|
||||||
|
Midnight is a special case for `hh`, as the standard supports both
|
||||||
|
00:00 and 24:00 as a representation. The decimal separator can be
|
||||||
|
either a dot or a comma.
|
||||||
|
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Support for fractional components other than seconds is part of the
|
||||||
|
ISO-8601 standard, but is not currently implemented in this parser.
|
||||||
|
|
||||||
|
Supported time zone offset formats are:
|
||||||
|
|
||||||
|
- `Z` (UTC)
|
||||||
|
- `±HH:MM`
|
||||||
|
- `±HHMM`
|
||||||
|
- `±HH`
|
||||||
|
|
||||||
|
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
||||||
|
with the exception of UTC, which will be represented as
|
||||||
|
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
||||||
|
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
||||||
|
|
||||||
|
:param dt_str:
|
||||||
|
A string or stream containing only an ISO-8601 datetime string
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.datetime` representing the string.
|
||||||
|
Unspecified components default to their lowest value.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
As of version 2.7.0, the strictness of the parser should not be
|
||||||
|
considered a stable part of the contract. Any valid ISO-8601 string
|
||||||
|
that parses correctly with the default settings will continue to
|
||||||
|
parse correctly in future versions, but invalid strings that
|
||||||
|
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
||||||
|
guaranteed to continue failing in future versions if they encode
|
||||||
|
a valid date.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
components, pos = self._parse_isodate(dt_str)
|
||||||
|
|
||||||
|
if len(dt_str) > pos:
|
||||||
|
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
||||||
|
components += self._parse_isotime(dt_str[pos + 1:])
|
||||||
|
else:
|
||||||
|
raise ValueError('String contains unknown ISO components')
|
||||||
|
|
||||||
|
if len(components) > 3 and components[3] == 24:
|
||||||
|
components[3] = 0
|
||||||
|
return datetime(*components) + timedelta(days=1)
|
||||||
|
|
||||||
|
return datetime(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_isodate(self, datestr):
|
||||||
|
"""
|
||||||
|
Parse the date portion of an ISO string.
|
||||||
|
|
||||||
|
:param datestr:
|
||||||
|
The string portion of an ISO string, without a separator
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.date` object
|
||||||
|
"""
|
||||||
|
components, pos = self._parse_isodate(datestr)
|
||||||
|
if pos < len(datestr):
|
||||||
|
raise ValueError('String contains unknown ISO ' +
|
||||||
|
'components: {}'.format(datestr))
|
||||||
|
return date(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_isotime(self, timestr):
|
||||||
|
"""
|
||||||
|
Parse the time portion of an ISO string.
|
||||||
|
|
||||||
|
:param timestr:
|
||||||
|
The time portion of an ISO string, without a separator
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.time` object
|
||||||
|
"""
|
||||||
|
components = self._parse_isotime(timestr)
|
||||||
|
if components[0] == 24:
|
||||||
|
components[0] = 0
|
||||||
|
return time(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
"""
|
||||||
|
Parse a valid ISO time zone string.
|
||||||
|
|
||||||
|
See :func:`isoparser.isoparse` for details on supported formats.
|
||||||
|
|
||||||
|
:param tzstr:
|
||||||
|
A string representing an ISO time zone offset
|
||||||
|
|
||||||
|
:param zero_as_utc:
|
||||||
|
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
||||||
|
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
||||||
|
specified) offsets equivalent to UTC.
|
||||||
|
"""
|
||||||
|
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
_DATE_SEP = b'-'
|
||||||
|
_TIME_SEP = b':'
|
||||||
|
_FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
|
||||||
|
|
||||||
|
def _parse_isodate(self, dt_str):
|
||||||
|
try:
|
||||||
|
return self._parse_isodate_common(dt_str)
|
||||||
|
except ValueError:
|
||||||
|
return self._parse_isodate_uncommon(dt_str)
|
||||||
|
|
||||||
|
def _parse_isodate_common(self, dt_str):
|
||||||
|
len_str = len(dt_str)
|
||||||
|
components = [1, 1, 1]
|
||||||
|
|
||||||
|
if len_str < 4:
|
||||||
|
raise ValueError('ISO string too short')
|
||||||
|
|
||||||
|
# Year
|
||||||
|
components[0] = int(dt_str[0:4])
|
||||||
|
pos = 4
|
||||||
|
if pos >= len_str:
|
||||||
|
return components, pos
|
||||||
|
|
||||||
|
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
||||||
|
if has_sep:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Month
|
||||||
|
if len_str - pos < 2:
|
||||||
|
raise ValueError('Invalid common month')
|
||||||
|
|
||||||
|
components[1] = int(dt_str[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
if pos >= len_str:
|
||||||
|
if has_sep:
|
||||||
|
return components, pos
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid ISO format')
|
||||||
|
|
||||||
|
if has_sep:
|
||||||
|
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
||||||
|
raise ValueError('Invalid separator in ISO string')
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Day
|
||||||
|
if len_str - pos < 2:
|
||||||
|
raise ValueError('Invalid common day')
|
||||||
|
components[2] = int(dt_str[pos:pos + 2])
|
||||||
|
return components, pos + 2
|
||||||
|
|
||||||
|
def _parse_isodate_uncommon(self, dt_str):
|
||||||
|
if len(dt_str) < 4:
|
||||||
|
raise ValueError('ISO string too short')
|
||||||
|
|
||||||
|
# All ISO formats start with the year
|
||||||
|
year = int(dt_str[0:4])
|
||||||
|
|
||||||
|
has_sep = dt_str[4:5] == self._DATE_SEP
|
||||||
|
|
||||||
|
pos = 4 + has_sep # Skip '-' if it's there
|
||||||
|
if dt_str[pos:pos + 1] == b'W':
|
||||||
|
# YYYY-?Www-?D?
|
||||||
|
pos += 1
|
||||||
|
weekno = int(dt_str[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
dayno = 1
|
||||||
|
if len(dt_str) > pos:
|
||||||
|
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
||||||
|
raise ValueError('Inconsistent use of dash separator')
|
||||||
|
|
||||||
|
pos += has_sep
|
||||||
|
|
||||||
|
dayno = int(dt_str[pos:pos + 1])
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
base_date = self._calculate_weekdate(year, weekno, dayno)
|
||||||
|
else:
|
||||||
|
# YYYYDDD or YYYY-DDD
|
||||||
|
if len(dt_str) - pos < 3:
|
||||||
|
raise ValueError('Invalid ordinal day')
|
||||||
|
|
||||||
|
ordinal_day = int(dt_str[pos:pos + 3])
|
||||||
|
pos += 3
|
||||||
|
|
||||||
|
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
||||||
|
raise ValueError('Invalid ordinal day' +
|
||||||
|
' {} for year {}'.format(ordinal_day, year))
|
||||||
|
|
||||||
|
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
||||||
|
|
||||||
|
components = [base_date.year, base_date.month, base_date.day]
|
||||||
|
return components, pos
|
||||||
|
|
||||||
|
def _calculate_weekdate(self, year, week, day):
|
||||||
|
"""
|
||||||
|
Calculate the day of corresponding to the ISO year-week-day calendar.
|
||||||
|
|
||||||
|
This function is effectively the inverse of
|
||||||
|
:func:`datetime.date.isocalendar`.
|
||||||
|
|
||||||
|
:param year:
|
||||||
|
The year in the ISO calendar
|
||||||
|
|
||||||
|
:param week:
|
||||||
|
The week in the ISO calendar - range is [1, 53]
|
||||||
|
|
||||||
|
:param day:
|
||||||
|
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.date`
|
||||||
|
"""
|
||||||
|
if not 0 < week < 54:
|
||||||
|
raise ValueError('Invalid week: {}'.format(week))
|
||||||
|
|
||||||
|
if not 0 < day < 8: # Range is 1-7
|
||||||
|
raise ValueError('Invalid weekday: {}'.format(day))
|
||||||
|
|
||||||
|
# Get week 1 for the specific year:
|
||||||
|
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
||||||
|
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
||||||
|
|
||||||
|
# Now add the specific number of weeks and days to get what we want
|
||||||
|
week_offset = (week - 1) * 7 + (day - 1)
|
||||||
|
return week_1 + timedelta(days=week_offset)
|
||||||
|
|
||||||
|
def _parse_isotime(self, timestr):
|
||||||
|
len_str = len(timestr)
|
||||||
|
components = [0, 0, 0, 0, None]
|
||||||
|
pos = 0
|
||||||
|
comp = -1
|
||||||
|
|
||||||
|
if len(timestr) < 2:
|
||||||
|
raise ValueError('ISO time too short')
|
||||||
|
|
||||||
|
has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP
|
||||||
|
|
||||||
|
while pos < len_str and comp < 5:
|
||||||
|
comp += 1
|
||||||
|
|
||||||
|
if timestr[pos:pos + 1] in b'-+Zz':
|
||||||
|
# Detect time zone boundary
|
||||||
|
components[-1] = self._parse_tzstr(timestr[pos:])
|
||||||
|
pos = len_str
|
||||||
|
break
|
||||||
|
|
||||||
|
if comp < 3:
|
||||||
|
# Hour, minute, second
|
||||||
|
components[comp] = int(timestr[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
if (has_sep and pos < len_str and
|
||||||
|
timestr[pos:pos + 1] == self._TIME_SEP):
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if comp == 3:
|
||||||
|
# Fraction of a second
|
||||||
|
frac = self._FRACTION_REGEX.match(timestr[pos:])
|
||||||
|
if not frac:
|
||||||
|
continue
|
||||||
|
|
||||||
|
us_str = frac.group(1)[:6] # Truncate to microseconds
|
||||||
|
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||||
|
pos += len(frac.group())
|
||||||
|
|
||||||
|
if pos < len_str:
|
||||||
|
raise ValueError('Unused components in ISO string')
|
||||||
|
|
||||||
|
if components[0] == 24:
|
||||||
|
# Standard supports 00:00 and 24:00 as representations of midnight
|
||||||
|
if any(component != 0 for component in components[1:4]):
|
||||||
|
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
if tzstr == b'Z' or tzstr == b'z':
|
||||||
|
return tz.UTC
|
||||||
|
|
||||||
|
if len(tzstr) not in {3, 5, 6}:
|
||||||
|
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
||||||
|
|
||||||
|
if tzstr[0:1] == b'-':
|
||||||
|
mult = -1
|
||||||
|
elif tzstr[0:1] == b'+':
|
||||||
|
mult = 1
|
||||||
|
else:
|
||||||
|
raise ValueError('Time zone offset requires sign')
|
||||||
|
|
||||||
|
hours = int(tzstr[1:3])
|
||||||
|
if len(tzstr) == 3:
|
||||||
|
minutes = 0
|
||||||
|
else:
|
||||||
|
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
||||||
|
|
||||||
|
if zero_as_utc and hours == 0 and minutes == 0:
|
||||||
|
return tz.UTC
|
||||||
|
else:
|
||||||
|
if minutes > 59:
|
||||||
|
raise ValueError('Invalid minutes in time zone offset')
|
||||||
|
|
||||||
|
if hours > 23:
|
||||||
|
raise ValueError('Invalid hours in time zone offset')
|
||||||
|
|
||||||
|
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ISOPARSER = isoparser()
|
||||||
|
isoparse = DEFAULT_ISOPARSER.isoparse
|
@ -0,0 +1,599 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
import operator
|
||||||
|
from math import copysign
|
||||||
|
|
||||||
|
from six import integer_types
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from ._common import weekday
|
||||||
|
|
||||||
|
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||||
|
|
||||||
|
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||||
|
|
||||||
|
|
||||||
|
class relativedelta(object):
|
||||||
|
"""
|
||||||
|
The relativedelta type is designed to be applied to an existing datetime and
|
||||||
|
can replace specific components of that datetime, or represents an interval
|
||||||
|
of time.
|
||||||
|
|
||||||
|
It is based on the specification of the excellent work done by M.-A. Lemburg
|
||||||
|
in his
|
||||||
|
`mx.DateTime <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.
|
||||||
|
|
||||||
|
There are two different ways to build a relativedelta instance. The
|
||||||
|
first one is passing it two date/datetime classes::
|
||||||
|
|
||||||
|
relativedelta(datetime1, datetime2)
|
||||||
|
|
||||||
|
The second one is passing it any number of the following keyword arguments::
|
||||||
|
|
||||||
|
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||||
|
|
||||||
|
year, month, day, hour, minute, second, microsecond:
|
||||||
|
Absolute information (argument is singular); adding or subtracting a
|
||||||
|
relativedelta with absolute information does not perform an arithmetic
|
||||||
|
operation, but rather REPLACES the corresponding value in the
|
||||||
|
original datetime with the value(s) in relativedelta.
|
||||||
|
|
||||||
|
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||||
|
Relative information, may be negative (argument is plural); adding
|
||||||
|
or subtracting a relativedelta with relative information performs
|
||||||
|
the corresponding arithmetic operation on the original datetime value
|
||||||
|
with the information in the relativedelta.
|
||||||
|
|
||||||
|
weekday:
|
||||||
|
One of the weekday instances (MO, TU, etc) available in the
|
||||||
|
relativedelta module. These instances may receive a parameter N,
|
||||||
|
specifying the Nth weekday, which could be positive or negative
|
||||||
|
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
||||||
|
+1. You can also use an integer, where 0=MO. This argument is always
|
||||||
|
relative e.g. if the calculated date is already Monday, using MO(1)
|
||||||
|
or MO(-1) won't change the day. To effectively make it absolute, use
|
||||||
|
it in combination with the day argument (e.g. day=1, MO(1) for first
|
||||||
|
Monday of the month).
|
||||||
|
|
||||||
|
leapdays:
|
||||||
|
Will add given days to the date found, if year is a leap
|
||||||
|
year, and the date found is post 28 of february.
|
||||||
|
|
||||||
|
yearday, nlyearday:
|
||||||
|
Set the yearday or the non-leap year day (jump leap days).
|
||||||
|
These are converted to day/month/leapdays information.
|
||||||
|
|
||||||
|
There are relative and absolute forms of the keyword
|
||||||
|
arguments. The plural is relative, and the singular is
|
||||||
|
absolute. For each argument in the order below, the absolute form
|
||||||
|
is applied first (by setting each attribute to that value) and
|
||||||
|
then the relative form (by adding the value to the attribute).
|
||||||
|
|
||||||
|
The order of attributes considered when this relativedelta is
|
||||||
|
added to a datetime is:
|
||||||
|
|
||||||
|
1. Year
|
||||||
|
2. Month
|
||||||
|
3. Day
|
||||||
|
4. Hours
|
||||||
|
5. Minutes
|
||||||
|
6. Seconds
|
||||||
|
7. Microseconds
|
||||||
|
|
||||||
|
Finally, weekday is applied, using the rule described above.
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
>>> from datetime import datetime
|
||||||
|
>>> from dateutil.relativedelta import relativedelta, MO
|
||||||
|
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
||||||
|
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
||||||
|
>>> dt + delta
|
||||||
|
datetime.datetime(2018, 4, 2, 14, 37)
|
||||||
|
|
||||||
|
First, the day is set to 1 (the first of the month), then 25 hours
|
||||||
|
are added, to get to the 2nd day and 14th hour, finally the
|
||||||
|
weekday is applied, but since the 2nd is already a Monday there is
|
||||||
|
no effect.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, dt1=None, dt2=None,
|
||||||
|
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||||
|
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||||
|
year=None, month=None, day=None, weekday=None,
|
||||||
|
yearday=None, nlyearday=None,
|
||||||
|
hour=None, minute=None, second=None, microsecond=None):
|
||||||
|
|
||||||
|
if dt1 and dt2:
|
||||||
|
# datetime is a subclass of date. So both must be date
|
||||||
|
if not (isinstance(dt1, datetime.date) and
|
||||||
|
isinstance(dt2, datetime.date)):
|
||||||
|
raise TypeError("relativedelta only diffs datetime/date")
|
||||||
|
|
||||||
|
# We allow two dates, or two datetimes, so we coerce them to be
|
||||||
|
# of the same type
|
||||||
|
if (isinstance(dt1, datetime.datetime) !=
|
||||||
|
isinstance(dt2, datetime.datetime)):
|
||||||
|
if not isinstance(dt1, datetime.datetime):
|
||||||
|
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||||
|
elif not isinstance(dt2, datetime.datetime):
|
||||||
|
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||||
|
|
||||||
|
self.years = 0
|
||||||
|
self.months = 0
|
||||||
|
self.days = 0
|
||||||
|
self.leapdays = 0
|
||||||
|
self.hours = 0
|
||||||
|
self.minutes = 0
|
||||||
|
self.seconds = 0
|
||||||
|
self.microseconds = 0
|
||||||
|
self.year = None
|
||||||
|
self.month = None
|
||||||
|
self.day = None
|
||||||
|
self.weekday = None
|
||||||
|
self.hour = None
|
||||||
|
self.minute = None
|
||||||
|
self.second = None
|
||||||
|
self.microsecond = None
|
||||||
|
self._has_time = 0
|
||||||
|
|
||||||
|
# Get year / month delta between the two
|
||||||
|
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
||||||
|
self._set_months(months)
|
||||||
|
|
||||||
|
# Remove the year/month delta so the timedelta is just well-defined
|
||||||
|
# time units (seconds, days and microseconds)
|
||||||
|
dtm = self.__radd__(dt2)
|
||||||
|
|
||||||
|
# If we've overshot our target, make an adjustment
|
||||||
|
if dt1 < dt2:
|
||||||
|
compare = operator.gt
|
||||||
|
increment = 1
|
||||||
|
else:
|
||||||
|
compare = operator.lt
|
||||||
|
increment = -1
|
||||||
|
|
||||||
|
while compare(dt1, dtm):
|
||||||
|
months += increment
|
||||||
|
self._set_months(months)
|
||||||
|
dtm = self.__radd__(dt2)
|
||||||
|
|
||||||
|
# Get the timedelta between the "months-adjusted" date and dt1
|
||||||
|
delta = dt1 - dtm
|
||||||
|
self.seconds = delta.seconds + delta.days * 86400
|
||||||
|
self.microseconds = delta.microseconds
|
||||||
|
else:
|
||||||
|
# Check for non-integer values in integer-only quantities
|
||||||
|
if any(x is not None and x != int(x) for x in (years, months)):
|
||||||
|
raise ValueError("Non-integer years and months are "
|
||||||
|
"ambiguous and not currently supported.")
|
||||||
|
|
||||||
|
# Relative information
|
||||||
|
self.years = int(years)
|
||||||
|
self.months = int(months)
|
||||||
|
self.days = days + weeks * 7
|
||||||
|
self.leapdays = leapdays
|
||||||
|
self.hours = hours
|
||||||
|
self.minutes = minutes
|
||||||
|
self.seconds = seconds
|
||||||
|
self.microseconds = microseconds
|
||||||
|
|
||||||
|
# Absolute information
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
self.day = day
|
||||||
|
self.hour = hour
|
||||||
|
self.minute = minute
|
||||||
|
self.second = second
|
||||||
|
self.microsecond = microsecond
|
||||||
|
|
||||||
|
if any(x is not None and int(x) != x
|
||||||
|
for x in (year, month, day, hour,
|
||||||
|
minute, second, microsecond)):
|
||||||
|
# For now we'll deprecate floats - later it'll be an error.
|
||||||
|
warn("Non-integer value passed as absolute information. " +
|
||||||
|
"This is not a well-defined condition and will raise " +
|
||||||
|
"errors in future versions.", DeprecationWarning)
|
||||||
|
|
||||||
|
if isinstance(weekday, integer_types):
|
||||||
|
self.weekday = weekdays[weekday]
|
||||||
|
else:
|
||||||
|
self.weekday = weekday
|
||||||
|
|
||||||
|
yday = 0
|
||||||
|
if nlyearday:
|
||||||
|
yday = nlyearday
|
||||||
|
elif yearday:
|
||||||
|
yday = yearday
|
||||||
|
if yearday > 59:
|
||||||
|
self.leapdays = -1
|
||||||
|
if yday:
|
||||||
|
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||||
|
243, 273, 304, 334, 366]
|
||||||
|
for idx, ydays in enumerate(ydayidx):
|
||||||
|
if yday <= ydays:
|
||||||
|
self.month = idx+1
|
||||||
|
if idx == 0:
|
||||||
|
self.day = yday
|
||||||
|
else:
|
||||||
|
self.day = yday-ydayidx[idx-1]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid year day (%d)" % yday)
|
||||||
|
|
||||||
|
self._fix()
|
||||||
|
|
||||||
|
def _fix(self):
|
||||||
|
if abs(self.microseconds) > 999999:
|
||||||
|
s = _sign(self.microseconds)
|
||||||
|
div, mod = divmod(self.microseconds * s, 1000000)
|
||||||
|
self.microseconds = mod * s
|
||||||
|
self.seconds += div * s
|
||||||
|
if abs(self.seconds) > 59:
|
||||||
|
s = _sign(self.seconds)
|
||||||
|
div, mod = divmod(self.seconds * s, 60)
|
||||||
|
self.seconds = mod * s
|
||||||
|
self.minutes += div * s
|
||||||
|
if abs(self.minutes) > 59:
|
||||||
|
s = _sign(self.minutes)
|
||||||
|
div, mod = divmod(self.minutes * s, 60)
|
||||||
|
self.minutes = mod * s
|
||||||
|
self.hours += div * s
|
||||||
|
if abs(self.hours) > 23:
|
||||||
|
s = _sign(self.hours)
|
||||||
|
div, mod = divmod(self.hours * s, 24)
|
||||||
|
self.hours = mod * s
|
||||||
|
self.days += div * s
|
||||||
|
if abs(self.months) > 11:
|
||||||
|
s = _sign(self.months)
|
||||||
|
div, mod = divmod(self.months * s, 12)
|
||||||
|
self.months = mod * s
|
||||||
|
self.years += div * s
|
||||||
|
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||||
|
or self.hour is not None or self.minute is not None or
|
||||||
|
self.second is not None or self.microsecond is not None):
|
||||||
|
self._has_time = 1
|
||||||
|
else:
|
||||||
|
self._has_time = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weeks(self):
|
||||||
|
return int(self.days / 7.0)
|
||||||
|
|
||||||
|
@weeks.setter
|
||||||
|
def weeks(self, value):
|
||||||
|
self.days = self.days - (self.weeks * 7) + value * 7
|
||||||
|
|
||||||
|
def _set_months(self, months):
|
||||||
|
self.months = months
|
||||||
|
if abs(self.months) > 11:
|
||||||
|
s = _sign(self.months)
|
||||||
|
div, mod = divmod(self.months * s, 12)
|
||||||
|
self.months = mod * s
|
||||||
|
self.years = div * s
|
||||||
|
else:
|
||||||
|
self.years = 0
|
||||||
|
|
||||||
|
def normalized(self):
|
||||||
|
"""
|
||||||
|
Return a version of this object represented entirely using integer
|
||||||
|
values for the relative attributes.
|
||||||
|
|
||||||
|
>>> relativedelta(days=1.5, hours=2).normalized()
|
||||||
|
relativedelta(days=+1, hours=+14)
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
||||||
|
"""
|
||||||
|
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
||||||
|
days = int(self.days)
|
||||||
|
|
||||||
|
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
||||||
|
hours = int(hours_f)
|
||||||
|
|
||||||
|
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
||||||
|
minutes = int(minutes_f)
|
||||||
|
|
||||||
|
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
||||||
|
seconds = int(seconds_f)
|
||||||
|
|
||||||
|
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
||||||
|
|
||||||
|
# Constructor carries overflow back up with call to _fix()
|
||||||
|
return self.__class__(years=self.years, months=self.months,
|
||||||
|
days=days, hours=hours, minutes=minutes,
|
||||||
|
seconds=seconds, microseconds=microseconds,
|
||||||
|
leapdays=self.leapdays, year=self.year,
|
||||||
|
month=self.month, day=self.day,
|
||||||
|
weekday=self.weekday, hour=self.hour,
|
||||||
|
minute=self.minute, second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if isinstance(other, relativedelta):
|
||||||
|
return self.__class__(years=other.years + self.years,
|
||||||
|
months=other.months + self.months,
|
||||||
|
days=other.days + self.days,
|
||||||
|
hours=other.hours + self.hours,
|
||||||
|
minutes=other.minutes + self.minutes,
|
||||||
|
seconds=other.seconds + self.seconds,
|
||||||
|
microseconds=(other.microseconds +
|
||||||
|
self.microseconds),
|
||||||
|
leapdays=other.leapdays or self.leapdays,
|
||||||
|
year=(other.year if other.year is not None
|
||||||
|
else self.year),
|
||||||
|
month=(other.month if other.month is not None
|
||||||
|
else self.month),
|
||||||
|
day=(other.day if other.day is not None
|
||||||
|
else self.day),
|
||||||
|
weekday=(other.weekday if other.weekday is not None
|
||||||
|
else self.weekday),
|
||||||
|
hour=(other.hour if other.hour is not None
|
||||||
|
else self.hour),
|
||||||
|
minute=(other.minute if other.minute is not None
|
||||||
|
else self.minute),
|
||||||
|
second=(other.second if other.second is not None
|
||||||
|
else self.second),
|
||||||
|
microsecond=(other.microsecond if other.microsecond
|
||||||
|
is not None else
|
||||||
|
self.microsecond))
|
||||||
|
if isinstance(other, datetime.timedelta):
|
||||||
|
return self.__class__(years=self.years,
|
||||||
|
months=self.months,
|
||||||
|
days=self.days + other.days,
|
||||||
|
hours=self.hours,
|
||||||
|
minutes=self.minutes,
|
||||||
|
seconds=self.seconds + other.seconds,
|
||||||
|
microseconds=self.microseconds + other.microseconds,
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
if not isinstance(other, datetime.date):
|
||||||
|
return NotImplemented
|
||||||
|
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||||
|
other = datetime.datetime.fromordinal(other.toordinal())
|
||||||
|
year = (self.year or other.year)+self.years
|
||||||
|
month = self.month or other.month
|
||||||
|
if self.months:
|
||||||
|
assert 1 <= abs(self.months) <= 12
|
||||||
|
month += self.months
|
||||||
|
if month > 12:
|
||||||
|
year += 1
|
||||||
|
month -= 12
|
||||||
|
elif month < 1:
|
||||||
|
year -= 1
|
||||||
|
month += 12
|
||||||
|
day = min(calendar.monthrange(year, month)[1],
|
||||||
|
self.day or other.day)
|
||||||
|
repl = {"year": year, "month": month, "day": day}
|
||||||
|
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value is not None:
|
||||||
|
repl[attr] = value
|
||||||
|
days = self.days
|
||||||
|
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||||
|
days += self.leapdays
|
||||||
|
ret = (other.replace(**repl)
|
||||||
|
+ datetime.timedelta(days=days,
|
||||||
|
hours=self.hours,
|
||||||
|
minutes=self.minutes,
|
||||||
|
seconds=self.seconds,
|
||||||
|
microseconds=self.microseconds))
|
||||||
|
if self.weekday:
|
||||||
|
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||||
|
jumpdays = (abs(nth) - 1) * 7
|
||||||
|
if nth > 0:
|
||||||
|
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||||
|
else:
|
||||||
|
jumpdays += (ret.weekday() - weekday) % 7
|
||||||
|
jumpdays *= -1
|
||||||
|
ret += datetime.timedelta(days=jumpdays)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self.__add__(other)
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
return self.__neg__().__radd__(other)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
if not isinstance(other, relativedelta):
|
||||||
|
return NotImplemented # In case the other object defines __rsub__
|
||||||
|
return self.__class__(years=self.years - other.years,
|
||||||
|
months=self.months - other.months,
|
||||||
|
days=self.days - other.days,
|
||||||
|
hours=self.hours - other.hours,
|
||||||
|
minutes=self.minutes - other.minutes,
|
||||||
|
seconds=self.seconds - other.seconds,
|
||||||
|
microseconds=self.microseconds - other.microseconds,
|
||||||
|
leapdays=self.leapdays or other.leapdays,
|
||||||
|
year=(self.year if self.year is not None
|
||||||
|
else other.year),
|
||||||
|
month=(self.month if self.month is not None else
|
||||||
|
other.month),
|
||||||
|
day=(self.day if self.day is not None else
|
||||||
|
other.day),
|
||||||
|
weekday=(self.weekday if self.weekday is not None else
|
||||||
|
other.weekday),
|
||||||
|
hour=(self.hour if self.hour is not None else
|
||||||
|
other.hour),
|
||||||
|
minute=(self.minute if self.minute is not None else
|
||||||
|
other.minute),
|
||||||
|
second=(self.second if self.second is not None else
|
||||||
|
other.second),
|
||||||
|
microsecond=(self.microsecond if self.microsecond
|
||||||
|
is not None else
|
||||||
|
other.microsecond))
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return self.__class__(years=abs(self.years),
|
||||||
|
months=abs(self.months),
|
||||||
|
days=abs(self.days),
|
||||||
|
hours=abs(self.hours),
|
||||||
|
minutes=abs(self.minutes),
|
||||||
|
seconds=abs(self.seconds),
|
||||||
|
microseconds=abs(self.microseconds),
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return self.__class__(years=-self.years,
|
||||||
|
months=-self.months,
|
||||||
|
days=-self.days,
|
||||||
|
hours=-self.hours,
|
||||||
|
minutes=-self.minutes,
|
||||||
|
seconds=-self.seconds,
|
||||||
|
microseconds=-self.microseconds,
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return not (not self.years and
|
||||||
|
not self.months and
|
||||||
|
not self.days and
|
||||||
|
not self.hours and
|
||||||
|
not self.minutes and
|
||||||
|
not self.seconds and
|
||||||
|
not self.microseconds and
|
||||||
|
not self.leapdays and
|
||||||
|
self.year is None and
|
||||||
|
self.month is None and
|
||||||
|
self.day is None and
|
||||||
|
self.weekday is None and
|
||||||
|
self.hour is None and
|
||||||
|
self.minute is None and
|
||||||
|
self.second is None and
|
||||||
|
self.microsecond is None)
|
||||||
|
# Compatibility with Python 2.x
|
||||||
|
__nonzero__ = __bool__
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
try:
|
||||||
|
f = float(other)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.__class__(years=int(self.years * f),
|
||||||
|
months=int(self.months * f),
|
||||||
|
days=int(self.days * f),
|
||||||
|
hours=int(self.hours * f),
|
||||||
|
minutes=int(self.minutes * f),
|
||||||
|
seconds=int(self.seconds * f),
|
||||||
|
microseconds=int(self.microseconds * f),
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
__rmul__ = __mul__
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, relativedelta):
|
||||||
|
return NotImplemented
|
||||||
|
if self.weekday or other.weekday:
|
||||||
|
if not self.weekday or not other.weekday:
|
||||||
|
return False
|
||||||
|
if self.weekday.weekday != other.weekday.weekday:
|
||||||
|
return False
|
||||||
|
n1, n2 = self.weekday.n, other.weekday.n
|
||||||
|
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||||
|
return False
|
||||||
|
return (self.years == other.years and
|
||||||
|
self.months == other.months and
|
||||||
|
self.days == other.days and
|
||||||
|
self.hours == other.hours and
|
||||||
|
self.minutes == other.minutes and
|
||||||
|
self.seconds == other.seconds and
|
||||||
|
self.microseconds == other.microseconds and
|
||||||
|
self.leapdays == other.leapdays and
|
||||||
|
self.year == other.year and
|
||||||
|
self.month == other.month and
|
||||||
|
self.day == other.day and
|
||||||
|
self.hour == other.hour and
|
||||||
|
self.minute == other.minute and
|
||||||
|
self.second == other.second and
|
||||||
|
self.microsecond == other.microsecond)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.weekday,
|
||||||
|
self.years,
|
||||||
|
self.months,
|
||||||
|
self.days,
|
||||||
|
self.hours,
|
||||||
|
self.minutes,
|
||||||
|
self.seconds,
|
||||||
|
self.microseconds,
|
||||||
|
self.leapdays,
|
||||||
|
self.year,
|
||||||
|
self.month,
|
||||||
|
self.day,
|
||||||
|
self.hour,
|
||||||
|
self.minute,
|
||||||
|
self.second,
|
||||||
|
self.microsecond,
|
||||||
|
))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __div__(self, other):
|
||||||
|
try:
|
||||||
|
reciprocal = 1 / float(other)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.__mul__(reciprocal)
|
||||||
|
|
||||||
|
__truediv__ = __div__
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
l = []
|
||||||
|
for attr in ["years", "months", "days", "leapdays",
|
||||||
|
"hours", "minutes", "seconds", "microseconds"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value:
|
||||||
|
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
||||||
|
for attr in ["year", "month", "day", "weekday",
|
||||||
|
"hour", "minute", "second", "microsecond"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value is not None:
|
||||||
|
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
||||||
|
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
||||||
|
attrs=", ".join(l))
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(x):
|
||||||
|
return int(copysign(1, x))
|
||||||
|
|
||||||
|
# vim:ts=4:sw=4:et
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,12 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .tz import *
|
||||||
|
from .tz import __doc__
|
||||||
|
|
||||||
|
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||||
|
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||||
|
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||||
|
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedTzFormatWarning(Warning):
|
||||||
|
"""Warning raised when time zones are parsed from deprecated formats."""
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,419 @@
|
|||||||
|
from six import PY2
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, tzinfo
|
||||||
|
|
||||||
|
|
||||||
|
ZERO = timedelta(0)
|
||||||
|
|
||||||
|
__all__ = ['tzname_in_python2', 'enfold']
|
||||||
|
|
||||||
|
|
||||||
|
def tzname_in_python2(namefunc):
|
||||||
|
"""Change unicode output into bytestrings in Python 2
|
||||||
|
|
||||||
|
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||||
|
to unicode strings
|
||||||
|
"""
|
||||||
|
if PY2:
|
||||||
|
@wraps(namefunc)
|
||||||
|
def adjust_encoding(*args, **kwargs):
|
||||||
|
name = namefunc(*args, **kwargs)
|
||||||
|
if name is not None:
|
||||||
|
name = name.encode()
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
return adjust_encoding
|
||||||
|
else:
|
||||||
|
return namefunc
|
||||||
|
|
||||||
|
|
||||||
|
# The following is adapted from Alexander Belopolsky's tz library
|
||||||
|
# https://github.com/abalkin/tz
|
||||||
|
if hasattr(datetime, 'fold'):
|
||||||
|
# This is the pre-python 3.6 fold situation
|
||||||
|
def enfold(dt, fold=1):
|
||||||
|
"""
|
||||||
|
Provides a unified interface for assigning the ``fold`` attribute to
|
||||||
|
datetimes both before and after the implementation of PEP-495.
|
||||||
|
|
||||||
|
:param fold:
|
||||||
|
The value for the ``fold`` attribute in the returned datetime. This
|
||||||
|
should be either 0 or 1.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||||
|
``fold`` for all versions of Python. In versions prior to
|
||||||
|
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||||
|
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||||
|
attribute added, if ``fold`` is 1.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
return dt.replace(fold=fold)
|
||||||
|
|
||||||
|
else:
|
||||||
|
class _DatetimeWithFold(datetime):
|
||||||
|
"""
|
||||||
|
This is a class designed to provide a PEP 495-compliant interface for
|
||||||
|
Python versions before 3.6. It is used only for dates in a fold, so
|
||||||
|
the ``fold`` attribute is fixed at ``1``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def replace(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a datetime with the same attributes, except for those
|
||||||
|
attributes given new values by whichever keyword arguments are
|
||||||
|
specified. Note that tzinfo=None can be specified to create a naive
|
||||||
|
datetime from an aware datetime with no conversion of date and time
|
||||||
|
data.
|
||||||
|
|
||||||
|
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
||||||
|
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
||||||
|
"""
|
||||||
|
argnames = (
|
||||||
|
'year', 'month', 'day', 'hour', 'minute', 'second',
|
||||||
|
'microsecond', 'tzinfo'
|
||||||
|
)
|
||||||
|
|
||||||
|
for arg, argname in zip(args, argnames):
|
||||||
|
if argname in kwargs:
|
||||||
|
raise TypeError('Duplicate argument: {}'.format(argname))
|
||||||
|
|
||||||
|
kwargs[argname] = arg
|
||||||
|
|
||||||
|
for argname in argnames:
|
||||||
|
if argname not in kwargs:
|
||||||
|
kwargs[argname] = getattr(self, argname)
|
||||||
|
|
||||||
|
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
||||||
|
|
||||||
|
return dt_class(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fold(self):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def enfold(dt, fold=1):
|
||||||
|
"""
|
||||||
|
Provides a unified interface for assigning the ``fold`` attribute to
|
||||||
|
datetimes both before and after the implementation of PEP-495.
|
||||||
|
|
||||||
|
:param fold:
|
||||||
|
The value for the ``fold`` attribute in the returned datetime. This
|
||||||
|
should be either 0 or 1.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||||
|
``fold`` for all versions of Python. In versions prior to
|
||||||
|
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||||
|
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||||
|
attribute added, if ``fold`` is 1.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
if getattr(dt, 'fold', 0) == fold:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
args = dt.timetuple()[:6]
|
||||||
|
args += (dt.microsecond, dt.tzinfo)
|
||||||
|
|
||||||
|
if fold:
|
||||||
|
return _DatetimeWithFold(*args)
|
||||||
|
else:
|
||||||
|
return datetime(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_fromutc_inputs(f):
|
||||||
|
"""
|
||||||
|
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
||||||
|
object and that ``self`` is attached as its ``tzinfo``.
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def fromutc(self, dt):
|
||||||
|
if not isinstance(dt, datetime):
|
||||||
|
raise TypeError("fromutc() requires a datetime argument")
|
||||||
|
if dt.tzinfo is not self:
|
||||||
|
raise ValueError("dt.tzinfo is not self")
|
||||||
|
|
||||||
|
return f(self, dt)
|
||||||
|
|
||||||
|
return fromutc
|
||||||
|
|
||||||
|
|
||||||
|
class _tzinfo(tzinfo):
|
||||||
|
"""
|
||||||
|
Base class for all ``dateutil`` ``tzinfo`` objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_ambiguous(self, dt):
|
||||||
|
"""
|
||||||
|
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||||
|
zone.
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=self)
|
||||||
|
|
||||||
|
wall_0 = enfold(dt, fold=0)
|
||||||
|
wall_1 = enfold(dt, fold=1)
|
||||||
|
|
||||||
|
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
||||||
|
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
||||||
|
|
||||||
|
return same_dt and not same_offset
|
||||||
|
|
||||||
|
def _fold_status(self, dt_utc, dt_wall):
|
||||||
|
"""
|
||||||
|
Determine the fold status of a "wall" datetime, given a representation
|
||||||
|
of the same datetime as a (naive) UTC datetime. This is calculated based
|
||||||
|
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
||||||
|
datetimes, and that this offset is the actual number of hours separating
|
||||||
|
``dt_utc`` and ``dt_wall``.
|
||||||
|
|
||||||
|
:param dt_utc:
|
||||||
|
Representation of the datetime as UTC
|
||||||
|
|
||||||
|
:param dt_wall:
|
||||||
|
Representation of the datetime as "wall time". This parameter must
|
||||||
|
either have a `fold` attribute or have a fold-naive
|
||||||
|
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
||||||
|
fail.
|
||||||
|
"""
|
||||||
|
if self.is_ambiguous(dt_wall):
|
||||||
|
delta_wall = dt_wall - dt_utc
|
||||||
|
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
||||||
|
else:
|
||||||
|
_fold = 0
|
||||||
|
|
||||||
|
return _fold
|
||||||
|
|
||||||
|
def _fold(self, dt):
|
||||||
|
return getattr(dt, 'fold', 0)
|
||||||
|
|
||||||
|
def _fromutc(self, dt):
|
||||||
|
"""
|
||||||
|
Given a timezone-aware datetime in a given timezone, calculates a
|
||||||
|
timezone-aware datetime in a new timezone.
|
||||||
|
|
||||||
|
Since this is the one time that we *know* we have an unambiguous
|
||||||
|
datetime object, we take this opportunity to determine whether the
|
||||||
|
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||||
|
occurrence, chronologically, of the ambiguous datetime).
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A timezone-aware :class:`datetime.datetime` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Re-implement the algorithm from Python's datetime.py
|
||||||
|
dtoff = dt.utcoffset()
|
||||||
|
if dtoff is None:
|
||||||
|
raise ValueError("fromutc() requires a non-None utcoffset() "
|
||||||
|
"result")
|
||||||
|
|
||||||
|
# The original datetime.py code assumes that `dst()` defaults to
|
||||||
|
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
||||||
|
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
||||||
|
dtdst = dt.dst()
|
||||||
|
if dtdst is None:
|
||||||
|
raise ValueError("fromutc() requires a non-None dst() result")
|
||||||
|
delta = dtoff - dtdst
|
||||||
|
|
||||||
|
dt += delta
|
||||||
|
# Set fold=1 so we can default to being in the fold for
|
||||||
|
# ambiguous dates.
|
||||||
|
dtdst = enfold(dt, fold=1).dst()
|
||||||
|
if dtdst is None:
|
||||||
|
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
||||||
|
"results; cannot convert")
|
||||||
|
return dt + dtdst
|
||||||
|
|
||||||
|
@_validate_fromutc_inputs
|
||||||
|
def fromutc(self, dt):
|
||||||
|
"""
|
||||||
|
Given a timezone-aware datetime in a given timezone, calculates a
|
||||||
|
timezone-aware datetime in a new timezone.
|
||||||
|
|
||||||
|
Since this is the one time that we *know* we have an unambiguous
|
||||||
|
datetime object, we take this opportunity to determine whether the
|
||||||
|
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||||
|
occurrence, chronologically, of the ambiguous datetime).
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A timezone-aware :class:`datetime.datetime` object.
|
||||||
|
"""
|
||||||
|
dt_wall = self._fromutc(dt)
|
||||||
|
|
||||||
|
# Calculate the fold status given the two datetimes.
|
||||||
|
_fold = self._fold_status(dt, dt_wall)
|
||||||
|
|
||||||
|
# Set the default fold value for ambiguous dates
|
||||||
|
return enfold(dt_wall, fold=_fold)
|
||||||
|
|
||||||
|
|
||||||
|
class tzrangebase(_tzinfo):
|
||||||
|
"""
|
||||||
|
This is an abstract base class for time zones represented by an annual
|
||||||
|
transition into and out of DST. Child classes should implement the following
|
||||||
|
methods:
|
||||||
|
|
||||||
|
* ``__init__(self, *args, **kwargs)``
|
||||||
|
* ``transitions(self, year)`` - this is expected to return a tuple of
|
||||||
|
datetimes representing the DST on and off transitions in standard
|
||||||
|
time.
|
||||||
|
|
||||||
|
A fully initialized ``tzrangebase`` subclass should also provide the
|
||||||
|
following attributes:
|
||||||
|
* ``hasdst``: Boolean whether or not the zone uses DST.
|
||||||
|
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
||||||
|
representing the respective UTC offsets.
|
||||||
|
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
||||||
|
abbreviations in DST and STD, respectively.
|
||||||
|
* ``_hasdst``: Whether or not the zone has DST.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError('tzrangebase is an abstract base class')
|
||||||
|
|
||||||
|
def utcoffset(self, dt):
|
||||||
|
isdst = self._isdst(dt)
|
||||||
|
|
||||||
|
if isdst is None:
|
||||||
|
return None
|
||||||
|
elif isdst:
|
||||||
|
return self._dst_offset
|
||||||
|
else:
|
||||||
|
return self._std_offset
|
||||||
|
|
||||||
|
def dst(self, dt):
|
||||||
|
isdst = self._isdst(dt)
|
||||||
|
|
||||||
|
if isdst is None:
|
||||||
|
return None
|
||||||
|
elif isdst:
|
||||||
|
return self._dst_base_offset
|
||||||
|
else:
|
||||||
|
return ZERO
|
||||||
|
|
||||||
|
@tzname_in_python2
|
||||||
|
def tzname(self, dt):
|
||||||
|
if self._isdst(dt):
|
||||||
|
return self._dst_abbr
|
||||||
|
else:
|
||||||
|
return self._std_abbr
|
||||||
|
|
||||||
|
def fromutc(self, dt):
|
||||||
|
""" Given a datetime in UTC, return local time """
|
||||||
|
if not isinstance(dt, datetime):
|
||||||
|
raise TypeError("fromutc() requires a datetime argument")
|
||||||
|
|
||||||
|
if dt.tzinfo is not self:
|
||||||
|
raise ValueError("dt.tzinfo is not self")
|
||||||
|
|
||||||
|
# Get transitions - if there are none, fixed offset
|
||||||
|
transitions = self.transitions(dt.year)
|
||||||
|
if transitions is None:
|
||||||
|
return dt + self.utcoffset(dt)
|
||||||
|
|
||||||
|
# Get the transition times in UTC
|
||||||
|
dston, dstoff = transitions
|
||||||
|
|
||||||
|
dston -= self._std_offset
|
||||||
|
dstoff -= self._std_offset
|
||||||
|
|
||||||
|
utc_transitions = (dston, dstoff)
|
||||||
|
dt_utc = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
||||||
|
|
||||||
|
if isdst:
|
||||||
|
dt_wall = dt + self._dst_offset
|
||||||
|
else:
|
||||||
|
dt_wall = dt + self._std_offset
|
||||||
|
|
||||||
|
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
||||||
|
|
||||||
|
return enfold(dt_wall, fold=_fold)
|
||||||
|
|
||||||
|
def is_ambiguous(self, dt):
|
||||||
|
"""
|
||||||
|
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||||
|
zone.
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
if not self.hasdst:
|
||||||
|
return False
|
||||||
|
|
||||||
|
start, end = self.transitions(dt.year)
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
return (end <= dt < end + self._dst_base_offset)
|
||||||
|
|
||||||
|
def _isdst(self, dt):
|
||||||
|
if not self.hasdst:
|
||||||
|
return False
|
||||||
|
elif dt is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transitions = self.transitions(dt.year)
|
||||||
|
|
||||||
|
if transitions is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
isdst = self._naive_isdst(dt, transitions)
|
||||||
|
|
||||||
|
# Handle ambiguous dates
|
||||||
|
if not isdst and self.is_ambiguous(dt):
|
||||||
|
return not self._fold(dt)
|
||||||
|
else:
|
||||||
|
return isdst
|
||||||
|
|
||||||
|
def _naive_isdst(self, dt, transitions):
|
||||||
|
dston, dstoff = transitions
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
if dston < dstoff:
|
||||||
|
isdst = dston <= dt < dstoff
|
||||||
|
else:
|
||||||
|
isdst = not dstoff <= dt < dston
|
||||||
|
|
||||||
|
return isdst
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dst_base_offset(self):
|
||||||
|
return self._dst_offset - self._std_offset
|
||||||
|
|
||||||
|
__hash__ = None
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s(...)" % self.__class__.__name__
|
||||||
|
|
||||||
|
__reduce__ = object.__reduce__
|
@ -0,0 +1,80 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
import weakref
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from six.moves import _thread
|
||||||
|
|
||||||
|
|
||||||
|
class _TzSingleton(type):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instance = None
|
||||||
|
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __call__(cls):
|
||||||
|
if cls.__instance is None:
|
||||||
|
cls.__instance = super(_TzSingleton, cls).__call__()
|
||||||
|
return cls.__instance
|
||||||
|
|
||||||
|
|
||||||
|
class _TzFactory(type):
|
||||||
|
def instance(cls, *args, **kwargs):
|
||||||
|
"""Alternate constructor that returns a fresh instance"""
|
||||||
|
return type.__call__(cls, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class _TzOffsetFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = weakref.WeakValueDictionary()
|
||||||
|
cls.__strong_cache = OrderedDict()
|
||||||
|
cls.__strong_cache_size = 8
|
||||||
|
|
||||||
|
cls._cache_lock = _thread.allocate_lock()
|
||||||
|
|
||||||
|
def __call__(cls, name, offset):
|
||||||
|
if isinstance(offset, timedelta):
|
||||||
|
key = (name, offset.total_seconds())
|
||||||
|
else:
|
||||||
|
key = (name, offset)
|
||||||
|
|
||||||
|
instance = cls.__instances.get(key, None)
|
||||||
|
if instance is None:
|
||||||
|
instance = cls.__instances.setdefault(key,
|
||||||
|
cls.instance(name, offset))
|
||||||
|
|
||||||
|
# This lock may not be necessary in Python 3. See GH issue #901
|
||||||
|
with cls._cache_lock:
|
||||||
|
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||||
|
|
||||||
|
# Remove an item if the strong cache is overpopulated
|
||||||
|
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||||
|
cls.__strong_cache.popitem(last=False)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class _TzStrFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = weakref.WeakValueDictionary()
|
||||||
|
cls.__strong_cache = OrderedDict()
|
||||||
|
cls.__strong_cache_size = 8
|
||||||
|
|
||||||
|
cls.__cache_lock = _thread.allocate_lock()
|
||||||
|
|
||||||
|
def __call__(cls, s, posix_offset=False):
|
||||||
|
key = (s, posix_offset)
|
||||||
|
instance = cls.__instances.get(key, None)
|
||||||
|
|
||||||
|
if instance is None:
|
||||||
|
instance = cls.__instances.setdefault(key,
|
||||||
|
cls.instance(s, posix_offset))
|
||||||
|
|
||||||
|
# This lock may not be necessary in Python 3. See GH issue #901
|
||||||
|
with cls.__cache_lock:
|
||||||
|
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||||
|
|
||||||
|
# Remove an item if the strong cache is overpopulated
|
||||||
|
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||||
|
cls.__strong_cache.popitem(last=False)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,370 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module provides an interface to the native time zone data on Windows,
|
||||||
|
including :py:class:`datetime.tzinfo` implementations.
|
||||||
|
|
||||||
|
Attempting to import this module on a non-Windows platform will raise an
|
||||||
|
:py:obj:`ImportError`.
|
||||||
|
"""
|
||||||
|
# This code was originally contributed by Jeffrey Harris.
|
||||||
|
import datetime
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from six.moves import winreg
|
||||||
|
from six import text_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
except ValueError:
|
||||||
|
# ValueError is raised on non-Windows systems for some horrible reason.
|
||||||
|
raise ImportError("Running tzwin on non-Windows system")
|
||||||
|
|
||||||
|
from ._common import tzrangebase
|
||||||
|
|
||||||
|
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
||||||
|
|
||||||
|
ONEWEEK = datetime.timedelta(7)
|
||||||
|
|
||||||
|
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||||
|
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||||
|
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||||
|
|
||||||
|
|
||||||
|
def _settzkeyname():
|
||||||
|
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||||
|
try:
|
||||||
|
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||||
|
TZKEYNAME = TZKEYNAMENT
|
||||||
|
except WindowsError:
|
||||||
|
TZKEYNAME = TZKEYNAME9X
|
||||||
|
handle.Close()
|
||||||
|
return TZKEYNAME
|
||||||
|
|
||||||
|
|
||||||
|
TZKEYNAME = _settzkeyname()
|
||||||
|
|
||||||
|
|
||||||
|
class tzres(object):
|
||||||
|
"""
|
||||||
|
Class for accessing ``tzres.dll``, which contains timezone name related
|
||||||
|
resources.
|
||||||
|
|
||||||
|
.. versionadded:: 2.5.0
|
||||||
|
"""
|
||||||
|
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
||||||
|
|
||||||
|
def __init__(self, tzres_loc='tzres.dll'):
|
||||||
|
# Load the user32 DLL so we can load strings from tzres
|
||||||
|
user32 = ctypes.WinDLL('user32')
|
||||||
|
|
||||||
|
# Specify the LoadStringW function
|
||||||
|
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.LPWSTR,
|
||||||
|
ctypes.c_int)
|
||||||
|
|
||||||
|
self.LoadStringW = user32.LoadStringW
|
||||||
|
self._tzres = ctypes.WinDLL(tzres_loc)
|
||||||
|
self.tzres_loc = tzres_loc
|
||||||
|
|
||||||
|
def load_name(self, offset):
|
||||||
|
"""
|
||||||
|
Load a timezone name from a DLL offset (integer).
|
||||||
|
|
||||||
|
>>> from dateutil.tzwin import tzres
|
||||||
|
>>> tzr = tzres()
|
||||||
|
>>> print(tzr.load_name(112))
|
||||||
|
'Eastern Standard Time'
|
||||||
|
|
||||||
|
:param offset:
|
||||||
|
A positive integer value referring to a string from the tzres dll.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Offsets found in the registry are generally of the form
|
||||||
|
``@tzres.dll,-114``. The offset in this case is 114, not -114.
|
||||||
|
|
||||||
|
"""
|
||||||
|
resource = self.p_wchar()
|
||||||
|
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
||||||
|
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
||||||
|
return resource[:nchar]
|
||||||
|
|
||||||
|
def name_from_string(self, tzname_str):
|
||||||
|
"""
|
||||||
|
Parse strings as returned from the Windows registry into the time zone
|
||||||
|
name as defined in the registry.
|
||||||
|
|
||||||
|
>>> from dateutil.tzwin import tzres
|
||||||
|
>>> tzr = tzres()
|
||||||
|
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
||||||
|
'Dateline Daylight Time'
|
||||||
|
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
||||||
|
'Eastern Standard Time'
|
||||||
|
|
||||||
|
:param tzname_str:
|
||||||
|
A timezone name string as returned from a Windows registry key.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns the localized timezone string from tzres.dll if the string
|
||||||
|
is of the form `@tzres.dll,-offset`, else returns the input string.
|
||||||
|
"""
|
||||||
|
if not tzname_str.startswith('@'):
|
||||||
|
return tzname_str
|
||||||
|
|
||||||
|
name_splt = tzname_str.split(',-')
|
||||||
|
try:
|
||||||
|
offset = int(name_splt[1])
|
||||||
|
except:
|
||||||
|
raise ValueError("Malformed timezone string.")
|
||||||
|
|
||||||
|
return self.load_name(offset)
|
||||||
|
|
||||||
|
|
||||||
|
class tzwinbase(tzrangebase):
|
||||||
|
"""tzinfo class based on win32's timezones available in the registry."""
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError('tzwinbase is an abstract base class')
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# Compare on all relevant dimensions, including name.
|
||||||
|
if not isinstance(other, tzwinbase):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return (self._std_offset == other._std_offset and
|
||||||
|
self._dst_offset == other._dst_offset and
|
||||||
|
self._stddayofweek == other._stddayofweek and
|
||||||
|
self._dstdayofweek == other._dstdayofweek and
|
||||||
|
self._stdweeknumber == other._stdweeknumber and
|
||||||
|
self._dstweeknumber == other._dstweeknumber and
|
||||||
|
self._stdhour == other._stdhour and
|
||||||
|
self._dsthour == other._dsthour and
|
||||||
|
self._stdminute == other._stdminute and
|
||||||
|
self._dstminute == other._dstminute and
|
||||||
|
self._std_abbr == other._std_abbr and
|
||||||
|
self._dst_abbr == other._dst_abbr)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list():
|
||||||
|
"""Return a list of all time zones known to the system."""
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
||||||
|
result = [winreg.EnumKey(tzkey, i)
|
||||||
|
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
"""
|
||||||
|
Return the display name of the time zone.
|
||||||
|
"""
|
||||||
|
return self._display
|
||||||
|
|
||||||
|
def transitions(self, year):
|
||||||
|
"""
|
||||||
|
For a given year, get the DST on and off transition times, expressed
|
||||||
|
always on the standard time side. For zones with no transitions, this
|
||||||
|
function returns ``None``.
|
||||||
|
|
||||||
|
:param year:
|
||||||
|
The year whose transitions you would like to query.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
||||||
|
``(dston, dstoff)`` for zones with an annual DST transition, or
|
||||||
|
``None`` for fixed offset zones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.hasdst:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
||||||
|
self._dsthour, self._dstminute,
|
||||||
|
self._dstweeknumber)
|
||||||
|
|
||||||
|
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
||||||
|
self._stdhour, self._stdminute,
|
||||||
|
self._stdweeknumber)
|
||||||
|
|
||||||
|
# Ambiguous dates default to the STD side
|
||||||
|
dstoff -= self._dst_base_offset
|
||||||
|
|
||||||
|
return dston, dstoff
|
||||||
|
|
||||||
|
def _get_hasdst(self):
|
||||||
|
return self._dstmonth != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dst_base_offset(self):
|
||||||
|
return self._dst_base_offset_
|
||||||
|
|
||||||
|
|
||||||
|
class tzwin(tzwinbase):
|
||||||
|
"""
|
||||||
|
Time zone object created from the zone info in the Windows registry
|
||||||
|
|
||||||
|
These are similar to :py:class:`dateutil.tz.tzrange` objects in that
|
||||||
|
the time zone data is provided in the format of a single offset rule
|
||||||
|
for either 0 or 2 time zone transitions per year.
|
||||||
|
|
||||||
|
:param: name
|
||||||
|
The name of a Windows time zone key, e.g. "Eastern Standard Time".
|
||||||
|
The full list of keys can be retrieved with :func:`tzwin.list`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
||||||
|
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||||
|
keydict = valuestodict(tzkey)
|
||||||
|
|
||||||
|
self._std_abbr = keydict["Std"]
|
||||||
|
self._dst_abbr = keydict["Dlt"]
|
||||||
|
|
||||||
|
self._display = keydict["Display"]
|
||||||
|
|
||||||
|
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||||
|
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||||
|
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||||
|
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
||||||
|
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||||
|
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||||
|
|
||||||
|
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||||
|
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||||
|
(self._stdmonth,
|
||||||
|
self._stddayofweek, # Sunday = 0
|
||||||
|
self._stdweeknumber, # Last = 5
|
||||||
|
self._stdhour,
|
||||||
|
self._stdminute) = tup[4:9]
|
||||||
|
|
||||||
|
(self._dstmonth,
|
||||||
|
self._dstdayofweek, # Sunday = 0
|
||||||
|
self._dstweeknumber, # Last = 5
|
||||||
|
self._dsthour,
|
||||||
|
self._dstminute) = tup[12:17]
|
||||||
|
|
||||||
|
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||||
|
self.hasdst = self._get_hasdst()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "tzwin(%s)" % repr(self._name)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (self.__class__, (self._name,))
|
||||||
|
|
||||||
|
|
||||||
|
class tzwinlocal(tzwinbase):
|
||||||
|
"""
|
||||||
|
Class representing the local time zone information in the Windows registry
|
||||||
|
|
||||||
|
While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
|
||||||
|
module) to retrieve time zone information, ``tzwinlocal`` retrieves the
|
||||||
|
rules directly from the Windows registry and creates an object like
|
||||||
|
:class:`dateutil.tz.tzwin`.
|
||||||
|
|
||||||
|
Because Windows does not have an equivalent of :func:`time.tzset`, on
|
||||||
|
Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
|
||||||
|
time zone settings *at the time that the process was started*, meaning
|
||||||
|
changes to the machine's time zone settings during the run of a program
|
||||||
|
on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
|
||||||
|
Because ``tzwinlocal`` reads the registry directly, it is unaffected by
|
||||||
|
this issue.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||||
|
keydict = valuestodict(tzlocalkey)
|
||||||
|
|
||||||
|
self._std_abbr = keydict["StandardName"]
|
||||||
|
self._dst_abbr = keydict["DaylightName"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
||||||
|
sn=self._std_abbr)
|
||||||
|
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||||
|
_keydict = valuestodict(tzkey)
|
||||||
|
self._display = _keydict["Display"]
|
||||||
|
except OSError:
|
||||||
|
self._display = None
|
||||||
|
|
||||||
|
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||||
|
dstoffset = stdoffset-keydict["DaylightBias"]
|
||||||
|
|
||||||
|
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||||
|
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||||
|
|
||||||
|
# For reasons unclear, in this particular key, the day of week has been
|
||||||
|
# moved to the END of the SYSTEMTIME structure.
|
||||||
|
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||||
|
|
||||||
|
(self._stdmonth,
|
||||||
|
self._stdweeknumber, # Last = 5
|
||||||
|
self._stdhour,
|
||||||
|
self._stdminute) = tup[1:5]
|
||||||
|
|
||||||
|
self._stddayofweek = tup[7]
|
||||||
|
|
||||||
|
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||||
|
|
||||||
|
(self._dstmonth,
|
||||||
|
self._dstweeknumber, # Last = 5
|
||||||
|
self._dsthour,
|
||||||
|
self._dstminute) = tup[1:5]
|
||||||
|
|
||||||
|
self._dstdayofweek = tup[7]
|
||||||
|
|
||||||
|
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||||
|
self.hasdst = self._get_hasdst()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "tzwinlocal()"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# str will return the standard name, not the daylight name.
|
||||||
|
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (self.__class__, ())
|
||||||
|
|
||||||
|
|
||||||
|
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||||
|
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
||||||
|
first = datetime.datetime(year, month, 1, hour, minute)
|
||||||
|
|
||||||
|
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
||||||
|
# Because 7 % 7 = 0
|
||||||
|
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
||||||
|
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
||||||
|
if (wd.month != month):
|
||||||
|
wd -= ONEWEEK
|
||||||
|
|
||||||
|
return wd
|
||||||
|
|
||||||
|
|
||||||
|
def valuestodict(key):
|
||||||
|
"""Convert a registry key's values to a dictionary."""
|
||||||
|
dout = {}
|
||||||
|
size = winreg.QueryInfoKey(key)[1]
|
||||||
|
tz_res = None
|
||||||
|
|
||||||
|
for i in range(size):
|
||||||
|
key_name, value, dtype = winreg.EnumValue(key, i)
|
||||||
|
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
||||||
|
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
||||||
|
# that to a proper signed integer
|
||||||
|
if value & (1 << 31):
|
||||||
|
value = value - (1 << 32)
|
||||||
|
elif dtype == winreg.REG_SZ:
|
||||||
|
# If it's a reference to the tzres DLL, load the actual string
|
||||||
|
if value.startswith('@tzres'):
|
||||||
|
tz_res = tz_res or tzres()
|
||||||
|
value = tz_res.name_from_string(value)
|
||||||
|
|
||||||
|
value = value.rstrip('\x00') # Remove trailing nulls
|
||||||
|
|
||||||
|
dout[key_name] = value
|
||||||
|
|
||||||
|
return dout
|
@ -0,0 +1,2 @@
|
|||||||
|
# tzwin has moved to dateutil.tz.win
|
||||||
|
from .tz.win import *
|
@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers general convenience and utility functions for dealing with
|
||||||
|
datetimes.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
|
||||||
|
def today(tzinfo=None):
|
||||||
|
"""
|
||||||
|
Returns a :py:class:`datetime` representing the current day at midnight
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The time zone to attach (also used to determine the current day).
|
||||||
|
|
||||||
|
:return:
|
||||||
|
A :py:class:`datetime.datetime` object representing the current day
|
||||||
|
at midnight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = datetime.now(tzinfo)
|
||||||
|
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||||
|
|
||||||
|
|
||||||
|
def default_tzinfo(dt, tzinfo):
|
||||||
|
"""
|
||||||
|
Sets the ``tzinfo`` parameter on naive datetimes only
|
||||||
|
|
||||||
|
This is useful for example when you are provided a datetime that may have
|
||||||
|
either an implicit or explicit time zone, such as when parsing a time zone
|
||||||
|
string.
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from dateutil.tz import tzoffset
|
||||||
|
>>> from dateutil.parser import parse
|
||||||
|
>>> from dateutil.utils import default_tzinfo
|
||||||
|
>>> dflt_tz = tzoffset("EST", -18000)
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00+00:00
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00-05:00
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
The datetime on which to replace the time zone
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||||
|
``dt`` if (and only if) it is naive.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an aware :py:class:`datetime.datetime`.
|
||||||
|
"""
|
||||||
|
if dt.tzinfo is not None:
|
||||||
|
return dt
|
||||||
|
else:
|
||||||
|
return dt.replace(tzinfo=tzinfo)
|
||||||
|
|
||||||
|
|
||||||
|
def within_delta(dt1, dt2, delta):
|
||||||
|
"""
|
||||||
|
Useful for comparing two datetimes that may a negilible difference
|
||||||
|
to be considered equal.
|
||||||
|
"""
|
||||||
|
delta = abs(delta)
|
||||||
|
difference = dt1 - dt2
|
||||||
|
return -delta <= difference <= delta
|
@ -0,0 +1,167 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import warnings
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tarfile import TarFile
|
||||||
|
from pkgutil import get_data
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from dateutil.tz import tzfile as _tzfile
|
||||||
|
|
||||||
|
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||||
|
|
||||||
|
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||||
|
METADATA_FN = 'METADATA'
|
||||||
|
|
||||||
|
|
||||||
|
class tzfile(_tzfile):
|
||||||
|
def __reduce__(self):
|
||||||
|
return (gettz, (self._filename,))
|
||||||
|
|
||||||
|
|
||||||
|
def getzoneinfofile_stream():
|
||||||
|
try:
|
||||||
|
return BytesIO(get_data(__name__, ZONEFILENAME))
|
||||||
|
except IOError as e: # TODO switch to FileNotFoundError?
|
||||||
|
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ZoneInfoFile(object):
|
||||||
|
def __init__(self, zonefile_stream=None):
|
||||||
|
if zonefile_stream is not None:
|
||||||
|
with TarFile.open(fileobj=zonefile_stream) as tf:
|
||||||
|
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
||||||
|
for zf in tf.getmembers()
|
||||||
|
if zf.isfile() and zf.name != METADATA_FN}
|
||||||
|
# deal with links: They'll point to their parent object. Less
|
||||||
|
# waste of memory
|
||||||
|
links = {zl.name: self.zones[zl.linkname]
|
||||||
|
for zl in tf.getmembers() if
|
||||||
|
zl.islnk() or zl.issym()}
|
||||||
|
self.zones.update(links)
|
||||||
|
try:
|
||||||
|
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
||||||
|
metadata_str = metadata_json.read().decode('UTF-8')
|
||||||
|
self.metadata = json.loads(metadata_str)
|
||||||
|
except KeyError:
|
||||||
|
# no metadata in tar file
|
||||||
|
self.metadata = None
|
||||||
|
else:
|
||||||
|
self.zones = {}
|
||||||
|
self.metadata = None
|
||||||
|
|
||||||
|
def get(self, name, default=None):
|
||||||
|
"""
|
||||||
|
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
||||||
|
for retrieving zones from the zone dictionary.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
The name of the zone to retrieve. (Generally IANA zone names)
|
||||||
|
|
||||||
|
:param default:
|
||||||
|
The value to return in the event of a missing key.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.zones.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
|
# The current API has gettz as a module function, although in fact it taps into
|
||||||
|
# a stateful class. So as a workaround for now, without changing the API, we
|
||||||
|
# will create a new "global" class instance the first time a user requests a
|
||||||
|
# timezone. Ugly, but adheres to the api.
|
||||||
|
#
|
||||||
|
# TODO: Remove after deprecation period.
|
||||||
|
_CLASS_ZONE_INSTANCE = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_zonefile_instance(new_instance=False):
|
||||||
|
"""
|
||||||
|
This is a convenience function which provides a :class:`ZoneInfoFile`
|
||||||
|
instance using the data provided by the ``dateutil`` package. By default, it
|
||||||
|
caches a single instance of the ZoneInfoFile object and returns that.
|
||||||
|
|
||||||
|
:param new_instance:
|
||||||
|
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
||||||
|
used as the cached instance for the next call. Otherwise, new instances
|
||||||
|
are created only as necessary.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`ZoneInfoFile` object.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
if new_instance:
|
||||||
|
zif = None
|
||||||
|
else:
|
||||||
|
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
||||||
|
|
||||||
|
if zif is None:
|
||||||
|
zif = ZoneInfoFile(getzoneinfofile_stream())
|
||||||
|
|
||||||
|
get_zonefile_instance._cached_instance = zif
|
||||||
|
|
||||||
|
return zif
|
||||||
|
|
||||||
|
|
||||||
|
def gettz(name):
|
||||||
|
"""
|
||||||
|
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
||||||
|
with dateutil.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
An IANA-style time zone name, as found in the zoneinfo file.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
It is generally inadvisable to use this function, and it is only
|
||||||
|
provided for API compatibility with earlier versions. This is *not*
|
||||||
|
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
||||||
|
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
||||||
|
for accessing the dateutil-specific zoneinfo (which may be out of
|
||||||
|
date compared to the system zoneinfo).
|
||||||
|
|
||||||
|
.. deprecated:: 2.6
|
||||||
|
If you need to use a specific zoneinfofile over the system zoneinfo,
|
||||||
|
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
||||||
|
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
||||||
|
|
||||||
|
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
||||||
|
dateutil-provided zoneinfo.
|
||||||
|
"""
|
||||||
|
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
||||||
|
"to use the dateutil-provided zoneinfo files, instantiate a "
|
||||||
|
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
||||||
|
"instead. See the documentation for details.",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
|
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||||
|
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||||
|
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
def gettz_db_metadata():
|
||||||
|
""" Get the zonefile metadata
|
||||||
|
|
||||||
|
See `zonefile_metadata`_
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
A dictionary with the database metadata
|
||||||
|
|
||||||
|
.. deprecated:: 2.6
|
||||||
|
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
||||||
|
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
||||||
|
"""
|
||||||
|
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
||||||
|
"versions, to use the dateutil-provided zoneinfo files, "
|
||||||
|
"ZoneInfoFile object and query the 'metadata' attribute "
|
||||||
|
"instead. See the documentation for details.",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
|
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||||
|
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||||
|
return _CLASS_ZONE_INSTANCE[0].metadata
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,53 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from subprocess import check_call
|
||||||
|
from tarfile import TarFile
|
||||||
|
|
||||||
|
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||||
|
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||||
|
|
||||||
|
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
zonedir = os.path.join(tmpdir, "zoneinfo")
|
||||||
|
moduledir = os.path.dirname(__file__)
|
||||||
|
try:
|
||||||
|
with TarFile.open(filename) as tf:
|
||||||
|
for name in zonegroups:
|
||||||
|
tf.extract(name, tmpdir)
|
||||||
|
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
||||||
|
try:
|
||||||
|
check_call(["zic", "-d", zonedir] + filepaths)
|
||||||
|
except OSError as e:
|
||||||
|
_print_on_nosuchfile(e)
|
||||||
|
raise
|
||||||
|
# write metadata file
|
||||||
|
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=4, sort_keys=True)
|
||||||
|
target = os.path.join(moduledir, ZONEFILENAME)
|
||||||
|
with TarFile.open(target, "w:%s" % format) as tf:
|
||||||
|
for entry in os.listdir(zonedir):
|
||||||
|
entrypath = os.path.join(zonedir, entry)
|
||||||
|
tf.add(entrypath, entry)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_on_nosuchfile(e):
|
||||||
|
"""Print helpful troubleshooting message
|
||||||
|
|
||||||
|
e is an exception raised by subprocess.check_call()
|
||||||
|
|
||||||
|
"""
|
||||||
|
if e.errno == 2:
|
||||||
|
logging.error(
|
||||||
|
"Could not find zic. Perhaps you need to install "
|
||||||
|
"libc-bin or some other package that provides it, "
|
||||||
|
"or it's not in your PATH?")
|
@ -0,0 +1,234 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import string
|
||||||
|
from shlex import shlex
|
||||||
|
from io import open
|
||||||
|
from collections import OrderedDict
|
||||||
|
from distutils.util import strtobool
|
||||||
|
|
||||||
|
# Useful for very coarse version differentiation.
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
from configparser import ConfigParser
|
||||||
|
text_type = str
|
||||||
|
else:
|
||||||
|
from ConfigParser import SafeConfigParser as ConfigParser
|
||||||
|
text_type = unicode
|
||||||
|
|
||||||
|
DEFAULT_ENCODING = 'UTF-8'
|
||||||
|
|
||||||
|
class UndefinedValueError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Undefined(object):
|
||||||
|
"""
|
||||||
|
Class to represent undefined type.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Reference instance to represent undefined values
|
||||||
|
undefined = Undefined()
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
"""
|
||||||
|
Handle .env file format used by Foreman.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, repository):
|
||||||
|
self.repository = repository
|
||||||
|
|
||||||
|
def _cast_boolean(self, value):
|
||||||
|
"""
|
||||||
|
Helper to convert config values to boolean as ConfigParser do.
|
||||||
|
"""
|
||||||
|
value = str(value)
|
||||||
|
return bool(value) if value == '' else bool(strtobool(value))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cast_do_nothing(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get(self, option, default=undefined, cast=undefined):
|
||||||
|
"""
|
||||||
|
Return the value for option or default if defined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We can't avoid __contains__ because value may be empty.
|
||||||
|
if option in os.environ:
|
||||||
|
value = os.environ[option]
|
||||||
|
elif option in self.repository:
|
||||||
|
value = self.repository[option]
|
||||||
|
else:
|
||||||
|
if isinstance(default, Undefined):
|
||||||
|
raise UndefinedValueError('{} not found. Declare it as envvar or define a default value.'.format(option))
|
||||||
|
|
||||||
|
value = default
|
||||||
|
|
||||||
|
if isinstance(cast, Undefined):
|
||||||
|
cast = self._cast_do_nothing
|
||||||
|
elif cast is bool:
|
||||||
|
cast = self._cast_boolean
|
||||||
|
|
||||||
|
return cast(value)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenient shortcut to get.
|
||||||
|
"""
|
||||||
|
return self.get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryEmpty(object):
|
||||||
|
def __init__(self, source='', encoding=DEFAULT_ENCODING):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryIni(RepositoryEmpty):
|
||||||
|
"""
|
||||||
|
Retrieves option keys from .ini files.
|
||||||
|
"""
|
||||||
|
SECTION = 'settings'
|
||||||
|
|
||||||
|
def __init__(self, source, encoding=DEFAULT_ENCODING):
|
||||||
|
self.parser = ConfigParser()
|
||||||
|
with open(source, encoding=encoding) as file_:
|
||||||
|
self.parser.readfp(file_)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return (key in os.environ or
|
||||||
|
self.parser.has_option(self.SECTION, key))
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.parser.get(self.SECTION, key)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryEnv(RepositoryEmpty):
|
||||||
|
"""
|
||||||
|
Retrieves option keys from .env files with fall back to os.environ.
|
||||||
|
"""
|
||||||
|
def __init__(self, source, encoding=DEFAULT_ENCODING):
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
with open(source, encoding=encoding) as file_:
|
||||||
|
for line in file_:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#') or '=' not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split('=', 1)
|
||||||
|
k = k.strip()
|
||||||
|
v = v.strip()
|
||||||
|
if len(v) >= 2 and ((v[0] == "'" and v[-1] == "'") or (v[0] == '"' and v[-1] == '"')):
|
||||||
|
v = v.strip('\'"')
|
||||||
|
self.data[k] = v
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in os.environ or key in self.data
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.data[key]
|
||||||
|
|
||||||
|
|
||||||
|
class AutoConfig(object):
|
||||||
|
"""
|
||||||
|
Autodetects the config file and type.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
search_path : str, optional
|
||||||
|
Initial search path. If empty, the default search path is the
|
||||||
|
caller's path.
|
||||||
|
|
||||||
|
"""
|
||||||
|
SUPPORTED = OrderedDict([
|
||||||
|
('settings.ini', RepositoryIni),
|
||||||
|
('.env', RepositoryEnv),
|
||||||
|
])
|
||||||
|
|
||||||
|
encoding = DEFAULT_ENCODING
|
||||||
|
|
||||||
|
def __init__(self, search_path=None):
|
||||||
|
self.search_path = search_path
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def _find_file(self, path):
|
||||||
|
# look for all files in the current path
|
||||||
|
for configfile in self.SUPPORTED:
|
||||||
|
filename = os.path.join(path, configfile)
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return filename
|
||||||
|
|
||||||
|
# search the parent
|
||||||
|
parent = os.path.dirname(path)
|
||||||
|
if parent and parent != os.path.abspath(os.sep):
|
||||||
|
return self._find_file(parent)
|
||||||
|
|
||||||
|
# reached root without finding any files.
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _load(self, path):
|
||||||
|
# Avoid unintended permission errors
|
||||||
|
try:
|
||||||
|
filename = self._find_file(os.path.abspath(path))
|
||||||
|
except Exception:
|
||||||
|
filename = ''
|
||||||
|
Repository = self.SUPPORTED.get(os.path.basename(filename), RepositoryEmpty)
|
||||||
|
|
||||||
|
self.config = Config(Repository(filename, encoding=self.encoding))
|
||||||
|
|
||||||
|
def _caller_path(self):
|
||||||
|
# MAGIC! Get the caller's module path.
|
||||||
|
frame = sys._getframe()
|
||||||
|
path = os.path.dirname(frame.f_back.f_back.f_code.co_filename)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if not self.config:
|
||||||
|
self._load(self.search_path or self._caller_path())
|
||||||
|
|
||||||
|
return self.config(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# A pré-instantiated AutoConfig to improve decouple's usability
|
||||||
|
# now just import config and start using with no configuration.
|
||||||
|
config = AutoConfig()
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
class Csv(object):
|
||||||
|
"""
|
||||||
|
Produces a csv parser that return a list of transformed elements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cast=text_type, delimiter=',', strip=string.whitespace, post_process=list):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
cast -- callable that transforms the item just before it's added to the list.
|
||||||
|
delimiter -- string of delimiters chars passed to shlex.
|
||||||
|
strip -- string of non-relevant characters to be passed to str.strip after the split.
|
||||||
|
post_process -- callable to post process all casted values. Default is `list`.
|
||||||
|
"""
|
||||||
|
self.cast = cast
|
||||||
|
self.delimiter = delimiter
|
||||||
|
self.strip = strip
|
||||||
|
self.post_process = post_process
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""The actual transformation"""
|
||||||
|
transform = lambda s: self.cast(s.strip(self.strip))
|
||||||
|
|
||||||
|
splitter = shlex(value, posix=True)
|
||||||
|
splitter.whitespace = self.delimiter
|
||||||
|
splitter.whitespace_split = True
|
||||||
|
|
||||||
|
return self.post_process(transform(s) for s in splitter)
|
@ -0,0 +1,46 @@
|
|||||||
|
from .compat import IS_TYPE_CHECKING
|
||||||
|
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def load_ipython_extension(ipython):
|
||||||
|
# type: (Any) -> None
|
||||||
|
from .ipython import load_ipython_extension
|
||||||
|
load_ipython_extension(ipython)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
|
||||||
|
# type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
|
||||||
|
"""Returns a string suitable for running as a shell script.
|
||||||
|
|
||||||
|
Useful for converting a arguments passed to a fabric task
|
||||||
|
to be passed to a `local` or `run` command.
|
||||||
|
"""
|
||||||
|
command = ['dotenv']
|
||||||
|
if quote:
|
||||||
|
command.append('-q %s' % quote)
|
||||||
|
if path:
|
||||||
|
command.append('-f %s' % path)
|
||||||
|
if action:
|
||||||
|
command.append(action)
|
||||||
|
if key:
|
||||||
|
command.append(key)
|
||||||
|
if value:
|
||||||
|
if ' ' in value:
|
||||||
|
command.append('"%s"' % value)
|
||||||
|
else:
|
||||||
|
command.append(value)
|
||||||
|
|
||||||
|
return ' '.join(command).strip()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['get_cli_string',
|
||||||
|
'load_dotenv',
|
||||||
|
'dotenv_values',
|
||||||
|
'get_key',
|
||||||
|
'set_key',
|
||||||
|
'unset_key',
|
||||||
|
'find_dotenv',
|
||||||
|
'load_ipython_extension']
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,145 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
|
try:
|
||||||
|
import click
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
|
||||||
|
'Run pip install "python-dotenv[cli]" to fix this.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING, to_env
|
||||||
|
from .main import dotenv_values, get_key, set_key, unset_key
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Any, List, Dict
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
help="Location of the .env file, defaults to .env file in current working directory.")
|
||||||
|
@click.option('-q', '--quote', default='always',
|
||||||
|
type=click.Choice(['always', 'never', 'auto']),
|
||||||
|
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
|
||||||
|
@click.version_option(version=__version__)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, file, quote):
|
||||||
|
# type: (click.Context, Any, Any) -> None
|
||||||
|
'''This script is used to set, get or unset values from a .env file.'''
|
||||||
|
ctx.obj = {}
|
||||||
|
ctx.obj['FILE'] = file
|
||||||
|
ctx.obj['QUOTE'] = quote
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
def list(ctx):
|
||||||
|
# type: (click.Context) -> None
|
||||||
|
'''Display all the stored key/value.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
dotenv_as_dict = dotenv_values(file)
|
||||||
|
for k, v in dotenv_as_dict.items():
|
||||||
|
click.echo('%s=%s' % (k, v))
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
@click.argument('value', required=True)
|
||||||
|
def set(ctx, key, value):
|
||||||
|
# type: (click.Context, Any, Any) -> None
|
||||||
|
'''Store the given key/value.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
quote = ctx.obj['QUOTE']
|
||||||
|
success, key, value = set_key(file, key, value, quote)
|
||||||
|
if success:
|
||||||
|
click.echo('%s=%s' % (key, value))
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
def get(ctx, key):
|
||||||
|
# type: (click.Context, Any) -> None
|
||||||
|
'''Retrieve the value for the given key.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
stored_value = get_key(file, key)
|
||||||
|
if stored_value:
|
||||||
|
click.echo('%s=%s' % (key, stored_value))
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
def unset(ctx, key):
|
||||||
|
# type: (click.Context, Any) -> None
|
||||||
|
'''Removes the given key.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
quote = ctx.obj['QUOTE']
|
||||||
|
success, key = unset_key(file, key, quote)
|
||||||
|
if success:
|
||||||
|
click.echo("Successfully removed %s" % key)
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(context_settings={'ignore_unknown_options': True})
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
|
||||||
|
def run(ctx, commandline):
|
||||||
|
# type: (click.Context, List[str]) -> None
|
||||||
|
"""Run command with environment variables present."""
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
dotenv_as_dict = {to_env(k): to_env(v) for (k, v) in dotenv_values(file).items() if v is not None}
|
||||||
|
|
||||||
|
if not commandline:
|
||||||
|
click.echo('No command given.')
|
||||||
|
exit(1)
|
||||||
|
ret = run_command(commandline, dotenv_as_dict)
|
||||||
|
exit(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command, env):
|
||||||
|
# type: (List[str], Dict[str, str]) -> int
|
||||||
|
"""Run command in sub process.
|
||||||
|
|
||||||
|
Runs the command in a sub process with the variables from `env`
|
||||||
|
added in the current environment variables.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
command: List[str]
|
||||||
|
The command and it's parameters
|
||||||
|
env: Dict
|
||||||
|
The additional environment variables
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The return code of the command
|
||||||
|
|
||||||
|
"""
|
||||||
|
# copy the current environment variables and add the vales from
|
||||||
|
# `env`
|
||||||
|
cmd_env = os.environ.copy()
|
||||||
|
cmd_env.update(env)
|
||||||
|
|
||||||
|
p = Popen(command,
|
||||||
|
universal_newlines=True,
|
||||||
|
bufsize=0,
|
||||||
|
shell=False,
|
||||||
|
env=cmd_env)
|
||||||
|
_, _ = p.communicate()
|
||||||
|
|
||||||
|
return p.returncode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
@ -0,0 +1,49 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
PY2 = sys.version_info[0] == 2 # type: bool
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
from StringIO import StringIO # noqa
|
||||||
|
else:
|
||||||
|
from io import StringIO # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def is_type_checking():
|
||||||
|
# type: () -> bool
|
||||||
|
try:
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
return TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
IS_TYPE_CHECKING = is_type_checking()
|
||||||
|
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Text
|
||||||
|
|
||||||
|
|
||||||
|
def to_env(text):
|
||||||
|
# type: (Text) -> str
|
||||||
|
"""
|
||||||
|
Encode a string the same way whether it comes from the environment or a `.env` file.
|
||||||
|
"""
|
||||||
|
if PY2:
|
||||||
|
return text.encode(sys.getfilesystemencoding() or "utf-8")
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def to_text(string):
|
||||||
|
# type: (str) -> Text
|
||||||
|
"""
|
||||||
|
Make a string Unicode if it isn't already.
|
||||||
|
|
||||||
|
This is useful for defining raw unicode strings because `ur"foo"` isn't valid in
|
||||||
|
Python 3.
|
||||||
|
"""
|
||||||
|
if PY2:
|
||||||
|
return string.decode("utf-8")
|
||||||
|
else:
|
||||||
|
return string
|
@ -0,0 +1,41 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
|
||||||
|
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
|
||||||
|
parse_argstring) # type: ignore
|
||||||
|
|
||||||
|
from .main import find_dotenv, load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
@magics_class
|
||||||
|
class IPythonDotEnv(Magics):
|
||||||
|
|
||||||
|
@magic_arguments()
|
||||||
|
@argument(
|
||||||
|
'-o', '--override', action='store_true',
|
||||||
|
help="Indicate to override existing variables"
|
||||||
|
)
|
||||||
|
@argument(
|
||||||
|
'-v', '--verbose', action='store_true',
|
||||||
|
help="Indicate function calls to be verbose"
|
||||||
|
)
|
||||||
|
@argument('dotenv_path', nargs='?', type=str, default='.env',
|
||||||
|
help='Search in increasingly higher folders for the `dotenv_path`')
|
||||||
|
@line_magic
|
||||||
|
def dotenv(self, line):
|
||||||
|
args = parse_argstring(self.dotenv, line)
|
||||||
|
# Locate the .env file
|
||||||
|
dotenv_path = args.dotenv_path
|
||||||
|
try:
|
||||||
|
dotenv_path = find_dotenv(dotenv_path, True, True)
|
||||||
|
except IOError:
|
||||||
|
print("cannot find .env file")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the .env file
|
||||||
|
load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
|
||||||
|
|
||||||
|
|
||||||
|
def load_ipython_extension(ipython):
|
||||||
|
"""Register the %dotenv magic."""
|
||||||
|
ipython.register_magics(IPythonDotEnv)
|
@ -0,0 +1,323 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env
|
||||||
|
from .parser import Binding, parse_stream
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import (
|
||||||
|
Dict, Iterator, Match, Optional, Pattern, Union, Text, IO, Tuple
|
||||||
|
)
|
||||||
|
if sys.version_info >= (3, 6):
|
||||||
|
_PathLike = os.PathLike
|
||||||
|
else:
|
||||||
|
_PathLike = Text
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
_StringIO = StringIO
|
||||||
|
else:
|
||||||
|
_StringIO = StringIO[Text]
|
||||||
|
|
||||||
|
__posix_variable = re.compile(
|
||||||
|
r"""
|
||||||
|
\$\{
|
||||||
|
(?P<name>[^\}:]*)
|
||||||
|
(?::-
|
||||||
|
(?P<default>[^\}]*)
|
||||||
|
)?
|
||||||
|
\}
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
) # type: Pattern[Text]
|
||||||
|
|
||||||
|
|
||||||
|
def with_warn_for_invalid_lines(mappings):
|
||||||
|
# type: (Iterator[Binding]) -> Iterator[Binding]
|
||||||
|
for mapping in mappings:
|
||||||
|
if mapping.error:
|
||||||
|
logger.warning(
|
||||||
|
"Python-dotenv could not parse statement starting at line %s",
|
||||||
|
mapping.original.line,
|
||||||
|
)
|
||||||
|
yield mapping
|
||||||
|
|
||||||
|
|
||||||
|
class DotEnv():
|
||||||
|
|
||||||
|
def __init__(self, dotenv_path, verbose=False, encoding=None, interpolate=True):
|
||||||
|
# type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text], bool) -> None
|
||||||
|
self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
|
||||||
|
self._dict = None # type: Optional[Dict[Text, Optional[Text]]]
|
||||||
|
self.verbose = verbose # type: bool
|
||||||
|
self.encoding = encoding # type: Union[None, Text]
|
||||||
|
self.interpolate = interpolate # type: bool
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _get_stream(self):
|
||||||
|
# type: () -> Iterator[IO[Text]]
|
||||||
|
if isinstance(self.dotenv_path, StringIO):
|
||||||
|
yield self.dotenv_path
|
||||||
|
elif os.path.isfile(self.dotenv_path):
|
||||||
|
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
|
||||||
|
yield stream
|
||||||
|
else:
|
||||||
|
if self.verbose:
|
||||||
|
logger.warning("File doesn't exist %s", self.dotenv_path)
|
||||||
|
yield StringIO('')
|
||||||
|
|
||||||
|
def dict(self):
|
||||||
|
# type: () -> Dict[Text, Optional[Text]]
|
||||||
|
"""Return dotenv as dict"""
|
||||||
|
if self._dict:
|
||||||
|
return self._dict
|
||||||
|
|
||||||
|
values = OrderedDict(self.parse())
|
||||||
|
self._dict = resolve_nested_variables(values) if self.interpolate else values
|
||||||
|
return self._dict
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
# type: () -> Iterator[Tuple[Text, Optional[Text]]]
|
||||||
|
with self._get_stream() as stream:
|
||||||
|
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
|
||||||
|
if mapping.key is not None:
|
||||||
|
yield mapping.key, mapping.value
|
||||||
|
|
||||||
|
def set_as_environment_variables(self, override=False):
|
||||||
|
# type: (bool) -> bool
|
||||||
|
"""
|
||||||
|
Load the current dotenv as system environemt variable.
|
||||||
|
"""
|
||||||
|
for k, v in self.dict().items():
|
||||||
|
if k in os.environ and not override:
|
||||||
|
continue
|
||||||
|
if v is not None:
|
||||||
|
os.environ[to_env(k)] = to_env(v)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
# type: (Text) -> Optional[Text]
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
data = self.dict()
|
||||||
|
|
||||||
|
if key in data:
|
||||||
|
return data[key]
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
logger.warning("Key %s not found in %s.", key, self.dotenv_path)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_key(dotenv_path, key_to_get):
|
||||||
|
# type: (Union[Text, _PathLike], Text) -> Optional[Text]
|
||||||
|
"""
|
||||||
|
Gets the value of a given key from the given .env
|
||||||
|
|
||||||
|
If the .env path given doesn't exist, fails
|
||||||
|
"""
|
||||||
|
return DotEnv(dotenv_path, verbose=True).get(key_to_get)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def rewrite(path):
|
||||||
|
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
|
||||||
|
with io.open(path) as source:
|
||||||
|
yield (source, dest) # type: ignore
|
||||||
|
except BaseException:
|
||||||
|
if os.path.isfile(dest.name):
|
||||||
|
os.unlink(dest.name)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
shutil.move(dest.name, path)
|
||||||
|
|
||||||
|
|
||||||
|
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
|
||||||
|
# type: (_PathLike, Text, Text, Text) -> Tuple[Optional[bool], Text, Text]
|
||||||
|
"""
|
||||||
|
Adds or Updates a key/value to the given .env
|
||||||
|
|
||||||
|
If the .env path given doesn't exist, fails instead of risking creating
|
||||||
|
an orphan .env somewhere in the filesystem
|
||||||
|
"""
|
||||||
|
value_to_set = value_to_set.strip("'").strip('"')
|
||||||
|
if not os.path.exists(dotenv_path):
|
||||||
|
logger.warning("Can't write to %s - it doesn't exist.", dotenv_path)
|
||||||
|
return None, key_to_set, value_to_set
|
||||||
|
|
||||||
|
if " " in value_to_set:
|
||||||
|
quote_mode = "always"
|
||||||
|
|
||||||
|
if quote_mode == "always":
|
||||||
|
value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
|
||||||
|
else:
|
||||||
|
value_out = value_to_set
|
||||||
|
line_out = "{}={}\n".format(key_to_set, value_out)
|
||||||
|
|
||||||
|
with rewrite(dotenv_path) as (source, dest):
|
||||||
|
replaced = False
|
||||||
|
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
||||||
|
if mapping.key == key_to_set:
|
||||||
|
dest.write(line_out)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
dest.write(mapping.original.string)
|
||||||
|
if not replaced:
|
||||||
|
dest.write(line_out)
|
||||||
|
|
||||||
|
return True, key_to_set, value_to_set
|
||||||
|
|
||||||
|
|
||||||
|
def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
|
||||||
|
# type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
|
||||||
|
"""
|
||||||
|
Removes a given key from the given .env
|
||||||
|
|
||||||
|
If the .env path given doesn't exist, fails
|
||||||
|
If the given key doesn't exist in the .env, fails
|
||||||
|
"""
|
||||||
|
if not os.path.exists(dotenv_path):
|
||||||
|
logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
|
||||||
|
return None, key_to_unset
|
||||||
|
|
||||||
|
removed = False
|
||||||
|
with rewrite(dotenv_path) as (source, dest):
|
||||||
|
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
||||||
|
if mapping.key == key_to_unset:
|
||||||
|
removed = True
|
||||||
|
else:
|
||||||
|
dest.write(mapping.original.string)
|
||||||
|
|
||||||
|
if not removed:
|
||||||
|
logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path)
|
||||||
|
return None, key_to_unset
|
||||||
|
|
||||||
|
return removed, key_to_unset
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_nested_variables(values):
|
||||||
|
# type: (Dict[Text, Optional[Text]]) -> Dict[Text, Optional[Text]]
|
||||||
|
def _replacement(name, default):
|
||||||
|
# type: (Text, Optional[Text]) -> Text
|
||||||
|
"""
|
||||||
|
get appropriate value for a variable name.
|
||||||
|
first search in environ, if not found,
|
||||||
|
then look into the dotenv variables
|
||||||
|
"""
|
||||||
|
default = default if default is not None else ""
|
||||||
|
ret = os.getenv(name, new_values.get(name, default))
|
||||||
|
return ret # type: ignore
|
||||||
|
|
||||||
|
def _re_sub_callback(match):
|
||||||
|
# type: (Match[Text]) -> Text
|
||||||
|
"""
|
||||||
|
From a match object gets the variable name and returns
|
||||||
|
the correct replacement
|
||||||
|
"""
|
||||||
|
matches = match.groupdict()
|
||||||
|
return _replacement(name=matches["name"], default=matches["default"]) # type: ignore
|
||||||
|
|
||||||
|
new_values = {}
|
||||||
|
|
||||||
|
for k, v in values.items():
|
||||||
|
new_values[k] = __posix_variable.sub(_re_sub_callback, v) if v is not None else None
|
||||||
|
|
||||||
|
return new_values
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_to_root(path):
|
||||||
|
# type: (Text) -> Iterator[Text]
|
||||||
|
"""
|
||||||
|
Yield directories starting from the given directory up to the root
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise IOError('Starting path not found')
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
|
||||||
|
last_dir = None
|
||||||
|
current_dir = os.path.abspath(path)
|
||||||
|
while last_dir != current_dir:
|
||||||
|
yield current_dir
|
||||||
|
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
|
||||||
|
last_dir, current_dir = current_dir, parent_dir
|
||||||
|
|
||||||
|
|
||||||
|
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
|
||||||
|
# type: (Text, bool, bool) -> Text
|
||||||
|
"""
|
||||||
|
Search in increasingly higher folders for the given file
|
||||||
|
|
||||||
|
Returns path to the file if found, or an empty string otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _is_interactive():
|
||||||
|
""" Decide whether this is running in a REPL or IPython notebook """
|
||||||
|
main = __import__('__main__', None, None, fromlist=['__file__'])
|
||||||
|
return not hasattr(main, '__file__')
|
||||||
|
|
||||||
|
if usecwd or _is_interactive() or getattr(sys, 'frozen', False):
|
||||||
|
# Should work without __file__, e.g. in REPL or IPython notebook.
|
||||||
|
path = os.getcwd()
|
||||||
|
else:
|
||||||
|
# will work for .py files
|
||||||
|
frame = sys._getframe()
|
||||||
|
# find first frame that is outside of this file
|
||||||
|
if PY2 and not __file__.endswith('.py'):
|
||||||
|
# in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
|
||||||
|
# for edge case of Python compiled for non-standard extension)
|
||||||
|
current_file = __file__.rsplit('.', 1)[0] + '.py'
|
||||||
|
else:
|
||||||
|
current_file = __file__
|
||||||
|
|
||||||
|
while frame.f_code.co_filename == current_file:
|
||||||
|
assert frame.f_back is not None
|
||||||
|
frame = frame.f_back
|
||||||
|
frame_filename = frame.f_code.co_filename
|
||||||
|
path = os.path.dirname(os.path.abspath(frame_filename))
|
||||||
|
|
||||||
|
for dirname in _walk_to_root(path):
|
||||||
|
check_path = os.path.join(dirname, filename)
|
||||||
|
if os.path.isfile(check_path):
|
||||||
|
return check_path
|
||||||
|
|
||||||
|
if raise_error_if_not_found:
|
||||||
|
raise IOError('File not found')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False, interpolate=True, **kwargs):
|
||||||
|
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, bool, Union[None, Text]) -> bool
|
||||||
|
"""Parse a .env file and then load all the variables found as environment variables.
|
||||||
|
|
||||||
|
- *dotenv_path*: absolute or relative path to .env file.
|
||||||
|
- *stream*: `StringIO` object with .env content.
|
||||||
|
- *verbose*: whether to output the warnings related to missing .env file etc. Defaults to `False`.
|
||||||
|
- *override*: where to override the system environment variables with the variables in `.env` file.
|
||||||
|
Defaults to `False`.
|
||||||
|
"""
|
||||||
|
f = dotenv_path or stream or find_dotenv()
|
||||||
|
return DotEnv(f, verbose=verbose, interpolate=interpolate, **kwargs).set_as_environment_variables(override=override)
|
||||||
|
|
||||||
|
|
||||||
|
def dotenv_values(dotenv_path=None, stream=None, verbose=False, interpolate=True, **kwargs):
|
||||||
|
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Union[None, Text]) -> Dict[Text, Optional[Text]] # noqa: E501
|
||||||
|
f = dotenv_path or stream or find_dotenv()
|
||||||
|
return DotEnv(f, verbose=verbose, interpolate=interpolate, **kwargs).dict()
|
@ -0,0 +1,237 @@
|
|||||||
|
import codecs
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING, to_text
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import ( # noqa:F401
|
||||||
|
IO, Iterator, Match, NamedTuple, Optional, Pattern, Sequence, Text,
|
||||||
|
Tuple
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_regex(string, extra_flags=0):
|
||||||
|
# type: (str, int) -> Pattern[Text]
|
||||||
|
return re.compile(to_text(string), re.UNICODE | extra_flags)
|
||||||
|
|
||||||
|
|
||||||
|
_newline = make_regex(r"(\r\n|\n|\r)")
|
||||||
|
_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
|
||||||
|
_whitespace = make_regex(r"[^\S\r\n]*")
|
||||||
|
_export = make_regex(r"(?:export[^\S\r\n]+)?")
|
||||||
|
_single_quoted_key = make_regex(r"'([^']+)'")
|
||||||
|
_unquoted_key = make_regex(r"([^=\#\s]+)")
|
||||||
|
_equal_sign = make_regex(r"(=[^\S\r\n]*)")
|
||||||
|
_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
|
||||||
|
_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
|
||||||
|
_unquoted_value_part = make_regex(r"([^ \r\n]*)")
|
||||||
|
_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
|
||||||
|
_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
|
||||||
|
_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
|
||||||
|
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
|
||||||
|
_single_quote_escapes = make_regex(r"\\[\\']")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# this is necessary because we only import these from typing
|
||||||
|
# when we are type checking, and the linter is upset if we
|
||||||
|
# re-import
|
||||||
|
import typing
|
||||||
|
|
||||||
|
Original = typing.NamedTuple(
|
||||||
|
"Original",
|
||||||
|
[
|
||||||
|
("string", typing.Text),
|
||||||
|
("line", int),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
Binding = typing.NamedTuple(
|
||||||
|
"Binding",
|
||||||
|
[
|
||||||
|
("key", typing.Optional[typing.Text]),
|
||||||
|
("value", typing.Optional[typing.Text]),
|
||||||
|
("original", Original),
|
||||||
|
("error", bool),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
from collections import namedtuple
|
||||||
|
Original = namedtuple( # type: ignore
|
||||||
|
"Original",
|
||||||
|
[
|
||||||
|
"string",
|
||||||
|
"line",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Binding = namedtuple( # type: ignore
|
||||||
|
"Binding",
|
||||||
|
[
|
||||||
|
"key",
|
||||||
|
"value",
|
||||||
|
"original",
|
||||||
|
"error",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Position:
|
||||||
|
def __init__(self, chars, line):
|
||||||
|
# type: (int, int) -> None
|
||||||
|
self.chars = chars
|
||||||
|
self.line = line
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start(cls):
|
||||||
|
# type: () -> Position
|
||||||
|
return cls(chars=0, line=1)
|
||||||
|
|
||||||
|
def set(self, other):
|
||||||
|
# type: (Position) -> None
|
||||||
|
self.chars = other.chars
|
||||||
|
self.line = other.line
|
||||||
|
|
||||||
|
def advance(self, string):
|
||||||
|
# type: (Text) -> None
|
||||||
|
self.chars += len(string)
|
||||||
|
self.line += len(re.findall(_newline, string))
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Reader:
|
||||||
|
def __init__(self, stream):
|
||||||
|
# type: (IO[Text]) -> None
|
||||||
|
self.string = stream.read()
|
||||||
|
self.position = Position.start()
|
||||||
|
self.mark = Position.start()
|
||||||
|
|
||||||
|
def has_next(self):
|
||||||
|
# type: () -> bool
|
||||||
|
return self.position.chars < len(self.string)
|
||||||
|
|
||||||
|
def set_mark(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.mark.set(self.position)
|
||||||
|
|
||||||
|
def get_marked(self):
|
||||||
|
# type: () -> Original
|
||||||
|
return Original(
|
||||||
|
string=self.string[self.mark.chars:self.position.chars],
|
||||||
|
line=self.mark.line,
|
||||||
|
)
|
||||||
|
|
||||||
|
def peek(self, count):
|
||||||
|
# type: (int) -> Text
|
||||||
|
return self.string[self.position.chars:self.position.chars + count]
|
||||||
|
|
||||||
|
def read(self, count):
|
||||||
|
# type: (int) -> Text
|
||||||
|
result = self.string[self.position.chars:self.position.chars + count]
|
||||||
|
if len(result) < count:
|
||||||
|
raise Error("read: End of string")
|
||||||
|
self.position.advance(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def read_regex(self, regex):
|
||||||
|
# type: (Pattern[Text]) -> Sequence[Text]
|
||||||
|
match = regex.match(self.string, self.position.chars)
|
||||||
|
if match is None:
|
||||||
|
raise Error("read_regex: Pattern not found")
|
||||||
|
self.position.advance(self.string[match.start():match.end()])
|
||||||
|
return match.groups()
|
||||||
|
|
||||||
|
|
||||||
|
def decode_escapes(regex, string):
|
||||||
|
# type: (Pattern[Text], Text) -> Text
|
||||||
|
def decode_match(match):
|
||||||
|
# type: (Match[Text]) -> Text
|
||||||
|
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
|
||||||
|
|
||||||
|
return regex.sub(decode_match, string)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_key(reader):
|
||||||
|
# type: (Reader) -> Optional[Text]
|
||||||
|
char = reader.peek(1)
|
||||||
|
if char == "#":
|
||||||
|
return None
|
||||||
|
elif char == "'":
|
||||||
|
(key,) = reader.read_regex(_single_quoted_key)
|
||||||
|
else:
|
||||||
|
(key,) = reader.read_regex(_unquoted_key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def parse_unquoted_value(reader):
|
||||||
|
# type: (Reader) -> Text
|
||||||
|
value = u""
|
||||||
|
while True:
|
||||||
|
(part,) = reader.read_regex(_unquoted_value_part)
|
||||||
|
value += part
|
||||||
|
after = reader.peek(2)
|
||||||
|
if len(after) < 2 or after[0] in u"\r\n" or after[1] in u" #\r\n":
|
||||||
|
return value
|
||||||
|
value += reader.read(2)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(reader):
|
||||||
|
# type: (Reader) -> Text
|
||||||
|
char = reader.peek(1)
|
||||||
|
if char == u"'":
|
||||||
|
(value,) = reader.read_regex(_single_quoted_value)
|
||||||
|
return decode_escapes(_single_quote_escapes, value)
|
||||||
|
elif char == u'"':
|
||||||
|
(value,) = reader.read_regex(_double_quoted_value)
|
||||||
|
return decode_escapes(_double_quote_escapes, value)
|
||||||
|
elif char in (u"", u"\n", u"\r"):
|
||||||
|
return u""
|
||||||
|
else:
|
||||||
|
return parse_unquoted_value(reader)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_binding(reader):
|
||||||
|
# type: (Reader) -> Binding
|
||||||
|
reader.set_mark()
|
||||||
|
try:
|
||||||
|
reader.read_regex(_multiline_whitespace)
|
||||||
|
if not reader.has_next():
|
||||||
|
return Binding(
|
||||||
|
key=None,
|
||||||
|
value=None,
|
||||||
|
original=reader.get_marked(),
|
||||||
|
error=False,
|
||||||
|
)
|
||||||
|
reader.read_regex(_export)
|
||||||
|
key = parse_key(reader)
|
||||||
|
reader.read_regex(_whitespace)
|
||||||
|
if reader.peek(1) == "=":
|
||||||
|
reader.read_regex(_equal_sign)
|
||||||
|
value = parse_value(reader) # type: Optional[Text]
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
reader.read_regex(_comment)
|
||||||
|
reader.read_regex(_end_of_line)
|
||||||
|
return Binding(
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
original=reader.get_marked(),
|
||||||
|
error=False,
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
reader.read_regex(_rest_of_line)
|
||||||
|
return Binding(
|
||||||
|
key=None,
|
||||||
|
value=None,
|
||||||
|
original=reader.get_marked(),
|
||||||
|
error=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stream(stream):
|
||||||
|
# type: (IO[Text]) -> Iterator[Binding]
|
||||||
|
reader = Reader(stream)
|
||||||
|
while reader.has_next():
|
||||||
|
yield parse_binding(reader)
|
@ -0,0 +1 @@
|
|||||||
|
# Marker file for PEP 561
|
@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.13.0"
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,54 @@
|
|||||||
|
Copyright 2017- Paul Ganssle <paul@ganssle.io>
|
||||||
|
Copyright 2017- dateutil contributors (see AUTHORS file)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
The above license applies to all contributions after 2017-12-01, as well as
|
||||||
|
all contributions that have been re-licensed (see AUTHORS file for the list of
|
||||||
|
contributors who have re-licensed their code).
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
dateutil - Extensions to the standard Python datetime module.
|
||||||
|
|
||||||
|
Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||||
|
Copyright (c) 2012-2014 - Tomi Pieviläinen <tomi.pievilainen@iki.fi>
|
||||||
|
Copyright (c) 2014-2016 - Yaron de Leeuw <me@jarondl.net>
|
||||||
|
Copyright (c) 2015- - Paul Ganssle <paul@ganssle.io>
|
||||||
|
Copyright (c) 2015- - dateutil contributors (see AUTHORS file)
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
The above BSD License Applies to all code, even that also covered by Apache 2.0.
|
@ -0,0 +1,200 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: python-dateutil
|
||||||
|
Version: 2.8.1
|
||||||
|
Summary: Extensions to the standard Python datetime module
|
||||||
|
Home-page: https://dateutil.readthedocs.io
|
||||||
|
Author: Gustavo Niemeyer
|
||||||
|
Author-email: gustavo@niemeyer.net
|
||||||
|
Maintainer: Paul Ganssle
|
||||||
|
Maintainer-email: dateutil@python.org
|
||||||
|
License: Dual License
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.3
|
||||||
|
Classifier: Programming Language :: Python :: 3.4
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Topic :: Software Development :: Libraries
|
||||||
|
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,>=2.7
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
Requires-Dist: six (>=1.5)
|
||||||
|
|
||||||
|
dateutil - powerful extensions to datetime
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
|pypi| |support| |licence|
|
||||||
|
|
||||||
|
|gitter| |readthedocs|
|
||||||
|
|
||||||
|
|travis| |appveyor| |pipelines| |coverage|
|
||||||
|
|
||||||
|
.. |pypi| image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square
|
||||||
|
:target: https://pypi.org/project/python-dateutil/
|
||||||
|
:alt: pypi version
|
||||||
|
|
||||||
|
.. |support| image:: https://img.shields.io/pypi/pyversions/python-dateutil.svg?style=flat-square
|
||||||
|
:target: https://pypi.org/project/python-dateutil/
|
||||||
|
:alt: supported Python version
|
||||||
|
|
||||||
|
.. |travis| image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square&label=Travis%20Build
|
||||||
|
:target: https://travis-ci.org/dateutil/dateutil
|
||||||
|
:alt: travis build status
|
||||||
|
|
||||||
|
.. |appveyor| image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square&logo=appveyor
|
||||||
|
:target: https://ci.appveyor.com/project/dateutil/dateutil
|
||||||
|
:alt: appveyor build status
|
||||||
|
|
||||||
|
.. |pipelines| image:: https://dev.azure.com/pythondateutilazure/dateutil/_apis/build/status/dateutil.dateutil?branchName=master
|
||||||
|
:target: https://dev.azure.com/pythondateutilazure/dateutil/_build/latest?definitionId=1&branchName=master
|
||||||
|
:alt: azure pipelines build status
|
||||||
|
|
||||||
|
.. |coverage| image:: https://codecov.io/github/dateutil/dateutil/coverage.svg?branch=master
|
||||||
|
:target: https://codecov.io/github/dateutil/dateutil?branch=master
|
||||||
|
:alt: Code coverage
|
||||||
|
|
||||||
|
.. |gitter| image:: https://badges.gitter.im/dateutil/dateutil.svg
|
||||||
|
:alt: Join the chat at https://gitter.im/dateutil/dateutil
|
||||||
|
:target: https://gitter.im/dateutil/dateutil
|
||||||
|
|
||||||
|
.. |licence| image:: https://img.shields.io/pypi/l/python-dateutil.svg?style=flat-square
|
||||||
|
:target: https://pypi.org/project/python-dateutil/
|
||||||
|
:alt: licence
|
||||||
|
|
||||||
|
.. |readthedocs| image:: https://img.shields.io/readthedocs/dateutil/latest.svg?style=flat-square&label=Read%20the%20Docs
|
||||||
|
:alt: Read the documentation at https://dateutil.readthedocs.io/en/latest/
|
||||||
|
:target: https://dateutil.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
The `dateutil` module provides powerful extensions to
|
||||||
|
the standard `datetime` module, available in Python.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
`dateutil` can be installed from PyPI using `pip` (note that the package name is
|
||||||
|
different from the importable name)::
|
||||||
|
|
||||||
|
pip install python-dateutil
|
||||||
|
|
||||||
|
Download
|
||||||
|
========
|
||||||
|
dateutil is available on PyPI
|
||||||
|
https://pypi.org/project/python-dateutil/
|
||||||
|
|
||||||
|
The documentation is hosted at:
|
||||||
|
https://dateutil.readthedocs.io/en/stable/
|
||||||
|
|
||||||
|
Code
|
||||||
|
====
|
||||||
|
The code and issue tracker are hosted on GitHub:
|
||||||
|
https://github.com/dateutil/dateutil/
|
||||||
|
|
||||||
|
Features
|
||||||
|
========
|
||||||
|
|
||||||
|
* Computing of relative deltas (next month, next year,
|
||||||
|
next Monday, last week of month, etc);
|
||||||
|
* Computing of relative deltas between two given
|
||||||
|
date and/or datetime objects;
|
||||||
|
* Computing of dates based on very flexible recurrence rules,
|
||||||
|
using a superset of the `iCalendar <https://www.ietf.org/rfc/rfc2445.txt>`_
|
||||||
|
specification. Parsing of RFC strings is supported as well.
|
||||||
|
* Generic parsing of dates in almost any string format;
|
||||||
|
* Timezone (tzinfo) implementations for tzfile(5) format
|
||||||
|
files (/etc/localtime, /usr/share/zoneinfo, etc), TZ
|
||||||
|
environment string (in all known formats), iCalendar
|
||||||
|
format files, given ranges (with help from relative deltas),
|
||||||
|
local machine timezone, fixed offset timezone, UTC timezone,
|
||||||
|
and Windows registry-based time zones.
|
||||||
|
* Internal up-to-date world timezone information based on
|
||||||
|
Olson's database.
|
||||||
|
* Computing of Easter Sunday dates for any given year,
|
||||||
|
using Western, Orthodox or Julian algorithms;
|
||||||
|
* A comprehensive test suite.
|
||||||
|
|
||||||
|
Quick example
|
||||||
|
=============
|
||||||
|
Here's a snapshot, just to give an idea about the power of the
|
||||||
|
package. For more examples, look at the documentation.
|
||||||
|
|
||||||
|
Suppose you want to know how much time is left, in
|
||||||
|
years/months/days/etc, before the next easter happening on a
|
||||||
|
year with a Friday 13th in August, and you want to get today's
|
||||||
|
date out of the "date" unix system command. Here is the code:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
>>> from dateutil.relativedelta import *
|
||||||
|
>>> from dateutil.easter import *
|
||||||
|
>>> from dateutil.rrule import *
|
||||||
|
>>> from dateutil.parser import *
|
||||||
|
>>> from datetime import *
|
||||||
|
>>> now = parse("Sat Oct 11 17:13:46 UTC 2003")
|
||||||
|
>>> today = now.date()
|
||||||
|
>>> year = rrule(YEARLY,dtstart=now,bymonth=8,bymonthday=13,byweekday=FR)[0].year
|
||||||
|
>>> rdelta = relativedelta(easter(year), today)
|
||||||
|
>>> print("Today is: %s" % today)
|
||||||
|
Today is: 2003-10-11
|
||||||
|
>>> print("Year with next Aug 13th on a Friday is: %s" % year)
|
||||||
|
Year with next Aug 13th on a Friday is: 2004
|
||||||
|
>>> print("How far is the Easter of that year: %s" % rdelta)
|
||||||
|
How far is the Easter of that year: relativedelta(months=+6)
|
||||||
|
>>> print("And the Easter of that year is: %s" % (today+rdelta))
|
||||||
|
And the Easter of that year is: 2004-04-11
|
||||||
|
|
||||||
|
Being exactly 6 months ahead was **really** a coincidence :)
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
We welcome many types of contributions - bug reports, pull requests (code, infrastructure or documentation fixes). For more information about how to contribute to the project, see the ``CONTRIBUTING.md`` file in the repository.
|
||||||
|
|
||||||
|
|
||||||
|
Author
|
||||||
|
======
|
||||||
|
The dateutil module was written by Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||||
|
in 2003.
|
||||||
|
|
||||||
|
It is maintained by:
|
||||||
|
|
||||||
|
* Gustavo Niemeyer <gustavo@niemeyer.net> 2003-2011
|
||||||
|
* Tomi Pieviläinen <tomi.pievilainen@iki.fi> 2012-2014
|
||||||
|
* Yaron de Leeuw <me@jarondl.net> 2014-2016
|
||||||
|
* Paul Ganssle <paul@ganssle.io> 2015-
|
||||||
|
|
||||||
|
Starting with version 2.4.1, all source and binary distributions will be signed
|
||||||
|
by a PGP key that has, at the very least, been signed by the key which made the
|
||||||
|
previous release. A table of release signing keys can be found below:
|
||||||
|
|
||||||
|
=========== ============================
|
||||||
|
Releases Signing key fingerprint
|
||||||
|
=========== ============================
|
||||||
|
2.4.1- `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ (|pgp_mirror|_)
|
||||||
|
=========== ============================
|
||||||
|
|
||||||
|
|
||||||
|
Contact
|
||||||
|
=======
|
||||||
|
Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of
|
||||||
|
conduct <https://www.python.org/psf/codeofconduct/>`_.
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
All contributions after December 1, 2017 released under dual license - either `Apache 2.0 License <https://www.apache.org/licenses/LICENSE-2.0>`_ or the `BSD 3-Clause License <https://opensource.org/licenses/BSD-3-Clause>`_. Contributions before December 1, 2017 - except those those explicitly relicensed - are released only under the BSD 3-Clause License.
|
||||||
|
|
||||||
|
|
||||||
|
.. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB:
|
||||||
|
https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
|
||||||
|
|
||||||
|
.. |pgp_mirror| replace:: mirror
|
||||||
|
.. _pgp_mirror: https://sks-keyservers.net/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
|
||||||
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
|||||||
|
dateutil/__init__.py,sha256=lXElASqwYGwqlrSWSeX19JwF5Be9tNecDa9ebk-0gmk,222
|
||||||
|
dateutil/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/_common.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/_version.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/easter.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/relativedelta.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/rrule.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/tzwin.cpython-36.pyc,,
|
||||||
|
dateutil/__pycache__/utils.cpython-36.pyc,,
|
||||||
|
dateutil/_common.py,sha256=77w0yytkrxlYbSn--lDVPUMabUXRR9I3lBv_vQRUqUY,932
|
||||||
|
dateutil/_version.py,sha256=U1JNX8P5pUNBtcStwfGyAUIMMHGZXhiTDTVXgAUWxs4,116
|
||||||
|
dateutil/easter.py,sha256=0liVsgqSx-NPhaFevOJaYgEbrSu2oQQ2o9m_OEBdc-s,2684
|
||||||
|
dateutil/parser/__init__.py,sha256=wWk6GFuxTpjoggCGtgkceJoti4pVjl4_fHQXpNOaSYg,1766
|
||||||
|
dateutil/parser/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
dateutil/parser/__pycache__/_parser.cpython-36.pyc,,
|
||||||
|
dateutil/parser/__pycache__/isoparser.cpython-36.pyc,,
|
||||||
|
dateutil/parser/_parser.py,sha256=F0w8h9txvatnYAmeJ1MMbIAvZHRzy3iFjv-AZqRovNs,58804
|
||||||
|
dateutil/parser/isoparser.py,sha256=BeEEqIeqhcgik5Cp1_G5Aztsqayp-MAr3aVqAKo1XRc,13098
|
||||||
|
dateutil/relativedelta.py,sha256=GjVxqpAVWnG67rdbf7pkoIlJvQqmju9NSfGCcqblc7U,24904
|
||||||
|
dateutil/rrule.py,sha256=dStRcOIj8jul-BurMKguc_IBckY-Qci1K6EYqNW8eUg,66514
|
||||||
|
dateutil/tz/__init__.py,sha256=F-Mz13v6jYseklQf9Te9J6nzcLDmq47gORa61K35_FA,444
|
||||||
|
dateutil/tz/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
dateutil/tz/__pycache__/_common.cpython-36.pyc,,
|
||||||
|
dateutil/tz/__pycache__/_factories.cpython-36.pyc,,
|
||||||
|
dateutil/tz/__pycache__/tz.cpython-36.pyc,,
|
||||||
|
dateutil/tz/__pycache__/win.cpython-36.pyc,,
|
||||||
|
dateutil/tz/_common.py,sha256=cgzDTANsOXvEc86cYF77EsliuSab8Puwpsl5-bX3_S4,12977
|
||||||
|
dateutil/tz/_factories.py,sha256=unb6XQNXrPMveksTCU-Ag8jmVZs4SojoPUcAHpWnrvU,2569
|
||||||
|
dateutil/tz/tz.py,sha256=npaGnA2M2LGUUerXzAml9rMM-BE771igYFcFETeC3JE,62851
|
||||||
|
dateutil/tz/win.py,sha256=xJszWgSwE1xPx_HJj4ZkepyukC_hNy016WMcXhbRaB8,12935
|
||||||
|
dateutil/tzwin.py,sha256=7Ar4vdQCnnM0mKR3MUjbIKsZrBVfHgdwsJZc_mGYRew,59
|
||||||
|
dateutil/utils.py,sha256=Agvhi7i3HuJdwHYCe9lDS63l_LNFUUlB2hmR3ZKNYwE,1959
|
||||||
|
dateutil/zoneinfo/__init__.py,sha256=KYg0pthCMjcp5MXSEiBJn3nMjZeNZav7rlJw5-tz1S4,5889
|
||||||
|
dateutil/zoneinfo/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
dateutil/zoneinfo/__pycache__/rebuild.cpython-36.pyc,,
|
||||||
|
dateutil/zoneinfo/dateutil-zoneinfo.tar.gz,sha256=6bZJKrN3mhnCqMgQgFSllQNNbtld9AnuPaRIXWoSH4o,153315
|
||||||
|
dateutil/zoneinfo/rebuild.py,sha256=2uFJQiW3Fl8fVogrSXisJMpLeHI1zGwpvBFF43QdeF0,1719
|
||||||
|
python_dateutil-2.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
python_dateutil-2.8.1.dist-info/LICENSE,sha256=ugD1Gg2SgjtaHN4n2LW50jIeZ-2NqbwWPv-W1eF-V34,2889
|
||||||
|
python_dateutil-2.8.1.dist-info/METADATA,sha256=u7pGPxvY3bP0MsvsWab9OeTybTnbLX011vZxRW12I1Y,7988
|
||||||
|
python_dateutil-2.8.1.dist-info/RECORD,,
|
||||||
|
python_dateutil-2.8.1.dist-info/WHEEL,sha256=8zNYZbwQSXoB9IfXOjPfeNwvAsALAjffgk27FqvCWbo,110
|
||||||
|
python_dateutil-2.8.1.dist-info/top_level.txt,sha256=4tjdWkhRZvF7LA_BYe_L9gB2w_p2a-z5y6ArjaRkot8,9
|
||||||
|
python_dateutil-2.8.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.33.6)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
dateutil
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1,418 @@
|
|||||||
|
Metadata-Version: 1.1
|
||||||
|
Name: python-decouple
|
||||||
|
Version: 3.3
|
||||||
|
Summary: Strict separation of settings from code.
|
||||||
|
Home-page: http://github.com/henriquebastos/python-decouple/
|
||||||
|
Author: Henrique Bastos
|
||||||
|
Author-email: henrique@bastos.net
|
||||||
|
License: MIT
|
||||||
|
Description: Python Decouple: Strict separation of settings from code
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
*Decouple* helps you to organize your settings so that you can
|
||||||
|
change parameters without having to redeploy your app.
|
||||||
|
|
||||||
|
It also makes it easy for you to:
|
||||||
|
|
||||||
|
#. store parameters in *ini* or *.env* files;
|
||||||
|
#. define comprehensive default values;
|
||||||
|
#. properly convert values to the correct data type;
|
||||||
|
#. have **only one** configuration module to rule all your instances.
|
||||||
|
|
||||||
|
It was originally designed for Django, but became an independent generic tool
|
||||||
|
for separating settings from code.
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/travis/henriquebastos/python-decouple.svg
|
||||||
|
:target: https://travis-ci.org/henriquebastos/python-decouple
|
||||||
|
:alt: Build Status
|
||||||
|
|
||||||
|
.. image:: https://landscape.io/github/henriquebastos/python-decouple/master/landscape.png
|
||||||
|
:target: https://landscape.io/github/henriquebastos/python-decouple/master
|
||||||
|
:alt: Code Health
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/python-decouple.svg
|
||||||
|
:target: https://pypi.python.org/pypi/python-decouple/
|
||||||
|
:alt: Latest PyPI version
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. contents:: Summary
|
||||||
|
|
||||||
|
|
||||||
|
Why?
|
||||||
|
====
|
||||||
|
|
||||||
|
Web framework's settings stores many different kinds of parameters:
|
||||||
|
|
||||||
|
* Locale and i18n;
|
||||||
|
* Middlewares and Installed Apps;
|
||||||
|
* Resource handles to the database, Memcached, and other backing services;
|
||||||
|
* Credentials to external services such as Amazon S3 or Twitter;
|
||||||
|
* Per-deploy values such as the canonical hostname for the instance.
|
||||||
|
|
||||||
|
The first 2 are *project settings* the last 3 are *instance settings*.
|
||||||
|
|
||||||
|
You should be able to change *instance settings* without redeploying your app.
|
||||||
|
|
||||||
|
Why not just use environment variables?
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
*Envvars* works, but since ``os.environ`` only returns strings, it's tricky.
|
||||||
|
|
||||||
|
Let's say you have an *envvar* ``DEBUG=False``. If you run:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if os.environ['DEBUG']:
|
||||||
|
print True
|
||||||
|
else:
|
||||||
|
print False
|
||||||
|
|
||||||
|
It will print **True**, because ``os.environ['DEBUG']`` returns the **string** ``"False"``.
|
||||||
|
Since it's a non-empty string, it will be evaluated as True.
|
||||||
|
|
||||||
|
*Decouple* provides a solution that doesn't look like a workaround: ``config('DEBUG', cast=bool)``.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pip install python-decouple
|
||||||
|
|
||||||
|
|
||||||
|
Then use it on your ``settings.py``.
|
||||||
|
|
||||||
|
#. Import the ``config`` object:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
#. Retrieve the configuration parameters:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
SECRET_KEY = config('SECRET_KEY')
|
||||||
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||||
|
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||||
|
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||||
|
|
||||||
|
Encodings
|
||||||
|
---------
|
||||||
|
Decouple's default encoding is `UTF-8`.
|
||||||
|
|
||||||
|
But you can specify your preferred encoding.
|
||||||
|
|
||||||
|
Since `config` is lazy and only opens the configuration file when it's first needed, you have the chance to change
|
||||||
|
it's encoding right after import.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
config.encoding = 'cp1251'
|
||||||
|
SECRET_KEY = config('SECRET_KEY')
|
||||||
|
|
||||||
|
If you wish to fallback to your system's default encoding do:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import locale
|
||||||
|
from decouple import config
|
||||||
|
config.encoding = locale.getpreferredencoding(False)
|
||||||
|
SECRET_KEY = config('SECRET_KEY')
|
||||||
|
|
||||||
|
Where the settings data are stored?
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
*Decouple* supports both *.ini* and *.env* files.
|
||||||
|
|
||||||
|
Ini file
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Simply create a ``settings.ini`` next to your configuration module in the form:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
DEBUG=True
|
||||||
|
TEMPLATE_DEBUG=%(DEBUG)s
|
||||||
|
SECRET_KEY=ARANDOMSECRETKEY
|
||||||
|
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
|
||||||
|
PERCENTILE=90%%
|
||||||
|
#COMMENTED=42
|
||||||
|
|
||||||
|
*Note*: Since ``ConfigParser`` supports *string interpolation*, to represent the character ``%`` you need to escape it as ``%%``.
|
||||||
|
|
||||||
|
Env file
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Simply create a ``.env`` text file on your repository's root directory in the form:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
DEBUG=True
|
||||||
|
TEMPLATE_DEBUG=True
|
||||||
|
SECRET_KEY=ARANDOMSECRETKEY
|
||||||
|
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
|
||||||
|
PERCENTILE=90%
|
||||||
|
#COMMENTED=42
|
||||||
|
|
||||||
|
Example: How do I use it with Django?
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
Given that I have a ``.env`` file at my repository root directory, here is a snippet of my ``settings.py``.
|
||||||
|
|
||||||
|
I also recommend using `pathlib <https://docs.python.org/3/library/pathlib.html>`_
|
||||||
|
and `dj-database-url <https://pypi.python.org/pypi/dj-database-url/>`_.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# coding: utf-8
|
||||||
|
from decouple import config
|
||||||
|
from unipath import Path
|
||||||
|
from dj_database_url import parse as db_url
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||||
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': config(
|
||||||
|
'DATABASE_URL',
|
||||||
|
default='sqlite:///' + BASE_DIR.child('db.sqlite3'),
|
||||||
|
cast=db_url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TIME_ZONE = 'America/Sao_Paulo'
|
||||||
|
USE_L10N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
SECRET_KEY = config('SECRET_KEY')
|
||||||
|
|
||||||
|
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||||
|
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||||
|
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||||
|
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||||
|
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Attention with *undefined* parameters
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
On the above example, all configuration parameters except ``SECRET_KEY = config('SECRET_KEY')``
|
||||||
|
have a default value to fallback if it does not exist on the ``.env`` file.
|
||||||
|
|
||||||
|
If ``SECRET_KEY`` is not present in the ``.env``, *decouple* will raise an ``UndefinedValueError``.
|
||||||
|
|
||||||
|
This *fail fast* policy helps you avoid chasing misbehaviors when you eventually forget a parameter.
|
||||||
|
|
||||||
|
Overriding config files with environment variables
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Sometimes you may want to change a parameter value without having to edit the ``.ini`` or ``.env`` files.
|
||||||
|
|
||||||
|
Since version 3.0, *decouple* respects the *unix way*.
|
||||||
|
Therefore environment variables have precedence over config files.
|
||||||
|
|
||||||
|
To override a config parameter you can simply do:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
DEBUG=True python manage.py
|
||||||
|
|
||||||
|
|
||||||
|
How it works?
|
||||||
|
=============
|
||||||
|
|
||||||
|
*Decouple* always searches for *Options* in this order:
|
||||||
|
|
||||||
|
#. Environment variables;
|
||||||
|
#. Repository: ini or .env file;
|
||||||
|
#. default argument passed to config.
|
||||||
|
|
||||||
|
There are 4 classes doing the magic:
|
||||||
|
|
||||||
|
|
||||||
|
- ``Config``
|
||||||
|
|
||||||
|
Coordinates all the configuration retrieval.
|
||||||
|
|
||||||
|
- ``RepositoryIni``
|
||||||
|
|
||||||
|
Can read values from ``os.environ`` and ini files, in that order.
|
||||||
|
|
||||||
|
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
|
||||||
|
|
||||||
|
- ``RepositoryEnv``
|
||||||
|
|
||||||
|
Can read values from ``os.environ`` and ``.env`` files.
|
||||||
|
|
||||||
|
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
|
||||||
|
|
||||||
|
- ``AutoConfig``
|
||||||
|
|
||||||
|
This is a *lazy* ``Config`` factory that detects which configuration repository you're using.
|
||||||
|
|
||||||
|
It recursively searches up your configuration module path looking for a
|
||||||
|
``settings.ini`` or a ``.env`` file.
|
||||||
|
|
||||||
|
Optionally, it accepts ``search_path`` argument to explicitly define
|
||||||
|
where the search starts.
|
||||||
|
|
||||||
|
The **config** object is an instance of ``AutoConfig`` that instantiates a ``Config`` with the proper ``Repository``
|
||||||
|
on the first time it is used.
|
||||||
|
|
||||||
|
|
||||||
|
Understanding the CAST argument
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
By default, all values returned by ``decouple`` are ``strings``, after all they are
|
||||||
|
read from ``text files`` or the ``envvars``.
|
||||||
|
|
||||||
|
However, your Python code may expect some other value type, for example:
|
||||||
|
|
||||||
|
* Django's ``DEBUG`` expects a boolean ``True`` or ``False``.
|
||||||
|
* Django's ``EMAIL_PORT`` expects an ``integer``.
|
||||||
|
* Django's ``ALLOWED_HOSTS`` expects a ``list`` of hostnames.
|
||||||
|
* Django's ``SECURE_PROXY_SSL_HEADER`` expects a ``tuple`` with two elements, the name of the header to look for and the required value.
|
||||||
|
|
||||||
|
To meet this need, the ``config`` function accepts a ``cast`` argument which
|
||||||
|
receives any *callable*, that will be used to *transform* the string value
|
||||||
|
into something else.
|
||||||
|
|
||||||
|
Let's see some examples for the above mentioned cases:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> os.environ['DEBUG'] = 'False'
|
||||||
|
>>> config('DEBUG', cast=bool)
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> os.environ['EMAIL_PORT'] = '42'
|
||||||
|
>>> config('EMAIL_PORT', cast=int)
|
||||||
|
42
|
||||||
|
|
||||||
|
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
|
||||||
|
>>> config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||||
|
['.localhost', '.herokuapp.com']
|
||||||
|
|
||||||
|
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
|
||||||
|
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
|
||||||
|
('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
|
||||||
|
As you can see, ``cast`` is very flexible. But the last example got a bit complex.
|
||||||
|
|
||||||
|
Built in Csv Helper
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To address the complexity of the last example, *Decouple* comes with an extensible *Csv helper*.
|
||||||
|
|
||||||
|
Let's improve the last example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> from decouple import Csv
|
||||||
|
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
|
||||||
|
>>> config('ALLOWED_HOSTS', cast=Csv())
|
||||||
|
['.localhost', '.herokuapp.com']
|
||||||
|
|
||||||
|
You can also have a `default` value that must be a string to be processed by `Csv`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> from decouple import Csv
|
||||||
|
>>> config('ALLOWED_HOSTS', default='127.0.0.1', cast=Csv())
|
||||||
|
['127.0.0.1']
|
||||||
|
|
||||||
|
You can also parametrize the *Csv Helper* to return other types of data.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> os.environ['LIST_OF_INTEGERS'] = '1,2,3,4,5'
|
||||||
|
>>> config('LIST_OF_INTEGERS', cast=Csv(int))
|
||||||
|
[1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
>>> os.environ['COMPLEX_STRING'] = '%virtual_env%\t *important stuff*\t trailing spaces '
|
||||||
|
>>> csv = Csv(cast=lambda s: s.upper(), delimiter='\t', strip=' %*')
|
||||||
|
>>> csv(os.environ['COMPLEX_STRING'])
|
||||||
|
['VIRTUAL_ENV', 'IMPORTANT STUFF', 'TRAILING SPACES']
|
||||||
|
|
||||||
|
By default *Csv* returns a ``list``, but you can get a ``tuple`` or whatever you want using the ``post_process`` argument:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
|
||||||
|
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
|
||||||
|
('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
|
||||||
|
|
||||||
|
Contribute
|
||||||
|
==========
|
||||||
|
|
||||||
|
Your contribution is welcome.
|
||||||
|
|
||||||
|
Setup your development environment:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
git clone git@github.com:henriquebastos/python-decouple.git
|
||||||
|
cd python-decouple
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
tox
|
||||||
|
|
||||||
|
*Decouple* supports both Python 2.7 and 3.6. Make sure you have both installed.
|
||||||
|
|
||||||
|
I use `pyenv <https://github.com/pyenv/pyenv#simple-python-version-management-pyenv>`_ to
|
||||||
|
manage multiple Python versions and I described my workspace setup on this article:
|
||||||
|
`The definitive guide to setup my Python workspace
|
||||||
|
<https://medium.com/@henriquebastos/the-definitive-guide-to-setup-my-python-workspace-628d68552e14>`_
|
||||||
|
|
||||||
|
You can submit pull requests and issues for discussion. However I only
|
||||||
|
consider merging tested code.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Henrique Bastos <henrique at bastos dot net>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
Platform: any
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Framework :: Django
|
||||||
|
Classifier: Framework :: Flask
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Natural Language :: English
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Topic :: Software Development :: Libraries
|
@ -0,0 +1,11 @@
|
|||||||
|
LICENSE
|
||||||
|
MANIFEST.in
|
||||||
|
README.rst
|
||||||
|
decouple.py
|
||||||
|
setup.cfg
|
||||||
|
setup.py
|
||||||
|
python_decouple.egg-info/PKG-INFO
|
||||||
|
python_decouple.egg-info/SOURCES.txt
|
||||||
|
python_decouple.egg-info/dependency_links.txt
|
||||||
|
python_decouple.egg-info/not-zip-safe
|
||||||
|
python_decouple.egg-info/top_level.txt
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
..\__pycache__\decouple.cpython-36.pyc
|
||||||
|
..\decouple.py
|
||||||
|
PKG-INFO
|
||||||
|
SOURCES.txt
|
||||||
|
dependency_links.txt
|
||||||
|
not-zip-safe
|
||||||
|
top_level.txt
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
decouple
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,87 @@
|
|||||||
|
python-dotenv
|
||||||
|
Copyright (c) 2014, Saurabh Kumar
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of python-dotenv nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
django-dotenv-rw
|
||||||
|
Copyright (c) 2013, Ted Tieken
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of django-dotenv nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Original django-dotenv
|
||||||
|
Copyright (c) 2013, Jacob Kaplan-Moss
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of django-dotenv nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,516 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: python-dotenv
|
||||||
|
Version: 0.13.0
|
||||||
|
Summary: Add .env support to your django/flask apps in development and deployments
|
||||||
|
Home-page: http://github.com/theskumar/python-dotenv
|
||||||
|
Author: Saurabh Kumar
|
||||||
|
Author-email: me+github@saurabh-kumar.com
|
||||||
|
License: UNKNOWN
|
||||||
|
Keywords: environment variables,deployments,settings,env,dotenv,configurations,python
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Intended Audience :: System Administrators
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Topic :: System :: Systems Administration
|
||||||
|
Classifier: Topic :: Utilities
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
Requires-Dist: typing ; python_version < "3.5"
|
||||||
|
Provides-Extra: cli
|
||||||
|
Requires-Dist: click (>=5.0) ; extra == 'cli'
|
||||||
|
|
||||||
|
```
|
||||||
|
_______ .__ __. ____ ____
|
||||||
|
| ____|| \ | | \ \ / /
|
||||||
|
| |__ | \| | \ \/ /
|
||||||
|
| __| | . ` | \ /
|
||||||
|
__ | |____ | |\ | \ /
|
||||||
|
(__)|_______||__| \__| \__/
|
||||||
|
```
|
||||||
|
python-dotenv | [![Build Status](https://travis-ci.org/theskumar/python-dotenv.svg?branch=master)](https://travis-ci.org/theskumar/python-dotenv) [![Coverage Status](https://coveralls.io/repos/theskumar/python-dotenv/badge.svg?branch=master)](https://coveralls.io/r/theskumar/python-dotenv?branch=master) [![PyPI version](https://badge.fury.io/py/python-dotenv.svg)](http://badge.fury.io/py/python-dotenv) [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/theskumar)
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
Reads the key-value pair from `.env` file and adds them to environment
|
||||||
|
variable. It is great for managing app settings during development and
|
||||||
|
in production using [12-factor](http://12factor.net/) principles.
|
||||||
|
|
||||||
|
> Do one thing, do it well!
|
||||||
|
|
||||||
|
## Usages
|
||||||
|
|
||||||
|
The easiest and most common usage consists on calling `load_dotenv` when
|
||||||
|
the application starts, which will load environment variables from a
|
||||||
|
file named `.env` in the current directory or any of its parents or from
|
||||||
|
the path specificied; after that, you can just call the
|
||||||
|
environment-related method you need as provided by `os.getenv`.
|
||||||
|
|
||||||
|
`.env` looks like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# a comment that will be ignored.
|
||||||
|
REDIS_ADDRESS=localhost:6379
|
||||||
|
MEANING_OF_LIFE=42
|
||||||
|
MULTILINE_VAR="hello\nworld"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can optionally prefix each line with the word `export`, which is totally ignored by this library, but might allow you to [`source`](https://bash.cyberciti.biz/guide/Source_command) the file in bash.
|
||||||
|
|
||||||
|
```
|
||||||
|
export S3_BUCKET=YOURS3BUCKET
|
||||||
|
export SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||||
|
```
|
||||||
|
|
||||||
|
Python-dotenv can interpolate variables using POSIX variable expansion.
|
||||||
|
|
||||||
|
The value of a variable is the first of the values defined in the following list:
|
||||||
|
|
||||||
|
- Value of that variable in the environment.
|
||||||
|
- Value of that variable in the `.env` file.
|
||||||
|
- Default value, if provided.
|
||||||
|
- Empty string.
|
||||||
|
|
||||||
|
Ensure that variables are surrounded with `{}` like `${HOME}` as bare
|
||||||
|
variables such as `$HOME` are not expanded.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
CONFIG_PATH=${HOME}/.config/foo
|
||||||
|
DOMAIN=example.org
|
||||||
|
EMAIL=admin@${DOMAIN}
|
||||||
|
DEBUG=${DEBUG:-false}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Install the latest version with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -U python-dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
Assuming you have created the `.env` file along-side your settings
|
||||||
|
module.
|
||||||
|
|
||||||
|
.
|
||||||
|
├── .env
|
||||||
|
└── settings.py
|
||||||
|
|
||||||
|
Add the following code to your `settings.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# settings.py
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# OR, the same with increased verbosity
|
||||||
|
load_dotenv(verbose=True)
|
||||||
|
|
||||||
|
# OR, explicitly providing path to '.env'
|
||||||
|
from pathlib import Path # python3 only
|
||||||
|
env_path = Path('.') / '.env'
|
||||||
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, parsed key/value from the `.env` file is now present as
|
||||||
|
system environment variable and they can be conveniently accessed via
|
||||||
|
`os.getenv()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# settings.py
|
||||||
|
import os
|
||||||
|
SECRET_KEY = os.getenv("EMAIL")
|
||||||
|
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD")
|
||||||
|
```
|
||||||
|
|
||||||
|
`load_dotenv` does not override existing System environment variables. To
|
||||||
|
override, pass `override=True` to `load_dotenv()`.
|
||||||
|
|
||||||
|
`load_dotenv` also accepts `encoding` parameter to open the `.env` file. The default encoding is platform dependent (whatever `locale.getpreferredencoding()` returns), but any encoding supported by Python can be used. See the [codecs](https://docs.python.org/3/library/codecs.html#standard-encodings) module for the list of supported encodings.
|
||||||
|
|
||||||
|
You can use `find_dotenv()` method that will try to find a `.env` file
|
||||||
|
by (a) guessing where to start using `__file__` or the working directory
|
||||||
|
-- allowing this to work in non-file contexts such as IPython notebooks
|
||||||
|
and the REPL, and then (b) walking up the directory tree looking for the
|
||||||
|
specified file -- called `.env` by default.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
load_dotenv(find_dotenv())
|
||||||
|
```
|
||||||
|
|
||||||
|
### In-memory filelikes
|
||||||
|
|
||||||
|
It is possible to not rely on the filesystem to parse filelikes from
|
||||||
|
other sources (e.g. from a network storage). `load_dotenv` and
|
||||||
|
`dotenv_values` accepts a filelike `stream`. Just be sure to rewind it
|
||||||
|
before passing.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from io import StringIO # Python2: from StringIO import StringIO
|
||||||
|
>>> from dotenv import dotenv_values
|
||||||
|
>>> filelike = StringIO('SPAM=EGGS\n')
|
||||||
|
>>> filelike.seek(0)
|
||||||
|
>>> parsed = dotenv_values(stream=filelike)
|
||||||
|
>>> parsed['SPAM']
|
||||||
|
'EGGS'
|
||||||
|
```
|
||||||
|
|
||||||
|
The returned value is dictionary with key-value pairs.
|
||||||
|
|
||||||
|
`dotenv_values` could be useful if you need to *consume* the envfile but
|
||||||
|
not *apply* it directly into the system environment.
|
||||||
|
|
||||||
|
### Django
|
||||||
|
|
||||||
|
If you are using Django, you should add the above loader script at the
|
||||||
|
top of `wsgi.py` and `manage.py`.
|
||||||
|
|
||||||
|
|
||||||
|
## IPython Support
|
||||||
|
|
||||||
|
You can use dotenv with IPython. You can either let the dotenv search
|
||||||
|
for `.env` with `%dotenv` or provide the path to the `.env` file explicitly; see
|
||||||
|
below for usages.
|
||||||
|
|
||||||
|
%load_ext dotenv
|
||||||
|
|
||||||
|
# Use find_dotenv to locate the file
|
||||||
|
%dotenv
|
||||||
|
|
||||||
|
# Specify a particular file
|
||||||
|
%dotenv relative/or/absolute/path/to/.env
|
||||||
|
|
||||||
|
# Use '-o' to indicate override of existing variables
|
||||||
|
%dotenv -o
|
||||||
|
|
||||||
|
# Use '-v' to turn verbose mode on
|
||||||
|
%dotenv -v
|
||||||
|
|
||||||
|
|
||||||
|
## Command-line Interface
|
||||||
|
|
||||||
|
For command-line support, use the CLI option during installation:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -U "python-dotenv[cli]"
|
||||||
|
```
|
||||||
|
|
||||||
|
A CLI interface `dotenv` is also included, which helps you manipulate
|
||||||
|
the `.env` file without manually opening it. The same CLI installed on
|
||||||
|
remote machine combined with fabric (discussed later) will enable you to
|
||||||
|
update your settings on a remote server; handy, isn't it!
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: dotenv [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
This script is used to set, get or unset values from a .env file.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f, --file PATH Location of the .env file, defaults to .env
|
||||||
|
file in current working directory.
|
||||||
|
-q, --quote [always|never|auto]
|
||||||
|
Whether to quote or not the variable values.
|
||||||
|
Default mode is always. This does not affect
|
||||||
|
parsing.
|
||||||
|
--help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
get Retrive the value for the given key.
|
||||||
|
list Display all the stored key/value.
|
||||||
|
run Run command with environment variables from .env file present
|
||||||
|
set Store the given key/value.
|
||||||
|
unset Removes the given key.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Setting config on Remote Servers
|
||||||
|
|
||||||
|
We make use of excellent [Fabric](http://www.fabfile.org/) to accomplish
|
||||||
|
this. Add a config task to your local fabfile; `dotenv_path` is the
|
||||||
|
location of the absolute path of `.env` file on the remote server.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# fabfile.py
|
||||||
|
|
||||||
|
import dotenv
|
||||||
|
from fabric.api import task, run, env
|
||||||
|
|
||||||
|
# absolute path to the location of .env on remote server.
|
||||||
|
env.dotenv_path = '/opt/myapp/.env'
|
||||||
|
|
||||||
|
@task
|
||||||
|
def config(action=None, key=None, value=None):
|
||||||
|
'''Manage project configuration via .env
|
||||||
|
|
||||||
|
e.g: fab config:set,<key>,<value>
|
||||||
|
fab config:get,<key>
|
||||||
|
fab config:unset,<key>
|
||||||
|
fab config:list
|
||||||
|
'''
|
||||||
|
run('touch %(dotenv_path)s' % env)
|
||||||
|
command = dotenv.get_cli_string(env.dotenv_path, action, key, value)
|
||||||
|
run(command)
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage is designed to mirror the Heroku config API very closely.
|
||||||
|
|
||||||
|
Get all your remote config info with `fab config`:
|
||||||
|
|
||||||
|
$ fab config
|
||||||
|
foo="bar"
|
||||||
|
|
||||||
|
Set remote config variables with `fab config:set,<key>,<value>`:
|
||||||
|
|
||||||
|
$ fab config:set,hello,world
|
||||||
|
|
||||||
|
Get a single remote config variables with `fab config:get,<key>`:
|
||||||
|
|
||||||
|
$ fab config:get,hello
|
||||||
|
|
||||||
|
Delete a remote config variables with `fab config:unset,<key>`:
|
||||||
|
|
||||||
|
$ fab config:unset,hello
|
||||||
|
|
||||||
|
Thanks entirely to fabric and not one bit to this project, you can chain
|
||||||
|
commands like so:
|
||||||
|
`fab config:set,<key1>,<value1> config:set,<key2>,<value2>`
|
||||||
|
|
||||||
|
$ fab config:set,hello,world config:set,foo,bar config:set,fizz=buzz
|
||||||
|
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- [Honcho](https://github.com/nickstenning/honcho) - For managing
|
||||||
|
Procfile-based applications.
|
||||||
|
- [django-dotenv](https://github.com/jpadilla/django-dotenv)
|
||||||
|
- [django-environ](https://github.com/joke2k/django-environ)
|
||||||
|
- [django-configuration](https://github.com/jezdez/django-configurations)
|
||||||
|
- [dump-env](https://github.com/sobolevn/dump-env)
|
||||||
|
- [environs](https://github.com/sloria/environs)
|
||||||
|
- [dynaconf](https://github.com/rochacbruno/dynaconf)
|
||||||
|
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
This project is currently maintained by [Saurabh Kumar](https://saurabh-kumar.com) and [Bertrand Bonnefoy-Claudet](https://github.com/bbc2) and would not
|
||||||
|
have been possible without the support of these [awesome
|
||||||
|
people](https://github.com/theskumar/python-dotenv/graphs/contributors).
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
|
||||||
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
*No unreleased change at this time.*
|
||||||
|
|
||||||
|
## [0.13.0] - 2020-04-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.12.0] - 2020-02-28
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Use current working directory to find `.env` when bundled by PyInstaller (#213 by
|
||||||
|
[@gergelyk]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]).
|
||||||
|
- Fix `dotenv run` crashing on environment variables without values (#237 by [@yannham]).
|
||||||
|
- Remove warning when last line is empty (#238 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.11.0] - 2020-02-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `interpolate` argument to `load_dotenv` and `dotenv_values` to disable interpolation
|
||||||
|
(#232 by [@ulyssessouza]).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Use logging instead of warnings (#231 by [@bbc2]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix installation in non-UTF-8 environments (#225 by [@altendky]).
|
||||||
|
- Fix PyPI classifiers (#228 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.10.5] - 2020-01-19
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix handling of malformed lines and lines without a value (#222 by [@bbc2]):
|
||||||
|
- Don't print warning when key has no value.
|
||||||
|
- Reject more malformed lines (e.g. "A: B", "a='b',c").
|
||||||
|
- Fix handling of lines with just a comment (#224 by [@bbc2]).
|
||||||
|
|
||||||
|
## [0.10.4] - 2020-01-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Make typing optional (#179 by [@techalchemy]).
|
||||||
|
- Print a warning on malformed line (#211 by [@bbc2]).
|
||||||
|
- Support keys without a value (#220 by [@ulyssessouza]).
|
||||||
|
|
||||||
|
## 0.10.3
|
||||||
|
|
||||||
|
- Improve interactive mode detection ([@andrewsmith])([#183]).
|
||||||
|
- Refactor parser to fix parsing inconsistencies ([@bbc2])([#170]).
|
||||||
|
- Interpret escapes as control characters only in double-quoted strings.
|
||||||
|
- Interpret `#` as start of comment only if preceded by whitespace.
|
||||||
|
|
||||||
|
## 0.10.2
|
||||||
|
|
||||||
|
- Add type hints and expose them to users ([@qnighy])([#172])
|
||||||
|
- `load_dotenv` and `dotenv_values` now accept an `encoding` parameter, defaults to `None`
|
||||||
|
([@theskumar])([@earlbread])([#161])
|
||||||
|
- Fix `str`/`unicode` inconsistency in Python 2: values are always `str` now. ([@bbc2])([#121])
|
||||||
|
- Fix Unicode error in Python 2, introduced in 0.10.0. ([@bbc2])([#176])
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
- Fix parsing of variable without a value ([@asyncee])([@bbc2])([#158])
|
||||||
|
|
||||||
|
## 0.10.0
|
||||||
|
|
||||||
|
- Add support for UTF-8 in unquoted values ([@bbc2])([#148])
|
||||||
|
- Add support for trailing comments ([@bbc2])([#148])
|
||||||
|
- Add backslashes support in values ([@bbc2])([#148])
|
||||||
|
- Add support for newlines in values ([@bbc2])([#148])
|
||||||
|
- Force environment variables to str with Python2 on Windows ([@greyli])
|
||||||
|
- Drop Python 3.3 support ([@greyli])
|
||||||
|
- Fix stderr/-out/-in redirection ([@venthur])
|
||||||
|
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
- Add `--version` parameter to cli ([@venthur])
|
||||||
|
- Enable loading from current directory ([@cjauvin])
|
||||||
|
- Add 'dotenv run' command for calling arbitrary shell script with .env ([@venthur])
|
||||||
|
|
||||||
|
## 0.8.1
|
||||||
|
|
||||||
|
- Add tests for docs ([@Flimm])
|
||||||
|
- Make 'cli' support optional. Use `pip install python-dotenv[cli]`. ([@theskumar])
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- `set_key` and `unset_key` only modified the affected file instead of
|
||||||
|
parsing and re-writing file, this causes comments and other file
|
||||||
|
entact as it is.
|
||||||
|
- Add support for `export` prefix in the line.
|
||||||
|
- Internal refractoring ([@theskumar])
|
||||||
|
- Allow `load_dotenv` and `dotenv_values` to work with `StringIO())` ([@alanjds])([@theskumar])([#78])
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
- Remove hard dependency on iPython ([@theskumar])
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
- Add support to override system environment variable via .env.
|
||||||
|
([@milonimrod](https://github.com/milonimrod))
|
||||||
|
([\#63](https://github.com/theskumar/python-dotenv/issues/63))
|
||||||
|
- Disable ".env not found" warning by default
|
||||||
|
([@maxkoryukov](https://github.com/maxkoryukov))
|
||||||
|
([\#57](https://github.com/theskumar/python-dotenv/issues/57))
|
||||||
|
|
||||||
|
## 0.6.5
|
||||||
|
|
||||||
|
- Add support for special characters `\`.
|
||||||
|
([@pjona](https://github.com/pjona))
|
||||||
|
([\#60](https://github.com/theskumar/python-dotenv/issues/60))
|
||||||
|
|
||||||
|
## 0.6.4
|
||||||
|
|
||||||
|
- Fix issue with single quotes ([@Flimm])
|
||||||
|
([\#52](https://github.com/theskumar/python-dotenv/issues/52))
|
||||||
|
|
||||||
|
## 0.6.3
|
||||||
|
|
||||||
|
- Handle unicode exception in setup.py
|
||||||
|
([\#46](https://github.com/theskumar/python-dotenv/issues/46))
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
- Fix dotenv list command ([@ticosax](https://github.com/ticosax))
|
||||||
|
- Add iPython Suport
|
||||||
|
([@tillahoffmann](https://github.com/tillahoffmann))
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
- Drop support for Python 2.6
|
||||||
|
- Handle escaped charaters and newlines in quoted values. (Thanks
|
||||||
|
[@iameugenejo](https://github.com/iameugenejo))
|
||||||
|
- Remove any spaces around unquoted key/value. (Thanks
|
||||||
|
[@paulochf](https://github.com/paulochf))
|
||||||
|
- Added POSIX variable expansion. (Thanks
|
||||||
|
[@hugochinchilla](https://github.com/hugochinchilla))
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
- Fix find\_dotenv - it now start search from the file where this
|
||||||
|
function is called from.
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
- Add `find_dotenv` method that will try to find a `.env` file.
|
||||||
|
(Thanks [@isms](https://github.com/isms))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
- cli: Added `-q/--quote` option to control the behaviour of quotes
|
||||||
|
around values in `.env`. (Thanks
|
||||||
|
[@hugochinchilla](https://github.com/hugochinchilla)).
|
||||||
|
- Improved test coverage.
|
||||||
|
|
||||||
|
[#78]: https://github.com/theskumar/python-dotenv/issues/78
|
||||||
|
[#121]: https://github.com/theskumar/python-dotenv/issues/121
|
||||||
|
[#148]: https://github.com/theskumar/python-dotenv/issues/148
|
||||||
|
[#158]: https://github.com/theskumar/python-dotenv/issues/158
|
||||||
|
[#170]: https://github.com/theskumar/python-dotenv/issues/170
|
||||||
|
[#172]: https://github.com/theskumar/python-dotenv/issues/172
|
||||||
|
[#176]: https://github.com/theskumar/python-dotenv/issues/176
|
||||||
|
[#183]: https://github.com/theskumar/python-dotenv/issues/183
|
||||||
|
|
||||||
|
[@Flimm]: https://github.com/Flimm
|
||||||
|
[@alanjds]: https://github.com/alanjds
|
||||||
|
[@altendky]: https://github.com/altendky
|
||||||
|
[@andrewsmith]: https://github.com/andrewsmith
|
||||||
|
[@asyncee]: https://github.com/asyncee
|
||||||
|
[@bbc2]: https://github.com/bbc2
|
||||||
|
[@cjauvin]: https://github.com/cjauvin
|
||||||
|
[@earlbread]: https://github.com/earlbread
|
||||||
|
[@gergelyk]: https://github.com/gergelyk
|
||||||
|
[@greyli]: https://github.com/greyli
|
||||||
|
[@qnighy]: https://github.com/qnighy
|
||||||
|
[@techalchemy]: https://github.com/techalchemy
|
||||||
|
[@theskumar]: https://github.com/theskumar
|
||||||
|
[@ulyssessouza]: https://github.com/ulyssessouza
|
||||||
|
[@venthur]: https://github.com/venthur
|
||||||
|
[@yannham]: https://github.com/yannham
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.13.0...HEAD
|
||||||
|
[0.13.0]: https://github.com/theskumar/python-dotenv/compare/v0.12.0...v0.13.0
|
||||||
|
[0.12.0]: https://github.com/theskumar/python-dotenv/compare/v0.11.0...v0.12.0
|
||||||
|
[0.11.0]: https://github.com/theskumar/python-dotenv/compare/v0.10.5...v0.11.0
|
||||||
|
[0.10.5]: https://github.com/theskumar/python-dotenv/compare/v0.10.4...v0.10.5
|
||||||
|
[0.10.4]: https://github.com/theskumar/python-dotenv/compare/v0.10.3...v0.10.4
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
../../Scripts/dotenv.exe,sha256=dw0T34FXAUzXkNkWW-HgU_p0emUWRHEMu6QuJi593yM,106359
|
||||||
|
dotenv/__init__.py,sha256=7Do7SaK79Arfb8Wkyfrn-7wCj2k8VkE6quTyz88a0xc,1339
|
||||||
|
dotenv/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
dotenv/__pycache__/cli.cpython-36.pyc,,
|
||||||
|
dotenv/__pycache__/compat.cpython-36.pyc,,
|
||||||
|
dotenv/__pycache__/ipython.cpython-36.pyc,,
|
||||||
|
dotenv/__pycache__/main.cpython-36.pyc,,
|
||||||
|
dotenv/__pycache__/parser.cpython-36.pyc,,
|
||||||
|
dotenv/__pycache__/version.cpython-36.pyc,,
|
||||||
|
dotenv/cli.py,sha256=tuz5Y8cgYvY8GULgOQP9VTh1OxTYBxx6sZyToWVRw40,4026
|
||||||
|
dotenv/compat.py,sha256=IhMmBmXjtrs7VdKaDKNCjie-TAgDC2EI5p0BSrNfRo8,964
|
||||||
|
dotenv/ipython.py,sha256=Olsq1Q8qwEW1xwfTwlqaJ6tjXM7fIgexXltmAsoaTq4,1342
|
||||||
|
dotenv/main.py,sha256=GcspkzRjOPAOyuGklW_Tqwe4AZDtaiKVhpBeWu_CQUY,10830
|
||||||
|
dotenv/parser.py,sha256=5_qwL94nf6LtH0Tgs_pxlau-8bZQHN9yk8OlQl3qiJ8,6534
|
||||||
|
dotenv/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
||||||
|
dotenv/version.py,sha256=DgpLNbv0e1LIEOOe54Db8_390i9pelMEFEnsBsNmyhA,23
|
||||||
|
python_dotenv-0.13.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
python_dotenv-0.13.0.dist-info/LICENSE,sha256=0nIJqz0WJ4Ko-OOHK5s1PEngksmqRnpkUiiDQH2NEDA,4600
|
||||||
|
python_dotenv-0.13.0.dist-info/METADATA,sha256=kRhodSYEgZa7WVGk9YtkDC38N8kkXJ8otxIpliv8Zr0,17107
|
||||||
|
python_dotenv-0.13.0.dist-info/RECORD,,
|
||||||
|
python_dotenv-0.13.0.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
|
||||||
|
python_dotenv-0.13.0.dist-info/entry_points.txt,sha256=c80zWSdgWj260RWtcIS4k0kmahyXf4elttqHYusYwdk,61
|
||||||
|
python_dotenv-0.13.0.dist-info/top_level.txt,sha256=eyqUH4SHJNr6ahOYlxIunTr4XinE8Z5ajWLdrK3r0D8,7
|
@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.34.2)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
[console_scripts]
|
||||||
|
dotenv=dotenv.cli:cli
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
dotenv
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,78 @@
|
|||||||
|
urllib3-1.25.9.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
urllib3-1.25.9.dist-info/LICENSE.txt,sha256=fA0TbuBYU4mt8tJWcbuZaHofdZKfRlt_Fu4_Ado3JV4,1115
|
||||||
|
urllib3-1.25.9.dist-info/METADATA,sha256=QVc-HCXpe7Dm_RDmd-GpzKT-LvxBgwsPsLEiE5kUjEI,39852
|
||||||
|
urllib3-1.25.9.dist-info/RECORD,,
|
||||||
|
urllib3-1.25.9.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
|
||||||
|
urllib3-1.25.9.dist-info/top_level.txt,sha256=EMiXL2sKrTcmrMxIHTqdc3ET54pQI2Y072LexFEemvo,8
|
||||||
|
urllib3/__init__.py,sha256=rdFZCO1L7e8861ZTvo8AiSKwxCe9SnWQUQwJ599YV9c,2683
|
||||||
|
urllib3/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/_collections.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/connection.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/connectionpool.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/exceptions.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/fields.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/filepost.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/poolmanager.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/request.cpython-36.pyc,,
|
||||||
|
urllib3/__pycache__/response.cpython-36.pyc,,
|
||||||
|
urllib3/_collections.py,sha256=GouVsNzwg6jADZTmimMI6oqmwKSswnMo9dh5tGNVWO4,10792
|
||||||
|
urllib3/connection.py,sha256=Fln8a_bkegdNMkFoSOwyI0PJvL1OqzVUO6ifihKOTpc,14461
|
||||||
|
urllib3/connectionpool.py,sha256=egdaX-Db_LVXifDxv3JY0dHIpQqDv0wC0_9Eeh8FkPM,35725
|
||||||
|
urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
urllib3/contrib/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/__pycache__/_appengine_environ.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/__pycache__/appengine.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/__pycache__/ntlmpool.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/__pycache__/pyopenssl.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/__pycache__/securetransport.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/__pycache__/socks.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/_appengine_environ.py,sha256=bDbyOEhW2CKLJcQqAKAyrEHN-aklsyHFKq6vF8ZFsmk,957
|
||||||
|
urllib3/contrib/_securetransport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
urllib3/contrib/_securetransport/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/_securetransport/__pycache__/bindings.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/_securetransport/__pycache__/low_level.cpython-36.pyc,,
|
||||||
|
urllib3/contrib/_securetransport/bindings.py,sha256=mullWYFaghBdRWla6HYU-TBgFRTPLBEfxj3jplbeJmQ,16886
|
||||||
|
urllib3/contrib/_securetransport/low_level.py,sha256=V7GnujxnWZh2N2sMsV5N4d9Imymokkm3zBwgt77_bSE,11956
|
||||||
|
urllib3/contrib/appengine.py,sha256=9RyUW5vKy4VPa2imtwBNWYKILrypr-K6UXEHUYsf0JY,11010
|
||||||
|
urllib3/contrib/ntlmpool.py,sha256=a402AwGN_Ll3N-4ur_AS6UrU-ycUtlnYqoBF76lORg8,4160
|
||||||
|
urllib3/contrib/pyopenssl.py,sha256=qQKqQXvlSvpCa2yEPxpdv18lS71SMESr9XzH9K9x3KI,16565
|
||||||
|
urllib3/contrib/securetransport.py,sha256=vBDFjSnH2gWa-ztMKVaiwW46K1mlDZKqvo_VAonfdcY,32401
|
||||||
|
urllib3/contrib/socks.py,sha256=nzDMgDIFJWVubKHqvIn2-SKCO91hhJInP92WgHChGzA,7036
|
||||||
|
urllib3/exceptions.py,sha256=D2Jvab7M7m_n0rnmBmq481paoVT32VvVeB6VeQM0y-w,7172
|
||||||
|
urllib3/fields.py,sha256=kroD76QK-GdHHW7f_AUN4XxDC3OQPI2FFrS9eSL4BCs,8553
|
||||||
|
urllib3/filepost.py,sha256=vj0qbrpT1AFzvvW4SuC8M5kJiw7wftHcSr-7b8UpPpw,2440
|
||||||
|
urllib3/packages/__init__.py,sha256=h4BLhD4tLaBx1adaDtKXfupsgqY0wWLXb_f1_yVlV6A,108
|
||||||
|
urllib3/packages/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/packages/__pycache__/six.cpython-36.pyc,,
|
||||||
|
urllib3/packages/backports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
urllib3/packages/backports/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/packages/backports/__pycache__/makefile.cpython-36.pyc,,
|
||||||
|
urllib3/packages/backports/makefile.py,sha256=005wrvH-_pWSnTFqQ2sdzzh4zVCtQUUQ4mR2Yyxwc0A,1418
|
||||||
|
urllib3/packages/six.py,sha256=adx4z-eM_D0Vvu0IIqVzFACQ_ux9l64y7DkSEfbxCDs,32536
|
||||||
|
urllib3/packages/ssl_match_hostname/__init__.py,sha256=ywgKMtfHi1-DrXlzPfVAhzsLzzqcK7GT6eLgdode1Fg,688
|
||||||
|
urllib3/packages/ssl_match_hostname/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/packages/ssl_match_hostname/__pycache__/_implementation.cpython-36.pyc,,
|
||||||
|
urllib3/packages/ssl_match_hostname/_implementation.py,sha256=6dZ-q074g7XhsJ27MFCgkct8iVNZB3sMZvKhf-KUVy0,5679
|
||||||
|
urllib3/poolmanager.py,sha256=iWEAIGrVNGoOmQyfiFwCqG-IyYy6GIQ-jJ9QCsX9li4,17861
|
||||||
|
urllib3/request.py,sha256=hhoHvEEatyd9Tn5EbGjQ0emn-ENMCyY591yNWTneINA,6018
|
||||||
|
urllib3/response.py,sha256=eo1Sfkn2x44FtjgP3qwwDsG9ak84spQAxEGy7Ovd4Pc,28221
|
||||||
|
urllib3/util/__init__.py,sha256=bWNaav_OT-1L7-sxm59cGb59rDORlbhb_4noduM5m0U,1038
|
||||||
|
urllib3/util/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/connection.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/queue.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/request.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/response.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/retry.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/ssl_.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/timeout.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/url.cpython-36.pyc,,
|
||||||
|
urllib3/util/__pycache__/wait.cpython-36.pyc,,
|
||||||
|
urllib3/util/connection.py,sha256=NsxUAKQ98GKywta--zg57CdVpeTCI6N-GElCq78Dl8U,4637
|
||||||
|
urllib3/util/queue.py,sha256=myTX3JDHntglKQNBf3b6dasHH-uF-W59vzGSQiFdAfI,497
|
||||||
|
urllib3/util/request.py,sha256=C-6-AWffxZG03AdRGoY59uqsn4CVItKU6gjxz7Hc3Mc,3815
|
||||||
|
urllib3/util/response.py,sha256=_WbTQr8xRQuJuY2rTIZxVdJD6mnEOtQupjaK_bF_Vj8,2573
|
||||||
|
urllib3/util/retry.py,sha256=3wbv7SdzYNOxPcBiFkPCubTbK1_6vWSepznOXirhUfA,15543
|
||||||
|
urllib3/util/ssl_.py,sha256=R64MEN6Bh-YJq8b14kCb6hbV8L1p8oq4rcZiBow3tTQ,14511
|
||||||
|
urllib3/util/timeout.py,sha256=3qawUo-TZq4q7tyeRToMIOdNGEOBjOOQVq7nHnLryP4,9947
|
||||||
|
urllib3/util/url.py,sha256=jvkBGN64wo_Mx6Q6JYpFCGxamxbI2NdFoNQVTr7PUOM,13964
|
||||||
|
urllib3/util/wait.py,sha256=k46KzqIYu3Vnzla5YW3EvtInNlU_QycFqQAghIOxoAg,5406
|
@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.34.2)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
urllib3
|
@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
urllib3 - Thread-safe connection pooling and re-using.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url
|
||||||
|
|
||||||
|
from . import exceptions
|
||||||
|
from .filepost import encode_multipart_formdata
|
||||||
|
from .poolmanager import PoolManager, ProxyManager, proxy_from_url
|
||||||
|
from .response import HTTPResponse
|
||||||
|
from .util.request import make_headers
|
||||||
|
from .util.url import get_host
|
||||||
|
from .util.timeout import Timeout
|
||||||
|
from .util.retry import Retry
|
||||||
|
|
||||||
|
|
||||||
|
# Set default logging handler to avoid "No handler found" warnings.
|
||||||
|
import logging
|
||||||
|
from logging import NullHandler
|
||||||
|
|
||||||
|
__author__ = "Andrey Petrov (andrey.petrov@shazow.net)"
|
||||||
|
__license__ = "MIT"
|
||||||
|
__version__ = "1.25.9"
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"HTTPConnectionPool",
|
||||||
|
"HTTPSConnectionPool",
|
||||||
|
"PoolManager",
|
||||||
|
"ProxyManager",
|
||||||
|
"HTTPResponse",
|
||||||
|
"Retry",
|
||||||
|
"Timeout",
|
||||||
|
"add_stderr_logger",
|
||||||
|
"connection_from_url",
|
||||||
|
"disable_warnings",
|
||||||
|
"encode_multipart_formdata",
|
||||||
|
"get_host",
|
||||||
|
"make_headers",
|
||||||
|
"proxy_from_url",
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|
||||||
|
def add_stderr_logger(level=logging.DEBUG):
|
||||||
|
"""
|
||||||
|
Helper for quickly adding a StreamHandler to the logger. Useful for
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
Returns the handler after adding it.
|
||||||
|
"""
|
||||||
|
# This method needs to be in this __init__.py to get the __name__ correct
|
||||||
|
# even if urllib3 is vendored within another package.
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(level)
|
||||||
|
logger.debug("Added a stderr logging handler to logger: %s", __name__)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
|
||||||
|
# ... Clean up.
|
||||||
|
del NullHandler
|
||||||
|
|
||||||
|
|
||||||
|
# All warning filters *must* be appended unless you're really certain that they
|
||||||
|
# shouldn't be: otherwise, it's very hard for users to use most Python
|
||||||
|
# mechanisms to silence them.
|
||||||
|
# SecurityWarning's always go off by default.
|
||||||
|
warnings.simplefilter("always", exceptions.SecurityWarning, append=True)
|
||||||
|
# SubjectAltNameWarning's should go off once per host
|
||||||
|
warnings.simplefilter("default", exceptions.SubjectAltNameWarning, append=True)
|
||||||
|
# InsecurePlatformWarning's don't vary between requests, so we keep it default.
|
||||||
|
warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True)
|
||||||
|
# SNIMissingWarnings should go off only once.
|
||||||
|
warnings.simplefilter("default", exceptions.SNIMissingWarning, append=True)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_warnings(category=exceptions.HTTPWarning):
|
||||||
|
"""
|
||||||
|
Helper for quickly disabling all urllib3 warnings.
|
||||||
|
"""
|
||||||
|
warnings.simplefilter("ignore", category)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,336 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections.abc import Mapping, MutableMapping
|
||||||
|
except ImportError:
|
||||||
|
from collections import Mapping, MutableMapping
|
||||||
|
try:
|
||||||
|
from threading import RLock
|
||||||
|
except ImportError: # Platform-specific: No threads available
|
||||||
|
|
||||||
|
class RLock:
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from .exceptions import InvalidHeader
|
||||||
|
from .packages.six import iterkeys, itervalues, PY3
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"]
|
||||||
|
|
||||||
|
|
||||||
|
_Null = object()
|
||||||
|
|
||||||
|
|
||||||
|
class RecentlyUsedContainer(MutableMapping):
|
||||||
|
"""
|
||||||
|
Provides a thread-safe dict-like container which maintains up to
|
||||||
|
``maxsize`` keys while throwing away the least-recently-used keys beyond
|
||||||
|
``maxsize``.
|
||||||
|
|
||||||
|
:param maxsize:
|
||||||
|
Maximum number of recent elements to retain.
|
||||||
|
|
||||||
|
:param dispose_func:
|
||||||
|
Every time an item is evicted from the container,
|
||||||
|
``dispose_func(value)`` is called. Callback which will get called
|
||||||
|
"""
|
||||||
|
|
||||||
|
ContainerCls = OrderedDict
|
||||||
|
|
||||||
|
def __init__(self, maxsize=10, dispose_func=None):
|
||||||
|
self._maxsize = maxsize
|
||||||
|
self.dispose_func = dispose_func
|
||||||
|
|
||||||
|
self._container = self.ContainerCls()
|
||||||
|
self.lock = RLock()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
# Re-insert the item, moving it to the end of the eviction line.
|
||||||
|
with self.lock:
|
||||||
|
item = self._container.pop(key)
|
||||||
|
self._container[key] = item
|
||||||
|
return item
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
evicted_value = _Null
|
||||||
|
with self.lock:
|
||||||
|
# Possibly evict the existing value of 'key'
|
||||||
|
evicted_value = self._container.get(key, _Null)
|
||||||
|
self._container[key] = value
|
||||||
|
|
||||||
|
# If we didn't evict an existing value, we might have to evict the
|
||||||
|
# least recently used item from the beginning of the container.
|
||||||
|
if len(self._container) > self._maxsize:
|
||||||
|
_key, evicted_value = self._container.popitem(last=False)
|
||||||
|
|
||||||
|
if self.dispose_func and evicted_value is not _Null:
|
||||||
|
self.dispose_func(evicted_value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
with self.lock:
|
||||||
|
value = self._container.pop(key)
|
||||||
|
|
||||||
|
if self.dispose_func:
|
||||||
|
self.dispose_func(value)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
with self.lock:
|
||||||
|
return len(self._container)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Iteration over this class is unlikely to be threadsafe."
|
||||||
|
)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
with self.lock:
|
||||||
|
# Copy pointers to all values, then wipe the mapping
|
||||||
|
values = list(itervalues(self._container))
|
||||||
|
self._container.clear()
|
||||||
|
|
||||||
|
if self.dispose_func:
|
||||||
|
for value in values:
|
||||||
|
self.dispose_func(value)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
with self.lock:
|
||||||
|
return list(iterkeys(self._container))
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPHeaderDict(MutableMapping):
|
||||||
|
"""
|
||||||
|
:param headers:
|
||||||
|
An iterable of field-value pairs. Must not contain multiple field names
|
||||||
|
when compared case-insensitively.
|
||||||
|
|
||||||
|
:param kwargs:
|
||||||
|
Additional field-value pairs to pass in to ``dict.update``.
|
||||||
|
|
||||||
|
A ``dict`` like container for storing HTTP Headers.
|
||||||
|
|
||||||
|
Field names are stored and compared case-insensitively in compliance with
|
||||||
|
RFC 7230. Iteration provides the first case-sensitive key seen for each
|
||||||
|
case-insensitive pair.
|
||||||
|
|
||||||
|
Using ``__setitem__`` syntax overwrites fields that compare equal
|
||||||
|
case-insensitively in order to maintain ``dict``'s api. For fields that
|
||||||
|
compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
|
||||||
|
in a loop.
|
||||||
|
|
||||||
|
If multiple fields that are equal case-insensitively are passed to the
|
||||||
|
constructor or ``.update``, the behavior is undefined and some will be
|
||||||
|
lost.
|
||||||
|
|
||||||
|
>>> headers = HTTPHeaderDict()
|
||||||
|
>>> headers.add('Set-Cookie', 'foo=bar')
|
||||||
|
>>> headers.add('set-cookie', 'baz=quxx')
|
||||||
|
>>> headers['content-length'] = '7'
|
||||||
|
>>> headers['SET-cookie']
|
||||||
|
'foo=bar, baz=quxx'
|
||||||
|
>>> headers['Content-Length']
|
||||||
|
'7'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, headers=None, **kwargs):
|
||||||
|
super(HTTPHeaderDict, self).__init__()
|
||||||
|
self._container = OrderedDict()
|
||||||
|
if headers is not None:
|
||||||
|
if isinstance(headers, HTTPHeaderDict):
|
||||||
|
self._copy_from(headers)
|
||||||
|
else:
|
||||||
|
self.extend(headers)
|
||||||
|
if kwargs:
|
||||||
|
self.extend(kwargs)
|
||||||
|
|
||||||
|
def __setitem__(self, key, val):
|
||||||
|
self._container[key.lower()] = [key, val]
|
||||||
|
return self._container[key.lower()]
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
val = self._container[key.lower()]
|
||||||
|
return ", ".join(val[1:])
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self._container[key.lower()]
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key.lower() in self._container
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Mapping) and not hasattr(other, "keys"):
|
||||||
|
return False
|
||||||
|
if not isinstance(other, type(self)):
|
||||||
|
other = type(self)(other)
|
||||||
|
return dict((k.lower(), v) for k, v in self.itermerged()) == dict(
|
||||||
|
(k.lower(), v) for k, v in other.itermerged()
|
||||||
|
)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
if not PY3: # Python 2
|
||||||
|
iterkeys = MutableMapping.iterkeys
|
||||||
|
itervalues = MutableMapping.itervalues
|
||||||
|
|
||||||
|
__marker = object()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._container)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
# Only provide the originally cased names
|
||||||
|
for vals in self._container.values():
|
||||||
|
yield vals[0]
|
||||||
|
|
||||||
|
def pop(self, key, default=__marker):
|
||||||
|
"""D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||||
|
If key is not found, d is returned if given, otherwise KeyError is raised.
|
||||||
|
"""
|
||||||
|
# Using the MutableMapping function directly fails due to the private marker.
|
||||||
|
# Using ordinary dict.pop would expose the internal structures.
|
||||||
|
# So let's reinvent the wheel.
|
||||||
|
try:
|
||||||
|
value = self[key]
|
||||||
|
except KeyError:
|
||||||
|
if default is self.__marker:
|
||||||
|
raise
|
||||||
|
return default
|
||||||
|
else:
|
||||||
|
del self[key]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def discard(self, key):
|
||||||
|
try:
|
||||||
|
del self[key]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add(self, key, val):
|
||||||
|
"""Adds a (name, value) pair, doesn't overwrite the value if it already
|
||||||
|
exists.
|
||||||
|
|
||||||
|
>>> headers = HTTPHeaderDict(foo='bar')
|
||||||
|
>>> headers.add('Foo', 'baz')
|
||||||
|
>>> headers['foo']
|
||||||
|
'bar, baz'
|
||||||
|
"""
|
||||||
|
key_lower = key.lower()
|
||||||
|
new_vals = [key, val]
|
||||||
|
# Keep the common case aka no item present as fast as possible
|
||||||
|
vals = self._container.setdefault(key_lower, new_vals)
|
||||||
|
if new_vals is not vals:
|
||||||
|
vals.append(val)
|
||||||
|
|
||||||
|
def extend(self, *args, **kwargs):
|
||||||
|
"""Generic import function for any type of header-like object.
|
||||||
|
Adapted version of MutableMapping.update in order to insert items
|
||||||
|
with self.add instead of self.__setitem__
|
||||||
|
"""
|
||||||
|
if len(args) > 1:
|
||||||
|
raise TypeError(
|
||||||
|
"extend() takes at most 1 positional "
|
||||||
|
"arguments ({0} given)".format(len(args))
|
||||||
|
)
|
||||||
|
other = args[0] if len(args) >= 1 else ()
|
||||||
|
|
||||||
|
if isinstance(other, HTTPHeaderDict):
|
||||||
|
for key, val in other.iteritems():
|
||||||
|
self.add(key, val)
|
||||||
|
elif isinstance(other, Mapping):
|
||||||
|
for key in other:
|
||||||
|
self.add(key, other[key])
|
||||||
|
elif hasattr(other, "keys"):
|
||||||
|
for key in other.keys():
|
||||||
|
self.add(key, other[key])
|
||||||
|
else:
|
||||||
|
for key, value in other:
|
||||||
|
self.add(key, value)
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
self.add(key, value)
|
||||||
|
|
||||||
|
def getlist(self, key, default=__marker):
|
||||||
|
"""Returns a list of all the values for the named field. Returns an
|
||||||
|
empty list if the key doesn't exist."""
|
||||||
|
try:
|
||||||
|
vals = self._container[key.lower()]
|
||||||
|
except KeyError:
|
||||||
|
if default is self.__marker:
|
||||||
|
return []
|
||||||
|
return default
|
||||||
|
else:
|
||||||
|
return vals[1:]
|
||||||
|
|
||||||
|
# Backwards compatibility for httplib
|
||||||
|
getheaders = getlist
|
||||||
|
getallmatchingheaders = getlist
|
||||||
|
iget = getlist
|
||||||
|
|
||||||
|
# Backwards compatibility for http.cookiejar
|
||||||
|
get_all = getlist
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
|
||||||
|
|
||||||
|
def _copy_from(self, other):
|
||||||
|
for key in other:
|
||||||
|
val = other.getlist(key)
|
||||||
|
if isinstance(val, list):
|
||||||
|
# Don't need to convert tuples
|
||||||
|
val = list(val)
|
||||||
|
self._container[key.lower()] = [key] + val
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
clone = type(self)()
|
||||||
|
clone._copy_from(self)
|
||||||
|
return clone
|
||||||
|
|
||||||
|
def iteritems(self):
|
||||||
|
"""Iterate over all header lines, including duplicate ones."""
|
||||||
|
for key in self:
|
||||||
|
vals = self._container[key.lower()]
|
||||||
|
for val in vals[1:]:
|
||||||
|
yield vals[0], val
|
||||||
|
|
||||||
|
def itermerged(self):
|
||||||
|
"""Iterate over all headers, merging duplicate ones together."""
|
||||||
|
for key in self:
|
||||||
|
val = self._container[key.lower()]
|
||||||
|
yield val[0], ", ".join(val[1:])
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return list(self.iteritems())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_httplib(cls, message): # Python 2
|
||||||
|
"""Read headers from a Python 2 httplib message object."""
|
||||||
|
# python2.7 does not expose a proper API for exporting multiheaders
|
||||||
|
# efficiently. This function re-reads raw lines from the message
|
||||||
|
# object and extracts the multiheaders properly.
|
||||||
|
obs_fold_continued_leaders = (" ", "\t")
|
||||||
|
headers = []
|
||||||
|
|
||||||
|
for line in message.headers:
|
||||||
|
if line.startswith(obs_fold_continued_leaders):
|
||||||
|
if not headers:
|
||||||
|
# We received a header line that starts with OWS as described
|
||||||
|
# in RFC-7230 S3.2.4. This indicates a multiline header, but
|
||||||
|
# there exists no previous header to which we can attach it.
|
||||||
|
raise InvalidHeader(
|
||||||
|
"Header continuation with no previous header: %s" % line
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
key, value = headers[-1]
|
||||||
|
headers[-1] = (key, value + " " + line.strip())
|
||||||
|
continue
|
||||||
|
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
headers.append((key, value.strip()))
|
||||||
|
|
||||||
|
return cls(headers)
|
@ -0,0 +1,423 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from socket import error as SocketError, timeout as SocketTimeout
|
||||||
|
import warnings
|
||||||
|
from .packages import six
|
||||||
|
from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection
|
||||||
|
from .packages.six.moves.http_client import HTTPException # noqa: F401
|
||||||
|
|
||||||
|
try: # Compiled with SSL?
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
BaseSSLError = ssl.SSLError
|
||||||
|
except (ImportError, AttributeError): # Platform-specific: No SSL.
|
||||||
|
ssl = None
|
||||||
|
|
||||||
|
class BaseSSLError(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Python 3: not a no-op, we're adding this to the namespace so it can be imported.
|
||||||
|
ConnectionError = ConnectionError
|
||||||
|
except NameError:
|
||||||
|
# Python 2
|
||||||
|
class ConnectionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
from .exceptions import (
|
||||||
|
NewConnectionError,
|
||||||
|
ConnectTimeoutError,
|
||||||
|
SubjectAltNameWarning,
|
||||||
|
SystemTimeWarning,
|
||||||
|
)
|
||||||
|
from .packages.ssl_match_hostname import match_hostname, CertificateError
|
||||||
|
|
||||||
|
from .util.ssl_ import (
|
||||||
|
resolve_cert_reqs,
|
||||||
|
resolve_ssl_version,
|
||||||
|
assert_fingerprint,
|
||||||
|
create_urllib3_context,
|
||||||
|
ssl_wrap_socket,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from .util import connection
|
||||||
|
|
||||||
|
from ._collections import HTTPHeaderDict
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
port_by_scheme = {"http": 80, "https": 443}
|
||||||
|
|
||||||
|
# When it comes time to update this value as a part of regular maintenance
|
||||||
|
# (ie test_recent_date is failing) update it to ~6 months before the current date.
|
||||||
|
RECENT_DATE = datetime.date(2019, 1, 1)
|
||||||
|
|
||||||
|
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
|
||||||
|
|
||||||
|
|
||||||
|
class DummyConnection(object):
|
||||||
|
"""Used to detect a failed ConnectionCls import."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPConnection(_HTTPConnection, object):
|
||||||
|
"""
|
||||||
|
Based on httplib.HTTPConnection but provides an extra constructor
|
||||||
|
backwards-compatibility layer between older and newer Pythons.
|
||||||
|
|
||||||
|
Additional keyword parameters are used to configure attributes of the connection.
|
||||||
|
Accepted parameters include:
|
||||||
|
|
||||||
|
- ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`
|
||||||
|
- ``source_address``: Set the source address for the current connection.
|
||||||
|
- ``socket_options``: Set specific options on the underlying socket. If not specified, then
|
||||||
|
defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling
|
||||||
|
Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.
|
||||||
|
|
||||||
|
For example, if you wish to enable TCP Keep Alive in addition to the defaults,
|
||||||
|
you might pass::
|
||||||
|
|
||||||
|
HTTPConnection.default_socket_options + [
|
||||||
|
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_port = port_by_scheme["http"]
|
||||||
|
|
||||||
|
#: Disable Nagle's algorithm by default.
|
||||||
|
#: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``
|
||||||
|
default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
|
||||||
|
|
||||||
|
#: Whether this connection verifies the host's certificate.
|
||||||
|
is_verified = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
if not six.PY2:
|
||||||
|
kw.pop("strict", None)
|
||||||
|
|
||||||
|
# Pre-set source_address.
|
||||||
|
self.source_address = kw.get("source_address")
|
||||||
|
|
||||||
|
#: The socket options provided by the user. If no options are
|
||||||
|
#: provided, we use the default options.
|
||||||
|
self.socket_options = kw.pop("socket_options", self.default_socket_options)
|
||||||
|
|
||||||
|
_HTTPConnection.__init__(self, *args, **kw)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
"""
|
||||||
|
Getter method to remove any trailing dots that indicate the hostname is an FQDN.
|
||||||
|
|
||||||
|
In general, SSL certificates don't include the trailing dot indicating a
|
||||||
|
fully-qualified domain name, and thus, they don't validate properly when
|
||||||
|
checked against a domain name that includes the dot. In addition, some
|
||||||
|
servers may not expect to receive the trailing dot when provided.
|
||||||
|
|
||||||
|
However, the hostname with trailing dot is critical to DNS resolution; doing a
|
||||||
|
lookup with the trailing dot will properly only resolve the appropriate FQDN,
|
||||||
|
whereas a lookup without a trailing dot will search the system's search domain
|
||||||
|
list. Thus, it's important to keep the original host around for use only in
|
||||||
|
those cases where it's appropriate (i.e., when doing DNS lookup to establish the
|
||||||
|
actual TCP connection across which we're going to send HTTP requests).
|
||||||
|
"""
|
||||||
|
return self._dns_host.rstrip(".")
|
||||||
|
|
||||||
|
@host.setter
|
||||||
|
def host(self, value):
|
||||||
|
"""
|
||||||
|
Setter for the `host` property.
|
||||||
|
|
||||||
|
We assume that only urllib3 uses the _dns_host attribute; httplib itself
|
||||||
|
only uses `host`, and it seems reasonable that other libraries follow suit.
|
||||||
|
"""
|
||||||
|
self._dns_host = value
|
||||||
|
|
||||||
|
def _new_conn(self):
|
||||||
|
""" Establish a socket connection and set nodelay settings on it.
|
||||||
|
|
||||||
|
:return: New socket connection.
|
||||||
|
"""
|
||||||
|
extra_kw = {}
|
||||||
|
if self.source_address:
|
||||||
|
extra_kw["source_address"] = self.source_address
|
||||||
|
|
||||||
|
if self.socket_options:
|
||||||
|
extra_kw["socket_options"] = self.socket_options
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = connection.create_connection(
|
||||||
|
(self._dns_host, self.port), self.timeout, **extra_kw
|
||||||
|
)
|
||||||
|
|
||||||
|
except SocketTimeout:
|
||||||
|
raise ConnectTimeoutError(
|
||||||
|
self,
|
||||||
|
"Connection to %s timed out. (connect timeout=%s)"
|
||||||
|
% (self.host, self.timeout),
|
||||||
|
)
|
||||||
|
|
||||||
|
except SocketError as e:
|
||||||
|
raise NewConnectionError(
|
||||||
|
self, "Failed to establish a new connection: %s" % e
|
||||||
|
)
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _prepare_conn(self, conn):
|
||||||
|
self.sock = conn
|
||||||
|
# Google App Engine's httplib does not define _tunnel_host
|
||||||
|
if getattr(self, "_tunnel_host", None):
|
||||||
|
# TODO: Fix tunnel so it doesn't depend on self.sock state.
|
||||||
|
self._tunnel()
|
||||||
|
# Mark this connection as not reusable
|
||||||
|
self.auto_open = 0
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
conn = self._new_conn()
|
||||||
|
self._prepare_conn(conn)
|
||||||
|
|
||||||
|
def putrequest(self, method, url, *args, **kwargs):
|
||||||
|
"""Send a request to the server"""
|
||||||
|
match = _CONTAINS_CONTROL_CHAR_RE.search(method)
|
||||||
|
if match:
|
||||||
|
raise ValueError(
|
||||||
|
"Method cannot contain non-token characters %r (found at least %r)"
|
||||||
|
% (method, match.group())
|
||||||
|
)
|
||||||
|
|
||||||
|
return _HTTPConnection.putrequest(self, method, url, *args, **kwargs)
|
||||||
|
|
||||||
|
def request_chunked(self, method, url, body=None, headers=None):
|
||||||
|
"""
|
||||||
|
Alternative to the common request method, which sends the
|
||||||
|
body with chunked encoding and not as one block
|
||||||
|
"""
|
||||||
|
headers = HTTPHeaderDict(headers if headers is not None else {})
|
||||||
|
skip_accept_encoding = "accept-encoding" in headers
|
||||||
|
skip_host = "host" in headers
|
||||||
|
self.putrequest(
|
||||||
|
method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host
|
||||||
|
)
|
||||||
|
for header, value in headers.items():
|
||||||
|
self.putheader(header, value)
|
||||||
|
if "transfer-encoding" not in headers:
|
||||||
|
self.putheader("Transfer-Encoding", "chunked")
|
||||||
|
self.endheaders()
|
||||||
|
|
||||||
|
if body is not None:
|
||||||
|
stringish_types = six.string_types + (bytes,)
|
||||||
|
if isinstance(body, stringish_types):
|
||||||
|
body = (body,)
|
||||||
|
for chunk in body:
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
if not isinstance(chunk, bytes):
|
||||||
|
chunk = chunk.encode("utf8")
|
||||||
|
len_str = hex(len(chunk))[2:]
|
||||||
|
self.send(len_str.encode("utf-8"))
|
||||||
|
self.send(b"\r\n")
|
||||||
|
self.send(chunk)
|
||||||
|
self.send(b"\r\n")
|
||||||
|
|
||||||
|
# After the if clause, to always have a closed body
|
||||||
|
self.send(b"0\r\n\r\n")
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSConnection(HTTPConnection):
|
||||||
|
default_port = port_by_scheme["https"]
|
||||||
|
|
||||||
|
cert_reqs = None
|
||||||
|
ca_certs = None
|
||||||
|
ca_cert_dir = None
|
||||||
|
ca_cert_data = None
|
||||||
|
ssl_version = None
|
||||||
|
assert_fingerprint = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host,
|
||||||
|
port=None,
|
||||||
|
key_file=None,
|
||||||
|
cert_file=None,
|
||||||
|
key_password=None,
|
||||||
|
strict=None,
|
||||||
|
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||||
|
ssl_context=None,
|
||||||
|
server_hostname=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
|
||||||
|
HTTPConnection.__init__(self, host, port, strict=strict, timeout=timeout, **kw)
|
||||||
|
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
self.key_password = key_password
|
||||||
|
self.ssl_context = ssl_context
|
||||||
|
self.server_hostname = server_hostname
|
||||||
|
|
||||||
|
# Required property for Google AppEngine 1.9.0 which otherwise causes
|
||||||
|
# HTTPS requests to go out as HTTP. (See Issue #356)
|
||||||
|
self._protocol = "https"
|
||||||
|
|
||||||
|
def set_cert(
|
||||||
|
self,
|
||||||
|
key_file=None,
|
||||||
|
cert_file=None,
|
||||||
|
cert_reqs=None,
|
||||||
|
key_password=None,
|
||||||
|
ca_certs=None,
|
||||||
|
assert_hostname=None,
|
||||||
|
assert_fingerprint=None,
|
||||||
|
ca_cert_dir=None,
|
||||||
|
ca_cert_data=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This method should only be called once, before the connection is used.
|
||||||
|
"""
|
||||||
|
# If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also
|
||||||
|
# have an SSLContext object in which case we'll use its verify_mode.
|
||||||
|
if cert_reqs is None:
|
||||||
|
if self.ssl_context is not None:
|
||||||
|
cert_reqs = self.ssl_context.verify_mode
|
||||||
|
else:
|
||||||
|
cert_reqs = resolve_cert_reqs(None)
|
||||||
|
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
self.cert_reqs = cert_reqs
|
||||||
|
self.key_password = key_password
|
||||||
|
self.assert_hostname = assert_hostname
|
||||||
|
self.assert_fingerprint = assert_fingerprint
|
||||||
|
self.ca_certs = ca_certs and os.path.expanduser(ca_certs)
|
||||||
|
self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)
|
||||||
|
self.ca_cert_data = ca_cert_data
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# Add certificate verification
|
||||||
|
conn = self._new_conn()
|
||||||
|
hostname = self.host
|
||||||
|
|
||||||
|
# Google App Engine's httplib does not define _tunnel_host
|
||||||
|
if getattr(self, "_tunnel_host", None):
|
||||||
|
self.sock = conn
|
||||||
|
# Calls self._set_hostport(), so self.host is
|
||||||
|
# self._tunnel_host below.
|
||||||
|
self._tunnel()
|
||||||
|
# Mark this connection as not reusable
|
||||||
|
self.auto_open = 0
|
||||||
|
|
||||||
|
# Override the host with the one we're requesting data from.
|
||||||
|
hostname = self._tunnel_host
|
||||||
|
|
||||||
|
server_hostname = hostname
|
||||||
|
if self.server_hostname is not None:
|
||||||
|
server_hostname = self.server_hostname
|
||||||
|
|
||||||
|
is_time_off = datetime.date.today() < RECENT_DATE
|
||||||
|
if is_time_off:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"System time is way off (before {0}). This will probably "
|
||||||
|
"lead to SSL verification errors"
|
||||||
|
).format(RECENT_DATE),
|
||||||
|
SystemTimeWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wrap socket using verification with the root certs in
|
||||||
|
# trusted_root_certs
|
||||||
|
default_ssl_context = False
|
||||||
|
if self.ssl_context is None:
|
||||||
|
default_ssl_context = True
|
||||||
|
self.ssl_context = create_urllib3_context(
|
||||||
|
ssl_version=resolve_ssl_version(self.ssl_version),
|
||||||
|
cert_reqs=resolve_cert_reqs(self.cert_reqs),
|
||||||
|
)
|
||||||
|
|
||||||
|
context = self.ssl_context
|
||||||
|
context.verify_mode = resolve_cert_reqs(self.cert_reqs)
|
||||||
|
|
||||||
|
# Try to load OS default certs if none are given.
|
||||||
|
# Works well on Windows (requires Python3.4+)
|
||||||
|
if (
|
||||||
|
not self.ca_certs
|
||||||
|
and not self.ca_cert_dir
|
||||||
|
and not self.ca_cert_data
|
||||||
|
and default_ssl_context
|
||||||
|
and hasattr(context, "load_default_certs")
|
||||||
|
):
|
||||||
|
context.load_default_certs()
|
||||||
|
|
||||||
|
self.sock = ssl_wrap_socket(
|
||||||
|
sock=conn,
|
||||||
|
keyfile=self.key_file,
|
||||||
|
certfile=self.cert_file,
|
||||||
|
key_password=self.key_password,
|
||||||
|
ca_certs=self.ca_certs,
|
||||||
|
ca_cert_dir=self.ca_cert_dir,
|
||||||
|
ca_cert_data=self.ca_cert_data,
|
||||||
|
server_hostname=server_hostname,
|
||||||
|
ssl_context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.assert_fingerprint:
|
||||||
|
assert_fingerprint(
|
||||||
|
self.sock.getpeercert(binary_form=True), self.assert_fingerprint
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
context.verify_mode != ssl.CERT_NONE
|
||||||
|
and not getattr(context, "check_hostname", False)
|
||||||
|
and self.assert_hostname is not False
|
||||||
|
):
|
||||||
|
# While urllib3 attempts to always turn off hostname matching from
|
||||||
|
# the TLS library, this cannot always be done. So we check whether
|
||||||
|
# the TLS Library still thinks it's matching hostnames.
|
||||||
|
cert = self.sock.getpeercert()
|
||||||
|
if not cert.get("subjectAltName", ()):
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Certificate for {0} has no `subjectAltName`, falling back to check for a "
|
||||||
|
"`commonName` for now. This feature is being removed by major browsers and "
|
||||||
|
"deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 "
|
||||||
|
"for details.)".format(hostname)
|
||||||
|
),
|
||||||
|
SubjectAltNameWarning,
|
||||||
|
)
|
||||||
|
_match_hostname(cert, self.assert_hostname or server_hostname)
|
||||||
|
|
||||||
|
self.is_verified = (
|
||||||
|
context.verify_mode == ssl.CERT_REQUIRED
|
||||||
|
or self.assert_fingerprint is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _match_hostname(cert, asserted_hostname):
|
||||||
|
try:
|
||||||
|
match_hostname(cert, asserted_hostname)
|
||||||
|
except CertificateError as e:
|
||||||
|
log.warning(
|
||||||
|
"Certificate did not match expected hostname: %s. Certificate: %s",
|
||||||
|
asserted_hostname,
|
||||||
|
cert,
|
||||||
|
)
|
||||||
|
# Add cert to exception and reraise so client code can inspect
|
||||||
|
# the cert when catching the exception, if they want to
|
||||||
|
e._peer_cert = cert
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if not ssl:
|
||||||
|
HTTPSConnection = DummyConnection # noqa: F811
|
||||||
|
|
||||||
|
|
||||||
|
VerifiedHTTPSConnection = HTTPSConnection
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue