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

1# type: ignore 

2""" 

3Update checker for MoAI-ADK using PyPI API 

4 

5""" 

6 

7import json 

8import logging 

9import re 

10import urllib.request 

11from dataclasses import dataclass 

12from datetime import datetime, timedelta 

13from typing import Optional 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18@dataclass 

19class UpdateInfo: 

20 """Update information""" 

21 

22 available: bool 

23 latest_version: Optional[str] 

24 

25 

26class UpdateChecker: 

27 """Checks for MoAI-ADK updates from PyPI with 300-second caching""" 

28 

29 # Configuration 

30 _CACHE_TTL_SECONDS = 300 

31 _PYPI_API_URL = "https://pypi.org/pypi/moai-adk/json" 

32 _TIMEOUT_SECONDS = 5 

33 

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 

40 

41 def check_for_update(self, current_version: str) -> UpdateInfo: 

42 """ 

43 Check for available updates 

44 

45 Args: 

46 current_version: Current MoAI-ADK version (e.g., "0.20.1") 

47 

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 

54 

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 

59 

60 def _fetch_latest_version(self, current_version: str) -> UpdateInfo: 

61 """ 

62 Fetch latest version from PyPI API 

63 

64 Args: 

65 current_version: Current version string 

66 

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")) 

75 

76 latest_version = data.get("info", {}).get("version") 

77 

78 if not latest_version: 

79 return UpdateInfo(available=False, latest_version=None) 

80 

81 # Compare versions 

82 available = self._is_update_available(current_version, latest_version) 

83 

84 return UpdateInfo( 

85 available=available, 

86 latest_version=latest_version if available else None, 

87 ) 

88 

89 except Exception as e: 

90 logger.debug(f"Error checking for updates: {e}") 

91 return UpdateInfo(available=False, latest_version=None) 

92 

93 @staticmethod 

94 def _is_update_available(current: str, latest: str) -> bool: 

95 """ 

96 Compare versions to determine if update is available 

97 

98 Args: 

99 current: Current version string 

100 latest: Latest version string 

101 

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") 

109 

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 ] 

117 

118 # Compare version tuples 

119 return tuple(latest_parts) > tuple(current_parts) 

120 

121 except Exception as e: 

122 logger.debug(f"Error comparing versions: {e}") 

123 return False 

124 

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 

130 

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