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