__init__.py 5.8 KB

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