src\wily\cache.py
-- -- --- --- --- --- --- --- ------- ------- ------- | """
-- -- --- --- --- --- --- --- ------- ------- ------- | A module for working with the .wily/ cache directory.
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | This API is not intended to be public and should not be consumed directly.
-- -- --- --- --- --- --- --- ------- ------- ------- | The API in this module is for archivers and commands to work with the local cache
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | """
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | import json
-- -- --- --- --- --- --- --- ------- ------- ------- | import os.path
-- -- --- --- --- --- --- --- ------- ------- ------- | import pathlib
-- -- --- --- --- --- --- --- ------- ------- ------- | import shutil
-- -- --- --- --- --- --- --- ------- ------- ------- | from typing import Any, Dict, List, Union
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily import __version__, logger
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily.archivers import ALL_ARCHIVERS, Archiver, Revision
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily.config.types import WilyConfig
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily.lang import _
-- -- --- --- --- --- --- --- ------- ------- ------- | from wily.operators import resolve_operator
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | def exists(config: WilyConfig) -> bool:
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | """
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | Check whether the .wily/ directory exists.
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 |
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | :param config: The configuration
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 |
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | :return: Whether the .wily directory exists
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | """
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | exists = (
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | pathlib.Path(config.cache_path).exists()
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | and pathlib.Path(config.cache_path).is_dir()
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | )
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | if not exists:
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | return False
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | index_path = pathlib.Path(config.cache_path) / "index.json"
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | if index_path.exists():
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | with open(index_path) as out:
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | index = json.load(out)
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | if index["version"] != __version__:
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | # TODO: Inspect the versions properly.
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | logger.warning(
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | "Wily cache is old, you may incur errors until you rebuild the cache."
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | )
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | else:
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | logger.warning(
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | "Wily cache was not versioned, you may incur errors until you rebuild the cache."
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | )
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | create_index(config)
-- 05 004 007 004 007 011 011 0038.05 0076.11 0002.00 | return True
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | def create_index(config: WilyConfig) -> None:
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | """Create the root index."""
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | filename = pathlib.Path(config.cache_path) / "index.json"
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | index = {"version": __version__}
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | with open(filename, "w") as out:
-- 01 001 002 001 002 003 003 0004.75 0002.38 0000.50 | out.write(json.dumps(index, indent=2))
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | def create(config: WilyConfig) -> str:
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | Create a wily cache.
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 |
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | :param config: The configuration
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | :return: The path to the cache
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | """
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | if exists(config):
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | logger.debug("Wily cache exists, skipping")
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | return config.cache_path
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | logger.debug(f"Creating wily cache {config.cache_path}")
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | pathlib.Path(config.cache_path).mkdir(parents=True, exist_ok=True)
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | create_index(config)
-- 02 000 000 000 000 000 000 0000.00 0000.00 0000.00 | return config.cache_path
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | def clean(config: WilyConfig) -> None:
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | """
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | Delete a wily cache.
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 |
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | :param config: The configuration
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | """
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | if not exists(config):
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | logger.debug("Wily cache does not exist, skipping")
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | return
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | shutil.rmtree(config.cache_path)
-- 02 001 001 001 001 002 002 0002.00 0001.00 0000.50 | logger.debug("Deleted wily cache")
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | def store(
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | config: WilyConfig,
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | archiver: Union[Archiver, str],
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | revision: Revision,
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | stats: Dict[str, Any],
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | ) -> pathlib.Path:
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | """
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | Store a revision record within an archiver folder.
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 |
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | :param config: The configuration
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | :param archiver: The archiver to get name from (e.g. 'git')
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | :param revision: The revision
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | :param stats: The collected data
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 |
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | :return: The absolute path to the created file
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | """
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | root = pathlib.Path(config.cache_path) / str(archiver)
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 |
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | if not root.exists():
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | logger.debug("Creating wily cache")
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | root.mkdir()
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 |
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | # fix absolute path references.
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | if config.path != ".":
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | for operator, operator_data in list(stats["operator_data"].items()):
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | if operator_data:
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | new_operator_data = operator_data.copy()
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | for k, v in list(operator_data.items()):
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | if os.path.isabs(k):
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | new_key = os.path.relpath(str(k), str(config.path))
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | else:
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | new_key = str(k)
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | del new_operator_data[k]
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | new_operator_data[new_key] = v
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | del stats["operator_data"][operator]
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | stats["operator_data"][operator] = new_operator_data
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 |
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | logger.debug(f"Creating {revision.key} output")
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | filename = root / (revision.key + ".json")
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | if filename.exists():
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | raise RuntimeError(f"File {filename} already exists, index may be corrupt.")
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | with open(filename, "w") as out:
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | out.write(json.dumps(stats, indent=2))
-- 08 004 009 005 009 013 014 0051.81 0103.61 0002.00 | return filename
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | def store_archiver_index(
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | config: WilyConfig, archiver: Union[Archiver, str], index: List[Dict[str, Any]]
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | ) -> pathlib.Path:
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | """
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | Store an archiver's index record for faster search.
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 |
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | :param config: The configuration
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | :param archiver: The archiver to get name from (e.g. 'git')
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | :param index: The archiver index record
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 |
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | :return: The absolute path to the created file
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | """
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | root = pathlib.Path(config.cache_path) / str(archiver)
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 |
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | if not root.exists():
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | root.mkdir()
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | logger.debug("Created archiver directory")
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 |
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | index = sorted(index, key=lambda k: k["date"], reverse=True)
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 |
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | filename = root / "index.json"
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | with open(filename, "w") as out:
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | out.write(json.dumps(index, indent=2))
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | logger.debug("Created index output")
-- 02 002 005 003 005 007 008 0022.46 0022.46 0001.00 | return filename
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | def list_archivers(config: WilyConfig) -> List[str]:
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | """
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | List the names of archivers with data.
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | :param config: The configuration
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 |
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | :return: A list of archiver names
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | """
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | root = pathlib.Path(config.cache_path)
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | result = []
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | for name in ALL_ARCHIVERS.keys():
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | if (root / name).exists():
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | result.append(name)
-- 03 001 002 001 002 003 003 0004.75 0002.38 0000.50 | return result
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | def get_default_metrics(config: WilyConfig) -> List[str]:
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | """
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | Get the default metrics for a configuration.
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | :param config: The configuration
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | :return: Return the list of default metrics in this index
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | """
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | archivers = list_archivers(config)
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | default_metrics = []
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | for archiver in archivers:
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | index = get_archiver_index(config, archiver)
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | if len(index) == 0:
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | logger.warning(_("No records found in the index, no metrics available"))
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | return []
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 |
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | operators = index[0]["operators"]
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | for operator in operators:
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | o = resolve_operator(operator)
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | if o.operator_cls.default_metric_index is not None:
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | metric = o.operator_cls.metrics[o.operator_cls.default_metric_index]
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | default_metrics.append(f"{o.operator_cls.name}.{metric.name}")
-- 05 002 004 002 004 006 006 0015.51 0015.51 0001.00 | return default_metrics
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | def has_archiver_index(config: WilyConfig, archiver: Union[Archiver, str]) -> bool:
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | """
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | Check if this archiver has an index file.
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param config: The configuration
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param archiver: The name of the archiver type (e.g. 'git')
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :return: Whether the archiver's index exists.
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | """
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | root = pathlib.Path(config.cache_path) / str(archiver) / "index.json"
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | return root.exists()
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | def get_archiver_index(config: WilyConfig, archiver: Union[Archiver, str]) -> Any:
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | """
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | Get the contents of the archiver index file.
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param config: The configuration
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param archiver: The name of the archiver type (e.g. 'git')
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :return: The index data
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | """
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | root = pathlib.Path(config.cache_path) / str(archiver)
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | with (root / "index.json").open("r") as index_f:
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | index = json.load(index_f)
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | return index
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- -- --- --- --- --- --- --- ------- ------- ------- |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | def get(
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | config: WilyConfig, archiver: Union[Archiver, str], revision: str
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | ) -> Dict[Any, Any]:
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | """
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | Get the data for a given revision.
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 |
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param config: The configuration
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param archiver: The archiver to get name from (e.g. 'git')
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :param revision: The revision ID
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | :return: The data record for that revision
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | """
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | root = pathlib.Path(config.cache_path) / str(archiver)
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | # TODO : string escaping!!!
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | with (root / f"{revision}.json").open("r") as rev_f:
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | index = json.load(rev_f)
-- 01 001 004 002 004 005 006 0013.93 0006.97 0000.50 | return index