| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- """
- Version management for Dune Weaver
- Handles current version reading and GitHub API integration for latest version checking
- """
- import asyncio
- import aiohttp
- import json
- import os
- import time
- from pathlib import Path
- from typing import Dict, Optional
- import logging
- logger = logging.getLogger(__name__)
- class VersionManager:
- def __init__(self):
- self.repo_owner = "tuanchris"
- self.repo_name = "dune-weaver"
- self.github_api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
- self._current_version = None
- # Caching for GitHub API to avoid rate limits and slow requests
- self._latest_release_cache = None
- self._cache_timestamp = None
- self._cache_duration = 3600 # Cache for 1 hour (in seconds)
-
- async def get_current_version(self) -> str:
- """Read current version from VERSION file (async)"""
- if self._current_version is None:
- try:
- version_file = Path(__file__).parent.parent.parent / "VERSION"
- if version_file.exists():
- self._current_version = await asyncio.to_thread(version_file.read_text)
- self._current_version = self._current_version.strip()
- else:
- logger.warning("VERSION file not found, using default version")
- self._current_version = "1.0.0"
- except Exception as e:
- logger.error(f"Error reading VERSION file: {e}")
- self._current_version = "1.0.0"
- return self._current_version
-
- async def get_latest_release(self, force_refresh: bool = False) -> Dict[str, any]:
- """Get latest release info from GitHub API with caching"""
- # Check if we have a valid cache
- current_time = time.time()
- if not force_refresh and self._latest_release_cache is not None and self._cache_timestamp is not None:
- cache_age = current_time - self._cache_timestamp
- if cache_age < self._cache_duration:
- logger.debug(f"Returning cached version info (age: {cache_age:.0f}s)")
- return self._latest_release_cache
- # Cache miss or expired - fetch from GitHub
- logger.info("Fetching latest release from GitHub API")
- try:
- async with aiohttp.ClientSession() as session:
- async with session.get(
- f"{self.github_api_url}/releases/latest",
- timeout=aiohttp.ClientTimeout(total=10)
- ) as response:
- if response.status == 200:
- data = await response.json()
- release_data = {
- "version": data.get("tag_name", "").lstrip("v"),
- "name": data.get("name", ""),
- "published_at": data.get("published_at", ""),
- "html_url": data.get("html_url", ""),
- "body": data.get("body", ""),
- "prerelease": data.get("prerelease", False)
- }
- # Update cache
- self._latest_release_cache = release_data
- self._cache_timestamp = current_time
- logger.info(f"Cached new release info: {release_data.get('version')}")
- return release_data
- elif response.status == 404:
- # No releases found
- logger.info("No releases found on GitHub")
- return None
- else:
- logger.warning(f"GitHub API returned status {response.status}")
- # Return cached data if available, even if stale
- return self._latest_release_cache
- except asyncio.TimeoutError:
- logger.warning("Timeout while fetching latest release from GitHub")
- # Return cached data if available
- return self._latest_release_cache
- except Exception as e:
- logger.error(f"Error fetching latest release: {e}")
- # Return cached data if available
- return self._latest_release_cache
-
- def compare_versions(self, version1: str, version2: str) -> int:
- """Compare two semantic versions. Returns -1, 0, or 1"""
- try:
- # Parse semantic versions (e.g., "1.2.3")
- v1_parts = [int(x) for x in version1.split('.')]
- v2_parts = [int(x) for x in version2.split('.')]
-
- # Pad shorter version with zeros
- max_len = max(len(v1_parts), len(v2_parts))
- v1_parts.extend([0] * (max_len - len(v1_parts)))
- v2_parts.extend([0] * (max_len - len(v2_parts)))
-
- if v1_parts < v2_parts:
- return -1
- elif v1_parts > v2_parts:
- return 1
- else:
- return 0
-
- except (ValueError, AttributeError):
- logger.warning(f"Invalid version format: {version1} vs {version2}")
- return 0
-
- async def get_version_info(self, force_refresh: bool = False) -> Dict[str, any]:
- """Get complete version information
- Args:
- force_refresh: If True, bypass cache and fetch from GitHub API
- """
- current = await self.get_current_version()
- latest_release = await self.get_latest_release(force_refresh=force_refresh)
- if latest_release:
- latest = latest_release["version"]
- comparison = self.compare_versions(current, latest)
- update_available = comparison < 0
- else:
- latest = current # Fallback if no releases found
- update_available = False
- return {
- "current": current,
- "latest": latest,
- "update_available": update_available,
- "latest_release": latest_release
- }
- def clear_cache(self):
- """Clear the cached version data"""
- self._latest_release_cache = None
- self._cache_timestamp = None
- logger.info("Version cache cleared")
- # Global instance
- version_manager = VersionManager()
|