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.
Files changed (140) hide show
  1. stouputils/__init__.py +40 -0
  2. stouputils/__main__.py +86 -0
  3. stouputils/_deprecated.py +37 -0
  4. stouputils/all_doctests.py +160 -0
  5. stouputils/applications/__init__.py +22 -0
  6. stouputils/applications/automatic_docs.py +634 -0
  7. stouputils/applications/upscaler/__init__.py +39 -0
  8. stouputils/applications/upscaler/config.py +128 -0
  9. stouputils/applications/upscaler/image.py +247 -0
  10. stouputils/applications/upscaler/video.py +287 -0
  11. stouputils/archive.py +344 -0
  12. stouputils/backup.py +488 -0
  13. stouputils/collections.py +244 -0
  14. stouputils/continuous_delivery/__init__.py +27 -0
  15. stouputils/continuous_delivery/cd_utils.py +243 -0
  16. stouputils/continuous_delivery/github.py +522 -0
  17. stouputils/continuous_delivery/pypi.py +130 -0
  18. stouputils/continuous_delivery/pyproject.py +147 -0
  19. stouputils/continuous_delivery/stubs.py +86 -0
  20. stouputils/ctx.py +408 -0
  21. stouputils/data_science/config/get.py +51 -0
  22. stouputils/data_science/config/set.py +125 -0
  23. stouputils/data_science/data_processing/image/__init__.py +66 -0
  24. stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
  25. stouputils/data_science/data_processing/image/axis_flip.py +58 -0
  26. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
  27. stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
  28. stouputils/data_science/data_processing/image/blur.py +59 -0
  29. stouputils/data_science/data_processing/image/brightness.py +54 -0
  30. stouputils/data_science/data_processing/image/canny.py +110 -0
  31. stouputils/data_science/data_processing/image/clahe.py +92 -0
  32. stouputils/data_science/data_processing/image/common.py +30 -0
  33. stouputils/data_science/data_processing/image/contrast.py +53 -0
  34. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
  35. stouputils/data_science/data_processing/image/denoise.py +378 -0
  36. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
  37. stouputils/data_science/data_processing/image/invert.py +64 -0
  38. stouputils/data_science/data_processing/image/laplacian.py +60 -0
  39. stouputils/data_science/data_processing/image/median_blur.py +52 -0
  40. stouputils/data_science/data_processing/image/noise.py +59 -0
  41. stouputils/data_science/data_processing/image/normalize.py +65 -0
  42. stouputils/data_science/data_processing/image/random_erase.py +66 -0
  43. stouputils/data_science/data_processing/image/resize.py +69 -0
  44. stouputils/data_science/data_processing/image/rotation.py +80 -0
  45. stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
  46. stouputils/data_science/data_processing/image/sharpening.py +55 -0
  47. stouputils/data_science/data_processing/image/shearing.py +64 -0
  48. stouputils/data_science/data_processing/image/threshold.py +64 -0
  49. stouputils/data_science/data_processing/image/translation.py +71 -0
  50. stouputils/data_science/data_processing/image/zoom.py +83 -0
  51. stouputils/data_science/data_processing/image_augmentation.py +118 -0
  52. stouputils/data_science/data_processing/image_preprocess.py +183 -0
  53. stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
  54. stouputils/data_science/data_processing/technique.py +481 -0
  55. stouputils/data_science/dataset/__init__.py +45 -0
  56. stouputils/data_science/dataset/dataset.py +292 -0
  57. stouputils/data_science/dataset/dataset_loader.py +135 -0
  58. stouputils/data_science/dataset/grouping_strategy.py +296 -0
  59. stouputils/data_science/dataset/image_loader.py +100 -0
  60. stouputils/data_science/dataset/xy_tuple.py +696 -0
  61. stouputils/data_science/metric_dictionnary.py +106 -0
  62. stouputils/data_science/metric_utils.py +847 -0
  63. stouputils/data_science/mlflow_utils.py +206 -0
  64. stouputils/data_science/models/abstract_model.py +149 -0
  65. stouputils/data_science/models/all.py +85 -0
  66. stouputils/data_science/models/base_keras.py +765 -0
  67. stouputils/data_science/models/keras/all.py +38 -0
  68. stouputils/data_science/models/keras/convnext.py +62 -0
  69. stouputils/data_science/models/keras/densenet.py +50 -0
  70. stouputils/data_science/models/keras/efficientnet.py +60 -0
  71. stouputils/data_science/models/keras/mobilenet.py +56 -0
  72. stouputils/data_science/models/keras/resnet.py +52 -0
  73. stouputils/data_science/models/keras/squeezenet.py +233 -0
  74. stouputils/data_science/models/keras/vgg.py +42 -0
  75. stouputils/data_science/models/keras/xception.py +38 -0
  76. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
  77. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
  78. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
  79. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
  80. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
  81. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
  82. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
  83. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
  84. stouputils/data_science/models/keras_utils/visualizations.py +416 -0
  85. stouputils/data_science/models/model_interface.py +939 -0
  86. stouputils/data_science/models/sandbox.py +116 -0
  87. stouputils/data_science/range_tuple.py +234 -0
  88. stouputils/data_science/scripts/augment_dataset.py +77 -0
  89. stouputils/data_science/scripts/exhaustive_process.py +133 -0
  90. stouputils/data_science/scripts/preprocess_dataset.py +70 -0
  91. stouputils/data_science/scripts/routine.py +168 -0
  92. stouputils/data_science/utils.py +285 -0
  93. stouputils/decorators.py +605 -0
  94. stouputils/image.py +441 -0
  95. stouputils/installer/__init__.py +18 -0
  96. stouputils/installer/common.py +67 -0
  97. stouputils/installer/downloader.py +101 -0
  98. stouputils/installer/linux.py +144 -0
  99. stouputils/installer/main.py +223 -0
  100. stouputils/installer/windows.py +136 -0
  101. stouputils/io.py +486 -0
  102. stouputils/parallel.py +483 -0
  103. stouputils/print.py +482 -0
  104. stouputils/py.typed +1 -0
  105. stouputils/stouputils/__init__.pyi +15 -0
  106. stouputils/stouputils/_deprecated.pyi +12 -0
  107. stouputils/stouputils/all_doctests.pyi +46 -0
  108. stouputils/stouputils/applications/__init__.pyi +2 -0
  109. stouputils/stouputils/applications/automatic_docs.pyi +106 -0
  110. stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
  111. stouputils/stouputils/applications/upscaler/config.pyi +18 -0
  112. stouputils/stouputils/applications/upscaler/image.pyi +109 -0
  113. stouputils/stouputils/applications/upscaler/video.pyi +60 -0
  114. stouputils/stouputils/archive.pyi +67 -0
  115. stouputils/stouputils/backup.pyi +109 -0
  116. stouputils/stouputils/collections.pyi +86 -0
  117. stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
  118. stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
  119. stouputils/stouputils/continuous_delivery/github.pyi +162 -0
  120. stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
  121. stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
  122. stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
  123. stouputils/stouputils/ctx.pyi +211 -0
  124. stouputils/stouputils/decorators.pyi +252 -0
  125. stouputils/stouputils/image.pyi +172 -0
  126. stouputils/stouputils/installer/__init__.pyi +5 -0
  127. stouputils/stouputils/installer/common.pyi +39 -0
  128. stouputils/stouputils/installer/downloader.pyi +24 -0
  129. stouputils/stouputils/installer/linux.pyi +39 -0
  130. stouputils/stouputils/installer/main.pyi +57 -0
  131. stouputils/stouputils/installer/windows.pyi +31 -0
  132. stouputils/stouputils/io.pyi +213 -0
  133. stouputils/stouputils/parallel.pyi +216 -0
  134. stouputils/stouputils/print.pyi +136 -0
  135. stouputils/stouputils/version_pkg.pyi +15 -0
  136. stouputils/version_pkg.py +189 -0
  137. stouputils-1.14.0.dist-info/METADATA +178 -0
  138. stouputils-1.14.0.dist-info/RECORD +140 -0
  139. stouputils-1.14.0.dist-info/WHEEL +4 -0
  140. stouputils-1.14.0.dist-info/entry_points.txt +3 -0
