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