Coverage for src/package/updates.py: 100.00%
85 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-18 15:02 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-18 15:02 +0000
1#!/usr/bin/env python3
3# Standard libraries
4from datetime import datetime
5from os import access, environ, W_OK
6from time import localtime, strftime, time
7from typing import Optional
9# Modules libraries
10from packaging.version import Version as PackageVersion
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
20# Updates class
21class Updates:
23 # Members
24 __enabled: bool
25 __name: str
26 __settings: Settings
28 # Constructor
29 def __init__(
30 self,
31 name: str,
32 settings: Settings,
33 ) -> None:
35 # Initialize members
36 self.__name = name
37 self.__settings = settings
39 # Detect migration
40 self.__migration()
42 # Prepare enabled
43 if not self.__settings.has('updates', 'enabled'):
44 self.__settings.set_bool('updates', 'enabled', True)
46 # Check enabled
47 self.__enabled = bool(self.__settings.get_bool('updates', 'enabled')) and \
48 not environ.get(Bundle.ENV_DEBUG_UPDATES_DISABLE, '')
50 # Migration
51 def __migration(self) -> None:
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'
59 # Refresh package version
60 if not package_version or current_version != package_version:
61 self.__settings.set('package', 'version', current_version)
63 # Checker
64 def check(
65 self,
66 older: bool = False,
67 ) -> bool:
69 # Reference version
70 version = '0.0.0' if older else Version.get()
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):
77 # Show updates message
78 release_date = datetime.utcfromtimestamp(Bundle.RELEASE_FIRST_TIMESTAMP)
79 Updates.message(
80 name=self.__name,
81 older=older,
82 available=available,
83 date=release_date,
84 )
85 return True
87 # Check if not offline
88 if not environ.get(Bundle.ENV_DEBUG_UPDATES_OFFLINE, ''):
90 # Modules libraries, pylint: disable=import-outside-toplevel
91 from update_checker import UpdateChecker
93 # Check for updates
94 check = UpdateChecker(bypass_cache=True).check(
95 package_name=self.__name,
96 package_version=version,
97 )
98 if check: # pragma: no cover
100 # Show updates message
101 Updates.message(
102 name=self.__name,
103 older=older,
104 available=check.available_version,
105 date=check.release_date,
106 )
107 return True
109 # Older offline failure
110 if older:
112 # Show offline message
113 Updates.message(
114 name=self.__name,
115 offline=True,
116 )
117 return True
119 # Result
120 return False
122 # Daily
123 @property
124 def daily(self) -> bool:
126 # Acquire updates check last timestamp
127 last = self.__settings.get('updates', 'last_timestamp')
129 # Fake test updates
130 if Bundle.ENV_DEBUG_UPDATES_DAILY in environ:
131 last = None
133 # Handle daily checks
134 current = int(time())
135 if not last or strftime('%Y-%m-%d', localtime(current)) != strftime(
136 '%Y-%m-%d', localtime(int(last))):
137 self.__settings.set('updates', 'last_timestamp', current)
138 return True
140 # Default fallback
141 return False
143 # Enabled
144 @property
145 def enabled(self) -> bool:
146 return self.__enabled
148 # Message, pylint: disable=too-many-arguments,too-many-positional-arguments
149 @staticmethod
150 def message(
151 name: str,
152 offline: bool = False,
153 older: bool = False,
154 available: Optional[str] = None,
155 date: Optional[datetime] = None,
156 recommended: Optional[str] = None,
157 ) -> None:
159 # Modules libraries, pylint: disable=import-outside-toplevel
160 from update_checker import pretty_date
162 # Create message box
163 box = Boxes()
165 # Acquire current version
166 version = Version.get()
168 # Detect package installer
169 package_install: str = ''
170 if Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path():
171 package_install = 'pipx upgrade' # pragma: no cover
172 else:
173 package_install = 'pip3 install -U' # pragma: no cover
175 # Detect package ownership
176 writable = access(__file__, W_OK)
177 if Platform.IS_USER_SUDO or not writable:
178 package_install = 'sudo ' + package_install # pragma: no cover
180 # Prepare package specification
181 package_specification: str = ''
182 if 'pipx' in package_install:
183 package_specification = f'{name}' # pragma: no cover
184 else:
185 package_specification = f'{name}>={available}' # pragma: no cover
187 # Evaluate same version
188 same = available and available == version
190 # Version message prefix
191 version_outdated = not offline and available and not older and not same
192 version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \
193 f' {Colors.RED if version_outdated else Colors.GREEN}{version}'
195 # Recommended version message
196 if recommended:
197 available = recommended
198 box.add(f'{version_prefix} {Colors.BOLD}older than recommended version '
199 f'{Colors.RED}{recommended}{Colors.BOLD}')
201 # Offline version message
202 elif offline:
203 box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down')
205 # Updated version message
206 elif same:
207 assert date is not None
208 box.add(
209 f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!'
210 )
212 # Older version message
213 elif older:
214 assert date is not None
215 box.add(
216 f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \
217 f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!'
218 )
220 # Newer version message
221 else:
222 assert date is not None
223 box.add(
224 f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \
225 f' to {Colors.GREEN}{available}{Colors.BOLD}!'
226 )
228 # Changelog message
229 box.add(
230 f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases'
231 )
233 # Update message
234 if available:
235 box.add(
236 f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \
237 f"Run {Colors.GREEN}{package_install} '{package_specification}'"
238 )
240 # Print message box
241 box.print()