stouputils 1.14.3__py3-none-any.whl → 1.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. stouputils/data_science/config/get.py +51 -51
  2. stouputils/data_science/data_processing/image/__init__.py +66 -66
  3. stouputils/data_science/data_processing/image/auto_contrast.py +79 -79
  4. stouputils/data_science/data_processing/image/axis_flip.py +58 -58
  5. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -74
  6. stouputils/data_science/data_processing/image/binary_threshold.py +73 -73
  7. stouputils/data_science/data_processing/image/blur.py +59 -59
  8. stouputils/data_science/data_processing/image/brightness.py +54 -54
  9. stouputils/data_science/data_processing/image/canny.py +110 -110
  10. stouputils/data_science/data_processing/image/clahe.py +92 -92
  11. stouputils/data_science/data_processing/image/common.py +30 -30
  12. stouputils/data_science/data_processing/image/contrast.py +53 -53
  13. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -74
  14. stouputils/data_science/data_processing/image/denoise.py +378 -378
  15. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -123
  16. stouputils/data_science/data_processing/image/invert.py +64 -64
  17. stouputils/data_science/data_processing/image/laplacian.py +60 -60
  18. stouputils/data_science/data_processing/image/median_blur.py +52 -52
  19. stouputils/data_science/data_processing/image/noise.py +59 -59
  20. stouputils/data_science/data_processing/image/normalize.py +65 -65
  21. stouputils/data_science/data_processing/image/random_erase.py +66 -66
  22. stouputils/data_science/data_processing/image/resize.py +69 -69
  23. stouputils/data_science/data_processing/image/rotation.py +80 -80
  24. stouputils/data_science/data_processing/image/salt_pepper.py +68 -68
  25. stouputils/data_science/data_processing/image/sharpening.py +55 -55
  26. stouputils/data_science/data_processing/image/shearing.py +64 -64
  27. stouputils/data_science/data_processing/image/threshold.py +64 -64
  28. stouputils/data_science/data_processing/image/translation.py +71 -71
  29. stouputils/data_science/data_processing/image/zoom.py +83 -83
  30. stouputils/data_science/data_processing/image_augmentation.py +118 -118
  31. stouputils/data_science/data_processing/image_preprocess.py +183 -183
  32. stouputils/data_science/data_processing/prosthesis_detection.py +359 -359
  33. stouputils/data_science/data_processing/technique.py +481 -481
  34. stouputils/data_science/dataset/__init__.py +45 -45
  35. stouputils/data_science/dataset/dataset.py +292 -292
  36. stouputils/data_science/dataset/dataset_loader.py +135 -135
  37. stouputils/data_science/dataset/grouping_strategy.py +296 -296
  38. stouputils/data_science/dataset/image_loader.py +100 -100
  39. stouputils/data_science/dataset/xy_tuple.py +696 -696
  40. stouputils/data_science/metric_dictionnary.py +106 -106
  41. stouputils/data_science/mlflow_utils.py +206 -206
  42. stouputils/data_science/models/abstract_model.py +149 -149
  43. stouputils/data_science/models/all.py +85 -85
  44. stouputils/data_science/models/keras/all.py +38 -38
  45. stouputils/data_science/models/keras/convnext.py +62 -62
  46. stouputils/data_science/models/keras/densenet.py +50 -50
  47. stouputils/data_science/models/keras/efficientnet.py +60 -60
  48. stouputils/data_science/models/keras/mobilenet.py +56 -56
  49. stouputils/data_science/models/keras/resnet.py +52 -52
  50. stouputils/data_science/models/keras/squeezenet.py +233 -233
  51. stouputils/data_science/models/keras/vgg.py +42 -42
  52. stouputils/data_science/models/keras/xception.py +38 -38
  53. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -20
  54. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -219
  55. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -148
  56. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -31
  57. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -249
  58. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -66
  59. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -12
  60. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -56
  61. stouputils/data_science/models/keras_utils/visualizations.py +416 -416
  62. stouputils/data_science/models/sandbox.py +116 -116
  63. stouputils/data_science/range_tuple.py +234 -234
  64. stouputils/data_science/utils.py +285 -285
  65. stouputils/decorators.py +53 -39
  66. stouputils/decorators.pyi +2 -2
  67. stouputils/installer/__init__.py +18 -18
  68. stouputils/installer/linux.py +144 -144
  69. stouputils/installer/main.py +223 -223
  70. stouputils/installer/windows.py +136 -136
  71. stouputils/io.py +16 -9
  72. stouputils/parallel.py +88 -2
  73. stouputils/parallel.pyi +21 -1
  74. stouputils/print.py +229 -2
  75. stouputils/print.pyi +90 -1
  76. stouputils/py.typed +1 -1
  77. {stouputils-1.14.3.dist-info → stouputils-1.15.1.dist-info}/METADATA +1 -1
  78. {stouputils-1.14.3.dist-info → stouputils-1.15.1.dist-info}/RECORD +80 -80
  79. {stouputils-1.14.3.dist-info → stouputils-1.15.1.dist-info}/WHEEL +1 -1
  80. {stouputils-1.14.3.dist-info → stouputils-1.15.1.dist-info}/entry_points.txt +0 -0
