Skill Tracer
Skills › Scientific Agent Skills › imaging-data-commons › check_version.py

check_version.py

138 lines · skills/imaging-data-commons/scripts/check_version.py at 5f2bc03

1#!/usr/bin/env python3
2"""Check the idc-index package and this skill for required/available updates.
3
4Run FIRST at the start of an IDC session: python scripts/check_version.py
5
6- Verifies that idc-index is installed and at least MIN_VERSION. It never
7 installs or upgrades anything itself: if the requirement is not met it prints
8 the command to run — targeting the interpreter that ran this script, via uv
9 when uv is available — and exits non-zero, leaving the choice of Python
10 environment to the caller.
11- Notifies (only) when a newer idc-index (PyPI) or skill release (GitHub) is
12 available. Network checks are best-effort and silently skipped offline.
14Keep MIN_VERSION and SKILL_VERSION in sync with metadata.idc-index and
15metadata.source-skill-version, respectively, in SKILL.md.
16"""
17import re
18import shlex
19import shutil
20import sys
22MIN_VERSION = "0.12.5" # keep in sync with metadata.idc-index in SKILL.md
23SKILL_VERSION = "1.8.1" # upstream metadata.source-skill-version, not local metadata.version
24REPO = "ImagingDataCommons/imaging-data-commons-skill"
26_LEADING_DIGITS = re.compile(r"\d+")
29def parse_version(v):
30 """Numeric 3-tuple for comparison (string comparison misorders multi-digit parts).
32 Tolerates pre-release and suffixed tags by taking the leading digits of each
33 component: "0.13.0rc1" and "v1.7.0-beta" parse as (0, 13, 0) and (1, 7, 0)
34 rather than raising. A pre-release therefore compares equal to its base
35 release, which keeps the update notices conservative instead of advertising
36 unreleased versions.
37 """
38 parts = []
39 for part in v.lstrip("v").split(".")[:3]:
40 match = _LEADING_DIGITS.match(part)
41 parts.append(int(match.group()) if match else 0)
42 return tuple(parts + [0] * (3 - len(parts)))
45def fetch_json(url, *keys):
46 """Best-effort JSON fetch, drilling into nested keys; None if unreachable."""
47 import json
48 import urllib.request
49 try:
50 with urllib.request.urlopen(url, timeout=5) as response:
51 data = json.load(response)
52 for key in keys:
53 data = data[key]
54 return data
55 except Exception:
56 return None
59def install_commands(spec, upgrade=False):
60 """Install commands for the interpreter running this script, preferred first.
62 `-m pip` is offered when pip is available in that environment; naming the
63 interpreter explicitly keeps the install out of whatever other environment a bare
64 `pip` on PATH would resolve to. `uv` is offered ahead of it when it is on PATH, with
65 `--python` for the same reason — otherwise `uv pip install` targets the active
66 virtual environment, which is not necessarily this one.
68 Neither form overrides the PEP 668 guard on an externally managed interpreter. Both
69 refuse there, which is the intended outcome, not a gap to work around.
70 """
71 # Print POSIX-shell commands; paths and specifications must remain single arguments.
72 flag = "--upgrade " if upgrade else ""
73 interpreter = shlex.quote(sys.executable)
74 requirement = shlex.quote(spec)
75 commands = [f"{interpreter} -m pip install {flag}{requirement}"]
76 if shutil.which("uv"):
77 commands.insert(0, f"uv pip install --python {interpreter} {flag}{requirement}")
78 return commands
81def print_install_instructions(spec):
82 """Print how to install `spec` — this script never modifies the environment."""
83 print("\nInstall the vetted version with:\n")
84 for command in install_commands(spec):
85 print(f" {command}")
86 print()
87 print("Use a virtual environment where you can; on an externally managed system Python")
88 print("(PEP 668), create and activate a virtual environment, then re-run this script there.")
89 print("Re-run this script once the install finishes.")
92def check_minimum():
93 """Check the installed idc-index against MIN_VERSION.
95 Returns the installed version string, or None if idc-index is missing or
96 older than MIN_VERSION — in which case install instructions are printed and
97 the caller should not proceed until they have been followed.
98 """
99 try:
100 import idc_index
101 except ImportError:
102 print(f"idc-index is not installed; this skill requires {MIN_VERSION} or newer.")
103 print_install_instructions(f"idc-index=={MIN_VERSION}")
104 return None
106 installed = idc_index.__version__
107 if parse_version(installed) < parse_version(MIN_VERSION):
108 print(f"idc-index {installed} is below the pinned minimum {MIN_VERSION}.")
109 print_install_instructions(f"idc-index=={MIN_VERSION}")
110 return None
112 print(f"idc-index {installed} meets pinned minimum ({MIN_VERSION})")
113 return installed
116def notify_updates(installed):
117 """Print notices when newer idc-index or skill versions are available."""
118 if installed:
119 pkg = fetch_json("https://pypi.org/pypi/idc-index/json", "info", "version")
120 if pkg and parse_version(pkg) > parse_version(installed):
121 print(f"[INFO] idc-index {pkg} available — to update: "
122 f"{install_commands('idc-index', upgrade=True)[0]}")
124 tag = fetch_json(f"https://api.github.com/repos/{REPO}/releases/latest", "tag_name")
125 if tag and parse_version(tag) > parse_version(SKILL_VERSION):
126 print(f"[INFO] Skill {tag.lstrip('v')} available (you have {SKILL_VERSION}): "
127 f"https://github.com/{REPO}/releases/latest")
130def main():
131 """Exit 0 when the pinned minimum is installed, 1 when the caller must install it."""
132 installed = check_minimum()
133 notify_updates(installed)
134 return 0 if installed else 1
137if __name__ == "__main__":
138 sys.exit(main())