Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support [tool.insta-science] cache config. #16

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions python/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# insta-science

## 0.4.2

Actually implement support for `[tool.insta-science] cache` configuration as claimed in the README.

## 0.4.1

Fix cache location on Windows and document `insta-science` configuration.
Expand Down
2 changes: 1 addition & 1 deletion python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![CI](https://img.shields.io/github/actions/workflow/status/a-scie/science-installers/python-ci.yml)](https://github.com/a-scie/science-installers/actions/workflows/python-ci.yml)

The `insta-science` Python project distribution provides two convenience console scripts to make
bootstrapping `science` for use in Python project easier:
bootstrapping `science` for use in Python projects easier:
+ `insta-science`: This is a shim script that ensures `science` is installed and then forwards all
supplied arguments to it. Instead of `science`, just use `insta-science`. You can configure the
`science` version to use, where to find `science` binaries and where to install them via the
Expand Down
13 changes: 9 additions & 4 deletions python/insta_science/_internal/a_scie.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from packaging.version import Version

from .cache import DOWNLOAD_CACHE
from .cache import DownloadCache
from .fetcher import fetch_and_verify
from .hashing import Digest, Fingerprint
from .model import Science, ScienceExe, Url
Expand All @@ -26,6 +26,7 @@ class _LoadResult:


def _load_project_release(
cache: DownloadCache,
base_url: Url,
binary_name: str,
version: Version | None = None,
Expand All @@ -41,6 +42,7 @@ def _load_project_release(
ttl = timedelta(days=5)
path = fetch_and_verify(
url=Url(f"{base_url}/{version_path}/{qualified_binary_name}"),
cache=cache,
namespace=_DOWNLOAD_NAMESPACE,
fingerprint=fingerprint,
executable=True,
Expand All @@ -49,9 +51,12 @@ def _load_project_release(
return _LoadResult(path=path, binary_name=qualified_binary_name)


def science(spec: Science = Science(), platform: Platform = CURRENT_PLATFORM) -> ScienceExe:
def science(
cache: DownloadCache, spec: Science = Science(), platform: Platform = CURRENT_PLATFORM
) -> ScienceExe:
return spec.exe(
_load_project_release(
cache=cache,
base_url=spec.base_url,
binary_name="science-fat",
version=spec.version,
Expand All @@ -61,6 +66,6 @@ def science(spec: Science = Science(), platform: Platform = CURRENT_PLATFORM) ->
)


def iter_science_exes() -> Iterator[ScienceExe]:
for path in DOWNLOAD_CACHE.iter_entries(namespace=_DOWNLOAD_NAMESPACE):
def iter_science_exes(cache: DownloadCache) -> Iterator[ScienceExe]:
for path in cache.iter_entries(namespace=_DOWNLOAD_NAMESPACE):
yield ScienceExe(path)
22 changes: 15 additions & 7 deletions python/insta_science/_internal/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from pathlib import Path, PurePath
from typing import Iterator, Union

import appdirs
from filelock import FileLock
from typing_extensions import TypeAlias

from insta_science._internal.du import DiskUsage
from .du import DiskUsage


@dataclass(frozen=True)
Expand Down Expand Up @@ -106,10 +106,18 @@ def usage(self) -> DiskUsage:
return DiskUsage.collect(str(self._base))


DOWNLOAD_CACHE = DownloadCache(
base_dir=Path(
os.environ.get(
"INSTA_SCIENCE_CACHE", appdirs.user_cache_dir(appname="insta-science", appauthor=False)
def download_cache(cache_dir: PurePath | None = None) -> DownloadCache:
return DownloadCache(
base_dir=Path(
os.path.expanduser(
os.environ.get(
"INSTA_SCIENCE_CACHE",
(
str(cache_dir)
if cache_dir
else appdirs.user_cache_dir(appname="insta-science", appauthor=False)
),
)
)
)
)
)
5 changes: 3 additions & 2 deletions python/insta_science/_internal/fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from tqdm import tqdm

from . import hashing
from .cache import DOWNLOAD_CACHE, Missing
from .cache import DownloadCache, Missing
from .errors import InputError
from .hashing import Digest, ExpectedDigest, Fingerprint
from .model import Url
Expand Down Expand Up @@ -129,6 +129,7 @@ def _expected_digest(

def fetch_and_verify(
url: Url,
cache: DownloadCache,
namespace: str,
fingerprint: Digest | Fingerprint | Url | None = None,
digest_algorithm: str = hashing.DEFAULT_ALGORITHM,
Expand All @@ -137,7 +138,7 @@ def fetch_and_verify(
headers: Mapping[str, str] | None = None,
) -> PurePath:
verified_fingerprint = False
with DOWNLOAD_CACHE.get_or_create(url, namespace=namespace, ttl=ttl) as cache_result:
with cache.get_or_create(url, namespace=namespace, ttl=ttl) as cache_result:
if isinstance(cache_result, Missing):
# TODO(John Sirois): XXX: Log or invoke callback for logging.
# click.secho(f"Downloading {url} ...", fg="green")
Expand Down
6 changes: 6 additions & 0 deletions python/insta_science/_internal/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,9 @@ def spec(

def exe(self, path: PurePath) -> ScienceExe:
return ScienceExe(path=path, _version=self.version)


@dataclass(frozen=True)
class Configuration:
science: Science = Science()
cache: PurePath | None = None
48 changes: 35 additions & 13 deletions python/insta_science/_internal/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

from __future__ import annotations

import os.path
from pathlib import PurePath
from typing import Any, cast

from packaging.version import InvalidVersion, Version

from .errors import InputError
from .hashing import Digest, Fingerprint
from .model import Science, Url
from .model import Configuration, Science, Url
from .project import PyProjectToml


Expand All @@ -22,17 +24,21 @@ def _assert_dict_str_keys(obj: Any, *, path: str) -> dict[str, Any]:
return cast("dict[str, Any]", obj)


def configured_science(pyproject_toml: PyProjectToml) -> Science:
def parse_configuration(pyproject_toml: PyProjectToml) -> Configuration:
pyproject_data = pyproject_toml.parse()
try:
insta_science_data = _assert_dict_str_keys(
pyproject_data["tool"]["insta-science"]["science"], path="[tool.insta-science.science]"
pyproject_data["tool"]["insta-science"], path="[tool.insta-science]"
)
except KeyError:
return Science()
return Configuration()

science_data = _assert_dict_str_keys(
insta_science_data.pop("science", {}), path="[tool.insta-science.science]"
)

version: Version | None = None
version_str = insta_science_data.pop("version", None)
version_str = science_data.pop("version", None)
if version_str:
if not isinstance(version_str, str):
raise InputError(
Expand All @@ -49,7 +55,7 @@ def configured_science(pyproject_toml: PyProjectToml) -> Science:

digest: Digest | None = None
digest_data = _assert_dict_str_keys(
insta_science_data.pop("digest", {}), path="[tool.insta-science.science.digest]"
science_data.pop("digest", {}), path="[tool.insta-science.science.digest]"
)
if digest_data:
try:
Expand Down Expand Up @@ -87,7 +93,7 @@ def configured_science(pyproject_toml: PyProjectToml) -> Science:
digest = Digest(size, fingerprint=Fingerprint(fingerprint))

base_url: Url | None = None
base_url_str = insta_science_data.pop("base-url", None)
base_url_str = science_data.pop("base-url", None)
if base_url_str:
if not isinstance(base_url_str, str):
raise InputError(
Expand All @@ -96,10 +102,10 @@ def configured_science(pyproject_toml: PyProjectToml) -> Science:
)
base_url = Url(base_url_str)

if insta_science_data:
if science_data:
raise InputError(
f"Unexpected configuration keys in the [tool.insta-science.science] table: "
f"{' '.join(insta_science_data)}"
f"{' '.join(science_data)}"
)

if digest and not version:
Expand All @@ -108,8 +114,24 @@ def configured_science(pyproject_toml: PyProjectToml) -> Science:
"[tool.insta-science.science] `version` is set."
)

return (
Science(version=version, digest=digest, base_url=base_url)
if base_url
else Science(version=version, digest=digest)
cache_str = insta_science_data.pop("cache", None)
if cache_str and not isinstance(cache_str, str):
raise InputError(
f"The [tool.insta-science] `cache` value should be a string; given: {cache_str} of "
f"type {type(cache_str)}."
)

if insta_science_data:
raise InputError(
f"Unexpected configuration keys in the [tool.insta-science] table: "
f"{' '.join(insta_science_data)}"
)

return Configuration(
science=(
Science(version=version, digest=digest, base_url=base_url)
if base_url
else Science(version=version, digest=digest)
),
cache=PurePath(os.path.expanduser(cache_str)) if cache_str else None,
)
31 changes: 18 additions & 13 deletions python/insta_science/_internal/science.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
import httpx

from . import a_scie, parser, project
from .cache import DOWNLOAD_CACHE, Missing
from .cache import DownloadCache, Missing, download_cache
from .errors import InputError, ScienceNotFound
from .hashing import ExpectedDigest
from .model import Science, ScienceExe
from .model import Configuration, Science, ScienceExe
from .platform import CURRENT_PLATFORM

_PATH_EXES_NAMESPACE = "path-exes"


def _find_science_on_path(spec: Science) -> ScienceExe | None:
def _find_science_on_path(cache: DownloadCache, spec: Science) -> ScienceExe | None:
url = "file://<just-a-cache-key>/science"
ttl: timedelta | None = None
if spec.version:
Expand All @@ -31,9 +31,7 @@ def _find_science_on_path(spec: Science) -> ScienceExe | None:
else:
ttl = timedelta(days=5)

with DOWNLOAD_CACHE.get_or_create(
url=url, namespace=_PATH_EXES_NAMESPACE, ttl=ttl
) as cache_result:
with cache.get_or_create(url=url, namespace=_PATH_EXES_NAMESPACE, ttl=ttl) as cache_result:
if isinstance(cache_result, Missing):
for binary_name in (
CURRENT_PLATFORM.binary_name("science"),
Expand Down Expand Up @@ -61,11 +59,12 @@ def _find_science_on_path(spec: Science) -> ScienceExe | None:
return spec.exe(cache_result.path)


def ensure_installed(spec: Science | None = None) -> ScienceExe:
def ensure_installed(spec: Science | None = None, cache_dir: PurePath | None = None) -> ScienceExe:
"""Ensures an appropriate science binary is installed and returns its path.

Args:
spec: An optional specification of which science binary is required.
cache_dir: An optional custom cache dir to use for caching the science binary.

Returns:
The path of a science binary meeting the supplied ``spec``, if any.
Expand All @@ -75,12 +74,18 @@ def ensure_installed(spec: Science | None = None) -> ScienceExe:
install was parsed from ``pyproject.toml`` and found to have errors.
ScienceNotFound: The science binary could not be found locally or downloaded.
"""
if spec is None:
if spec is None or cache_dir is None:
pyproject_toml = project.find_pyproject_toml()
spec = parser.configured_science(pyproject_toml) if pyproject_toml else Science()
configuration = (
parser.parse_configuration(pyproject_toml) if pyproject_toml else Configuration()
)
cache_dir = cache_dir or configuration.cache
spec = spec or configuration.science

cache = download_cache(cache_dir=cache_dir)

try:
return _find_science_on_path(spec) or a_scie.science(spec)
return _find_science_on_path(cache, spec) or a_scie.science(cache, spec)
except (
OSError,
CalledProcessError,
Expand All @@ -92,7 +97,7 @@ def ensure_installed(spec: Science | None = None) -> ScienceExe:
raise ScienceNotFound(str(e))


def iter_science_exes() -> Iterator[ScienceExe]:
yield from a_scie.iter_science_exes()
for path in DOWNLOAD_CACHE.iter_entries(namespace=_PATH_EXES_NAMESPACE):
def iter_science_exes(cache: DownloadCache) -> Iterator[ScienceExe]:
yield from a_scie.iter_science_exes(cache)
for path in cache.iter_entries(namespace=_PATH_EXES_NAMESPACE):
yield ScienceExe(path)
Loading