stouputils 1.12.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 (138) hide show
  1. stouputils/__init__.py +40 -0
  2. stouputils/__init__.pyi +14 -0
  3. stouputils/__main__.py +81 -0
  4. stouputils/_deprecated.py +37 -0
  5. stouputils/_deprecated.pyi +12 -0
  6. stouputils/all_doctests.py +160 -0
  7. stouputils/all_doctests.pyi +46 -0
  8. stouputils/applications/__init__.py +22 -0
  9. stouputils/applications/__init__.pyi +2 -0
  10. stouputils/applications/automatic_docs.py +634 -0
  11. stouputils/applications/automatic_docs.pyi +106 -0
  12. stouputils/applications/upscaler/__init__.py +39 -0
  13. stouputils/applications/upscaler/__init__.pyi +3 -0
  14. stouputils/applications/upscaler/config.py +128 -0
  15. stouputils/applications/upscaler/config.pyi +18 -0
  16. stouputils/applications/upscaler/image.py +247 -0
  17. stouputils/applications/upscaler/image.pyi +109 -0
  18. stouputils/applications/upscaler/video.py +287 -0
  19. stouputils/applications/upscaler/video.pyi +60 -0
  20. stouputils/archive.py +344 -0
  21. stouputils/archive.pyi +67 -0
  22. stouputils/backup.py +488 -0
  23. stouputils/backup.pyi +109 -0
  24. stouputils/collections.py +244 -0
  25. stouputils/collections.pyi +86 -0
  26. stouputils/continuous_delivery/__init__.py +27 -0
  27. stouputils/continuous_delivery/__init__.pyi +5 -0
  28. stouputils/continuous_delivery/cd_utils.py +243 -0
  29. stouputils/continuous_delivery/cd_utils.pyi +129 -0
  30. stouputils/continuous_delivery/github.py +522 -0
  31. stouputils/continuous_delivery/github.pyi +162 -0
  32. stouputils/continuous_delivery/pypi.py +91 -0
  33. stouputils/continuous_delivery/pypi.pyi +43 -0
  34. stouputils/continuous_delivery/pyproject.py +147 -0
  35. stouputils/continuous_delivery/pyproject.pyi +67 -0
  36. stouputils/continuous_delivery/stubs.py +86 -0
  37. stouputils/continuous_delivery/stubs.pyi +39 -0
  38. stouputils/ctx.py +408 -0
  39. stouputils/ctx.pyi +211 -0
  40. stouputils/data_science/config/get.py +51 -0
  41. stouputils/data_science/config/set.py +125 -0
  42. stouputils/data_science/data_processing/image/__init__.py +66 -0
  43. stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
  44. stouputils/data_science/data_processing/image/axis_flip.py +58 -0
  45. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
  46. stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
  47. stouputils/data_science/data_processing/image/blur.py +59 -0
  48. stouputils/data_science/data_processing/image/brightness.py +54 -0
  49. stouputils/data_science/data_processing/image/canny.py +110 -0
  50. stouputils/data_science/data_processing/image/clahe.py +92 -0
  51. stouputils/data_science/data_processing/image/common.py +30 -0
  52. stouputils/data_science/data_processing/image/contrast.py +53 -0
  53. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
  54. stouputils/data_science/data_processing/image/denoise.py +378 -0
  55. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
  56. stouputils/data_science/data_processing/image/invert.py +64 -0
  57. stouputils/data_science/data_processing/image/laplacian.py +60 -0
  58. stouputils/data_science/data_processing/image/median_blur.py +52 -0
  59. stouputils/data_science/data_processing/image/noise.py +59 -0
  60. stouputils/data_science/data_processing/image/normalize.py +65 -0
  61. stouputils/data_science/data_processing/image/random_erase.py +66 -0
  62. stouputils/data_science/data_processing/image/resize.py +69 -0
  63. stouputils/data_science/data_processing/image/rotation.py +80 -0
  64. stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
  65. stouputils/data_science/data_processing/image/sharpening.py +55 -0
  66. stouputils/data_science/data_processing/image/shearing.py +64 -0
  67. stouputils/data_science/data_processing/image/threshold.py +64 -0
  68. stouputils/data_science/data_processing/image/translation.py +71 -0
  69. stouputils/data_science/data_processing/image/zoom.py +83 -0
  70. stouputils/data_science/data_processing/image_augmentation.py +118 -0
  71. stouputils/data_science/data_processing/image_preprocess.py +183 -0
  72. stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
  73. stouputils/data_science/data_processing/technique.py +481 -0
  74. stouputils/data_science/dataset/__init__.py +45 -0
  75. stouputils/data_science/dataset/dataset.py +292 -0
  76. stouputils/data_science/dataset/dataset_loader.py +135 -0
  77. stouputils/data_science/dataset/grouping_strategy.py +296 -0
  78. stouputils/data_science/dataset/image_loader.py +100 -0
  79. stouputils/data_science/dataset/xy_tuple.py +696 -0
  80. stouputils/data_science/metric_dictionnary.py +106 -0
  81. stouputils/data_science/metric_utils.py +847 -0
  82. stouputils/data_science/mlflow_utils.py +206 -0
  83. stouputils/data_science/models/abstract_model.py +149 -0
  84. stouputils/data_science/models/all.py +85 -0
  85. stouputils/data_science/models/base_keras.py +765 -0
  86. stouputils/data_science/models/keras/all.py +38 -0
  87. stouputils/data_science/models/keras/convnext.py +62 -0
  88. stouputils/data_science/models/keras/densenet.py +50 -0
  89. stouputils/data_science/models/keras/efficientnet.py +60 -0
  90. stouputils/data_science/models/keras/mobilenet.py +56 -0
  91. stouputils/data_science/models/keras/resnet.py +52 -0
  92. stouputils/data_science/models/keras/squeezenet.py +233 -0
  93. stouputils/data_science/models/keras/vgg.py +42 -0
  94. stouputils/data_science/models/keras/xception.py +38 -0
  95. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
  96. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
  97. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
  98. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
  99. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
  100. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
  101. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
  102. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
  103. stouputils/data_science/models/keras_utils/visualizations.py +416 -0
  104. stouputils/data_science/models/model_interface.py +939 -0
  105. stouputils/data_science/models/sandbox.py +116 -0
  106. stouputils/data_science/range_tuple.py +234 -0
  107. stouputils/data_science/scripts/augment_dataset.py +77 -0
  108. stouputils/data_science/scripts/exhaustive_process.py +133 -0
  109. stouputils/data_science/scripts/preprocess_dataset.py +70 -0
  110. stouputils/data_science/scripts/routine.py +168 -0
  111. stouputils/data_science/utils.py +285 -0
  112. stouputils/decorators.py +595 -0
  113. stouputils/decorators.pyi +242 -0
  114. stouputils/image.py +441 -0
  115. stouputils/image.pyi +172 -0
  116. stouputils/installer/__init__.py +18 -0
  117. stouputils/installer/__init__.pyi +5 -0
  118. stouputils/installer/common.py +67 -0
  119. stouputils/installer/common.pyi +39 -0
  120. stouputils/installer/downloader.py +101 -0
  121. stouputils/installer/downloader.pyi +24 -0
  122. stouputils/installer/linux.py +144 -0
  123. stouputils/installer/linux.pyi +39 -0
  124. stouputils/installer/main.py +223 -0
  125. stouputils/installer/main.pyi +57 -0
  126. stouputils/installer/windows.py +136 -0
  127. stouputils/installer/windows.pyi +31 -0
  128. stouputils/io.py +486 -0
  129. stouputils/io.pyi +213 -0
  130. stouputils/parallel.py +453 -0
  131. stouputils/parallel.pyi +211 -0
  132. stouputils/print.py +527 -0
  133. stouputils/print.pyi +146 -0
  134. stouputils/py.typed +1 -0
  135. stouputils-1.12.1.dist-info/METADATA +179 -0
  136. stouputils-1.12.1.dist-info/RECORD +138 -0
  137. stouputils-1.12.1.dist-info/WHEEL +4 -0
  138. stouputils-1.12.1.dist-info/entry_points.txt +3 -0