stouputils/decorators.py CHANGED
@@ -198,7 +198,7 @@ def timeout(
198
198
  >>> slow_function() # Raises TimeoutError after 2 seconds
199
199
  Traceback (most recent call last):
200
200
  ...
201
- TimeoutError: Function 'slow_function' timed out after 2.0 seconds
201
+ TimeoutError: Function 'slow_function()' timed out after 2.0 seconds
202
202
 
203
203
  >>> @timeout(seconds=1.0, message="Custom timeout message")
204
204
  ... def another_slow_function():
@@ -209,59 +209,73 @@ def timeout(
209
209
  TimeoutError: Custom timeout message
210
210
  """
211
211
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
212
+ # Check if we can use signal-based timeout (Unix only)
213
+ import os
214
+ use_signal: bool = os.name != 'nt' # Not Windows
215
+
216
+ if use_signal:
217
+ try:
218
+ import signal
219
+ # Verify SIGALRM is available
220
+ use_signal = hasattr(signal, 'SIGALRM')
221
+ except ImportError:
222
+ use_signal = False
223
+
212
224
  @wraps(func)
213
225
  def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
214
226
  # Build timeout message
215
227
  msg: str = message if message else f"Function '{_get_func_name(func)}()' timed out after {seconds} seconds"
216
228
 
217
- # Try to use signal-based timeout (Unix only, main thread only)
218
- try:
229
+ # Use signal-based timeout on Unix (main thread only)
230
+ if use_signal:
219
231
  import signal
220
- def timeout_handler(signum: int, frame: Any) -> None:
221
- raise TimeoutError(msg)
232
+ import threading
222
233
 
223
- # Set the signal handler and alarm
224
- old_handler = signal.signal(signal.SIGALRM, timeout_handler) # type: ignore
225
- signal.setitimer(signal.ITIMER_REAL, seconds) # type: ignore
234
+ # Signal only works in main thread
235
+ if threading.current_thread() is threading.main_thread():
236
+ def timeout_handler(signum: int, frame: Any) -> None:
237
+ raise TimeoutError(msg)
226
238
 
227
- try:
228
- result = func(*args, **kwargs)
229
- finally:
230
- # Cancel the alarm and restore the old handler
231
- signal.setitimer(signal.ITIMER_REAL, 0) # type: ignore
232
- signal.signal(signal.SIGALRM, old_handler) # type: ignore
239
+ # Set the signal handler and alarm
240
+ old_handler = signal.signal(signal.SIGALRM, timeout_handler) # type: ignore
241
+ signal.setitimer(signal.ITIMER_REAL, seconds) # type: ignore
233
242
 
234
- return result
243
+ try:
244
+ result = func(*args, **kwargs)
245
+ finally:
246
+ # Cancel the alarm and restore the old handler
247
+ signal.setitimer(signal.ITIMER_REAL, 0) # type: ignore
248
+ signal.signal(signal.SIGALRM, old_handler) # type: ignore
235
249
 
236
- except (ValueError, AttributeError) as e:
237
- # SIGALRM not available (Windows) or not in main thread
238
- # Fall back to polling-based timeout (less precise but portable)
239
- import threading
250
+ return result
240
251
 
241
- result_container: list[Any] = []
242
- exception_container: list[BaseException] = []
252
+ # Fall back to polling-based timeout (Windows or non-main thread)
253
+ import threading
243
254
 
244
- def target() -> None:
245
- try:
246
- result_container.append(func(*args, **kwargs))
247
- except BaseException as e_2:
248
- exception_container.append(e_2)
255
+ result_container: list[Any] = []
256
+ exception_container: list[BaseException] = []
257
+
258
+ def target() -> None:
259
+ try:
260
+ result_container.append(func(*args, **kwargs))
261
+ except BaseException as e_2:
262
+ exception_container.append(e_2)
249
263
 
250
- thread = threading.Thread(target=target, daemon=True)
251
- thread.start()
252
- thread.join(timeout=seconds)
264
+ thread = threading.Thread(target=target, daemon=True)
265
+ thread.start()
266
+ thread.join(timeout=seconds)
253
267
 
254
- if thread.is_alive():
255
- # Thread is still running, timeout occurred
256
- raise TimeoutError(msg) from e
268
+ if thread.is_alive():
269
+ # Thread is still running, timeout occurred
270
+ raise TimeoutError(msg)
257
271
 
258
- # Check if an exception was raised in the thread
259
- if exception_container:
260
- raise exception_container[0] from e
272
+ # Check if an exception was raised in the thread
273
+ if exception_container:
274
+ raise exception_container[0]
261
275
 
262
- # Return the result if available
263
- if result_container:
264
- return result_container[0]
276
+ # Return the result if available
277
+ if result_container:
278
+ return result_container[0]
265
279
 
266
280
  wrapper.__name__ = _get_wrapper_name("stouputils.decorators.timeout", func)
267
281
  return wrapper
@@ -454,7 +468,7 @@ def abstract(
454
468
  >>> Base().method()
455
469
  Traceback (most recent call last):
456
470
  ...
457
- NotImplementedError: Function 'method' is abstract and must be implemented by a subclass
471
+ NotImplementedError: Function 'method()' is abstract and must be implemented by a subclass
458
472
  """
459
473
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
460
474
  message: str = f"Function '{_get_func_name(func)}()' is abstract and must be implemented by a subclass"
stouputils/decorators.pyi CHANGED
@@ -88,7 +88,7 @@ def timeout(func: Callable[..., Any] | None = None, *, seconds: float = 60.0, me
88
88
  \t\t>>> slow_function() # Raises TimeoutError after 2 seconds
89
89
  \t\tTraceback (most recent call last):
90
90
  \t\t\t...
91
- \t\tTimeoutError: Function \'slow_function\' timed out after 2.0 seconds
91
+ \t\tTimeoutError: Function \'slow_function()\' timed out after 2.0 seconds
92
92
 
93
93
  \t\t>>> @timeout(seconds=1.0, message="Custom timeout message")
94
94
  \t\t... def another_slow_function():
@@ -190,7 +190,7 @@ def abstract(func: Callable[..., Any] | None = None, *, error_log: LogLevels = .
190
190
  \t\t>>> Base().method()
191
191
  \t\tTraceback (most recent call last):
192
192
  \t\t\t...
193
- \t\tNotImplementedError: Function 'method' is abstract and must be implemented by a subclass
193
+ \t\tNotImplementedError: Function 'method()' is abstract and must be implemented by a subclass
194
194
  \t"""
195
195
  def deprecated(func: Callable[..., Any] | None = None, *, message: str = '', version: str = '', error_log: LogLevels = ...) -> Callable[..., Any]:
196
196
  ''' Decorator that marks a function as deprecated.
@@ -1,18 +1,18 @@
1
- """ Installer module for stouputils.
2
-
3
- Provides functions for platform-agnostic installation tasks by dispatching
4
- to platform-specific implementations (Windows, Linux/macOS).
5
-
6
- It handles getting installation paths, adding programs to the PATH environment variable,
7
- and installing programs from local zip files or URLs.
8
- """
9
- # ruff: noqa: F403
10
- # ruff: noqa: F405
11
-
12
- # Imports
13
- from .common import *
14
- from .downloader import *
15
- from .linux import *
16
- from .main import *
17
- from .windows import *
18
-
1
+ """ Installer module for stouputils.
2
+
3
+ Provides functions for platform-agnostic installation tasks by dispatching
4
+ to platform-specific implementations (Windows, Linux/macOS).
5
+
6
+ It handles getting installation paths, adding programs to the PATH environment variable,
7
+ and installing programs from local zip files or URLs.
8
+ """
9
+ # ruff: noqa: F403
10
+ # ruff: noqa: F405
11
+
12
+ # Imports
13
+ from .common import *
14
+ from .downloader import *
15
+ from .linux import *
16
+ from .main import *
17
+ from .windows import *
18
+
@@ -1,144 +1,144 @@
1
- """ Installer module for Linux/macOS specific functions.
2
-
3
- Provides Linux/macOS specific implementations for checking admin privileges,
4
- determining appropriate installation paths (global/local), and suggesting
5
- how to add directories to the system's PATH environment variable.
6
- """
7
- # Imports
8
- import os
9
-
10
- from ..decorators import LogLevels, handle_error
11
- from ..io import clean_path
12
- from ..print import debug, info, warning
13
- from .common import ask_install_type, prompt_for_path
14
-
15
-
16
- # Functions
17
- @handle_error(message="Failed to suggest how to add to PATH (Linux)", error_log=LogLevels.WARNING_TRACEBACK)
18
- def add_to_path_linux(install_path: str) -> bool:
19
- """ Suggest how to add install_path to PATH on Linux.
20
-
21
- Checks the current shell and provides instructions for adding the path
22
- to the appropriate configuration file (e.g., .bashrc, .zshrc, config.fish).
23
-
24
- Args:
25
- install_path (str): The path to add to the PATH environment variable.
26
-
27
- Returns:
28
- bool: True if instructions were provided, False otherwise (e.g., unknown shell).
29
- """
30
- shell_config_files: dict[str, str] = {
31
- "bash": "~/.bashrc",
32
- "zsh": "~/.zshrc",
33
- "fish": "~/.config/fish/config.fish"
34
- }
35
- current_shell: str = os.environ.get("SHELL", "").split('/')[-1]
36
- config_file: str | None = shell_config_files.get(current_shell)
37
-
38
- if config_file:
39
- export_cmd: str = ""
40
- if current_shell == "fish":
41
- export_cmd = f"set -gx PATH $PATH {install_path}"
42
- else:
43
- export_cmd = f"export PATH=\"$PATH:{install_path}\"" # Escape quotes for print
44
-
45
- debug(
46
- f"To add the installation directory to your PATH, add the following line to your '{config_file}':\n"
47
- f" {export_cmd}\n"
48
- f"Then restart your shell or run 'source {config_file}'."
49
- )
50
- return True
51
- else:
52
- warning(f"Could not determine your shell configuration file. Please add '{install_path}' to your PATH manually.")
53
- return False
54
-
55
-
56
- def check_admin_linux() -> bool:
57
- """ Check if the script is running with root privileges on Linux/macOS.
58
-
59
- Returns:
60
- bool: True if the effective user ID is 0 (root), False otherwise.
61
- """
62
- try:
63
- return os.geteuid() == 0 # type: ignore
64
- except AttributeError as e:
65
- # os.geteuid() is not available on all platforms (e.g., Windows)
66
- # This function should ideally only be called on Linux/macOS.
67
- warning(f"Could not determine user privileges on this platform: {e}")
68
- return False
69
- except Exception as e:
70
- warning(f"Error checking admin privileges: {e}")
71
- return False
72
-
73
-
74
- @handle_error(message="Failed to get installation path (Linux)", error_log=LogLevels.ERROR_TRACEBACK)
75
- def get_install_path_linux(
76
- program_name: str,
77
- ask_global: int = 0,
78
- add_path: bool = True,
79
- append_to_path: str = "",
80
- default_global: str = "/usr/local/bin",
81
- ) -> str:
82
- """ Get the installation path for the program on Linux/macOS.
83
-
84
- Args:
85
- program_name (str): The name of the program to install.
86
- ask_global (int): 0 = ask for anything, 1 = install globally, 2 = install locally
87
- add_path (bool): Whether to add the program to the PATH environment variable. (Only if installed globally)
88
- append_to_path (str): String to append to the installation path when adding to PATH.
89
- (ex: "bin" if executables are in the bin folder)
90
- default_global (str): The default global installation path.
91
- (Default is "/usr/local/bin" which is the most common location for executables on Linux/macOS,
92
- could be "/opt" or any other directory)
93
-
94
- Returns:
95
- str: The chosen installation path, or an empty string if installation is cancelled.
96
- """
97
- # Default paths
98
- default_local_path: str = clean_path(os.path.join(os.getcwd(), program_name))
99
-
100
- # Common global locations: /usr/local/bin for executables, /opt/ for self-contained apps
101
- # We assume 'program_name' might be an executable or a directory, /usr/local/ is safer
102
- default_global_path: str = clean_path(f"{default_global}/{program_name}") # Or potentially /opt/{program_name}
103
-
104
- # Ask user for installation type (global/local)
105
- install_type: str = ask_install_type(ask_global, default_local_path, default_global_path)
106
-
107
- # Handle global installation choice
108
- if install_type == 'g':
109
- if not check_admin_linux():
110
- warning(
111
- f"Global installation typically requires sudo privileges to write to "
112
- f"'{os.path.dirname(default_global_path)}'.\n"
113
- f"You may need to re-run the script with 'sudo'.\n"
114
- f"Install locally instead to '{default_local_path}'? (Y/n): "
115
- )
116
- if input().lower() == 'n':
117
- info("Installation cancelled.")
118
- return ""
119
- else:
120
- # Fallback to local path if user agrees
121
- return prompt_for_path(
122
- f"Falling back to local installation path: {default_local_path}.",
123
- default_local_path
124
- )
125
- else:
126
- # User is admin or proceeding with global install anyway
127
- install_path: str = prompt_for_path(
128
- f"Default global installation path is {default_global_path}.",
129
- default_global_path
130
- )
131
- if add_path:
132
- # Suggest adding the *directory* containing the program to PATH,
133
- # or the path itself if it seems like a directory install
134
- path_to_add: str = os.path.dirname(install_path) if os.path.isfile(install_path) else install_path
135
- add_to_path_linux(os.path.join(path_to_add, append_to_path))
136
- return install_path
137
-
138
- # Handle local installation choice
139
- else: # install_type == 'l'
140
- return prompt_for_path(
141
- f"Default local installation path is {default_local_path}.",
142
- default_local_path
143
- )
144
-
1
+ """ Installer module for Linux/macOS specific functions.
2
+
3
+ Provides Linux/macOS specific implementations for checking admin privileges,
4
+ determining appropriate installation paths (global/local), and suggesting
5
+ how to add directories to the system's PATH environment variable.
6
+ """
7
+ # Imports
8
+ import os
9
+
10
+ from ..decorators import LogLevels, handle_error
11
+ from ..io import clean_path
12
+ from ..print import debug, info, warning
13
+ from .common import ask_install_type, prompt_for_path
14
+
15
+
16
+ # Functions
17
+ @handle_error(message="Failed to suggest how to add to PATH (Linux)", error_log=LogLevels.WARNING_TRACEBACK)
18
+ def add_to_path_linux(install_path: str) -> bool:
19
+ """ Suggest how to add install_path to PATH on Linux.
20
+
21
+ Checks the current shell and provides instructions for adding the path
22
+ to the appropriate configuration file (e.g., .bashrc, .zshrc, config.fish).
23
+
24
+ Args:
25
+ install_path (str): The path to add to the PATH environment variable.
26
+
27
+ Returns:
28
+ bool: True if instructions were provided, False otherwise (e.g., unknown shell).
29
+ """
30
+ shell_config_files: dict[str, str] = {
31
+ "bash": "~/.bashrc",
32
+ "zsh": "~/.zshrc",
33
+ "fish": "~/.config/fish/config.fish"
34
+ }
35
+ current_shell: str = os.environ.get("SHELL", "").split('/')[-1]
36
+ config_file: str | None = shell_config_files.get(current_shell)
37
+
38
+ if config_file:
39
+ export_cmd: str = ""
40
+ if current_shell == "fish":
41
+ export_cmd = f"set -gx PATH $PATH {install_path}"
42
+ else:
43
+ export_cmd = f"export PATH=\"$PATH:{install_path}\"" # Escape quotes for print
44
+
45
+ debug(
46
+ f"To add the installation directory to your PATH, add the following line to your '{config_file}':\n"
47
+ f" {export_cmd}\n"
48
+ f"Then restart your shell or run 'source {config_file}'."
49
+ )
50
+ return True
51
+ else:
52
+ warning(f"Could not determine your shell configuration file. Please add '{install_path}' to your PATH manually.")
53
+ return False
54
+
55
+
56
+ def check_admin_linux() -> bool:
57
+ """ Check if the script is running with root privileges on Linux/macOS.
58
+
59
+ Returns:
60
+ bool: True if the effective user ID is 0 (root), False otherwise.
61
+ """
62
+ try:
63
+ return os.geteuid() == 0 # type: ignore
64
+ except AttributeError as e:
65
+ # os.geteuid() is not available on all platforms (e.g., Windows)
66
+ # This function should ideally only be called on Linux/macOS.
67
+ warning(f"Could not determine user privileges on this platform: {e}")
68
+ return False
69
+ except Exception as e:
70
+ warning(f"Error checking admin privileges: {e}")
71
+ return False
72
+
73
+
74
+ @handle_error(message="Failed to get installation path (Linux)", error_log=LogLevels.ERROR_TRACEBACK)
75
+ def get_install_path_linux(
76
+ program_name: str,
77
+ ask_global: int = 0,
78
+ add_path: bool = True,
79
+ append_to_path: str = "",
80
+ default_global: str = "/usr/local/bin",
81
+ ) -> str:
82
+ """ Get the installation path for the program on Linux/macOS.
83
+
84
+ Args:
85
+ program_name (str): The name of the program to install.
86
+ ask_global (int): 0 = ask for anything, 1 = install globally, 2 = install locally
87
+ add_path (bool): Whether to add the program to the PATH environment variable. (Only if installed globally)
88
+ append_to_path (str): String to append to the installation path when adding to PATH.
89
+ (ex: "bin" if executables are in the bin folder)
90
+ default_global (str): The default global installation path.
91
+ (Default is "/usr/local/bin" which is the most common location for executables on Linux/macOS,
92
+ could be "/opt" or any other directory)
93
+
94
+ Returns:
95
+ str: The chosen installation path, or an empty string if installation is cancelled.
96
+ """
97
+ # Default paths
98
+ default_local_path: str = clean_path(os.path.join(os.getcwd(), program_name))
99
+
100
+ # Common global locations: /usr/local/bin for executables, /opt/ for self-contained apps
101
+ # We assume 'program_name' might be an executable or a directory, /usr/local/ is safer
102
+ default_global_path: str = clean_path(f"{default_global}/{program_name}") # Or potentially /opt/{program_name}
103
+
104
+ # Ask user for installation type (global/local)
105
+ install_type: str = ask_install_type(ask_global, default_local_path, default_global_path)
106
+
107
+ # Handle global installation choice
108
+ if install_type == 'g':
109
+ if not check_admin_linux():
110
+ warning(
111
+ f"Global installation typically requires sudo privileges to write to "
112
+ f"'{os.path.dirname(default_global_path)}'.\n"
113
+ f"You may need to re-run the script with 'sudo'.\n"
114
+ f"Install locally instead to '{default_local_path}'? (Y/n): "
115
+ )
116
+ if input().lower() == 'n':
117
+ info("Installation cancelled.")
118
+ return ""
119
+ else:
120
+ # Fallback to local path if user agrees
121
+ return prompt_for_path(
122
+ f"Falling back to local installation path: {default_local_path}.",
123
+ default_local_path
124
+ )
125
+ else:
126
+ # User is admin or proceeding with global install anyway
127
+ install_path: str = prompt_for_path(
128
+ f"Default global installation path is {default_global_path}.",
129
+ default_global_path
130
+ )
131
+ if add_path:
132
+ # Suggest adding the *directory* containing the program to PATH,
133
+ # or the path itself if it seems like a directory install
134
+ path_to_add: str = os.path.dirname(install_path) if os.path.isfile(install_path) else install_path
135
+ add_to_path_linux(os.path.join(path_to_add, append_to_path))
136
+ return install_path
137
+
138
+ # Handle local installation choice
139
+ else: # install_type == 'l'
140
+ return prompt_for_path(
141
+ f"Default local installation path is {default_local_path}.",
142
+ default_local_path
143
+ )
144
+