Files
EvoScientist-Multi/EvoScientist/update_check.py
T
Xi Zhang f4a3617646 refactor(paths): unify global data directory to ~/.evoscientist and u… (#164)
* refactor(paths): unify global data directory to ~/.evoscientist and update related paths

* refactor(paths): update legacy session migration to respect XDG_CONFIG_HOME

* refactor(tests): clear XDG_CONFIG_HOME in legacy session migration tests for deterministic behavior
2026-04-18 15:01:22 +01:00

94 lines
2.8 KiB
Python

"""Background update check for EvoScientist.
Compares the installed version against PyPI and caches the result
(see ``CACHE_TTL``). All errors are silently swallowed so startup
is never blocked or degraded.
"""
from __future__ import annotations
import json
import logging
import time
from .config.settings import get_config_dir
logger = logging.getLogger(__name__)
PYPI_URL = "https://pypi.org/pypi/EvoScientist/json"
CACHE_DIR = get_config_dir()
CACHE_FILE = CACHE_DIR / "latest_version.json"
CACHE_TTL = 86_400 # 24 hours
def _installed_version() -> str:
"""Return the currently installed EvoScientist version."""
from importlib.metadata import version as _pkg_version
return _pkg_version("EvoScientist")
def _parse_version(v: str) -> tuple[int, ...]:
"""Parse a dotted version string into a comparable integer tuple."""
return tuple(int(x) for x in v.strip().split("."))
def get_latest_version() -> str | None:
"""Fetch the latest EvoScientist version from PyPI, with 24h caching."""
# Try cache first
try:
if CACHE_FILE.exists():
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
if time.time() - data.get("checked_at", 0) < CACHE_TTL:
return data["version"]
except Exception:
logger.debug("Failed to read update-check cache", exc_info=True)
# Fetch from PyPI (use urllib to avoid extra deps)
try:
import urllib.request
req = urllib.request.Request(
PYPI_URL,
headers={"User-Agent": "EvoScientist update-check"},
)
with urllib.request.urlopen(req, timeout=3) as resp:
latest: str = json.loads(resp.read())["info"]["version"]
except Exception:
logger.debug("Failed to fetch latest version from PyPI", exc_info=True)
return None
# Cache result
try:
CACHE_DIR.mkdir(parents=True, exist_ok=True)
CACHE_FILE.write_text(
json.dumps({"version": latest, "checked_at": time.time()}),
encoding="utf-8",
)
except Exception:
logger.debug("Failed to write update-check cache", exc_info=True)
return latest
def is_update_available() -> tuple[bool, str | None]:
"""Check whether a newer version of EvoScientist is available on PyPI.
Returns:
``(available, latest)`` tuple. *available* is ``True`` when
the PyPI version is strictly newer; *latest* is the version
string (or ``None`` when the check fails).
"""
latest = get_latest_version()
if latest is None:
return False, None
try:
current = _installed_version()
if _parse_version(latest) > _parse_version(current):
return True, latest
except (ValueError, TypeError):
logger.debug("Failed to compare versions", exc_info=True)
return False, None