Coverage for src / moai_adk / statusline / update_checker.py: 37.50%
56 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1# type: ignore
2"""
3Update checker for MoAI-ADK using PyPI API
5"""
7import json
8import logging
9import re
10import urllib.request
11from dataclasses import dataclass
12from datetime import datetime, timedelta
13from typing import Optional
15logger = logging.getLogger(__name__)
18@dataclass
19class UpdateInfo:
20 """Update information"""
22 available: bool
23 latest_version: Optional[str]
26class UpdateChecker:
27 """Checks for MoAI-ADK updates from PyPI with 300-second caching"""
29 # Configuration
30 _CACHE_TTL_SECONDS = 300
31 _PYPI_API_URL = "https://pypi.org/pypi/moai-adk/json"
32 _TIMEOUT_SECONDS = 5
34 def __init__(self):
35 """Initialize update checker"""
36 self._cached_info: Optional[UpdateInfo] = None
37 self._cache_time: Optional[datetime] = None
38 self._cache_ttl = timedelta(seconds=self._CACHE_TTL_SECONDS)
39 self._cached_version: Optional[str] = None
41 def check_for_update(self, current_version: str) -> UpdateInfo:
42 """
43 Check for available updates
45 Args:
46 current_version: Current MoAI-ADK version (e.g., "0.20.1")
48 Returns:
49 UpdateInfo with availability and latest version
50 """
51 # Check cache validity (same version)
52 if self._is_cache_valid() and self._cached_version == current_version:
53 return self._cached_info
55 # Fetch latest version from PyPI
56 update_info = self._fetch_latest_version(current_version)
57 self._update_cache_with(update_info, current_version)
58 return update_info
60 def _fetch_latest_version(self, current_version: str) -> UpdateInfo:
61 """
62 Fetch latest version from PyPI API
64 Args:
65 current_version: Current version string
67 Returns:
68 UpdateInfo from PyPI or error default
69 """
70 try:
71 with urllib.request.urlopen(
72 self._PYPI_API_URL, timeout=self._TIMEOUT_SECONDS
73 ) as response:
74 data = json.loads(response.read().decode("utf-8"))
76 latest_version = data.get("info", {}).get("version")
78 if not latest_version:
79 return UpdateInfo(available=False, latest_version=None)
81 # Compare versions
82 available = self._is_update_available(current_version, latest_version)
84 return UpdateInfo(
85 available=available,
86 latest_version=latest_version if available else None,
87 )
89 except Exception as e:
90 logger.debug(f"Error checking for updates: {e}")
91 return UpdateInfo(available=False, latest_version=None)
93 @staticmethod
94 def _is_update_available(current: str, latest: str) -> bool:
95 """
96 Compare versions to determine if update is available
98 Args:
99 current: Current version string
100 latest: Latest version string
102 Returns:
103 True if update is available
104 """
105 try:
106 # Parse version strings (remove 'v' prefix)
107 current_clean = current.lstrip("v")
108 latest_clean = latest.lstrip("v")
110 # Split by dots and convert to integers
111 current_parts = [
112 int(x) for x in re.split(r"[^\d]+", current_clean) if x.isdigit()
113 ]
114 latest_parts = [
115 int(x) for x in re.split(r"[^\d]+", latest_clean) if x.isdigit()
116 ]
118 # Compare version tuples
119 return tuple(latest_parts) > tuple(current_parts)
121 except Exception as e:
122 logger.debug(f"Error comparing versions: {e}")
123 return False
125 def _is_cache_valid(self) -> bool:
126 """Check if update cache is still valid"""
127 if self._cached_info is None or self._cache_time is None:
128 return False
129 return datetime.now() - self._cache_time < self._cache_ttl
131 def _update_cache_with(self, update_info: UpdateInfo, version: str) -> None:
132 """Update cache with update info"""
133 self._cached_info = update_info
134 self._cache_time = datetime.now()
135 self._cached_version = version