Coverage for src/qdrant_loader/utils/version_check.py: 95%
76 statements
« prev ^ index » next coverage.py v7.10.0, created at 2025-07-25 11:39 +0000
« prev ^ index » next coverage.py v7.10.0, created at 2025-07-25 11:39 +0000
1"""Version checking utility for QDrant Loader CLI."""
3import json
4import time
5from pathlib import Path
6from urllib.error import HTTPError, URLError
7from urllib.request import Request, urlopen
9from packaging import version
12class VersionChecker:
13 """Check for new versions of qdrant-loader on PyPI."""
15 PYPI_API_URL = "https://pypi.org/pypi/qdrant-loader/json"
16 CACHE_FILE = ".qdrant_loader_version_cache"
17 CACHE_DURATION = 24 * 60 * 60 # 24 hours in seconds
19 def __init__(self, current_version: str):
20 """Initialize version checker.
22 Args:
23 current_version: Current version of qdrant-loader
24 """
25 self.current_version = current_version
26 self.cache_path = Path.home() / self.CACHE_FILE
28 def _get_cache_data(self) -> dict | None:
29 """Get cached version data if still valid.
31 Returns:
32 Cached data if valid, None otherwise
33 """
34 try:
35 if not self.cache_path.exists():
36 return None
38 with open(self.cache_path) as f:
39 cache_data = json.load(f)
41 # Check if cache is still valid
42 cache_time = cache_data.get("timestamp", 0)
43 if time.time() - cache_time > self.CACHE_DURATION:
44 return None
46 return cache_data
47 except (json.JSONDecodeError, OSError):
48 return None
50 def _save_cache_data(self, data: dict) -> None:
51 """Save version data to cache.
53 Args:
54 data: Data to cache
55 """
56 try:
57 cache_data = {"timestamp": time.time(), **data}
58 with open(self.cache_path, "w") as f:
59 json.dump(cache_data, f)
60 except OSError:
61 # Silently fail if we can't write cache
62 pass
64 def _fetch_latest_version(self) -> str | None:
65 """Fetch latest version from PyPI.
67 Returns:
68 Latest version string or None if fetch failed
69 """
70 try:
71 # Create request with user agent
72 req = Request(
73 self.PYPI_API_URL,
74 headers={"User-Agent": f"qdrant-loader/{self.current_version}"},
75 )
77 # Set timeout to avoid hanging
78 with urlopen(req, timeout=5) as response:
79 data = json.loads(response.read().decode("utf-8"))
80 return data["info"]["version"]
81 except (URLError, HTTPError, json.JSONDecodeError, KeyError, OSError):
82 return None
84 def check_for_updates(self, silent: bool = False) -> tuple[bool, str | None]:
85 """Check if a newer version is available.
87 Args:
88 silent: If True, don't show any output
90 Returns:
91 Tuple of (has_update, latest_version)
92 """
93 # Skip check if current version is unknown
94 if self.current_version in ("unknown", "Unknown"):
95 return False, None
97 # Try to get from cache first
98 cache_data = self._get_cache_data()
99 if cache_data:
100 latest_version = cache_data.get("latest_version")
101 else:
102 # Fetch from PyPI
103 latest_version = self._fetch_latest_version()
104 if latest_version:
105 self._save_cache_data({"latest_version": latest_version})
107 if not latest_version:
108 return False, None
110 try:
111 # Compare versions
112 current_ver = version.parse(self.current_version)
113 latest_ver = version.parse(latest_version)
115 has_update = latest_ver > current_ver
116 return has_update, latest_version
117 except version.InvalidVersion:
118 return False, None
120 def show_update_notification(self, latest_version: str) -> None:
121 """Show update notification to user.
123 Args:
124 latest_version: Latest available version
125 """
126 print("\n🆕 A new version of qdrant-loader is available!")
127 print(f" Current: {self.current_version}")
128 print(f" Latest: {latest_version}")
129 print(
130 " Update: pip install --upgrade qdrant-loader qdrant-loader-mcp-server"
131 )
132 print()
135def check_version_async(current_version: str, silent: bool = False) -> None:
136 """Asynchronously check for version updates.
138 Args:
139 current_version: Current version of qdrant-loader
140 silent: If True, don't show any output
141 """
143 def _check():
144 checker = VersionChecker(current_version)
145 has_update, latest_version = checker.check_for_updates(silent=silent)
147 if has_update and latest_version and not silent:
148 checker.show_update_notification(latest_version)
150 # Run check in background thread to avoid blocking CLI
151 try:
152 import threading
154 thread = threading.Thread(target=_check, daemon=True)
155 thread.start()
156 except Exception:
157 # Silently fail if threading doesn't work
158 pass