stouputils/io.py ADDED
@@ -0,0 +1,486 @@
1
+ """
2
+ This module provides utilities for file management.
3
+
4
+ - get_root_path: Get the absolute path of the directory
5
+ - relative_path: Get the relative path of a file relative to a given directory
6
+ - json_dump: Writes the provided data to a JSON file with a specified indentation depth.
7
+ - json_load: Load a JSON file from the given path
8
+ - csv_dump: Writes data to a CSV file with customizable options
9
+ - csv_load: Load a CSV file from the given path
10
+ - super_copy: Copy a file (or a folder) from the source to the destination (always create the directory)
11
+ - super_open: Open a file with the given mode, creating the directory if it doesn't exist (only if writing)
12
+ - replace_tilde: Replace the "~" by the user's home directory
13
+ - clean_path: Clean the path by replacing backslashes with forward slashes and simplifying the path
14
+
15
+ .. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/io_module.gif
16
+ :alt: stouputils io examples
17
+ """
18
+
19
+ # Imports
20
+ import csv
21
+ import json
22
+ import os
23
+ import re
24
+ import shutil
25
+ from io import StringIO
26
+ from typing import IO, Any
27
+
28
+
29
+ # Function that takes a relative path and returns the absolute path of the directory
30
+ def get_root_path(relative_path: str, go_up: int = 0) -> str:
31
+ """ Get the absolute path of the directory.
32
+ Usually used to get the root path of the project using the __file__ variable.
33
+
34
+ Args:
35
+ relative_path (str): The path to get the absolute directory path from
36
+ go_up (int): Number of parent directories to go up (default: 0)
37
+ Returns:
38
+ str: The absolute path of the directory
39
+
40
+ Examples:
41
+
42
+ .. code-block:: python
43
+
44
+ > get_root_path(__file__)
45
+ 'C:/Users/Alexandre-PC/AppData/Local/Programs/Python/Python310/lib/site-packages/stouputils'
46
+
47
+ > get_root_path(__file__, 3)
48
+ 'C:/Users/Alexandre-PC/AppData/Local/Programs/Python/Python310'
49
+ """
50
+ return clean_path(
51
+ os.path.dirname(os.path.abspath(relative_path))
52
+ + "/.." * go_up
53
+ ) or "."
54
+
55
+ # Function that returns the relative path of a file
56
+ def relative_path(file_path: str, relative_to: str = "") -> str:
57
+ """ Get the relative path of a file relative to a given directory.
58
+
59
+ Args:
60
+ file_path (str): The path to get the relative path from
61
+ relative_to (str): The path to get the relative path to (default: current working directory -> os.getcwd())
62
+ Returns:
63
+ str: The relative path of the file
64
+ Examples:
65
+
66
+ >>> relative_path("D:/some/random/path/stouputils/io.py", "D:\\\\some")
67
+ 'random/path/stouputils/io.py'
68
+ >>> relative_path("D:/some/random/path/stouputils/io.py", "D:\\\\some\\\\")
69
+ 'random/path/stouputils/io.py'
70
+ """
71
+ if not relative_to:
72
+ relative_to = os.getcwd()
73
+ file_path = clean_path(file_path)
74
+ relative_to = clean_path(relative_to)
75
+ if file_path.startswith(relative_to):
76
+ return clean_path(os.path.relpath(file_path, relative_to)) or "."
77
+ else:
78
+ return file_path or "."
79
+
80
+ # JSON dump with indentation for levels
81
+ def json_dump(
82
+ data: Any,
83
+ file: IO[Any] | str | None = None,
84
+ max_level: int | None = 2,
85
+ indent: str | int = '\t',
86
+ suffix: str = "\n",
87
+ ensure_ascii: bool = False
88
+ ) -> str:
89
+ r""" Writes the provided data to a JSON file with a specified indentation depth.
90
+ For instance, setting max_level to 2 will limit the indentation to 2 levels.
91
+
92
+ Args:
93
+ data (Any): The data to dump (usually a dict or a list)
94
+ file (IO[Any] | str): The file object or path to dump the data to
95
+ max_level (int | None): The depth of indentation to stop at (-1 for infinite), None will default to 2
96
+ indent (str | int): The indentation character (default: '\t')
97
+ suffix (str): The suffix to add at the end of the string (default: '\n')
98
+ ensure_ascii (bool): Whether to escape non-ASCII characters (default: False)
99
+ Returns:
100
+ str: The content of the file in every case
101
+
102
+ >>> json_dump({"a": [[1,2,3]], "b": 2}, max_level = 0)
103
+ '{"a": [[1,2,3]],"b": 2}\n'
104
+ >>> json_dump({"a": [[1,2,3]], "b": 2}, max_level = 1)
105
+ '{\n\t"a": [[1,2,3]],\n\t"b": 2\n}\n'
106
+ >>> json_dump({"a": [[1,2,3]], "b": 2}, max_level = 2)
107
+ '{\n\t"a": [\n\t\t[1,2,3]\n\t],\n\t"b": 2\n}\n'
108
+ >>> json_dump({"a": [[1,2,3]], "b": 2}, max_level = 3)
109
+ '{\n\t"a": [\n\t\t[\n\t\t\t1,\n\t\t\t2,\n\t\t\t3\n\t\t]\n\t],\n\t"b": 2\n}\n'
110
+ >>> json_dump({"éà": "üñ"}, ensure_ascii = True, max_level = 0)
111
+ '{"\\u00e9\\u00e0": "\\u00fc\\u00f1"}\n'
112
+ >>> json_dump({"éà": "üñ"}, ensure_ascii = False, max_level = 0)
113
+ '{"éà": "üñ"}\n'
114
+ """
115
+ # Handle None values for max_level
116
+ if max_level is None:
117
+ max_level = 2
118
+
119
+ # Dump content with 2-space indent and replace it with the desired indent
120
+ content: str = json.dumps(data, indent=indent, ensure_ascii=ensure_ascii)
121
+
122
+ # Limit max depth of indentation
123
+ if max_level > -1:
124
+ escape: str = re.escape(indent if isinstance(indent, str) else ' '*indent)
125
+ pattern: re.Pattern[str] = re.compile(
126
+ r"\n" + escape + "{" + str(max_level + 1) + r",}(.*)"
127
+ r"|\n" + escape + "{" + str(max_level) + r"}([}\]])"
128
+ )
129
+ content = pattern.sub(r"\1\2", content)
130
+
131
+ # Final newline and write
132
+ content += suffix
133
+ if file:
134
+ if isinstance(file, str):
135
+ with super_open(file, "w") as f:
136
+ f.write(content)
137
+ else:
138
+ file.write(content)
139
+ return content
140
+
141
+ # JSON load from file path
142
+ def json_load(file_path: str) -> Any:
143
+ """ Load a JSON file from the given path
144
+
145
+ Args:
146
+ file_path (str): The path to the JSON file
147
+ Returns:
148
+ Any: The content of the JSON file
149
+ """
150
+ with open(file_path) as f:
151
+ return json.loads(f.read())
152
+
153
+ # CSV dump to file
154
+ def csv_dump(
155
+ data: Any,
156
+ file: IO[Any] | str | None = None,
157
+ delimiter: str = ',',
158
+ has_header: bool = True,
159
+ index: bool = False,
160
+ *args: Any,
161
+ **kwargs: Any
162
+ ) -> str:
163
+ """ Writes data to a CSV file with customizable options and returns the CSV content as a string.
164
+
165
+ Args:
166
+ data (list[list[Any]] | list[dict[str, Any]] | pd.DataFrame | pl.DataFrame):
167
+ The data to write, either a list of lists, list of dicts, pandas DataFrame, or Polars DataFrame
168
+ file (IO[Any] | str): The file object or path to dump the data to
169
+ delimiter (str): The delimiter to use (default: ',')
170
+ has_header (bool): Whether to include headers (default: True, applies to dict and DataFrame data)
171
+ index (bool): Whether to include the index (default: False, only applies to pandas DataFrame)
172
+ *args (Any): Additional positional arguments to pass to the underlying CSV writer or DataFrame method
173
+ **kwargs (Any): Additional keyword arguments to pass to the underlying CSV writer or DataFrame method
174
+ Returns:
175
+ str: The CSV content as a string
176
+
177
+ Examples:
178
+
179
+ >>> csv_dump([["a", "b", "c"], [1, 2, 3], [4, 5, 6]])
180
+ 'a,b,c\\r\\n1,2,3\\r\\n4,5,6\\r\\n'
181
+
182
+ >>> csv_dump([{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}])
183
+ 'name,age\\r\\nAlice,30\\r\\nBob,25\\r\\n'
184
+ """
185
+ if isinstance(data, str | bytes | dict):
186
+ raise ValueError("Data must be a list of lists, list of dicts, pandas DataFrame, or Polars DataFrame")
187
+ output = StringIO()
188
+ done: bool = False
189
+
190
+ # Handle Polars DataFrame
191
+ try:
192
+ import polars as pl # type: ignore
193
+ if isinstance(data, pl.DataFrame):
194
+ copy_kwargs = kwargs.copy()
195
+ copy_kwargs.setdefault("separator", delimiter)
196
+ copy_kwargs.setdefault("include_header", has_header)
197
+ data.write_csv(output, *args, **copy_kwargs)
198
+ done = True
199
+ except Exception:
200
+ pass
201
+
202
+ # Handle pandas DataFrame
203
+ if not done:
204
+ try:
205
+ import pandas as pd # type: ignore
206
+ if isinstance(data, pd.DataFrame):
207
+ copy_kwargs = kwargs.copy()
208
+ copy_kwargs.setdefault("index", index)
209
+ copy_kwargs.setdefault("sep", delimiter)
210
+ copy_kwargs.setdefault("header", has_header)
211
+ data.to_csv(output, *args, **copy_kwargs)
212
+ except Exception:
213
+ pass
214
+
215
+ if not done:
216
+ # Handle list of dicts
217
+ data = list(data) # Ensure list and not other iterable
218
+ if isinstance(data[0], dict):
219
+ fieldnames = list(data[0].keys()) # type: ignore
220
+ kwargs.setdefault("fieldnames", fieldnames)
221
+ kwargs.setdefault("delimiter", delimiter)
222
+ dict_writer = csv.DictWriter(output, *args, **kwargs)
223
+ if has_header:
224
+ dict_writer.writeheader()
225
+ dict_writer.writerows(data) # type: ignore
226
+ done = True
227
+
228
+ # Handle list of lists
229
+ else:
230
+ kwargs.setdefault("delimiter", delimiter)
231
+ list_writer = csv.writer(output, *args, **kwargs)
232
+ list_writer.writerows(data) # type: ignore
233
+ done = True
234
+
235
+ # If still not done, raise error
236
+ if not done:
237
+ output.close()
238
+ raise ValueError(f"Data must be a list of lists, list of dicts, pandas DataFrame, or Polars DataFrame, got {type(data)} instead")
239
+
240
+ # Get content and write to file if needed
241
+ content: str = output.getvalue()
242
+ if file:
243
+ if isinstance(file, str):
244
+ with super_open(file, "w") as f:
245
+ f.write(content)
246
+ else:
247
+ file.write(content)
248
+ output.close()
249
+ return content
250
+
251
+ # CSV load from file path
252
+ def csv_load(file_path: str, delimiter: str = ',', has_header: bool = True, as_dict: bool = False, as_dataframe: bool = False, use_polars: bool = False, *args: Any, **kwargs: Any) -> Any:
253
+ """ Load a CSV file from the given path
254
+
255
+ Args:
256
+ file_path (str): The path to the CSV file
257
+ delimiter (str): The delimiter used in the CSV (default: ',')
258
+ has_header (bool): Whether the CSV has a header row (default: True)
259
+ as_dict (bool): Whether to return data as list of dicts (default: False)
260
+ as_dataframe (bool): Whether to return data as a DataFrame (default: False)
261
+ use_polars (bool): Whether to use Polars instead of pandas for DataFrame (default: False, requires polars)
262
+ *args: Additional positional arguments to pass to the underlying CSV reader or DataFrame method
263
+ **kwargs: Additional keyword arguments to pass to the underlying CSV reader or DataFrame method
264
+ Returns:
265
+ list[list[str]] | list[dict[str, str]] | pd.DataFrame | pl.DataFrame: The content of the CSV file
266
+
267
+ Examples:
268
+
269
+ .. code-block:: python
270
+
271
+ > Assuming "test.csv" contains: a,b,c\\n1,2,3\\n4,5,6
272
+ > csv_load("test.csv")
273
+ [['1', '2', '3'], ['4', '5', '6']]
274
+
275
+ > csv_load("test.csv", as_dict=True)
276
+ [{'a': '1', 'b': '2', 'c': '3'}, {'a': '4', 'b': '5', 'c': '6'}]
277
+
278
+ > csv_load("test.csv", as_dataframe=True)
279
+ a b c
280
+ 0 1 2 3
281
+ 1 4 5 6
282
+
283
+ .. code-block:: console
284
+
285
+ > csv_load("test.csv", as_dataframe=True, use_polars=True)
286
+ shape: (2, 3)
287
+ ┌─────┬─────┬─────┐
288
+ │ a ┆ b ┆ c │
289
+ │ --- ┆ --- ┆ --- │
290
+ │ i64 ┆ i64 ┆ i64 │
291
+ ╞═════╪═════╪═════╡
292
+ │ 1 ┆ 2 ┆ 3 │
293
+ │ 4 ┆ 5 ┆ 6 │
294
+ └─────┴─────┴─────┘
295
+ """ # noqa: E101
296
+ # Handle DataFrame loading
297
+ if as_dataframe:
298
+ if use_polars:
299
+ import polars as pl # type: ignore
300
+ if not os.path.exists(file_path):
301
+ return pl.DataFrame() # type: ignore
302
+ kwargs.setdefault("separator", delimiter)
303
+ kwargs.setdefault("has_header", has_header)
304
+ return pl.read_csv(file_path, *args, **kwargs) # type: ignore
305
+ else:
306
+ import pandas as pd # type: ignore
307
+ if not os.path.exists(file_path):
308
+ return pd.DataFrame() # type: ignore
309
+ kwargs.setdefault("sep", delimiter)
310
+ kwargs.setdefault("header", 0 if has_header else None)
311
+ return pd.read_csv(file_path, *args, **kwargs) # type: ignore
312
+
313
+ # Handle dict or list
314
+ if not os.path.exists(file_path):
315
+ return []
316
+ with super_open(file_path, "r") as f:
317
+ if as_dict or has_header:
318
+ kwargs.setdefault("delimiter", delimiter)
319
+ reader = csv.DictReader(f, *args, **kwargs)
320
+ return list(reader)
321
+ else:
322
+ kwargs.setdefault("delimiter", delimiter)
323
+ reader = csv.reader(f, *args, **kwargs)
324
+ return list(reader)
325
+
326
+ # For easy file copy
327
+ def super_copy(src: str, dst: str, create_dir: bool = True, symlink: bool = False) -> str:
328
+ """ Copy a file (or a folder) from the source to the destination
329
+
330
+ Args:
331
+ src (str): The source path
332
+ dst (str): The destination path
333
+ create_dir (bool): Whether to create the directory if it doesn't exist (default: True)
334
+ symlink (bool): Whether to create a symlink instead of copying (Linux only, default: True)
335
+ Returns:
336
+ str: The destination path
337
+ """
338
+ # Disable symlink functionality on Windows as it uses shortcuts instead of proper symlinks
339
+ if os.name == "nt":
340
+ symlink = False
341
+
342
+ # Create destination directory if needed
343
+ if create_dir:
344
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
345
+
346
+ # Handle directory copying
347
+ if os.path.isdir(src):
348
+ if symlink:
349
+
350
+ # Remove existing destination if it's different from source
351
+ if os.path.exists(dst):
352
+ if os.path.samefile(src, dst) is False:
353
+ if os.path.isdir(dst):
354
+ shutil.rmtree(dst)
355
+ else:
356
+ os.remove(dst)
357
+ return os.symlink(src.rstrip('/'), dst.rstrip('/'), target_is_directory=True) or dst
358
+ else:
359
+ return os.symlink(src.rstrip('/'), dst.rstrip('/'), target_is_directory=True) or dst
360
+
361
+ # Regular directory copy
362
+ else:
363
+ return shutil.copytree(src, dst, dirs_exist_ok = True)
364
+
365
+ # Handle file copying
366
+ else:
367
+ if symlink:
368
+
369
+ # Remove existing destination if it's different from source
370
+ if os.path.exists(dst):
371
+ if os.path.samefile(src, dst) is False:
372
+ os.remove(dst)
373
+ return os.symlink(src, dst, target_is_directory=False) or dst
374
+ else:
375
+ return os.symlink(src, dst, target_is_directory=False) or dst
376
+
377
+ # Regular file copy
378
+ else:
379
+ return shutil.copy(src, dst)
380
+ return ""
381
+
382
+ # For easy file management
383
+ def super_open(file_path: str, mode: str, encoding: str = "utf-8") -> IO[Any]:
384
+ """ Open a file with the given mode, creating the directory if it doesn't exist (only if writing)
385
+
386
+ Args:
387
+ file_path (str): The path to the file
388
+ mode (str): The mode to open the file with, ex: "w", "r", "a", "wb", "rb", "ab"
389
+ encoding (str): The encoding to use when opening the file (default: "utf-8")
390
+ Returns:
391
+ open: The file object, ready to be used
392
+ """
393
+ # Make directory
394
+ file_path = clean_path(file_path)
395
+ if "/" in file_path and ("w" in mode or "a" in mode):
396
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
397
+
398
+ # Open file and return
399
+ if "b" in mode:
400
+ return open(file_path, mode)
401
+ else:
402
+ return open(file_path, mode, encoding = encoding) # Always use utf-8 encoding to avoid issues
403
+
404
+ def read_file(file_path: str, encoding: str = "utf-8") -> str:
405
+ """ Read the content of a file and return it as a string
406
+
407
+ Args:
408
+ file_path (str): The path to the file
409
+ encoding (str): The encoding to use when opening the file (default: "utf-8")
410
+ Returns:
411
+ str: The content of the file
412
+ """
413
+ with super_open(file_path, "r", encoding=encoding) as f:
414
+ return f.read()
415
+
416
+ # Function that replace the "~" by the user's home directory
417
+ def replace_tilde(path: str) -> str:
418
+ """ Replace the "~" by the user's home directory
419
+
420
+ Args:
421
+ path (str): The path to replace the "~" by the user's home directory
422
+ Returns:
423
+ str: The path with the "~" replaced by the user's home directory
424
+ Examples:
425
+
426
+ .. code-block:: python
427
+
428
+ > replace_tilde("~/Documents/test.txt")
429
+ '/home/user/Documents/test.txt'
430
+ """
431
+ return path.replace("~", os.path.expanduser("~")).replace("\\", "/")
432
+
433
+ # Utility function to clean the path
434
+ def clean_path(file_path: str, trailing_slash: bool = True) -> str:
435
+ """ Clean the path by replacing backslashes with forward slashes and simplifying the path
436
+
437
+ Args:
438
+ file_path (str): The path to clean
439
+ trailing_slash (bool): Whether to keep the trailing slash, ex: "test/" -> "test/"
440
+ Returns:
441
+ str: The cleaned path
442
+ Examples:
443
+ >>> clean_path("C:\\\\Users\\\\Stoupy\\\\Documents\\\\test.txt")
444
+ 'C:/Users/Stoupy/Documents/test.txt'
445
+
446
+ >>> clean_path("Some Folder////")
447
+ 'Some Folder/'
448
+
449
+ >>> clean_path("test/uwu/1/../../")
450
+ 'test/'
451
+
452
+ >>> clean_path("some/./folder/../")
453
+ 'some/'
454
+
455
+ >>> clean_path("folder1/folder2/../../folder3")
456
+ 'folder3'
457
+
458
+ >>> clean_path("./test/./folder/")
459
+ 'test/folder/'
460
+
461
+ >>> clean_path("C:/folder1\\\\folder2")
462
+ 'C:/folder1/folder2'
463
+ """
464
+ # Replace tilde
465
+ file_path = replace_tilde(str(file_path))
466
+
467
+ # Check if original path ends with slash
468
+ ends_with_slash: bool = file_path.endswith('/') or file_path.endswith('\\')
469
+
470
+ # Use os.path.normpath to clean up the path
471
+ file_path = os.path.normpath(file_path)
472
+
473
+ # Convert backslashes to forward slashes
474
+ file_path = file_path.replace(os.sep, '/')
475
+
476
+ # Add trailing slash back if original had one
477
+ if ends_with_slash and not file_path.endswith('/'):
478
+ file_path += '/'
479
+
480
+ # Remove trailing slash if requested
481
+ if not trailing_slash and file_path.endswith('/'):
482
+ file_path = file_path[:-1]
483
+
484
+ # Return the cleaned path
485
+ return file_path if file_path != "." else ""
486
+