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