python (3.11.7)
       1  """
       2  Script to automatically generate a JSON file containing time zone information.
       3  
       4  This is done to allow "pinning" a small subset of the tzdata in the tests,
       5  since we are testing properties of a file that may be subject to change. For
       6  example, the behavior in the far future of any given zone is likely to change,
       7  but "does this give the right answer for this file in 2040" is still an
       8  important property to test.
       9  
      10  This must be run from a computer with zoneinfo data installed.
      11  """
      12  from __future__ import annotations
      13  
      14  import base64
      15  import functools
      16  import json
      17  import lzma
      18  import pathlib
      19  import textwrap
      20  import typing
      21  
      22  import zoneinfo
      23  
      24  KEYS = [
      25      "Africa/Abidjan",
      26      "Africa/Casablanca",
      27      "America/Los_Angeles",
      28      "America/Santiago",
      29      "Asia/Tokyo",
      30      "Australia/Sydney",
      31      "Europe/Dublin",
      32      "Europe/Lisbon",
      33      "Europe/London",
      34      "Pacific/Kiritimati",
      35      "UTC",
      36  ]
      37  
      38  TEST_DATA_LOC = pathlib.Path(__file__).parent
      39  
      40  
      41  @functools.lru_cache(maxsize=None)
      42  def get_zoneinfo_path() -> pathlib.Path:
      43      """Get the first zoneinfo directory on TZPATH containing the "UTC" zone."""
      44      key = "UTC"
      45      for path in map(pathlib.Path, zoneinfo.TZPATH):
      46          if (path / key).exists():
      47              return path
      48      else:
      49          raise OSError("Cannot find time zone data.")
      50  
      51  
      52  def get_zoneinfo_metadata() -> typing.Dict[str, str]:
      53      path = get_zoneinfo_path()
      54  
      55      tzdata_zi = path / "tzdata.zi"
      56      if not tzdata_zi.exists():
      57          # tzdata.zi is necessary to get the version information
      58          raise OSError("Time zone data does not include tzdata.zi.")
      59  
      60      with open(tzdata_zi, "r") as f:
      61          version_line = next(f)
      62  
      63      _, version = version_line.strip().rsplit(" ", 1)
      64  
      65      if (
      66          not version[0:4].isdigit()
      67          or len(version) < 5
      68          or not version[4:].isalpha()
      69      ):
      70          raise ValueError(
      71              "Version string should be YYYYx, "
      72              + "where YYYY is the year and x is a letter; "
      73              + f"found: {version}"
      74          )
      75  
      76      return {"version": version}
      77  
      78  
      79  def get_zoneinfo(key: str) -> bytes:
      80      path = get_zoneinfo_path()
      81  
      82      with open(path / key, "rb") as f:
      83          return f.read()
      84  
      85  
      86  def encode_compressed(data: bytes) -> typing.List[str]:
      87      compressed_zone = lzma.compress(data)
      88      raw = base64.b85encode(compressed_zone)
      89  
      90      raw_data_str = raw.decode("utf-8")
      91  
      92      data_str = textwrap.wrap(raw_data_str, width=70)
      93      return data_str
      94  
      95  
      96  def load_compressed_keys() -> typing.Dict[str, typing.List[str]]:
      97      output = {key: encode_compressed(get_zoneinfo(key)) for key in KEYS}
      98  
      99      return output
     100  
     101  
     102  def update_test_data(fname: str = "zoneinfo_data.json") -> None:
     103      TEST_DATA_LOC.mkdir(exist_ok=True, parents=True)
     104  
     105      # Annotation required: https://github.com/python/mypy/issues/8772
     106      json_kwargs: typing.Dict[str, typing.Any] = dict(
     107          indent=2, sort_keys=True,
     108      )
     109  
     110      compressed_keys = load_compressed_keys()
     111      metadata = get_zoneinfo_metadata()
     112      output = {
     113          "metadata": metadata,
     114          "data": compressed_keys,
     115      }
     116  
     117      with open(TEST_DATA_LOC / fname, "w") as f:
     118          json.dump(output, f, **json_kwargs)
     119  
     120  
     121  if __name__ == "__main__":
     122      update_test_data()