1- # Standard
1+ #region Imports
2+ # standard imports
23import os
34import platform
45import subprocess
78import tempfile
89from pathlib import Path
910
10- # Third -party
11+ # third -party imports
1112import httpx
1213from packaging import version
1314
14- # Rich library
15+ # rich library
1516from rich .console import Console
1617from rich .panel import Panel
1718from rich .text import Text
1819from rich .prompt import Confirm
1920
20- # Local
21+ # local imports
2122from redfetch .__about__ import __version__
2223from redfetch import config
2324from diskcache import Cache
25+ #endregion Imports
2426
2527
28+ #region Module setup
29+
2630def _get_pypi_url () -> str :
2731 """Pick PyPI JSON URL, favouring `REDFETCH_PYPI_URL` if set."""
2832 env_url = os .getenv ("REDFETCH_PYPI_URL" )
@@ -41,6 +45,10 @@ def _get_pypi_url() -> str:
4145def get_current_version ():
4246 return __version__
4347
48+ #endregion Module setup
49+
50+
51+ #region Version caching
4452
4553_UPDATE_CACHE_TTL_SECONDS = 10 * 60 # 10 minutes
4654
@@ -72,6 +80,20 @@ def clear_pypi_cache() -> None:
7280 _meta_cache = None
7381
7482
83+ def fetch_latest_version_from_pypi ():
84+ response = httpx .get (PYPI_URL , timeout = 10.0 )
85+ response .raise_for_status ()
86+ data = response .json ()
87+ # On TestPyPI, prefer the highest available release (including pre-releases)
88+ if "test.pypi.org" in PYPI_URL :
89+ releases = list (data .get ("releases" , {}).keys ())
90+ if releases :
91+ releases .sort (key = version .parse )
92+ return releases [- 1 ]
93+ # Default: whatever PyPI reports as the latest stable version
94+ return data ["info" ]["version" ]
95+
96+
7597def fetch_latest_version_cached ():
7698 """Fetch latest PyPI version with a 2-hour disk-backed cache."""
7799 global _meta_cache
@@ -85,20 +107,10 @@ def fetch_latest_version_cached():
85107 _meta_cache .set (cache_key , latest , expire = _UPDATE_CACHE_TTL_SECONDS )
86108 return latest
87109
110+ #endregion Version caching
88111
89- def fetch_latest_version_from_pypi ():
90- response = httpx .get (PYPI_URL , timeout = 10.0 )
91- response .raise_for_status ()
92- data = response .json ()
93- # On TestPyPI, prefer the highest available release (including pre-releases)
94- if "test.pypi.org" in PYPI_URL :
95- releases = list (data .get ("releases" , {}).keys ())
96- if releases :
97- releases .sort (key = version .parse )
98- return releases [- 1 ]
99- # Default: whatever PyPI reports as the latest stable version
100- return data ["info" ]["version" ]
101112
113+ #region Updating
102114
103115def get_executable_path ():
104116 executable_path = os .environ .get ('PYAPP' )
@@ -167,56 +179,6 @@ def get_update_command():
167179 return commands .get (method )
168180
169181
170- def check_for_update (relaunch : bool = False ):
171- current_version = get_current_version ()
172-
173- try :
174- latest_version = fetch_latest_version_cached ()
175-
176- if version .parse (latest_version ) > version .parse (current_version ):
177- version_info = Panel (
178- Text .assemble (
179- ("An update for redfetch is available! 🚡\n \n " , "bold green" ),
180- ("Local version: " , "dim" ),
181- (f"{ current_version } \n " , "cyan" ),
182- ("Latest version: " , "dim" ),
183- (f"{ latest_version } " , "cyan bold" )
184- ),
185- title = "Update Available" ,
186- expand = False
187- )
188- console .print (version_info )
189-
190- # Handle PYAPP separately
191- if os .getenv ('PYAPP' ):
192- if Confirm .ask ("Would you like to update now?" ):
193- return self_update (relaunch = relaunch )
194- else :
195- console .print ("[yellow]Update skipped. You can manually update later.[/yellow]" )
196- return False
197-
198- # Get the appropriate update command
199- update_command = get_update_command ()
200- if not update_command :
201- console .print ("[red]Could not determine update method.[/red]" )
202- return False
203-
204- command_panel = Panel (
205- Text (" " .join (update_command ), style = "bold cyan" ),
206- title = "Update Command" ,
207- expand = False
208- )
209- console .print (command_panel )
210-
211- if Confirm .ask ("Would you like to run this command to update?" ):
212- return pip_update_redfetch (update_command , latest_version )
213- else :
214- console .print ("[yellow]Update skipped. You can manually update later.[/yellow]" )
215- except Exception as e :
216- console .print (f"[bold red]Error checking for updates:[/bold red] { e } " )
217- return False
218-
219-
220182def pip_update_redfetch (update_command , latest_version ):
221183 try :
222184 console .print (f"\n [bold]Updating redfetch to version { latest_version } ...[/bold]\n " )
@@ -289,6 +251,60 @@ def self_update(relaunch: bool = False):
289251 sys .exit (1 )
290252
291253
254+ def check_for_update (relaunch : bool = False ):
255+ current_version = get_current_version ()
256+
257+ try :
258+ latest_version = fetch_latest_version_cached ()
259+
260+ if version .parse (latest_version ) > version .parse (current_version ):
261+ version_info = Panel (
262+ Text .assemble (
263+ ("An update for redfetch is available! 🚡\n \n " , "bold green" ),
264+ ("Local version: " , "dim" ),
265+ (f"{ current_version } \n " , "cyan" ),
266+ ("Latest version: " , "dim" ),
267+ (f"{ latest_version } " , "cyan bold" )
268+ ),
269+ title = "Update Available" ,
270+ expand = False
271+ )
272+ console .print (version_info )
273+
274+ # Handle PYAPP separately
275+ if os .getenv ('PYAPP' ):
276+ if Confirm .ask ("Would you like to update now?" ):
277+ return self_update (relaunch = relaunch )
278+ else :
279+ console .print ("[yellow]Update skipped. You can manually update later.[/yellow]" )
280+ return False
281+
282+ # Get the appropriate update command
283+ update_command = get_update_command ()
284+ if not update_command :
285+ console .print ("[red]Could not determine update method.[/red]" )
286+ return False
287+
288+ command_panel = Panel (
289+ Text (" " .join (update_command ), style = "bold cyan" ),
290+ title = "Update Command" ,
291+ expand = False
292+ )
293+ console .print (command_panel )
294+
295+ if Confirm .ask ("Would you like to run this command to update?" ):
296+ return pip_update_redfetch (update_command , latest_version )
297+ else :
298+ console .print ("[yellow]Update skipped. You can manually update later.[/yellow]" )
299+ except Exception as e :
300+ console .print (f"[bold red]Error checking for updates:[/bold red] { e } " )
301+ return False
302+
303+ #endregion Updating
304+
305+
306+ #region Uninstalling
307+
292308def self_remove ():
293309 """Remove with PYAPP."""
294310 try :
@@ -377,6 +393,59 @@ def _release_disk_caches() -> None:
377393 raise RuntimeError ("; " .join (errors ))
378394
379395
396+ def generate_removal_commands (paths ):
397+ """Generate OS-specific commands to remove the given directories."""
398+ system = platform .system ()
399+ if system == 'Windows' :
400+ # Generate PowerShell commands
401+ console .print ("[bold]These directories may be removed manually after you make sure there's nothing you need from them, you can do so by running the following PowerShell commands:[/bold]\n " )
402+ commands = []
403+ for path in sorted (paths ):
404+ # Escape quotes and handle special characters
405+ escaped_path = path .replace ("'" , "''" )
406+ command = f"Remove-Item -LiteralPath '{ escaped_path } ' -Recurse -Force"
407+ commands .append (command )
408+ console .print (f" { command } " )
409+ else :
410+ # Assuming Unix-like system
411+ console .print ("[bold]You can remove these directories by running the following commands in your terminal:[/bold]\n " )
412+ commands = []
413+ for path in sorted (paths ):
414+ # Escape single quotes
415+ escaped_path = path .replace ("'" , "'\\ ''" )
416+ command = f"rm -rf '{ escaped_path } '"
417+ commands .append (command )
418+ console .print (f" { command } " )
419+ console .print ("\n [bold yellow]These directories must be removed manually.[/bold yellow]" )
420+ return commands
421+
422+
423+ def write_commands_to_file (commands , paths ):
424+ """Write the removal commands and additional information to a text file and open it on Windows."""
425+ # Only write and open the file on Windows
426+ if platform .system () == 'Windows' :
427+ file_path = os .path .join (os .path .expanduser ("~" ), "redfetch_removal_commands.txt" )
428+ with open (file_path , 'w' ) as file :
429+ file .write ("Manual Cleanup Instructions:\n " )
430+ file .write ("The following directories may contain files downloaded by redfetch. You can remove them manually if you want:\n " )
431+ for path in sorted (paths ):
432+ file .write (f" - { path } \n " )
433+ file .write ("\n Make sure there's nothing you want in them. When ready to delete, you can use:\n \n " )
434+
435+ for command in commands :
436+ file .write (command + '\n ' )
437+
438+ # Automatically open the file with the default text editor
439+ try :
440+ os .startfile (file_path )
441+ except Exception as e :
442+ console .print (f"[red]Failed to open the file: { e } [/red]" )
443+ console .print (f"Please open the file manually: [cyan]{ file_path } [/cyan]" )
444+ else :
445+ # On non-Windows systems, the important information is already printed to the console
446+ console .print ("[yellow]After that, you can remove the redfetch package.[/yellow]" )
447+
448+
380449def uninstall ():
381450 """Guide the user through the uninstallation process."""
382451 # Import the logout function from auth module
@@ -530,55 +599,4 @@ def should_print_path(path):
530599 # Optionally, exit the program
531600 sys .exit (0 )
532601
533-
534- def generate_removal_commands (paths ):
535- """Generate OS-specific commands to remove the given directories."""
536- system = platform .system ()
537- if system == 'Windows' :
538- # Generate PowerShell commands
539- console .print ("[bold]These directories may be removed manually after you make sure there's nothing you need from them, you can do so by running the following PowerShell commands:[/bold]\n " )
540- commands = []
541- for path in sorted (paths ):
542- # Escape quotes and handle special characters
543- escaped_path = path .replace ("'" , "''" )
544- command = f"Remove-Item -LiteralPath '{ escaped_path } ' -Recurse -Force"
545- commands .append (command )
546- console .print (f" { command } " )
547- else :
548- # Assuming Unix-like system
549- console .print ("[bold]You can remove these directories by running the following commands in your terminal:[/bold]\n " )
550- commands = []
551- for path in sorted (paths ):
552- # Escape single quotes
553- escaped_path = path .replace ("'" , "'\\ ''" )
554- command = f"rm -rf '{ escaped_path } '"
555- commands .append (command )
556- console .print (f" { command } " )
557- console .print ("\n [bold yellow]These directories must be removed manually.[/bold yellow]" )
558- return commands
559-
560-
561- def write_commands_to_file (commands , paths ):
562- """Write the removal commands and additional information to a text file and open it on Windows."""
563- # Only write and open the file on Windows
564- if platform .system () == 'Windows' :
565- file_path = os .path .join (os .path .expanduser ("~" ), "redfetch_removal_commands.txt" )
566- with open (file_path , 'w' ) as file :
567- file .write ("Manual Cleanup Instructions:\n " )
568- file .write ("The following directories may contain files downloaded by redfetch. You can remove them manually if you want:\n " )
569- for path in sorted (paths ):
570- file .write (f" - { path } \n " )
571- file .write ("\n Make sure there's nothing you want in them. When ready to delete, you can use:\n \n " )
572-
573- for command in commands :
574- file .write (command + '\n ' )
575-
576- # Automatically open the file with the default text editor
577- try :
578- os .startfile (file_path )
579- except Exception as e :
580- console .print (f"[red]Failed to open the file: { e } [/red]" )
581- console .print (f"Please open the file manually: [cyan]{ file_path } [/cyan]" )
582- else :
583- # On non-Windows systems, the important information is already printed to the console
584- console .print ("[yellow]After that, you can remove the redfetch package.[/yellow]" )
602+ #endregion Uninstalling
0 commit comments