stouputils/image.pyi ADDED
@@ -0,0 +1,172 @@
1
+ import numpy as np
2
+ from .io import super_open as super_open
3
+ from .print import debug as debug, info as info
4
+ from PIL import Image
5
+ from collections.abc import Callable
6
+ from numpy.typing import NDArray
7
+ from typing import Any, TypeVar
8
+
9
+ PIL_Image_or_NDArray = TypeVar('PIL_Image_or_NDArray', bound='Image.Image | NDArray[np.number]')
10
+
11
+ def image_resize[PIL_Image_or_NDArray](image: PIL_Image_or_NDArray, max_result_size: int, resampling: Image.Resampling | None = None, min_or_max: Callable[[int, int], int] = ..., return_type: type[PIL_Image_or_NDArray] | str = 'same', keep_aspect_ratio: bool = True) -> Any:
12
+ ''' Resize an image while preserving its aspect ratio by default.
13
+ \tScales the image so that its largest dimension equals max_result_size.
14
+
15
+ \tArgs:
16
+ \t\timage (Image.Image | np.ndarray): The image to resize.
17
+ \t\tmax_result_size (int): Maximum size for the largest dimension.
18
+ \t\tresampling (Image.Resampling | None): PIL resampling filter to use (default: Image.Resampling.LANCZOS).
19
+ \t\tmin_or_max (Callable): Function to use to get the minimum or maximum of the two ratios.
20
+ \t\treturn_type (type | str): Type of the return value (Image.Image, np.ndarray, or "same" to match input type).
21
+ \t\tkeep_aspect_ratio (bool): Whether to keep the aspect ratio.
22
+ \tReturns:
23
+ \t\tImage.Image | NDArray[np.number]: The resized image with preserved aspect ratio.
24
+ \tExamples:
25
+ \t\t>>> # Test with (height x width x channels) numpy array
26
+ \t\t>>> import numpy as np
27
+ \t\t>>> array = np.random.randint(0, 255, (100, 50, 3), dtype=np.uint8)
28
+ \t\t>>> image_resize(array, 100).shape
29
+ \t\t(100, 50, 3)
30
+ \t\t>>> image_resize(array, 100, min_or_max=max).shape
31
+ \t\t(100, 50, 3)
32
+ \t\t>>> image_resize(array, 100, min_or_max=min).shape
33
+ \t\t(200, 100, 3)
34
+
35
+ \t\t>>> # Test with PIL Image
36
+ \t\t>>> from PIL import Image
37
+ \t\t>>> pil_image: Image.Image = Image.new(\'RGB\', (200, 100))
38
+ \t\t>>> image_resize(pil_image, 50).size
39
+ \t\t(50, 25)
40
+ \t\t>>> # Test with different return types
41
+ \t\t>>> resized_array = image_resize(array, 50, return_type=np.ndarray)
42
+ \t\t>>> isinstance(resized_array, np.ndarray)
43
+ \t\tTrue
44
+ \t\t>>> resized_array.shape
45
+ \t\t(50, 25, 3)
46
+ \t\t>>> # Test with different resampling methods
47
+ \t\t>>> image_resize(pil_image, 50, resampling=Image.Resampling.NEAREST).size
48
+ \t\t(50, 25)
49
+ \t'''
50
+ def auto_crop[PIL_Image_or_NDArray](image: PIL_Image_or_NDArray, mask: NDArray[np.bool_] | None = None, threshold: int | float | Callable[[NDArray[np.number]], int | float] | None = None, return_type: type[PIL_Image_or_NDArray] | str = 'same', contiguous: bool = True) -> Any:
51
+ ''' Automatically crop an image to remove zero or uniform regions.
52
+
53
+ \tThis function crops the image to keep only the region where pixels are non-zero
54
+ \t(or above a threshold). It can work with a mask or directly analyze the image.
55
+
56
+ \tArgs:
57
+ \t\timage (Image.Image | NDArray):\t The image to crop.
58
+ \t\tmask (NDArray[bool] | None): Optional binary mask indicating regions to keep.
59
+ \t\tthreshold (int | float | Callable): Threshold value or function (default: np.min).
60
+ \t\treturn_type (type | str): Type of the return value (Image.Image, NDArray[np.number], or "same" to match input type).
61
+ \t\tcontiguous (bool): If True (default), crop to bounding box. If False, remove entire rows/columns with no content.
62
+ \tReturns:
63
+ \t\tImage.Image | NDArray[np.number]: The cropped image.
64
+
65
+ \tExamples:
66
+ \t\t>>> # Test with numpy array with zeros on edges
67
+ \t\t>>> import numpy as np
68
+ \t\t>>> array = np.zeros((100, 100, 3), dtype=np.uint8)
69
+ \t\t>>> array[20:80, 30:70] = 255 # White rectangle in center
70
+ \t\t>>> cropped = auto_crop(array, return_type=np.ndarray)
71
+ \t\t>>> cropped.shape
72
+ \t\t(60, 40, 3)
73
+
74
+ \t\t>>> # Test with custom mask
75
+ \t\t>>> mask = np.zeros((100, 100), dtype=bool)
76
+ \t\t>>> mask[10:90, 10:90] = True
77
+ \t\t>>> cropped_with_mask = auto_crop(array, mask=mask, return_type=np.ndarray)
78
+ \t\t>>> cropped_with_mask.shape
79
+ \t\t(80, 80, 3)
80
+
81
+ \t\t>>> # Test with PIL Image
82
+ \t\t>>> from PIL import Image
83
+ \t\t>>> pil_image = Image.new(\'RGB\', (100, 100), (0, 0, 0))
84
+ \t\t>>> from PIL import ImageDraw
85
+ \t\t>>> draw = ImageDraw.Draw(pil_image)
86
+ \t\t>>> draw.rectangle([25, 25, 75, 75], fill=(255, 255, 255))
87
+ \t\t>>> cropped_pil = auto_crop(pil_image)
88
+ \t\t>>> cropped_pil.size
89
+ \t\t(51, 51)
90
+
91
+ \t\t>>> # Test with threshold
92
+ \t\t>>> array_gray = np.ones((100, 100), dtype=np.uint8) * 10
93
+ \t\t>>> array_gray[20:80, 30:70] = 255
94
+ \t\t>>> cropped_threshold = auto_crop(array_gray, threshold=50, return_type=np.ndarray)
95
+ \t\t>>> cropped_threshold.shape
96
+ \t\t(60, 40)
97
+
98
+ \t\t>>> # Test with callable threshold (using lambda to avoid min value)
99
+ \t\t>>> array_gray2 = np.ones((100, 100), dtype=np.uint8) * 10
100
+ \t\t>>> array_gray2[20:80, 30:70] = 255
101
+ \t\t>>> cropped_max = auto_crop(array_gray2, threshold=lambda x: 50, return_type=np.ndarray)
102
+ \t\t>>> cropped_max.shape
103
+ \t\t(60, 40)
104
+
105
+ \t>>> # Test with non-contiguous crop
106
+ \t>>> array_sparse = np.zeros((100, 100, 3), dtype=np.uint8)
107
+ \t>>> array_sparse[10, 10] = 255
108
+ \t>>> array_sparse[50, 50] = 255
109
+ \t>>> array_sparse[90, 90] = 255
110
+ \t>>> cropped_contiguous = auto_crop(array_sparse, contiguous=True, return_type=np.ndarray)
111
+ \t>>> cropped_contiguous.shape # Bounding box from (10,10) to (90,90)
112
+ \t(81, 81, 3)
113
+ \t>>> cropped_non_contiguous = auto_crop(array_sparse, contiguous=False, return_type=np.ndarray)
114
+ \t>>> cropped_non_contiguous.shape # Only rows/cols 10, 50, 90
115
+ \t(3, 3, 3)
116
+
117
+ \t>>> # Test with 3D crop on depth dimension
118
+ \t>>> array_3d = np.zeros((50, 50, 10), dtype=np.uint8)
119
+ \t>>> array_3d[10:40, 10:40, 2:8] = 255 # Content only in depth slices 2-7
120
+ \t>>> cropped_3d = auto_crop(array_3d, contiguous=True, return_type=np.ndarray)
121
+ \t>>> cropped_3d.shape # Should crop all 3 dimensions
122
+ \t(30, 30, 6)
123
+ \t'''
124
+ def numpy_to_gif(path: str, array: NDArray[np.integer | np.floating | np.bool_], duration: int = 100, loop: int = 0, mkdir: bool = True, **kwargs: Any) -> None:
125
+ ''' Generate a \'.gif\' file from a numpy array for 3D/4D visualization.
126
+
127
+ \tArgs:
128
+ \t\tpath (str): Path to the output .gif file.
129
+ \t\tarray (NDArray): Numpy array to be dumped (must be 3D or 4D).
130
+ \t\t\t3D: (depth, height, width) - e.g. (64, 1024, 1024)
131
+ \t\t\t4D: (depth, height, width, channels) - e.g. (50, 64, 1024, 3)
132
+ \t\tduration (int): Duration between frames in milliseconds.
133
+ \t\tloop (int): Number of loops (0 = infinite).
134
+ \t\tmkdir (bool): Create the directory if it does not exist.
135
+ \t\t**kwargs (Any): Additional keyword arguments for PIL.Image.save().
136
+
137
+ \tExamples:
138
+
139
+ \t\t.. code-block:: python
140
+
141
+ \t\t\t> # 3D array example
142
+ \t\t\t> array = np.random.randint(0, 256, (10, 100, 100), dtype=np.uint8)
143
+ \t\t\t> numpy_to_gif("output_10_frames_100x100.gif", array, duration=200, loop=0)
144
+
145
+ \t\t\t> # 4D array example (batch of 3D images)
146
+ \t\t\t> array_4d = np.random.randint(0, 256, (5, 10, 100, 3), dtype=np.uint8)
147
+ \t\t\t> numpy_to_gif("output_50_frames_100x100.gif", array_4d, duration=200)
148
+
149
+ \t\t\t> total_duration = 1000 # 1 second
150
+ \t\t\t> numpy_to_gif("output_1s.gif", array, duration=total_duration // len(array))
151
+ \t'''
152
+ def numpy_to_obj(path: str, array: NDArray[np.integer | np.floating | np.bool_], threshold: float = 0.5, step_size: int = 1, pad_array: bool = True, verbose: int = 0) -> None:
153
+ ''' Generate a \'.obj\' file from a numpy array for 3D visualization using marching cubes.
154
+
155
+ \tArgs:
156
+ \t\tpath (str): Path to the output .obj file.
157
+ \t\tarray (NDArray): Numpy array to be dumped (must be 3D).
158
+ \t\tthreshold (float): Threshold level for marching cubes (0.5 for binary data).
159
+ \t\tstep_size (int): Step size for marching cubes (higher = simpler mesh, faster generation).
160
+ \t\tpad_array (bool): If True, pad array with zeros to ensure closed volumes for border cells.
161
+ \t\tverbose (int): Verbosity level (0 = no output, 1 = some output, 2 = full output).
162
+
163
+ \tExamples:
164
+
165
+ \t\t.. code-block:: python
166
+
167
+ \t\t\t> array = np.random.rand(64, 64, 64) > 0.5 # Binary volume
168
+ \t\t\t> numpy_to_obj("output_mesh.obj", array, threshold=0.5, step_size=2, pad_array=True, verbose=1)
169
+
170
+ \t\t\t> array = my_3d_data # Some 3D numpy array (e.g. human lung scan)
171
+ \t\t\t> numpy_to_obj("output_mesh.obj", array, threshold=0.3)
172
+ \t'''
@@ -0,0 +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
+
@@ -0,0 +1,5 @@
1
+ from .common import *
2
+ from .downloader import *
3
+ from .linux import *
4
+ from .main import *
5
+ from .windows import *
@@ -0,0 +1,67 @@
1
+ """ Common functions used by the Linux and Windows installers modules. """
2
+ # Imports
3
+ from typing import Literal
4
+
5
+ from ..print import warning
6
+
7
+
8
+ # Functions
9
+ def prompt_for_path(prompt_message: str, default_path: str) -> str:
10
+ """ Prompt the user to override a default path.
11
+
12
+ Args:
13
+ prompt_message (str): The message to display to the user.
14
+ default_path (str): The default path to suggest.
15
+
16
+ Returns:
17
+ str: The path entered by the user, or the default path if they pressed Enter.
18
+ """
19
+ warning(f"{prompt_message}\nPress Enter to use this path, or type a new path to override it: ")
20
+ return input() or default_path
21
+
22
+
23
+ def ask_install_type(ask_global: int, default_local_path: str, default_global_path: str | None) -> Literal["g", "l"]:
24
+ """ Determine the installation type (global 'g' or local 'l') based on user input.
25
+
26
+ Args:
27
+ ask_global (int): 0 = ask, 1 = force global, 2 = force local.
28
+ default_local_path (str): The default local path.
29
+ default_global_path (str | None): The default global path (if applicable).
30
+
31
+ Returns:
32
+ Literal["g", "l"]: 'g' for global install, 'l' for local install.
33
+
34
+ Examples:
35
+ .. code-block:: python
36
+
37
+ > # Ask the user while providing default paths
38
+ > install_choice: str = ask_install_type(0, f"{os.getcwd()}/MyProgram", "C:\\Program Files\\MyProgram")
39
+ g
40
+
41
+ > # Don't ask, force global
42
+ > install_choice: str = ask_install_type(1, ...)
43
+ g
44
+
45
+ > # Don't ask, force local
46
+ > install_choice: str = ask_install_type(2, ...)
47
+ l
48
+ """
49
+ install_choice: str = ""
50
+ if ask_global == 0:
51
+ if default_global_path:
52
+ global_prompt: str = f"(Globally would target '{default_global_path}')"
53
+ else:
54
+ global_prompt: str = "(Global install not well-defined)"
55
+ warning(
56
+ f"Install globally (requires admin/sudo, suggests adding to PATH) or locally? (G/l):\n"
57
+ f"{global_prompt}, locally would be '{default_local_path}')"
58
+ )
59
+ install_choice = input().lower()
60
+ elif ask_global == 1:
61
+ install_choice = "g"
62
+ elif ask_global == 2:
63
+ install_choice = "l"
64
+
65
+ # Default to global unless user explicitly chooses local ('l')
66
+ return 'l' if install_choice == 'l' else 'g'
67
+
@@ -0,0 +1,39 @@
1
+ from ..print import warning as warning
2
+ from typing import Literal
3
+
4
+ def prompt_for_path(prompt_message: str, default_path: str) -> str:
5
+ """ Prompt the user to override a default path.
6
+
7
+ \tArgs:
8
+ \t\tprompt_message (str): The message to display to the user.
9
+ \t\tdefault_path (str): The default path to suggest.
10
+
11
+ \tReturns:
12
+ \t\tstr: The path entered by the user, or the default path if they pressed Enter.
13
+ \t"""
14
+ def ask_install_type(ask_global: int, default_local_path: str, default_global_path: str | None) -> Literal['g', 'l']:
15
+ ''' Determine the installation type (global \'g\' or local \'l\') based on user input.
16
+
17
+ \tArgs:
18
+ \t\task_global (int): 0 = ask, 1 = force global, 2 = force local.
19
+ \t\tdefault_local_path (str): The default local path.
20
+ \t\tdefault_global_path (str | None): The default global path (if applicable).
21
+
22
+ \tReturns:
23
+ \t\tLiteral["g", "l"]: \'g\' for global install, \'l\' for local install.
24
+
25
+ \tExamples:
26
+ \t\t.. code-block:: python
27
+
28
+ \t\t\t> # Ask the user while providing default paths
29
+ \t\t\t> install_choice: str = ask_install_type(0, f"{os.getcwd()}/MyProgram", "C:\\Program Files\\MyProgram")
30
+ \t\t\tg
31
+
32
+ \t\t\t> # Don\'t ask, force global
33
+ \t\t\t> install_choice: str = ask_install_type(1, ...)
34
+ \t\t\tg
35
+
36
+ \t\t\t> # Don\'t ask, force local
37
+ \t\t\t> install_choice: str = ask_install_type(2, ...)
38
+ \t\t\tl
39
+ \t'''
@@ -0,0 +1,101 @@
1
+ """ Downloader module for the installer subpackage.
2
+
3
+ Provides functions for downloading and installing programs from URLs.
4
+ It handles platform-specific downloads, checking if programs are already installed,
5
+ and setting up the downloaded programs for use.
6
+
7
+ This module works with the main installer module to provide a complete installation
8
+ solution for programs that need to be downloaded from the internet.
9
+ """
10
+ # Imports
11
+ import os
12
+ import platform
13
+ import subprocess
14
+ import sys
15
+
16
+ from ..print import info, warning
17
+ from .main import install_program
18
+
19
+
20
+ # Functions
21
+ def download_executable(download_urls: dict[str, str], program_name: str, append_to_path: str = "") -> bool:
22
+ """ Ask the user if they want to download the program (ex: waifu2x-ncnn-vulkan).
23
+ If yes, try to download the program from the GitHub releases page.
24
+
25
+ Args:
26
+ download_urls (dict[str, str]): The URLs to download the program from.
27
+ program_name (str): The name of the program to download.
28
+
29
+ Returns:
30
+ bool: True if the program is now ready to use, False otherwise.
31
+ """
32
+ # Ask the user if they want to download the upscaler
33
+ program_url: str = next(iter(download_urls.values())).split("/download/")[0]
34
+ warning(
35
+ f"Program executable not found, would you like to download it automatically from GitHub? (Y/n) :\n"
36
+ f"({program_url})"
37
+ )
38
+
39
+ # Handle the user's response
40
+ if input().lower() == "n":
41
+ info("User declined to download the upscaler.")
42
+ return False
43
+
44
+ # Get the platform
45
+ system: str = platform.system()
46
+ download_url: str = download_urls.get(system, "")
47
+ if not download_url:
48
+ warning(
49
+ f"Unsupported platform: {system}, please download the program manually from the following URL:\n"
50
+ f" {program_url}"
51
+ )
52
+ return False
53
+
54
+ # Download the upscaler
55
+ if not install_program(download_url, program_name=program_name, append_to_path=append_to_path):
56
+ warning("Failed to download the upscaler, please download it manually from the following URL:")
57
+ print(f" {download_url}")
58
+ return False
59
+
60
+ return True
61
+
62
+ def check_executable(
63
+ executable: str,
64
+ executable_help_text: str,
65
+ download_urls: dict[str, str],
66
+ append_to_path: str = ""
67
+ ) -> None:
68
+ """ Check if the executable exists, optionally download it if it doesn't.
69
+
70
+ Args:
71
+ executable (str): The path to the executable.
72
+ executable_help_text (str): The help text to check for in the executable's output.
73
+ download_urls (dict[str, str]): The URLs to download the executable from.
74
+ append_to_path (str): The path to append to the executable's path.
75
+ (ex: "bin" if executables are in the bin folder)
76
+ """
77
+ program_name: str = os.path.basename(executable)
78
+ try_download: bool = True
79
+
80
+ # Run the command, capture output, don't check exit code immediately
81
+ try:
82
+ result: subprocess.CompletedProcess[str] = subprocess.run(
83
+ [executable, "-h"],
84
+ capture_output=True,
85
+ text=True, # Decode stdout/stderr as text
86
+ check=False # Don't raise exception on non-zero exit code
87
+ )
88
+
89
+ # If the command failed (no help text matching), try to download the upscaler
90
+ try_download: bool = executable_help_text.lower() not in result.stdout.lower()
91
+ except FileNotFoundError:
92
+ try_download: bool = True
93
+
94
+ # If the command failed, try to download the upscaler
95
+ if try_download:
96
+ if not download_executable(download_urls, program_name, append_to_path=append_to_path):
97
+ warning(f"'{program_name}' is required but not available. Exiting.")
98
+ else:
99
+ info(f"'{program_name}' downloaded successfully, please restart the script.")
100
+ sys.exit(1)
101
+
@@ -0,0 +1,24 @@
1
+ from ..print import info as info, warning as warning
2
+ from .main import install_program as install_program
3
+
4
+ def download_executable(download_urls: dict[str, str], program_name: str, append_to_path: str = '') -> bool:
5
+ """ Ask the user if they want to download the program (ex: waifu2x-ncnn-vulkan).
6
+ \tIf yes, try to download the program from the GitHub releases page.
7
+
8
+ \tArgs:
9
+ \t\tdownload_urls (dict[str, str]): The URLs to download the program from.
10
+ \t\tprogram_name (str): The name of the program to download.
11
+
12
+ \tReturns:
13
+ \t\tbool: True if the program is now ready to use, False otherwise.
14
+ \t"""
15
+ def check_executable(executable: str, executable_help_text: str, download_urls: dict[str, str], append_to_path: str = '') -> None:
16
+ ''' Check if the executable exists, optionally download it if it doesn\'t.
17
+
18
+ \tArgs:
19
+ \t\texecutable (str): The path to the executable.
20
+ \t\texecutable_help_text (str): The help text to check for in the executable\'s output.
21
+ \t\tdownload_urls (dict[str, str]): The URLs to download the executable from.
22
+ \t\tappend_to_path (str): The path to append to the executable\'s path.
23
+ \t\t\t(ex: "bin" if executables are in the bin folder)
24
+ \t'''
@@ -0,0 +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
+
@@ -0,0 +1,39 @@
1
+ from ..decorators import LogLevels as LogLevels, handle_error as handle_error
2
+ from ..io import clean_path as clean_path
3
+ from ..print import debug as debug, info as info, warning as warning
4
+ from .common import ask_install_type as ask_install_type, prompt_for_path as prompt_for_path
5
+
6
+ def add_to_path_linux(install_path: str) -> bool:
7
+ """ Suggest how to add install_path to PATH on Linux.
8
+
9
+ \tChecks the current shell and provides instructions for adding the path
10
+ \tto the appropriate configuration file (e.g., .bashrc, .zshrc, config.fish).
11
+
12
+ \tArgs:
13
+ \t\tinstall_path (str): The path to add to the PATH environment variable.
14
+
15
+ \tReturns:
16
+ \t\tbool: True if instructions were provided, False otherwise (e.g., unknown shell).
17
+ \t"""
18
+ def check_admin_linux() -> bool:
19
+ """ Check if the script is running with root privileges on Linux/macOS.
20
+
21
+ \tReturns:
22
+ \t\tbool: True if the effective user ID is 0 (root), False otherwise.
23
+ \t"""
24
+ def get_install_path_linux(program_name: str, ask_global: int = 0, add_path: bool = True, append_to_path: str = '', default_global: str = '/usr/local/bin') -> str:
25
+ ''' Get the installation path for the program on Linux/macOS.
26
+
27
+ \tArgs:
28
+ \t\tprogram_name (str): The name of the program to install.
29
+ \t\task_global (int): 0 = ask for anything, 1 = install globally, 2 = install locally
30
+ \t\tadd_path (bool): Whether to add the program to the PATH environment variable. (Only if installed globally)
31
+ \t\tappend_to_path (str): String to append to the installation path when adding to PATH.
32
+ \t\t\t(ex: "bin" if executables are in the bin folder)
33
+ \t\tdefault_global (str): The default global installation path.
34
+ \t\t\t(Default is "/usr/local/bin" which is the most common location for executables on Linux/macOS,
35
+ \t\t\tcould be "/opt" or any other directory)
36
+
37
+ \tReturns:
38
+ \t\tstr: The chosen installation path, or an empty string if installation is cancelled.
39
+ \t'''