Version Checker
The Version Checker service provides intelligent PyPI version checking with advanced caching strategies and HTTP optimization. It efficiently determines when SpecifyX updates are available while minimizing network requests and respecting PyPI's resources.
Overview
The service implements smart caching with ETag support to reduce unnecessary network requests while ensuring users receive timely update notifications. It handles network failures gracefully and provides offline fallback capabilities.
Key Features
Intelligent Caching
- 15-minute cache duration for version checks
- ETag-based conditional requests to PyPI
- Stale cache fallback for offline scenarios
- Automatic cache invalidation and refresh
Network Optimization
- HTTP ETag support for efficient requests
- Proper User-Agent identification
- Timeout handling for reliable operations
- Graceful degradation on network failures
Version Comparison
- Semantic version parsing using packaging.version
- Accurate update detection across version schemes
- Handles pre-release and development versions
- Robust error handling for malformed versions
Core Class
PyPIVersionChecker
class PyPIVersionChecker:
def __init__(self, package_name: str = "specifyx"):
self.package_name = package_name
self.cache_dir = Path(user_cache_dir("specifyx", "SpecifyX"))
self.cache_file = self.cache_dir / "version_cache.json"
self.cache_duration = timedelta(minutes=15)
self.api_url = f"https://pypi.org/pypi/{package_name}/json"
Primary Methods
Version Checking
# Check for updates with caching
has_update, current, latest = checker.check_for_updates(use_cache=True)
# Force fresh check bypassing cache
has_update, current, latest = checker.check_for_updates(use_cache=False)
# Get just the latest version
latest_version = checker.get_latest_version(use_cache=True)
Cache Management
# Clear cached version data
checker.clear_cache()
# Get cache debugging information
cache_info = checker.get_cache_info()
# Returns: {
# "last_check": "2025-01-15T10:30:00Z",
# "cache_age_hours": 2.5,
# "cache_file": "/path/to/cache/version_cache.json",
# "latest_version": "1.2.3",
# "current_version": "1.2.0",
# "etag": "\"abc123\""
# }
Caching Strategy
Multi-Level Cache System
The service implements a sophisticated caching strategy:
- Fresh Cache: Valid within 15-minute window, used directly
- Stale Cache: Expired cache used for ETag requests and offline fallback
- ETag Optimization: Conditional requests to avoid unnecessary downloads
Cache Lifecycle
# Fresh cache check (within 15 minutes)
fresh_cache = self._load_cache() if use_cache else None
# Stale cache for ETag/fallback
stale_cache = self._read_cache_stale() if use_cache else None
# Use fresh cache if current version matches
if fresh_cache and fresh_cache.get("current_version") == current_version:
latest_version = fresh_cache.get("latest_version")
else:
# Fetch with ETag support using stale cache
latest_version = self._fetch_latest_version(stale_cache)
HTTP Optimization
ETag Support
def _fetch_latest_version(self, cached_data: Optional[Dict[str, Any]]) -> Optional[str]:
headers = {"User-Agent": self.user_agent, "Accept": "application/json"}
# Add ETag for conditional request
if cached_data and "etag" in cached_data:
headers["If-None-Match"] = cached_data["etag"]
response = client.get(self.api_url, headers=headers)
# Handle 304 Not Modified
if response.status_code == 304 and cached_data:
return cached_data.get("latest_version")
Request Handling
- User-Agent: Proper identification as
specifyx/1.0.0 (pypi-update-checker)
- Timeouts: 10-second timeout for all requests
- Status Codes: Explicit handling of 200, 304, and error conditions
- Error Recovery: Network failures fall back to cached data
Version Comparison Logic
Semantic Version Parsing
def check_for_updates(self, use_cache: bool = True) -> tuple[bool, str, Optional[str]]:
current_version = self._get_current_version()
latest_version = self._fetch_latest_version(stale_cache)
if latest_version is None:
return False, current_version, None
# Use packaging.version for accurate comparison
try:
has_update = parse(latest_version) > parse(current_version)
return has_update, current_version, latest_version
except Exception:
# Version parsing error - assume no update
return False, current_version, latest_version
Current Version Detection
def _get_current_version(self) -> str:
"""Get current version from package metadata"""
try:
from importlib.metadata import version
return version(self.package_name)
except Exception:
return "0.0.0" # Fallback for development/unknown versions
Cache File Format
Cache Structure
{
"latest_version": "1.2.3",
"current_version": "1.2.0",
"last_check": "2025-01-15T10:30:00.123456+00:00",
"etag": "\"W/\\\"abc123def456\\\"\""
}
Cache Validation
- Time-based: Cache expires after 15 minutes
- Version-based: Cache invalidated if current version changes
- ETag-based: Server determines if content changed
Error Handling and Resilience
Network Error Recovery
try:
response = client.get(self.api_url, headers=headers)
# Handle response...
except (httpx.RequestError, httpx.HTTPStatusError, KeyError, json.JSONDecodeError):
logging.warning("PyPI version check failed; using cached version if available")
if cached_data:
return cached_data.get("latest_version")
Graceful Degradation
- Network Failures: Fall back to cached versions
- Malformed Responses: Log warnings and use cache
- Version Parsing Errors: Assume no update available
- Cache Corruption: Silently rebuild cache on next request
Cache Information and Debugging
Get Cache Details
cache_info = checker.get_cache_info()
if cache_info:
print(f"Last check: {cache_info['last_check']}")
print(f"Cache age: {cache_info['cache_age_hours']:.1f} hours")
print(f"Cache file: {cache_info['cache_file']}")
print(f"Latest version: {cache_info['latest_version']}")
print(f"ETag: {cache_info.get('etag', 'none')}")
else:
print("No cache data available")
Cache Age Calculation
cache_age_hours = (
datetime.now(timezone.utc) -
datetime.fromisoformat(cache_data["last_check"])
).total_seconds() / 3600
Configuration and Customization
Customizable Parameters
# Custom package name
checker = PyPIVersionChecker(package_name="my-package")
# Cache configuration (hardcoded but customizable)
self.cache_duration = timedelta(minutes=15)
self.api_url = f"https://pypi.org/pypi/{package_name}/json"
User Agent Formatting
self.user_agent = f"specifyx/{self._get_current_version()} (pypi-update-checker)"
# Results in: "specifyx/1.2.0 (pypi-update-checker)"
Integration Points
The Version Checker integrates with:
- Update Service: Provides version checking backend
- httpx: Modern HTTP client for PyPI requests
- packaging: Semantic version parsing and comparison
- platformdirs: Cross-platform cache directory management
Usage Examples
Basic Version Checking
from specify_cli.services.version_checker import PyPIVersionChecker
checker = PyPIVersionChecker()
# Check for updates
has_update, current, latest = checker.check_for_updates()
if has_update:
print(f"Update available: {current} → {latest}")
else:
print(f"Current version {current} is up to date")
Cache Management
# Force fresh check
has_update, current, latest = checker.check_for_updates(use_cache=False)
# Clear cache and check again
checker.clear_cache()
has_update, current, latest = checker.check_for_updates()
# Get cache information
cache_info = checker.get_cache_info()
if cache_info:
print(f"Cache last updated: {cache_info['last_check']}")
print(f"Cache age: {cache_info['cache_age_hours']:.1f} hours")
Custom Package Checking
# Check different package
other_checker = PyPIVersionChecker(package_name="some-other-package")
latest = other_checker.get_latest_version()
print(f"Latest version of some-other-package: {latest}")
Offline-Aware Usage
# Check with graceful offline handling
try:
has_update, current, latest = checker.check_for_updates()
if latest is None:
print("Unable to check for updates (offline?)")
elif has_update:
print(f"Update available: {current} → {latest}")
else:
print("Up to date")
except Exception as e:
print(f"Version check failed: {e}")
Performance Characteristics
Network Efficiency
- ETag Requests: Save bandwidth with 304 Not Modified responses
- Cache Duration: 15-minute cache reduces API load
- Request Timeout: 10-second timeout prevents hanging
- User Agent: Proper identification for PyPI analytics
Response Times
- Cache Hit: Instant response from cached data
- ETag 304: ~100-200ms for conditional request
- Full Fetch: ~500-1000ms for complete PyPI response
- Network Failure: ~10s timeout + instant cache fallback
Storage Usage
- Cache File: ~200 bytes per package
- Cache Directory: Shared with other SpecifyX cache data
- Cleanup: Old cache files automatically replaced
Security Considerations
- HTTPS Only: All PyPI requests use HTTPS
- No Code Execution: Only parses JSON responses
- Input Validation: Package names validated through PyPI API
- Timeout Protection: Prevents hanging on slow networks
- Error Boundaries: Exceptions contained and logged
The Version Checker provides efficient, reliable, and user-friendly version checking that respects both PyPI's resources and user bandwidth while ensuring timely update notifications.