Coverage for src/package/updates.py: 100.00%
82 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 21:20 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 21:20 +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
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 ) -> None:
158 # Modules libraries, pylint: disable=import-outside-toplevel
159 from update_checker import pretty_date
161 # Create message box
162 box = Boxes()
164 # Acquire current version
165 version = Version.get()
167 # Detect package installer
168 package_install: str = ''
169 if Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path():
170 package_install = 'pipx upgrade' # pragma: no cover
171 else:
172 package_install = 'pip3 install -U' # pragma: no cover
174 # Detect package ownership
175 writable = access(__file__, W_OK)
176 if Platform.IS_USER_SUDO or not writable:
177 package_install = 'sudo ' + package_install # pragma: no cover
179 # Prepare package specification
180 package_specification: str = ''
181 if 'pipx' in package_install:
182 package_specification = f'{name}' # pragma: no cover
183 else:
184 package_specification = f'{name}>={available}' # pragma: no cover
186 # Evaluate same version
187 same = available and available == version
189 # Version message prefix
190 version_outdated = not offline and available and not older and not same
191 version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \
192 f' {Colors.RED if version_outdated else Colors.GREEN}{version}'
194 # Offline version message
195 if offline:
196 box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down')
198 # Updated version message
199 elif same:
200 assert date is not None
201 box.add(
202 f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!'
203 )
205 # Older version message
206 elif older:
207 assert date is not None
208 box.add(
209 f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \
210 f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!'
211 )
213 # Newer version message
214 else:
215 assert date is not None
216 box.add(
217 f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \
218 f' to {Colors.GREEN}{available}{Colors.BOLD}!'
219 )
221 # Changelog message
222 box.add(
223 f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases'
224 )
226 # Update message
227 if available:
228 box.add(
229 f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \
230 f"Run {Colors.GREEN}{package_install} '{package_specification}'"
231 )
233 # Print message box
234 box.print()