Source code for snakemake.assets

# This module handles the download of non python assets.
# It should not use any modules that are not part of the standard library because it will
# be called before the setup (and dependency deployment) of the snakemake package.

from dataclasses import dataclass
import hashlib
import importlib.resources
from pathlib import Path
from typing import Dict, Optional
import urllib.request
import urllib.error


[docs] class AssetDownloadError(Exception): pass
[docs] @dataclass class Asset: url: str version: Optional[str] = None sha256: Optional[str] = None
[docs] def get_content(self) -> bytes: """Get and validate asset content.""" url = self.url.format(version=self.version) if self.version else self.url req = urllib.request.Request(url, headers={"User-Agent": "snakemake"}) err = None for _ in range(6): try: resp = urllib.request.urlopen(req) content = resp.read() except urllib.error.URLError as e: err = AssetDownloadError(f"Failed to download asset {url}: {e}") continue if self.sha256 is not None: content_sha = hashlib.sha256(content).hexdigest() if self.sha256 != content_sha: err = AssetDownloadError( f"Checksum mismatch when downloading asset {self.url} " f"(sha: {content_sha}). First 100 bytes:\n{content[:100].decode()}" ) continue return content assert err is not None raise err
[docs] class Assets: _base_path: Optional[Path] = None spec: Dict[str, Asset] = { "snakemake/LICENSE.md": Asset( url="https://raw.githubusercontent.com/snakemake/snakemake/main/LICENSE.md", sha256="84a1a82b05c80637744d3fe8257235c15380efa6cc32608adf4b21f17af5d2b8", ), "pygments/LICENSE": Asset( url="https://raw.githubusercontent.com/pygments/pygments/master/LICENSE", sha256="a9d66f1d526df02e29dce73436d34e56e8632f46c275bbdffc70569e882f9f17", ), "tailwindcss/LICENSE": Asset( url="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/refs/tags/v{version}/LICENSE", sha256="60e0b68c0f35c078eef3a5d29419d0b03ff84ec1df9c3f9d6e39a519a5ae7985", version="3.0.23", ), "tailwindcss/tailwind.css": Asset( url="https://cdn.tailwindcss.com/{version}?plugins=forms@0.5.9,typography@0.5.2", # The tailwindcss cdn checksum is not stable. Since this is only included # as CSS styles, the risk is low. version="3.4.16", ), "react/LICENSE": Asset( url="https://raw.githubusercontent.com/facebook/react/refs/tags/v{version}/LICENSE", sha256="52412d7bc7ce4157ea628bbaacb8829e0a9cb3c58f57f99176126bc8cf2bfc85", version="18.2.0", ), "react/react.production.min.js": Asset( url="https://cdnjs.cloudflare.com/ajax/libs/react/{version}/umd/react.production.min.js", sha256="4b4969fa4ef3594324da2c6d78ce8766fbbc2fd121fff395aedf997db0a99a06", version="18.2.0", ), "react/react-dom.production.min.js": Asset( url="https://cdnjs.cloudflare.com/ajax/libs/react-dom/{version}/umd/react-dom.production.min.js", sha256="21758ed084cd0e37e735722ee4f3957ea960628a29dfa6c3ce1a1d47a2d6e4f7", version="18.2.0", ), "vega/vega.js": Asset( url="https://cdnjs.cloudflare.com/ajax/libs/vega/{version}/vega.js", sha256="b34c43055ef5d39a093e937522955dc359fbaec6c5b0259ae2de4c9da698e9fe", version="5.21.0", ), "vega/LICENSE": Asset( url="https://raw.githubusercontent.com/vega/vega/refs/tags/v{version}/LICENSE", sha256="b75f7ed0af20dedadf92c52bc236161bcf0d294ff2e6e34ca76403203349f71d", version="5.21.0", ), "vega-lite/vega-lite.js": Asset( url="https://cdnjs.cloudflare.com/ajax/libs/vega-lite/{version}/vega-lite.js", sha256="6eb7f93121cd9f44cf8640244f87c5e143f87c7a0b6cd113da4a9e41e3adf0aa", version="5.2.0", ), "vega-lite/LICENSE": Asset( url="https://raw.githubusercontent.com/vega/vega-lite/refs/tags/v{version}/LICENSE", sha256="f618900fd0d64046963b29f40590cdd1e341a2f41449f99110d82fd81fea808c", version="5.2.0", ), "vega-embed/vega-embed.js": Asset( url="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/{version}/vega-embed.js", sha256="4e546c1f86eb200333606440e92f76e2940b905757018d9672cd1708e4e6ff0a", version="6.20.8", ), "vega-embed/LICENSE": Asset( url="https://raw.githubusercontent.com/vega/vega-embed/refs/tags/v{version}/LICENSE", sha256="32df67148f0fc3db0eb9e263a7b75d07f1eb14c61955005a4a39c6918d10d137", version="6.20.8", ), "heroicons/LICENSE": Asset( url="https://raw.githubusercontent.com/tailwindlabs/heroicons/refs/tags/v{version}/LICENSE", sha256="75523ddd65d9620bea09f84e89d0c373b4205a3708b8a1e9f9598a5438a3e641", version="1.0.3", ), "prop-types/prop-types.min.js": Asset( url="https://cdnjs.cloudflare.com/ajax/libs/prop-types/{version}/prop-types.min.js", sha256="4c88350517ee82aa4f3368e67ef1a453ca6636dcfa6449b4e3d6faa5c877066e", version="15.7.2", ), "prop-types/LICENSE": Asset( url="https://raw.githubusercontent.com/facebook/prop-types/refs/tags/v{version}/LICENSE", sha256="f657f99d3fb9647db92628e96007aabb46e5f04f33e49999075aab8e250ca7ce", version="15.7.2", ), }
[docs] @classmethod def deploy(cls) -> None: # this has to work from setup.py without being able to load the snakemake # modules. base_path = Path(__file__).parent / "data" for asset_path, asset in cls.spec.items(): target_path = base_path / asset_path if target_path.exists(): with open(target_path, "rb") as fin: # file is already present, check if it is up to date if (asset.sha256 is not None) and ( asset.sha256 == hashlib.sha256(fin.read()).hexdigest() ): continue target_path.parent.mkdir(parents=True, exist_ok=True) with open(target_path, "wb") as fout: fout.write(asset.get_content())
[docs] @classmethod def get_content(cls, asset_path: str) -> str: try: return (cls.base_path() / asset_path).read_text(encoding="utf-8") except FileNotFoundError: from snakemake.logging import logger logger.warning( f"Asset {asset_path} not found (development setup?), downloading..." ) return cls.spec[asset_path].get_content().decode("utf-8")
[docs] @classmethod def get_version(cls, asset_path: str) -> Optional[str]: if asset_path in cls.spec: return cls.spec[asset_path].version else: return None
[docs] @classmethod def base_path(cls) -> Path: # this is called from within snakemake, so we can use importlib.resources if cls._base_path is None: cls._base_path = importlib.resources.files("snakemake.assets") / "data" return cls._base_path