update_repo.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. # Copyright 2020 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Quick and dirty utility to download latest icon assets for github."""
  15. from absl import app
  16. from absl import flags
  17. import json
  18. from pathlib import Path
  19. import requests
  20. import time
  21. from typing import NamedTuple, Sequence, Tuple
  22. from zipfile import ZipFile
  23. FLAGS = flags.FLAGS
  24. flags.DEFINE_bool(
  25. "skip_existing",
  26. False,
  27. "Do not download if local file exists, even if an update is available.",
  28. )
  29. flags.DEFINE_bool("fetch", True, "Whether we can attempt to download assets.")
  30. flags.DEFINE_bool("explode_zips", True, "Whether to unzip any zip assets.")
  31. flags.DEFINE_integer("icon_limit", 0, "If > 0, the max # of icons to process.")
  32. _METADATA_URL = "http://fonts.google.com/metadata/icons"
  33. class Asset(NamedTuple):
  34. src_url_pattern: str
  35. dest_dir_pattern: str
  36. class Fetch(NamedTuple):
  37. src_url: str
  38. dest_file: Path
  39. class Icon(NamedTuple):
  40. name: str
  41. category: str
  42. version: int
  43. sizes_px: Tuple[int, ...]
  44. _ASSETS = (
  45. Asset(
  46. "https://{host}/s/i/{stylistic_set}/{icon.name}/v{icon.version}/{size_px}px.svg",
  47. "src/{icon.category}/{icon.name}/{stylistic_set}/{size_px}px.svg",
  48. ),
  49. Asset(
  50. "https://{host}/s/i/{stylistic_set}/{icon.name}/v{icon.version}/black-android.zip",
  51. "android/{icon.category}/{icon.name}/{stylistic_set}/black.zip",
  52. ),
  53. )
  54. def _latest_metadata():
  55. resp = requests.get(_METADATA_URL)
  56. resp.raise_for_status()
  57. raw_json = resp.text[5:]
  58. return json.loads(raw_json)
  59. def _current_versions():
  60. return Path("current_versions.json")
  61. def _version_key(icon: Icon):
  62. return f"{icon.category}::{icon.name}"
  63. def _icons(metadata):
  64. for raw_icon in metadata["icons"]:
  65. yield Icon(
  66. raw_icon["name"],
  67. raw_icon["categories"][0],
  68. raw_icon["version"],
  69. tuple(raw_icon["sizes_px"]),
  70. )
  71. def _create_fetch(asset, args):
  72. src_url = asset.src_url_pattern.format(**args)
  73. dest_file = asset.dest_dir_pattern.format(**args)
  74. dest_file = (Path(__file__) / "../.." / dest_file).resolve()
  75. return Fetch(src_url, dest_file)
  76. def _do_fetch(fetch):
  77. resp = requests.get(fetch.src_url)
  78. resp.raise_for_status()
  79. fetch.dest_file.parent.mkdir(parents=True, exist_ok=True)
  80. fetch.dest_file.write_bytes(resp.content)
  81. def _do_fetches(fetches):
  82. print(f"Starting {len(fetches)} fetches")
  83. start_t = time.monotonic()
  84. print_t = start_t
  85. for idx, fetch in enumerate(fetches):
  86. _do_fetch(fetch)
  87. t = time.monotonic()
  88. if t - print_t > 5:
  89. print_t = t
  90. est_complete = (t - start_t) * (len(fetches) / (idx + 1))
  91. print(f"{idx}/{len(fetches)}, estimating {int(est_complete)}s left")
  92. def _unzip_target(zip_path: Path):
  93. return zip_path.parent.resolve() / zip_path.stem
  94. def _explode_zips(zips: Sequence[Path]):
  95. for zip_path in zips:
  96. assert zip_path.suffix == ".zip", zip_path
  97. if not zip_path.is_file():
  98. continue
  99. unzip_target = _unzip_target(zip_path)
  100. print(f"Unzip {zip_path} => {unzip_target}")
  101. with ZipFile(zip_path) as zip_file:
  102. zip_file.extractall(unzip_target)
  103. zip_path.unlink()
  104. def _is_zip(p: Path):
  105. return p.suffix == ".zip"
  106. def _zips(fetches: Sequence[Fetch]):
  107. return [f.dest_file for f in fetches if _is_zip(f.dest_file)]
  108. def _should_skip(fetch: Fetch):
  109. if not FLAGS.skip_existing:
  110. return False
  111. if _is_zip(fetch.dest_file):
  112. return _unzip_target(fetch.dest_file).is_dir()
  113. return fetch.dest_file.is_file()
  114. def main(_):
  115. metadata = _latest_metadata()
  116. current_versions = json.loads(_current_versions().read_text())
  117. host = metadata["host"]
  118. stylistic_sets = tuple(s.replace(" ", "").lower() for s in metadata["families"])
  119. fetches = []
  120. skips = []
  121. num_changed = 0
  122. icons = tuple(_icons(metadata))
  123. if FLAGS.icon_limit > 0:
  124. icons = icons[: FLAGS.icon_limit]
  125. for icon in icons:
  126. ver_key = _version_key(icon)
  127. if icon.version <= current_versions.get(ver_key, 0):
  128. continue
  129. current_versions[ver_key] = icon.version
  130. num_changed += 1
  131. for size_px in icon.sizes_px:
  132. for stylistic_set in stylistic_sets:
  133. pattern_args = {
  134. "host": host,
  135. "stylistic_set": stylistic_set,
  136. "icon": icon,
  137. "size_px": size_px,
  138. }
  139. for asset in _ASSETS:
  140. fetch = _create_fetch(asset, pattern_args)
  141. if _should_skip(fetch):
  142. skips.append(fetch)
  143. else:
  144. fetches.append(fetch)
  145. print(f"{num_changed}/{len(icons)} have changed")
  146. if skips:
  147. print(f"{len(skips)} fetches skipped because assets exist")
  148. if fetches:
  149. if FLAGS.fetch:
  150. _do_fetches(fetches)
  151. else:
  152. print(f"fetch disabled; not fetching {len(fetches)} assets")
  153. if FLAGS.explode_zips:
  154. _explode_zips([f.dest_file for f in fetches + skips if _is_zip(f.dest_file)])
  155. with open(_current_versions(), "w") as f:
  156. json.dump(current_versions, f, indent=4, sort_keys=True)
  157. if __name__ == "__main__":
  158. app.run(main)