• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2import warnings
3import json
4
5from tarfile import TarFile
6from pkgutil import get_data
7from io import BytesIO
8
9from dateutil.tz import tzfile as _tzfile
10
11__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
12
13ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
14METADATA_FN = 'METADATA'
15
16
17class tzfile(_tzfile):
18    def __reduce__(self):
19        return (gettz, (self._filename,))
20
21
22def getzoneinfofile_stream():
23    try:
24        return BytesIO(get_data(__name__, ZONEFILENAME))
25    except IOError as e:  # TODO  switch to FileNotFoundError?
26        warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
27        return None
28
29
30class ZoneInfoFile(object):
31    def __init__(self, zonefile_stream=None):
32        if zonefile_stream is not None:
33            with TarFile.open(fileobj=zonefile_stream) as tf:
34                self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
35                              for zf in tf.getmembers()
36                              if zf.isfile() and zf.name != METADATA_FN}
37                # deal with links: They'll point to their parent object. Less
38                # waste of memory
39                links = {zl.name: self.zones[zl.linkname]
40                         for zl in tf.getmembers() if
41                         zl.islnk() or zl.issym()}
42                self.zones.update(links)
43                try:
44                    metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
45                    metadata_str = metadata_json.read().decode('UTF-8')
46                    self.metadata = json.loads(metadata_str)
47                except KeyError:
48                    # no metadata in tar file
49                    self.metadata = None
50        else:
51            self.zones = {}
52            self.metadata = None
53
54    def get(self, name, default=None):
55        """
56        Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
57        for retrieving zones from the zone dictionary.
58
59        :param name:
60            The name of the zone to retrieve. (Generally IANA zone names)
61
62        :param default:
63            The value to return in the event of a missing key.
64
65        .. versionadded:: 2.6.0
66
67        """
68        return self.zones.get(name, default)
69
70
71# The current API has gettz as a module function, although in fact it taps into
72# a stateful class. So as a workaround for now, without changing the API, we
73# will create a new "global" class instance the first time a user requests a
74# timezone. Ugly, but adheres to the api.
75#
76# TODO: Remove after deprecation period.
77_CLASS_ZONE_INSTANCE = []
78
79
80def get_zonefile_instance(new_instance=False):
81    """
82    This is a convenience function which provides a :class:`ZoneInfoFile`
83    instance using the data provided by the ``dateutil`` package. By default, it
84    caches a single instance of the ZoneInfoFile object and returns that.
85
86    :param new_instance:
87        If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
88        used as the cached instance for the next call. Otherwise, new instances
89        are created only as necessary.
90
91    :return:
92        Returns a :class:`ZoneInfoFile` object.
93
94    .. versionadded:: 2.6
95    """
96    if new_instance:
97        zif = None
98    else:
99        zif = getattr(get_zonefile_instance, '_cached_instance', None)
100
101    if zif is None:
102        zif = ZoneInfoFile(getzoneinfofile_stream())
103
104        get_zonefile_instance._cached_instance = zif
105
106    return zif
107
108
109def gettz(name):
110    """
111    This retrieves a time zone from the local zoneinfo tarball that is packaged
112    with dateutil.
113
114    :param name:
115        An IANA-style time zone name, as found in the zoneinfo file.
116
117    :return:
118        Returns a :class:`dateutil.tz.tzfile` time zone object.
119
120    .. warning::
121        It is generally inadvisable to use this function, and it is only
122        provided for API compatibility with earlier versions. This is *not*
123        equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
124        time zone based on the inputs, favoring system zoneinfo. This is ONLY
125        for accessing the dateutil-specific zoneinfo (which may be out of
126        date compared to the system zoneinfo).
127
128    .. deprecated:: 2.6
129        If you need to use a specific zoneinfofile over the system zoneinfo,
130        instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
131        :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
132
133        Use :func:`get_zonefile_instance` to retrieve an instance of the
134        dateutil-provided zoneinfo.
135    """
136    warnings.warn("zoneinfo.gettz() will be removed in future versions, "
137                  "to use the dateutil-provided zoneinfo files, instantiate a "
138                  "ZoneInfoFile object and use ZoneInfoFile.zones.get() "
139                  "instead. See the documentation for details.",
140                  DeprecationWarning)
141
142    if len(_CLASS_ZONE_INSTANCE) == 0:
143        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
144    return _CLASS_ZONE_INSTANCE[0].zones.get(name)
145
146
147def gettz_db_metadata():
148    """ Get the zonefile metadata
149
150    See `zonefile_metadata`_
151
152    :returns:
153        A dictionary with the database metadata
154
155    .. deprecated:: 2.6
156        See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
157        query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
158    """
159    warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
160                  "versions, to use the dateutil-provided zoneinfo files, "
161                  "ZoneInfoFile object and query the 'metadata' attribute "
162                  "instead. See the documentation for details.",
163                  DeprecationWarning)
164
165    if len(_CLASS_ZONE_INSTANCE) == 0:
166        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
167    return _CLASS_ZONE_INSTANCE[0].metadata
168