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