Every classification returned "HTTP Error 404" because the configured model gemma4:26b-64k no longer exists on the Ollama host: the tag was retired upstream and the local weights were deleted, so all pending updates fell back to manual review. Switch to the current gemma4:26b tag (25.2B, 262k context) and make it explicit in .env so the classifier no longer depends on the code default matching reality. PieFed intermittently returned an empty body, which surfaced as a current version error in the digest. Retry version fetches with a short backoff so one bad response no longer triggers an alert. Leave-one-out validation for the new model: immich 6/7 (the miss is the conditional-breaking example where the model follows the written rule that thresholds below the running version do not apply), freshrss 6/6, uptime-kuma 4/4.
112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
import json
|
|
import time
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from .comparator import compare_versions
|
|
from .extractor import build_upstream_fallback, extract_version
|
|
from .fetcher import authenticate_service, build_headers, fetch_response, fetch_socket_info
|
|
from .models import ServiceConfig
|
|
|
|
|
|
def fetch_version_with_retries(
|
|
url: str,
|
|
timeout: float,
|
|
user_agent: str,
|
|
extra_headers: Optional[Dict[str, str]] = None,
|
|
attempts: int = 3,
|
|
) -> tuple[str, Dict[str, str]]:
|
|
"""Fetch a version endpoint with retries for transient failures
|
|
(empty bodies, timeouts)."""
|
|
last_exc: Optional[Exception] = None
|
|
for attempt in range(attempts):
|
|
try:
|
|
body, headers = fetch_response(url, timeout, user_agent, extra_headers)
|
|
if body.strip():
|
|
return body, headers
|
|
last_exc = ValueError("empty response body")
|
|
except Exception as exc:
|
|
last_exc = exc
|
|
if attempt < attempts - 1:
|
|
time.sleep(2 * (attempt + 1))
|
|
raise last_exc if last_exc else RuntimeError("fetch failed")
|
|
|
|
|
|
def check_service(service: ServiceConfig, timeout: float, user_agent: str) -> Dict[str, Any]:
|
|
result: Dict[str, Any] = {
|
|
"name": service.name,
|
|
"current": None,
|
|
"latest": None,
|
|
"current_error": None,
|
|
"latest_error": None,
|
|
"upstream_url": service.upstream_latest_version_url,
|
|
"release_notes": None,
|
|
}
|
|
|
|
if service.current_version_url and service.current_version_extract:
|
|
try:
|
|
headers = build_headers(service.current_version_headers)
|
|
if service.login:
|
|
login_cookie = authenticate_service(service, timeout, user_agent)
|
|
if login_cookie:
|
|
headers.setdefault("Cookie", login_cookie)
|
|
body, response_headers = fetch_version_with_retries(
|
|
service.current_version_url, timeout, user_agent, headers
|
|
)
|
|
result["current"] = extract_version(body, service.current_version_extract, response_headers)
|
|
except Exception as exc:
|
|
result["current_error"] = str(exc)
|
|
elif service.login_socket:
|
|
try:
|
|
result["current"] = fetch_socket_info(service, timeout)
|
|
except Exception as exc:
|
|
result["current_error"] = str(exc)
|
|
|
|
if service.upstream_latest_version_url and service.upstream_latest_extract:
|
|
headers = build_headers(service.upstream_latest_headers)
|
|
try:
|
|
body, response_headers = fetch_version_with_retries(
|
|
service.upstream_latest_version_url, timeout, user_agent, headers
|
|
)
|
|
result["latest"] = extract_version(body, service.upstream_latest_extract, response_headers)
|
|
if result["latest"] is None:
|
|
raise ValueError("Latest version extraction returned empty")
|
|
try:
|
|
parsed = json.loads(body)
|
|
if isinstance(parsed, dict):
|
|
result["release_notes"] = parsed.get("body") or None
|
|
except (json.JSONDecodeError, AttributeError):
|
|
pass
|
|
except Exception as exc:
|
|
fallback = build_upstream_fallback(service.upstream_latest_version_url)
|
|
if fallback:
|
|
try:
|
|
body, response_headers = fetch_response(fallback["url"], timeout, user_agent, headers)
|
|
result["latest"] = extract_version(body, fallback["extract"], response_headers)
|
|
except Exception as fallback_exc:
|
|
result["latest_error"] = str(fallback_exc)
|
|
else:
|
|
result["latest_error"] = str(exc)
|
|
|
|
return result
|
|
|
|
|
|
def build_summary(results: List[Dict[str, Any]]) -> Tuple[str, bool]:
|
|
updates = [r for r in results if compare_versions(r["current"], r["latest"]) == 1]
|
|
errors = [r for r in results if r["current_error"] or r["latest_error"]]
|
|
|
|
lines = []
|
|
if updates:
|
|
lines.append("Updates available:")
|
|
for r in updates:
|
|
lines.append(f"- {r['name']}: {r['current'] or 'unknown'} -> {r['latest'] or 'unknown'}")
|
|
if errors:
|
|
lines.append("Errors:")
|
|
for r in errors:
|
|
if r["current_error"]:
|
|
lines.append(f"- {r['name']}: current version error")
|
|
if r["latest_error"]:
|
|
lines.append(f"- {r['name']}: latest version error")
|
|
|
|
if not lines:
|
|
return "All services up to date.", False
|
|
return "\n".join(lines), True
|