version_manager.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. """
  2. Version management for Dune Weaver
  3. Handles current version reading and GitHub API integration for latest version checking
  4. """
  5. import asyncio
  6. import aiohttp
  7. import json
  8. import os
  9. import time
  10. from pathlib import Path
  11. from typing import Dict, Optional
  12. import logging
  13. logger = logging.getLogger(__name__)
  14. class VersionManager:
  15. def __init__(self):
  16. self.repo_owner = "tuanchris"
  17. self.repo_name = "dune-weaver"
  18. self.github_api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
  19. self._current_version = None
  20. # Caching for GitHub API to avoid rate limits and slow requests
  21. self._latest_release_cache = None
  22. self._cache_timestamp = None
  23. self._cache_duration = 3600 # Cache for 1 hour (in seconds)
  24. async def get_current_version(self) -> str:
  25. """Read current version from VERSION file (async)"""
  26. if self._current_version is None:
  27. try:
  28. version_file = Path(__file__).parent.parent.parent / "VERSION"
  29. if version_file.exists():
  30. self._current_version = await asyncio.to_thread(version_file.read_text)
  31. self._current_version = self._current_version.strip()
  32. else:
  33. logger.warning("VERSION file not found, using default version")
  34. self._current_version = "1.0.0"
  35. except Exception as e:
  36. logger.error(f"Error reading VERSION file: {e}")
  37. self._current_version = "1.0.0"
  38. return self._current_version
  39. async def get_latest_release(self, force_refresh: bool = False) -> Dict[str, any]:
  40. """Get latest release info from GitHub API with caching"""
  41. # Check if we have a valid cache
  42. current_time = time.time()
  43. if not force_refresh and self._latest_release_cache is not None and self._cache_timestamp is not None:
  44. cache_age = current_time - self._cache_timestamp
  45. if cache_age < self._cache_duration:
  46. logger.debug(f"Returning cached version info (age: {cache_age:.0f}s)")
  47. return self._latest_release_cache
  48. # Cache miss or expired - fetch from GitHub
  49. logger.info("Fetching latest release from GitHub API")
  50. try:
  51. async with aiohttp.ClientSession() as session:
  52. async with session.get(
  53. f"{self.github_api_url}/releases/latest",
  54. timeout=aiohttp.ClientTimeout(total=10)
  55. ) as response:
  56. if response.status == 200:
  57. data = await response.json()
  58. release_data = {
  59. "version": data.get("tag_name", "").lstrip("v"),
  60. "name": data.get("name", ""),
  61. "published_at": data.get("published_at", ""),
  62. "html_url": data.get("html_url", ""),
  63. "body": data.get("body", ""),
  64. "prerelease": data.get("prerelease", False)
  65. }
  66. # Update cache
  67. self._latest_release_cache = release_data
  68. self._cache_timestamp = current_time
  69. logger.info(f"Cached new release info: {release_data.get('version')}")
  70. return release_data
  71. elif response.status == 404:
  72. # No releases found
  73. logger.info("No releases found on GitHub")
  74. return None
  75. else:
  76. logger.warning(f"GitHub API returned status {response.status}")
  77. # Return cached data if available, even if stale
  78. return self._latest_release_cache
  79. except asyncio.TimeoutError:
  80. logger.warning("Timeout while fetching latest release from GitHub")
  81. # Return cached data if available
  82. return self._latest_release_cache
  83. except Exception as e:
  84. logger.error(f"Error fetching latest release: {e}")
  85. # Return cached data if available
  86. return self._latest_release_cache
  87. def compare_versions(self, version1: str, version2: str) -> int:
  88. """Compare two semantic versions. Returns -1, 0, or 1"""
  89. try:
  90. # Parse semantic versions (e.g., "1.2.3")
  91. v1_parts = [int(x) for x in version1.split('.')]
  92. v2_parts = [int(x) for x in version2.split('.')]
  93. # Pad shorter version with zeros
  94. max_len = max(len(v1_parts), len(v2_parts))
  95. v1_parts.extend([0] * (max_len - len(v1_parts)))
  96. v2_parts.extend([0] * (max_len - len(v2_parts)))
  97. if v1_parts < v2_parts:
  98. return -1
  99. elif v1_parts > v2_parts:
  100. return 1
  101. else:
  102. return 0
  103. except (ValueError, AttributeError):
  104. logger.warning(f"Invalid version format: {version1} vs {version2}")
  105. return 0
  106. async def get_version_info(self, force_refresh: bool = False) -> Dict[str, any]:
  107. """Get complete version information
  108. Args:
  109. force_refresh: If True, bypass cache and fetch from GitHub API
  110. """
  111. current = await self.get_current_version()
  112. latest_release = await self.get_latest_release(force_refresh=force_refresh)
  113. if latest_release:
  114. latest = latest_release["version"]
  115. comparison = self.compare_versions(current, latest)
  116. update_available = comparison < 0
  117. else:
  118. latest = current # Fallback if no releases found
  119. update_available = False
  120. return {
  121. "current": current,
  122. "latest": latest,
  123. "update_available": update_available,
  124. "latest_release": latest_release
  125. }
  126. def clear_cache(self):
  127. """Clear the cached version data"""
  128. self._latest_release_cache = None
  129. self._cache_timestamp = None
  130. logger.info("Version cache cleared")
  131. # Global instance
  132. version_manager = VersionManager()