Coverage for src/package/updates.py: 100.00%

82 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-22 10:53 +0000

1#!/usr/bin/env python3 

2 

3# Standard libraries 

4from datetime import datetime, timezone 

5from os import access, environ, W_OK 

6from time import localtime, strftime, time 

7from typing import Optional 

8 

9# Modules libraries 

10from packaging.version import Version as PackageVersion 

11 

12# Components 

13from ..prints.boxes import Boxes 

14from ..prints.colors import Colors 

15from ..system.platform import Platform 

16from .bundle import Bundle 

17from .settings import Settings 

18from .version import Version 

19 

20# Updates class 

21class Updates: 

22 

23 # Members 

24 __enabled: bool 

25 __name: str 

26 __settings: Settings 

27 

28 # Constructor 

29 def __init__( 

30 self, 

31 name: str, 

32 settings: Settings, 

33 ) -> None: 

34 

35 # Initialize members 

36 self.__name = name 

37 self.__settings = settings 

38 

39 # Detect migration 

40 self.__migration() 

41 

42 # Prepare enabled 

43 if not self.__settings.has('updates', 'enabled'): 

44 self.__settings.set_bool('updates', 'enabled', True) 

45 

46 # Check enabled 

47 self.__enabled = bool(self.__settings.get_bool('updates', 'enabled')) and \ 

48 not environ.get(Bundle.ENV_DEBUG_UPDATES_DISABLE, '') 

49 

50 # Migration 

51 def __migration(self) -> None: 

52 

53 # Acquire versions 

54 current_version = Version.get() 

55 package_version = self.__settings.get('package', 'version') 

56 if not package_version: 

57 package_version = '0.0.0' 

58 

59 # Refresh package version 

60 if not package_version or current_version != package_version: 

61 self.__settings.set('package', 'version', current_version) 

62 

63 # Checker 

64 def check( 

65 self, 

66 older: bool = False, 

67 ) -> bool: 

68 

69 # Reference version 

70 version = '0.0.0' if older else Version.get() 

71 

72 # Fake test updates 

73 if Bundle.ENV_DEBUG_UPDATES_FAKE in environ: 

74 available = environ[Bundle.ENV_DEBUG_UPDATES_FAKE] 

75 if PackageVersion(available) >= PackageVersion(version): 

76 

77 # Show updates message 

78 release_date = datetime.fromtimestamp( 

79 Bundle.RELEASE_FIRST_TIMESTAMP, 

80 timezone.utc, 

81 ) 

82 Updates.message( 

83 name=self.__name, 

84 older=older, 

85 available=available, 

86 date=release_date, 

87 ) 

88 return True 

89 

90 # Check if not offline 

91 if not environ.get(Bundle.ENV_DEBUG_UPDATES_OFFLINE, ''): 

92 

93 # Modules libraries, pylint: disable=import-outside-toplevel 

94 from update_checker import UpdateChecker 

95 

96 # Check for updates 

97 check = UpdateChecker(bypass_cache=True).check( 

98 package_name=self.__name, 

99 package_version=version, 

100 ) 

101 if check: # pragma: no cover 

102 

103 # Show updates message 

104 Updates.message( 

105 name=self.__name, 

106 older=older, 

107 available=check.available_version, 

108 date=check.release_date, 

109 ) 

110 return True 

111 

112 # Older offline failure 

113 if older: 

114 

115 # Show offline message 

116 Updates.message( 

117 name=self.__name, 

118 offline=True, 

119 ) 

120 return True 

121 

122 # Result 

123 return False 

124 

125 # Daily 

126 @property 

127 def daily(self) -> bool: 

128 

129 # Acquire updates check last timestamp 

130 last = self.__settings.get('updates', 'last_timestamp') 

131 

132 # Fake test updates 

133 if Bundle.ENV_DEBUG_UPDATES_DAILY in environ: 

134 last = None 

135 

136 # Handle daily checks 

137 current = int(time()) 

138 if not last or strftime('%Y-%m-%d', localtime(current)) != strftime( 

139 '%Y-%m-%d', localtime(int(last))): 

140 self.__settings.set('updates', 'last_timestamp', current) 

141 return True 

142 

143 # Default fallback 

144 return False 

145 

146 # Enabled 

147 @property 

148 def enabled(self) -> bool: 

149 return self.__enabled 

150 

151 # Message, pylint: disable=too-many-arguments 

152 @staticmethod 

153 def message( 

154 name: str, 

155 offline: bool = False, 

156 older: bool = False, 

157 available: Optional[str] = None, 

158 date: Optional[datetime] = None, 

159 ) -> None: 

160 

161 # Modules libraries, pylint: disable=import-outside-toplevel 

162 from update_checker import pretty_date 

163 

164 # Create message box 

165 box = Boxes() 

166 

167 # Acquire current version 

168 version = Version.get() 

169 

170 # Detect package installer 

171 package_install: str = '' 

172 if Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path(): 

173 package_install = 'pipx upgrade' # pragma: no cover 

174 else: 

175 package_install = 'pip3 install -U' # pragma: no cover 

176 

177 # Detect package ownership 

178 writable = access(__file__, W_OK) 

179 if Platform.IS_USER_SUDO or not writable: 

180 package_install = 'sudo ' + package_install # pragma: no cover 

181 

182 # Prepare package specification 

183 package_specification: str = '' 

184 if 'pipx' in package_install: 

185 package_specification = f'{name}' # pragma: no cover 

186 else: 

187 package_specification = f'{name}>={available}' # pragma: no cover 

188 

189 # Evaluate same version 

190 same = available and available == version 

191 

192 # Version message prefix 

193 version_outdated = not offline and available and not older and not same 

194 version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \ 

195 f' {Colors.RED if version_outdated else Colors.GREEN}{version}' 

196 

197 # Offline version message 

198 if offline: 

199 box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down') 

200 

201 # Updated version message 

202 elif same: 

203 assert date is not None 

204 box.add( 

205 f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!' 

206 ) 

207 

208 # Older version message 

209 elif older: 

210 assert date is not None 

211 box.add( 

212 f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \ 

213 f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!' 

214 ) 

215 

216 # Newer version message 

217 else: 

218 assert date is not None 

219 box.add( 

220 f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \ 

221 f' to {Colors.GREEN}{available}{Colors.BOLD}!' 

222 ) 

223 

224 # Changelog message 

225 box.add( 

226 f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases' 

227 ) 

228 

229 # Update message 

230 if available: 

231 box.add( 

232 f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \ 

233 f"Run {Colors.GREEN}{package_install} '{package_specification}'" 

234 ) 

235 

236 # Print message box 

237 box.print()