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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 10:53 +0000
1#!/usr/bin/env python3
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
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.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
90 # Check if not offline
91 if not environ.get(Bundle.ENV_DEBUG_UPDATES_OFFLINE, ''):
93 # Modules libraries, pylint: disable=import-outside-toplevel
94 from update_checker import UpdateChecker
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
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
112 # Older offline failure
113 if older:
115 # Show offline message
116 Updates.message(
117 name=self.__name,
118 offline=True,
119 )
120 return True
122 # Result
123 return False
125 # Daily
126 @property
127 def daily(self) -> bool:
129 # Acquire updates check last timestamp
130 last = self.__settings.get('updates', 'last_timestamp')
132 # Fake test updates
133 if Bundle.ENV_DEBUG_UPDATES_DAILY in environ:
134 last = None
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
143 # Default fallback
144 return False
146 # Enabled
147 @property
148 def enabled(self) -> bool:
149 return self.__enabled
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:
161 # Modules libraries, pylint: disable=import-outside-toplevel
162 from update_checker import pretty_date
164 # Create message box
165 box = Boxes()
167 # Acquire current version
168 version = Version.get()
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
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
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
189 # Evaluate same version
190 same = available and available == version
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}'
197 # Offline version message
198 if offline:
199 box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down')
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 )
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 )
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 )
224 # Changelog message
225 box.add(
226 f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases'
227 )
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 )
236 # Print message box
237 box.print()