__init__.py 6.8 KB

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