stouputils 1.14.0__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.
- stouputils/__init__.py +40 -0
- stouputils/__main__.py +86 -0
- stouputils/_deprecated.py +37 -0
- stouputils/all_doctests.py +160 -0
- stouputils/applications/__init__.py +22 -0
- stouputils/applications/automatic_docs.py +634 -0
- stouputils/applications/upscaler/__init__.py +39 -0
- stouputils/applications/upscaler/config.py +128 -0
- stouputils/applications/upscaler/image.py +247 -0
- stouputils/applications/upscaler/video.py +287 -0
- stouputils/archive.py +344 -0
- stouputils/backup.py +488 -0
- stouputils/collections.py +244 -0
- stouputils/continuous_delivery/__init__.py +27 -0
- stouputils/continuous_delivery/cd_utils.py +243 -0
- stouputils/continuous_delivery/github.py +522 -0
- stouputils/continuous_delivery/pypi.py +130 -0
- stouputils/continuous_delivery/pyproject.py +147 -0
- stouputils/continuous_delivery/stubs.py +86 -0
- stouputils/ctx.py +408 -0
- stouputils/data_science/config/get.py +51 -0
- stouputils/data_science/config/set.py +125 -0
- stouputils/data_science/data_processing/image/__init__.py +66 -0
- stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
- stouputils/data_science/data_processing/image/axis_flip.py +58 -0
- stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
- stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
- stouputils/data_science/data_processing/image/blur.py +59 -0
- stouputils/data_science/data_processing/image/brightness.py +54 -0
- stouputils/data_science/data_processing/image/canny.py +110 -0
- stouputils/data_science/data_processing/image/clahe.py +92 -0
- stouputils/data_science/data_processing/image/common.py +30 -0
- stouputils/data_science/data_processing/image/contrast.py +53 -0
- stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
- stouputils/data_science/data_processing/image/denoise.py +378 -0
- stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
- stouputils/data_science/data_processing/image/invert.py +64 -0
- stouputils/data_science/data_processing/image/laplacian.py +60 -0
- stouputils/data_science/data_processing/image/median_blur.py +52 -0
- stouputils/data_science/data_processing/image/noise.py +59 -0
- stouputils/data_science/data_processing/image/normalize.py +65 -0
- stouputils/data_science/data_processing/image/random_erase.py +66 -0
- stouputils/data_science/data_processing/image/resize.py +69 -0
- stouputils/data_science/data_processing/image/rotation.py +80 -0
- stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
- stouputils/data_science/data_processing/image/sharpening.py +55 -0
- stouputils/data_science/data_processing/image/shearing.py +64 -0
- stouputils/data_science/data_processing/image/threshold.py +64 -0
- stouputils/data_science/data_processing/image/translation.py +71 -0
- stouputils/data_science/data_processing/image/zoom.py +83 -0
- stouputils/data_science/data_processing/image_augmentation.py +118 -0
- stouputils/data_science/data_processing/image_preprocess.py +183 -0
- stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
- stouputils/data_science/data_processing/technique.py +481 -0
- stouputils/data_science/dataset/__init__.py +45 -0
- stouputils/data_science/dataset/dataset.py +292 -0
- stouputils/data_science/dataset/dataset_loader.py +135 -0
- stouputils/data_science/dataset/grouping_strategy.py +296 -0
- stouputils/data_science/dataset/image_loader.py +100 -0
- stouputils/data_science/dataset/xy_tuple.py +696 -0
- stouputils/data_science/metric_dictionnary.py +106 -0
- stouputils/data_science/metric_utils.py +847 -0
- stouputils/data_science/mlflow_utils.py +206 -0
- stouputils/data_science/models/abstract_model.py +149 -0
- stouputils/data_science/models/all.py +85 -0
- stouputils/data_science/models/base_keras.py +765 -0
- stouputils/data_science/models/keras/all.py +38 -0
- stouputils/data_science/models/keras/convnext.py +62 -0
- stouputils/data_science/models/keras/densenet.py +50 -0
- stouputils/data_science/models/keras/efficientnet.py +60 -0
- stouputils/data_science/models/keras/mobilenet.py +56 -0
- stouputils/data_science/models/keras/resnet.py +52 -0
- stouputils/data_science/models/keras/squeezenet.py +233 -0
- stouputils/data_science/models/keras/vgg.py +42 -0
- stouputils/data_science/models/keras/xception.py +38 -0
- stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
- stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
- stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
- stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
- stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
- stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
- stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
- stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
- stouputils/data_science/models/keras_utils/visualizations.py +416 -0
- stouputils/data_science/models/model_interface.py +939 -0
- stouputils/data_science/models/sandbox.py +116 -0
- stouputils/data_science/range_tuple.py +234 -0
- stouputils/data_science/scripts/augment_dataset.py +77 -0
- stouputils/data_science/scripts/exhaustive_process.py +133 -0
- stouputils/data_science/scripts/preprocess_dataset.py +70 -0
- stouputils/data_science/scripts/routine.py +168 -0
- stouputils/data_science/utils.py +285 -0
- stouputils/decorators.py +605 -0
- stouputils/image.py +441 -0
- stouputils/installer/__init__.py +18 -0
- stouputils/installer/common.py +67 -0
- stouputils/installer/downloader.py +101 -0
- stouputils/installer/linux.py +144 -0
- stouputils/installer/main.py +223 -0
- stouputils/installer/windows.py +136 -0
- stouputils/io.py +486 -0
- stouputils/parallel.py +483 -0
- stouputils/print.py +482 -0
- stouputils/py.typed +1 -0
- stouputils/stouputils/__init__.pyi +15 -0
- stouputils/stouputils/_deprecated.pyi +12 -0
- stouputils/stouputils/all_doctests.pyi +46 -0
- stouputils/stouputils/applications/__init__.pyi +2 -0
- stouputils/stouputils/applications/automatic_docs.pyi +106 -0
- stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
- stouputils/stouputils/applications/upscaler/config.pyi +18 -0
- stouputils/stouputils/applications/upscaler/image.pyi +109 -0
- stouputils/stouputils/applications/upscaler/video.pyi +60 -0
- stouputils/stouputils/archive.pyi +67 -0
- stouputils/stouputils/backup.pyi +109 -0
- stouputils/stouputils/collections.pyi +86 -0
- stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
- stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
- stouputils/stouputils/continuous_delivery/github.pyi +162 -0
- stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
- stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
- stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
- stouputils/stouputils/ctx.pyi +211 -0
- stouputils/stouputils/decorators.pyi +252 -0
- stouputils/stouputils/image.pyi +172 -0
- stouputils/stouputils/installer/__init__.pyi +5 -0
- stouputils/stouputils/installer/common.pyi +39 -0
- stouputils/stouputils/installer/downloader.pyi +24 -0
- stouputils/stouputils/installer/linux.pyi +39 -0
- stouputils/stouputils/installer/main.pyi +57 -0
- stouputils/stouputils/installer/windows.pyi +31 -0
- stouputils/stouputils/io.pyi +213 -0
- stouputils/stouputils/parallel.pyi +216 -0
- stouputils/stouputils/print.pyi +136 -0
- stouputils/stouputils/version_pkg.pyi +15 -0
- stouputils/version_pkg.py +189 -0
- stouputils-1.14.0.dist-info/METADATA +178 -0
- stouputils-1.14.0.dist-info/RECORD +140 -0
- stouputils-1.14.0.dist-info/WHEEL +4 -0
- stouputils-1.14.0.dist-info/entry_points.txt +3 -0
stouputils/print.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for printing messages with different levels of importance.
|
|
3
|
+
|
|
4
|
+
If a message is printed multiple times, it will be displayed as "(xN) message"
|
|
5
|
+
where N is the number of times the message has been printed.
|
|
6
|
+
|
|
7
|
+
.. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/print_module.gif
|
|
8
|
+
:alt: stouputils print examples
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Imports
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
16
|
+
from typing import IO, Any, TextIO, TypeVar, cast
|
|
17
|
+
|
|
18
|
+
# Colors constants
|
|
19
|
+
RESET: str = "\033[0m"
|
|
20
|
+
RED: str = "\033[91m"
|
|
21
|
+
GREEN: str = "\033[92m"
|
|
22
|
+
YELLOW: str = "\033[93m"
|
|
23
|
+
BLUE: str = "\033[94m"
|
|
24
|
+
MAGENTA: str = "\033[95m"
|
|
25
|
+
CYAN: str = "\033[96m"
|
|
26
|
+
LINE_UP: str = "\033[1A"
|
|
27
|
+
|
|
28
|
+
# Constants
|
|
29
|
+
BAR_FORMAT: str = "{l_bar}{bar}" + MAGENTA + "| {n_fmt}/{total_fmt} [{rate_fmt}{postfix}, {elapsed}<{remaining}]" + RESET
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
32
|
+
# Enable colors on Windows 10 terminal if applicable
|
|
33
|
+
if os.name == "nt":
|
|
34
|
+
os.system("color")
|
|
35
|
+
|
|
36
|
+
# Print functions
|
|
37
|
+
previous_args_kwards: tuple[Any, Any] = ((), {})
|
|
38
|
+
nb_values: int = 1
|
|
39
|
+
import_time: float = time.time()
|
|
40
|
+
|
|
41
|
+
# Colored for loop function
|
|
42
|
+
def colored_for_loop[T](
|
|
43
|
+
iterable: Iterable[T],
|
|
44
|
+
desc: str = "Processing",
|
|
45
|
+
color: str = MAGENTA,
|
|
46
|
+
bar_format: str = BAR_FORMAT,
|
|
47
|
+
ascii: bool = False,
|
|
48
|
+
smooth_tqdm: bool = True,
|
|
49
|
+
**kwargs: Any
|
|
50
|
+
) -> Iterator[T]:
|
|
51
|
+
""" Function to iterate over a list with a colored TQDM progress bar like the other functions in this module.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
iterable (Iterable): List to iterate over
|
|
55
|
+
desc (str): Description of the function execution displayed in the progress bar
|
|
56
|
+
color (str): Color of the progress bar (Defaults to MAGENTA)
|
|
57
|
+
bar_format (str): Format of the progress bar (Defaults to BAR_FORMAT)
|
|
58
|
+
ascii (bool): Whether to use ASCII or Unicode characters for the progress bar (Defaults to False)
|
|
59
|
+
smooth_tqdm (bool): Whether to enable smooth progress bar updates by setting miniters=1 and mininterval=0.0 (Defaults to True)
|
|
60
|
+
**kwargs: Additional arguments to pass to the TQDM progress bar
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
T: Each item of the iterable
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
>>> for i in colored_for_loop(range(10), desc="Time sleeping loop"):
|
|
67
|
+
... time.sleep(0.01)
|
|
68
|
+
>>> # Time sleeping loop: 100%|██████████████████| 10/10 [ 95.72it/s, 00:00<00:00]
|
|
69
|
+
"""
|
|
70
|
+
if bar_format == BAR_FORMAT:
|
|
71
|
+
bar_format = bar_format.replace(MAGENTA, color)
|
|
72
|
+
desc = color + desc
|
|
73
|
+
|
|
74
|
+
if smooth_tqdm:
|
|
75
|
+
kwargs.setdefault("mininterval", 0.0)
|
|
76
|
+
try:
|
|
77
|
+
total = len(iterable) # type: ignore
|
|
78
|
+
import shutil
|
|
79
|
+
width = shutil.get_terminal_size().columns
|
|
80
|
+
kwargs.setdefault("miniters", max(1, total // width))
|
|
81
|
+
except (TypeError, OSError):
|
|
82
|
+
kwargs.setdefault("miniters", 1)
|
|
83
|
+
|
|
84
|
+
from tqdm.auto import tqdm
|
|
85
|
+
yield from tqdm(iterable, desc=desc, bar_format=bar_format, ascii=ascii, **kwargs)
|
|
86
|
+
|
|
87
|
+
def info(
|
|
88
|
+
*values: Any,
|
|
89
|
+
color: str = GREEN,
|
|
90
|
+
text: str = "INFO ",
|
|
91
|
+
prefix: str = "",
|
|
92
|
+
file: TextIO | list[TextIO] | None = None,
|
|
93
|
+
**print_kwargs: Any,
|
|
94
|
+
) -> None:
|
|
95
|
+
""" Print an information message looking like "[INFO HH:MM:SS] message" in green by default.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
values (Any): Values to print (like the print function)
|
|
99
|
+
color (str): Color of the message (default: GREEN)
|
|
100
|
+
text (str): Text of the message (default: "INFO ")
|
|
101
|
+
prefix (str): Prefix to add to the values
|
|
102
|
+
file (TextIO|list[TextIO]): File(s) to write the message to (default: sys.stdout)
|
|
103
|
+
print_kwargs (dict): Keyword arguments to pass to the print function
|
|
104
|
+
"""
|
|
105
|
+
# Use stdout if no file is specified
|
|
106
|
+
if file is None:
|
|
107
|
+
file = sys.stdout
|
|
108
|
+
|
|
109
|
+
# If file is a list, recursively call info() for each file
|
|
110
|
+
if isinstance(file, list):
|
|
111
|
+
for f in file:
|
|
112
|
+
info(*values, color=color, text=text, prefix=prefix, file=f, **print_kwargs)
|
|
113
|
+
else:
|
|
114
|
+
# Build the message with prefix, color, text and timestamp
|
|
115
|
+
message: str = f"{prefix}{color}[{text} {current_time()}]"
|
|
116
|
+
|
|
117
|
+
# If this is a repeated print, add a line up and counter
|
|
118
|
+
if is_same_print(*values, color=color, text=text, prefix=prefix, **print_kwargs):
|
|
119
|
+
message = f"{LINE_UP}{message} (x{nb_values})"
|
|
120
|
+
|
|
121
|
+
# Print the message with the values and reset color
|
|
122
|
+
print(message, *values, RESET, file=file, **print_kwargs)
|
|
123
|
+
|
|
124
|
+
def debug(*values: Any, **print_kwargs: Any) -> None:
|
|
125
|
+
""" Print a debug message looking like "[DEBUG HH:MM:SS] message" in cyan by default. """
|
|
126
|
+
if "text" not in print_kwargs:
|
|
127
|
+
print_kwargs["text"] = "DEBUG"
|
|
128
|
+
if "color" not in print_kwargs:
|
|
129
|
+
print_kwargs["color"] = CYAN
|
|
130
|
+
info(*values, **print_kwargs)
|
|
131
|
+
|
|
132
|
+
def alt_debug(*values: Any, **print_kwargs: Any) -> None:
|
|
133
|
+
""" Print a debug message looking like "[DEBUG HH:MM:SS] message" in blue by default. """
|
|
134
|
+
if "text" not in print_kwargs:
|
|
135
|
+
print_kwargs["text"] = "DEBUG"
|
|
136
|
+
if "color" not in print_kwargs:
|
|
137
|
+
print_kwargs["color"] = BLUE
|
|
138
|
+
info(*values, **print_kwargs)
|
|
139
|
+
|
|
140
|
+
def suggestion(*values: Any, **print_kwargs: Any) -> None:
|
|
141
|
+
""" Print a suggestion message looking like "[SUGGESTION HH:MM:SS] message" in cyan by default. """
|
|
142
|
+
if "text" not in print_kwargs:
|
|
143
|
+
print_kwargs["text"] = "SUGGESTION"
|
|
144
|
+
if "color" not in print_kwargs:
|
|
145
|
+
print_kwargs["color"] = CYAN
|
|
146
|
+
info(*values, **print_kwargs)
|
|
147
|
+
|
|
148
|
+
def progress(*values: Any, **print_kwargs: Any) -> None:
|
|
149
|
+
""" Print a progress message looking like "[PROGRESS HH:MM:SS] message" in magenta by default. """
|
|
150
|
+
if "text" not in print_kwargs:
|
|
151
|
+
print_kwargs["text"] = "PROGRESS"
|
|
152
|
+
if "color" not in print_kwargs:
|
|
153
|
+
print_kwargs["color"] = MAGENTA
|
|
154
|
+
info(*values, **print_kwargs)
|
|
155
|
+
|
|
156
|
+
def warning(*values: Any, **print_kwargs: Any) -> None:
|
|
157
|
+
""" Print a warning message looking like "[WARNING HH:MM:SS] message" in yellow by default and in sys.stderr. """
|
|
158
|
+
if "file" not in print_kwargs:
|
|
159
|
+
print_kwargs["file"] = sys.stderr
|
|
160
|
+
if "text" not in print_kwargs:
|
|
161
|
+
print_kwargs["text"] = "WARNING"
|
|
162
|
+
if "color" not in print_kwargs:
|
|
163
|
+
print_kwargs["color"] = YELLOW
|
|
164
|
+
info(*values, **print_kwargs)
|
|
165
|
+
|
|
166
|
+
def error(*values: Any, exit: bool = False, **print_kwargs: Any) -> None:
|
|
167
|
+
""" Print an error message (in sys.stderr and in red by default)
|
|
168
|
+
and optionally ask the user to continue or stop the program.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
values (Any): Values to print (like the print function)
|
|
172
|
+
exit (bool): Whether to ask the user to continue or stop the program,
|
|
173
|
+
false to ignore the error automatically and continue
|
|
174
|
+
print_kwargs (dict): Keyword arguments to pass to the print function
|
|
175
|
+
"""
|
|
176
|
+
file: TextIO = sys.stderr
|
|
177
|
+
if "file" in print_kwargs:
|
|
178
|
+
if isinstance(print_kwargs["file"], list):
|
|
179
|
+
file = cast(TextIO, print_kwargs["file"][0])
|
|
180
|
+
else:
|
|
181
|
+
file = print_kwargs["file"]
|
|
182
|
+
if "text" not in print_kwargs:
|
|
183
|
+
print_kwargs["text"] = "ERROR"
|
|
184
|
+
if "color" not in print_kwargs:
|
|
185
|
+
print_kwargs["color"] = RED
|
|
186
|
+
info(*values, **print_kwargs)
|
|
187
|
+
if exit:
|
|
188
|
+
try:
|
|
189
|
+
print("Press enter to ignore error and continue, or 'CTRL+C' to stop the program... ", file=file)
|
|
190
|
+
input()
|
|
191
|
+
except (KeyboardInterrupt, EOFError):
|
|
192
|
+
print(file=file)
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
def whatisit(
|
|
196
|
+
*values: Any,
|
|
197
|
+
print_function: Callable[..., None] = debug,
|
|
198
|
+
max_length: int = 250,
|
|
199
|
+
color: str = CYAN,
|
|
200
|
+
**print_kwargs: Any,
|
|
201
|
+
) -> None:
|
|
202
|
+
""" Print the type of each value and the value itself, with its id and length/shape.
|
|
203
|
+
|
|
204
|
+
The output format is: "type, <id id_number>: (length/shape) value"
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
values (Any): Values to print
|
|
208
|
+
print_function (Callable): Function to use to print the values (default: debug())
|
|
209
|
+
max_length (int): Maximum length of the value string to print (default: 250)
|
|
210
|
+
color (str): Color of the message (default: CYAN)
|
|
211
|
+
print_kwargs (dict): Keyword arguments to pass to the print function
|
|
212
|
+
"""
|
|
213
|
+
def _internal(value: Any) -> str:
|
|
214
|
+
""" Get the string representation of the value, with length or shape instead of length if shape is available """
|
|
215
|
+
|
|
216
|
+
# Build metadata parts list
|
|
217
|
+
metadata_parts: list[str] = []
|
|
218
|
+
|
|
219
|
+
# Get the dtype if available
|
|
220
|
+
try:
|
|
221
|
+
metadata_parts.append(f"dtype: {value.dtype}")
|
|
222
|
+
except (AttributeError, TypeError):
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
# Get the shape or length of the value
|
|
226
|
+
try:
|
|
227
|
+
metadata_parts.append(f"shape: {value.shape}")
|
|
228
|
+
except (AttributeError, TypeError):
|
|
229
|
+
try:
|
|
230
|
+
metadata_parts.append(f"length: {len(value)}")
|
|
231
|
+
except (AttributeError, TypeError):
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# Get the min and max if available (Iterable of numbers)
|
|
235
|
+
try:
|
|
236
|
+
if not isinstance(value, str | bytes | bytearray | dict | int | float):
|
|
237
|
+
import numpy as np
|
|
238
|
+
mini, maxi = np.min(value), np.max(value)
|
|
239
|
+
if mini != maxi:
|
|
240
|
+
metadata_parts.append(f"min: {mini}")
|
|
241
|
+
metadata_parts.append(f"max: {maxi}")
|
|
242
|
+
except (Exception):
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
# Combine metadata into a single parenthesized string
|
|
246
|
+
metadata_str: str = f"({', '.join(metadata_parts)}) " if metadata_parts else ""
|
|
247
|
+
|
|
248
|
+
# Get the string representation of the value
|
|
249
|
+
value = cast(Any, value)
|
|
250
|
+
value_str: str = str(value)
|
|
251
|
+
if len(value_str) > max_length:
|
|
252
|
+
value_str = value_str[:max_length] + "..."
|
|
253
|
+
if "\n" in value_str:
|
|
254
|
+
value_str = "\n" + value_str # Add a newline before the value if there is a newline in it.
|
|
255
|
+
|
|
256
|
+
# Return the formatted string
|
|
257
|
+
return f"{type(value)}, <id {id(value)}>: {metadata_str}{value_str}"
|
|
258
|
+
|
|
259
|
+
# Add the color to the message
|
|
260
|
+
if "color" not in print_kwargs:
|
|
261
|
+
print_kwargs["color"] = color
|
|
262
|
+
|
|
263
|
+
# Set text to "What is it?" if not already set
|
|
264
|
+
if "text" not in print_kwargs:
|
|
265
|
+
print_kwargs["text"] = "What is it?"
|
|
266
|
+
|
|
267
|
+
# Print the values
|
|
268
|
+
if len(values) > 1:
|
|
269
|
+
print_function("".join(f"\n {_internal(value)}" for value in values), **print_kwargs)
|
|
270
|
+
elif len(values) == 1:
|
|
271
|
+
print_function(_internal(values[0]), **print_kwargs)
|
|
272
|
+
|
|
273
|
+
def breakpoint(*values: Any, print_function: Callable[..., None] = warning, **print_kwargs: Any) -> None:
|
|
274
|
+
""" Breakpoint function, pause the program and print the values.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
values (Any): Values to print
|
|
278
|
+
print_function (Callable): Function to use to print the values (default: warning())
|
|
279
|
+
print_kwargs (dict): Keyword arguments to pass to the print function
|
|
280
|
+
"""
|
|
281
|
+
if "text" not in print_kwargs:
|
|
282
|
+
print_kwargs["text"] = "BREAKPOINT (press Enter)"
|
|
283
|
+
file: TextIO = sys.stderr
|
|
284
|
+
if "file" in print_kwargs:
|
|
285
|
+
if isinstance(print_kwargs["file"], list):
|
|
286
|
+
file = cast(TextIO, print_kwargs["file"][0])
|
|
287
|
+
else:
|
|
288
|
+
file = print_kwargs["file"]
|
|
289
|
+
whatisit(*values, print_function=print_function, **print_kwargs)
|
|
290
|
+
try:
|
|
291
|
+
input()
|
|
292
|
+
except (KeyboardInterrupt, EOFError):
|
|
293
|
+
print(file=file)
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# TeeMultiOutput class to duplicate output to multiple file-like objects
|
|
298
|
+
class TeeMultiOutput:
|
|
299
|
+
""" File-like object that duplicates output to multiple file-like objects.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
*files (IO[Any]): One or more file-like objects that have write and flush methods
|
|
303
|
+
strip_colors (bool): Strip ANSI color codes from output sent to non-stdout/stderr files
|
|
304
|
+
ascii_only (bool): Replace non-ASCII characters with their ASCII equivalents for non-stdout/stderr files
|
|
305
|
+
ignore_lineup (bool): Ignore lines containing LINE_UP escape sequence in non-terminal outputs
|
|
306
|
+
|
|
307
|
+
Examples:
|
|
308
|
+
>>> f = open("logfile.txt", "w")
|
|
309
|
+
>>> sys.stdout = TeeMultiOutput(sys.stdout, f)
|
|
310
|
+
>>> print("Hello World") # Output goes to both console and file
|
|
311
|
+
Hello World
|
|
312
|
+
>>> f.close() # TeeMultiOutput will handle any future writes to closed files gracefully
|
|
313
|
+
"""
|
|
314
|
+
def __init__(
|
|
315
|
+
self, *files: IO[Any], strip_colors: bool = True, ascii_only: bool = True, ignore_lineup: bool = True
|
|
316
|
+
) -> None:
|
|
317
|
+
# Flatten any TeeMultiOutput instances in files
|
|
318
|
+
flattened_files: list[IO[Any]] = []
|
|
319
|
+
for file in files:
|
|
320
|
+
if isinstance(file, TeeMultiOutput):
|
|
321
|
+
flattened_files.extend(file.files)
|
|
322
|
+
else:
|
|
323
|
+
flattened_files.append(file)
|
|
324
|
+
|
|
325
|
+
self.files: tuple[IO[Any], ...] = tuple(flattened_files)
|
|
326
|
+
""" File-like objects to write to """
|
|
327
|
+
self.strip_colors: bool = strip_colors
|
|
328
|
+
""" Whether to strip ANSI color codes from output sent to non-stdout/stderr files """
|
|
329
|
+
self.ascii_only: bool = ascii_only
|
|
330
|
+
""" Whether to replace non-ASCII characters with their ASCII equivalents for non-stdout/stderr files """
|
|
331
|
+
self.ignore_lineup: bool = ignore_lineup
|
|
332
|
+
""" Whether to ignore lines containing LINE_UP escape sequence in non-terminal outputs """
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def encoding(self) -> str:
|
|
336
|
+
""" Get the encoding of the first file, or "utf-8" as fallback.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
str: The encoding, ex: "utf-8", "ascii", "latin1", etc.
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
return self.files[0].encoding # type: ignore
|
|
343
|
+
except (IndexError, AttributeError):
|
|
344
|
+
return "utf-8"
|
|
345
|
+
|
|
346
|
+
def write(self, obj: str) -> int:
|
|
347
|
+
""" Write the object to all files while stripping colors if needed.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
obj (str): String to write
|
|
351
|
+
Returns:
|
|
352
|
+
int: Number of characters written to the first file
|
|
353
|
+
"""
|
|
354
|
+
files_to_remove: list[IO[Any]] = []
|
|
355
|
+
num_chars_written: int = 0
|
|
356
|
+
for i, f in enumerate(self.files):
|
|
357
|
+
try:
|
|
358
|
+
# Check if file is closed
|
|
359
|
+
if hasattr(f, "closed") and f.closed:
|
|
360
|
+
files_to_remove.append(f)
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
# Check if this file is a terminal/console or a regular file
|
|
364
|
+
content: str = obj
|
|
365
|
+
if not (hasattr(f, "isatty") and f.isatty()):
|
|
366
|
+
# Non-terminal files get processed content (stripped colors, ASCII-only, etc.)
|
|
367
|
+
|
|
368
|
+
# Skip content if it contains LINE_UP and ignore_lineup is True
|
|
369
|
+
if self.ignore_lineup and (LINE_UP in content or "\r" in content):
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
# Strip colors if needed
|
|
373
|
+
if self.strip_colors:
|
|
374
|
+
content = remove_colors(content)
|
|
375
|
+
|
|
376
|
+
# Replace Unicode block characters with ASCII equivalents
|
|
377
|
+
# Replace other problematic Unicode characters as needed
|
|
378
|
+
if self.ascii_only:
|
|
379
|
+
content = content.replace('█', '#')
|
|
380
|
+
content = ''.join(c if ord(c) < 128 else '?' for c in content)
|
|
381
|
+
|
|
382
|
+
# Write content to file
|
|
383
|
+
if i == 0:
|
|
384
|
+
num_chars_written = f.write(content)
|
|
385
|
+
else:
|
|
386
|
+
f.write(content)
|
|
387
|
+
|
|
388
|
+
except ValueError:
|
|
389
|
+
# ValueError is raised when writing to a closed file
|
|
390
|
+
files_to_remove.append(f)
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
# Remove closed files from the list
|
|
395
|
+
if files_to_remove:
|
|
396
|
+
self.files = tuple(f for f in self.files if f not in files_to_remove)
|
|
397
|
+
return num_chars_written
|
|
398
|
+
|
|
399
|
+
def flush(self) -> None:
|
|
400
|
+
""" Flush all files. """
|
|
401
|
+
for f in self.files:
|
|
402
|
+
try:
|
|
403
|
+
f.flush()
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
def fileno(self) -> int:
|
|
408
|
+
""" Return the file descriptor of the first file. """
|
|
409
|
+
return self.files[0].fileno() if hasattr(self.files[0], "fileno") else 0
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# Utility functions
|
|
413
|
+
def remove_colors(text: str) -> str:
|
|
414
|
+
""" Remove the colors from a text """
|
|
415
|
+
for color in [RESET, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, LINE_UP]:
|
|
416
|
+
text = text.replace(color, "")
|
|
417
|
+
return text
|
|
418
|
+
|
|
419
|
+
def is_same_print(*args: Any, **kwargs: Any) -> bool:
|
|
420
|
+
""" Checks if the current print call is the same as the previous one. """
|
|
421
|
+
global previous_args_kwards, nb_values
|
|
422
|
+
try:
|
|
423
|
+
if previous_args_kwards == (args, kwargs):
|
|
424
|
+
nb_values += 1
|
|
425
|
+
return True
|
|
426
|
+
except Exception:
|
|
427
|
+
# Comparison failed (e.g., comparing DataFrames or other complex objects)
|
|
428
|
+
# Use str() for comparison instead
|
|
429
|
+
current_str: str = str((args, kwargs))
|
|
430
|
+
previous_str: str = str(previous_args_kwards)
|
|
431
|
+
if previous_str == current_str:
|
|
432
|
+
nb_values += 1
|
|
433
|
+
return True
|
|
434
|
+
# Else, update previous args and reset counter
|
|
435
|
+
previous_args_kwards = (args, kwargs)
|
|
436
|
+
nb_values = 1
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
def current_time() -> str:
|
|
440
|
+
""" Get the current time as "HH:MM:SS" if less than 24 hours since import, else "YYYY-MM-DD HH:MM:SS" """
|
|
441
|
+
# If the import time is more than 24 hours, return the full datetime
|
|
442
|
+
if (time.time() - import_time) > (24 * 60 * 60):
|
|
443
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
|
444
|
+
else:
|
|
445
|
+
return time.strftime("%H:%M:%S")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# Test the print functions
|
|
449
|
+
if __name__ == "__main__":
|
|
450
|
+
info("Hello", "World")
|
|
451
|
+
time.sleep(0.5)
|
|
452
|
+
info("Hello", "World")
|
|
453
|
+
time.sleep(0.5)
|
|
454
|
+
info("Hello", "World")
|
|
455
|
+
time.sleep(0.5)
|
|
456
|
+
info("Not Hello World !")
|
|
457
|
+
time.sleep(0.5)
|
|
458
|
+
info("Hello", "World")
|
|
459
|
+
time.sleep(0.5)
|
|
460
|
+
info("Hello", "World")
|
|
461
|
+
|
|
462
|
+
# All remaining print functions
|
|
463
|
+
alt_debug("Hello", "World")
|
|
464
|
+
debug("Hello", "World")
|
|
465
|
+
suggestion("Hello", "World")
|
|
466
|
+
progress("Hello", "World")
|
|
467
|
+
warning("Hello", "World")
|
|
468
|
+
error("Hello", "World", exit=False)
|
|
469
|
+
whatisit("Hello")
|
|
470
|
+
whatisit("Hello", "World")
|
|
471
|
+
|
|
472
|
+
# Test whatisit with different types
|
|
473
|
+
import numpy as np
|
|
474
|
+
print()
|
|
475
|
+
whatisit(
|
|
476
|
+
123,
|
|
477
|
+
"Hello World",
|
|
478
|
+
[1, 2, 3, 4, 5],
|
|
479
|
+
np.array([[1, 2, 3], [4, 5, 6]]),
|
|
480
|
+
{"a": 1, "b": 2},
|
|
481
|
+
)
|
|
482
|
+
|
stouputils/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ._deprecated import *
|
|
2
|
+
from .all_doctests import *
|
|
3
|
+
from .archive import *
|
|
4
|
+
from .backup import *
|
|
5
|
+
from .collections import *
|
|
6
|
+
from .continuous_delivery import *
|
|
7
|
+
from .ctx import *
|
|
8
|
+
from .decorators import *
|
|
9
|
+
from .image import *
|
|
10
|
+
from .io import *
|
|
11
|
+
from .parallel import *
|
|
12
|
+
from .print import *
|
|
13
|
+
from .version_pkg import *
|
|
14
|
+
|
|
15
|
+
__version__: str
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .decorators import LogLevels as LogLevels, deprecated as deprecated
|
|
2
|
+
from .io import csv_dump as csv_dump, csv_load as csv_load, json_dump as json_dump, json_load as json_load
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
def super_csv_dump(*args: Any, **kwargs: Any) -> Any:
|
|
6
|
+
''' Deprecated function, use "csv_dump" instead. '''
|
|
7
|
+
def super_csv_load(*args: Any, **kwargs: Any) -> Any:
|
|
8
|
+
''' Deprecated function, use "csv_load" instead. '''
|
|
9
|
+
def super_json_dump(*args: Any, **kwargs: Any) -> Any:
|
|
10
|
+
''' Deprecated function, use "json_dump" instead. '''
|
|
11
|
+
def super_json_load(*args: Any, **kwargs: Any) -> Any:
|
|
12
|
+
''' Deprecated function, use "json_load" instead. '''
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from . import decorators as decorators
|
|
2
|
+
from .decorators import measure_time as measure_time
|
|
3
|
+
from .io import clean_path as clean_path, relative_path as relative_path
|
|
4
|
+
from .print import error as error, info as info, progress as progress, warning as warning
|
|
5
|
+
from doctest import TestResults as TestResults
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
|
|
8
|
+
def launch_tests(root_dir: str, strict: bool = True) -> int:
|
|
9
|
+
''' Main function to launch tests for all modules in the given directory.
|
|
10
|
+
|
|
11
|
+
\tArgs:
|
|
12
|
+
\t\troot_dir\t\t\t\t(str):\t\t\tRoot directory to search for modules
|
|
13
|
+
\t\tstrict\t\t\t\t\t(bool):\t\t\tModify the force_raise_exception variable to True in the decorators module
|
|
14
|
+
|
|
15
|
+
\tReturns:
|
|
16
|
+
\t\tint: The number of failed tests
|
|
17
|
+
|
|
18
|
+
\tExamples:
|
|
19
|
+
\t\t>>> launch_tests("unknown_dir")
|
|
20
|
+
\t\tTraceback (most recent call last):
|
|
21
|
+
\t\t\t...
|
|
22
|
+
\t\tValueError: No modules found in \'unknown_dir\'
|
|
23
|
+
|
|
24
|
+
\t.. code-block:: python
|
|
25
|
+
|
|
26
|
+
\t\t> if launch_tests("/path/to/source") > 0:
|
|
27
|
+
\t\t\tsys.exit(1)
|
|
28
|
+
\t\t[PROGRESS HH:MM:SS] Importing module \'module1\'\ttook 0.001s
|
|
29
|
+
\t\t[PROGRESS HH:MM:SS] Importing module \'module2\'\ttook 0.002s
|
|
30
|
+
\t\t[PROGRESS HH:MM:SS] Importing module \'module3\'\ttook 0.003s
|
|
31
|
+
\t\t[PROGRESS HH:MM:SS] Importing module \'module4\'\ttook 0.004s
|
|
32
|
+
\t\t[INFO HH:MM:SS] Testing 4 modules...
|
|
33
|
+
\t\t[PROGRESS HH:MM:SS] Testing module \'module1\'\ttook 0.005s
|
|
34
|
+
\t\t[PROGRESS HH:MM:SS] Testing module \'module2\'\ttook 0.006s
|
|
35
|
+
\t\t[PROGRESS HH:MM:SS] Testing module \'module3\'\ttook 0.007s
|
|
36
|
+
\t\t[PROGRESS HH:MM:SS] Testing module \'module4\'\ttook 0.008s
|
|
37
|
+
\t'''
|
|
38
|
+
def test_module_with_progress(module: ModuleType, separator: str) -> TestResults:
|
|
39
|
+
""" Test a module with testmod and measure the time taken with progress printing.
|
|
40
|
+
|
|
41
|
+
\tArgs:
|
|
42
|
+
\t\tmodule\t\t(ModuleType):\tModule to test
|
|
43
|
+
\t\tseparator\t(str):\t\t\tSeparator string for alignment in output
|
|
44
|
+
\tReturns:
|
|
45
|
+
\t\tTestResults: The results of the tests
|
|
46
|
+
\t"""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from ..continuous_delivery import version_to_float as version_to_float
|
|
2
|
+
from ..decorators import LogLevels as LogLevels, handle_error as handle_error, simple_cache as simple_cache
|
|
3
|
+
from ..io import clean_path as clean_path, json_dump as json_dump, super_open as super_open
|
|
4
|
+
from ..print import info as info
|
|
5
|
+
from collections.abc import Callable as Callable
|
|
6
|
+
|
|
7
|
+
REQUIREMENTS: list[str]
|
|
8
|
+
|
|
9
|
+
def check_dependencies(html_theme: str) -> None:
|
|
10
|
+
''' Check for each requirement if it is installed.
|
|
11
|
+
|
|
12
|
+
\tArgs:
|
|
13
|
+
\t\thtml_theme (str): HTML theme to use for the documentation, to check if it is installed (e.g. "breeze", "pydata_sphinx_theme", "furo", etc.)
|
|
14
|
+
\t'''
|
|
15
|
+
def get_sphinx_conf_content(project: str, project_dir: str, author: str, current_version: str, copyright: str, html_logo: str, html_favicon: str, html_theme: str = 'breeze', github_user: str = '', github_repo: str = '', version_list: list[str] | None = None, skip_undocumented: bool = True) -> str:
|
|
16
|
+
""" Get the content of the Sphinx configuration file.
|
|
17
|
+
|
|
18
|
+
\tArgs:
|
|
19
|
+
\t\tproject (str): Name of the project
|
|
20
|
+
\t\tproject_dir (str): Path to the project directory
|
|
21
|
+
\t\tauthor (str): Author of the project
|
|
22
|
+
\t\tcurrent_version (str): Current version
|
|
23
|
+
\t\tcopyright (str): Copyright information
|
|
24
|
+
\t\thtml_logo (str): URL to the logo
|
|
25
|
+
\t\thtml_favicon (str): URL to the favicon
|
|
26
|
+
\t\tgithub_user (str): GitHub username
|
|
27
|
+
\t\tgithub_repo (str): GitHub repository name
|
|
28
|
+
\t\tversion_list (list[str] | None): List of versions. Defaults to None
|
|
29
|
+
\t\tskip_undocumented (bool): Whether to skip undocumented members. Defaults to True
|
|
30
|
+
|
|
31
|
+
\tReturns:
|
|
32
|
+
\t\tstr: Content of the Sphinx configuration file
|
|
33
|
+
\t"""
|
|
34
|
+
def get_versions_from_github(github_user: str, github_repo: str, recent_minor_versions: int = 2) -> list[str]:
|
|
35
|
+
""" Get list of versions from GitHub gh-pages branch.
|
|
36
|
+
\tOnly shows detailed versions for the last N minor versions, and keeps only
|
|
37
|
+
\tthe latest patch version for older minor versions.
|
|
38
|
+
|
|
39
|
+
\tArgs:
|
|
40
|
+
\t\tgithub_user (str): GitHub username
|
|
41
|
+
\t\tgithub_repo (str): GitHub repository name
|
|
42
|
+
\t\trecent_minor_versions (int): Number of recent minor versions to show all patches for (-1 for all).
|
|
43
|
+
|
|
44
|
+
\tReturns:
|
|
45
|
+
\t\tlist[str]: List of versions, with 'latest' as first element
|
|
46
|
+
\t"""
|
|
47
|
+
def markdown_to_rst(markdown_content: str) -> str:
|
|
48
|
+
""" Convert markdown content to RST format.
|
|
49
|
+
|
|
50
|
+
\tArgs:
|
|
51
|
+
\t\tmarkdown_content (str): Markdown content
|
|
52
|
+
|
|
53
|
+
\tReturns:
|
|
54
|
+
\t\tstr: RST content
|
|
55
|
+
\t"""
|
|
56
|
+
def generate_index_rst(readme_path: str, index_path: str, project: str, github_user: str, github_repo: str, get_versions_function: Callable[[str, str, int], list[str]] = ..., recent_minor_versions: int = 2) -> None:
|
|
57
|
+
""" Generate index.rst from README.md content.
|
|
58
|
+
|
|
59
|
+
\tArgs:
|
|
60
|
+
\t\treadme_path (str): Path to the README.md file
|
|
61
|
+
\t\tindex_path (str): Path where index.rst should be created
|
|
62
|
+
\t\tproject (str): Name of the project
|
|
63
|
+
\t\tgithub_user (str): GitHub username
|
|
64
|
+
\t\tgithub_repo (str): GitHub repository name
|
|
65
|
+
\t\tget_versions_function (Callable[[str, str, int], list[str]]): Function to get versions from GitHub
|
|
66
|
+
\t\trecent_minor_versions (int): Number of recent minor versions to show all patches for. Defaults to 2
|
|
67
|
+
\t"""
|
|
68
|
+
def generate_documentation(source_dir: str, modules_dir: str, project_dir: str, build_dir: str) -> None:
|
|
69
|
+
""" Generate documentation using Sphinx.
|
|
70
|
+
|
|
71
|
+
\tArgs:
|
|
72
|
+
\t\tsource_dir (str): Source directory
|
|
73
|
+
\t\tmodules_dir (str): Modules directory
|
|
74
|
+
\t\tproject_dir (str): Project directory
|
|
75
|
+
\t\tbuild_dir (str): Build directory
|
|
76
|
+
\t"""
|
|
77
|
+
def generate_redirect_html(filepath: str) -> None:
|
|
78
|
+
""" Generate HTML content for redirect page.
|
|
79
|
+
|
|
80
|
+
\tArgs:
|
|
81
|
+
\t\tfilepath (str): Path to the file where the HTML content should be written
|
|
82
|
+
\t"""
|
|
83
|
+
def update_documentation(root_path: str, project: str, project_dir: str = '', author: str = 'Author', copyright: str = '2025, Author', html_logo: str = '', html_favicon: str = '', html_theme: str = 'breeze', github_user: str = '', github_repo: str = '', version: str | None = None, skip_undocumented: bool = True, recent_minor_versions: int = 2, get_versions_function: Callable[[str, str, int], list[str]] = ..., generate_index_function: Callable[..., None] = ..., generate_docs_function: Callable[..., None] = ..., generate_redirect_function: Callable[[str], None] = ..., get_conf_content_function: Callable[..., str] = ...) -> None:
|
|
84
|
+
''' Update the Sphinx documentation.
|
|
85
|
+
|
|
86
|
+
\tArgs:
|
|
87
|
+
\t\troot_path (str): Root path of the project
|
|
88
|
+
\t\tproject (str): Name of the project
|
|
89
|
+
\t\tproject_dir (str): Path to the project directory (to be used with generate_docs_function)
|
|
90
|
+
\t\tauthor (str): Author of the project
|
|
91
|
+
\t\tcopyright (str): Copyright information
|
|
92
|
+
\t\thtml_logo (str): URL to the logo
|
|
93
|
+
\t\thtml_favicon (str): URL to the favicon
|
|
94
|
+
\t\thtml_theme (str): Theme to use for the documentation. Defaults to "breeze"
|
|
95
|
+
\t\tgithub_user (str): GitHub username
|
|
96
|
+
\t\tgithub_repo (str): GitHub repository name
|
|
97
|
+
\t\tversion (str | None): Version to build documentation for (e.g. "1.0.0", defaults to "latest")
|
|
98
|
+
\t\tskip_undocumented (bool): Whether to skip undocumented members. Defaults to True
|
|
99
|
+
\t\trecent_minor_versions (int): Number of recent minor versions to show all patches for. Defaults to 2
|
|
100
|
+
|
|
101
|
+
\t\tget_versions_function (Callable[[str, str, int], list[str]]): Function to get versions from GitHub
|
|
102
|
+
\t\tgenerate_index_function (Callable[..., None]): Function to generate index.rst
|
|
103
|
+
\t\tgenerate_docs_function (Callable[..., None]): Function to generate documentation
|
|
104
|
+
\t\tgenerate_redirect_function (Callable[[str], None]): Function to create redirect file
|
|
105
|
+
\t\tget_conf_content_function (Callable[..., str]): Function to get Sphinx conf.py content
|
|
106
|
+
\t'''
|