stouputils 1.4.0__tar.gz → 1.4.1__tar.gz

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 (107) hide show
  1. {stouputils-1.4.0 → stouputils-1.4.1}/.gitignore +1 -0
  2. {stouputils-1.4.0 → stouputils-1.4.1}/PKG-INFO +1 -1
  3. {stouputils-1.4.0 → stouputils-1.4.1}/pyproject.toml +4 -1
  4. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/__init__.py +5 -5
  5. stouputils-1.4.1/stouputils/__main__.py +35 -0
  6. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/all_doctests.py +23 -12
  7. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/applications/upscaler/image.py +2 -2
  8. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/applications/upscaler/video.py +2 -2
  9. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/archive.py +129 -110
  10. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/backup.py +120 -119
  11. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/collections.py +2 -3
  12. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/ctx.py +87 -77
  13. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/base_keras.py +2 -3
  14. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/decorators.py +109 -109
  15. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/image.py +7 -0
  16. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/io.py +144 -141
  17. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/parallel.py +68 -85
  18. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/print.py +119 -29
  19. {stouputils-1.4.0 → stouputils-1.4.1}/LICENSE +0 -0
  20. {stouputils-1.4.0 → stouputils-1.4.1}/README.md +0 -0
  21. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/applications/__init__.py +0 -0
  22. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/applications/automatic_docs.py +0 -0
  23. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/applications/upscaler/__init__.py +0 -0
  24. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/applications/upscaler/config.py +0 -0
  25. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/continuous_delivery/__init__.py +0 -0
  26. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/continuous_delivery/cd_utils.py +0 -0
  27. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/continuous_delivery/github.py +0 -0
  28. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/continuous_delivery/pypi.py +0 -0
  29. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/continuous_delivery/pyproject.py +0 -0
  30. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/config/get.py +0 -0
  31. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/config/set.py +0 -0
  32. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/__init__.py +0 -0
  33. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/auto_contrast.py +0 -0
  34. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/axis_flip.py +0 -0
  35. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/bias_field_correction.py +0 -0
  36. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/binary_threshold.py +0 -0
  37. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/blur.py +0 -0
  38. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/brightness.py +0 -0
  39. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/canny.py +0 -0
  40. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/clahe.py +0 -0
  41. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/common.py +0 -0
  42. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/contrast.py +0 -0
  43. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/curvature_flow_filter.py +0 -0
  44. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/denoise.py +0 -0
  45. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/histogram_equalization.py +0 -0
  46. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/invert.py +0 -0
  47. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/laplacian.py +0 -0
  48. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/median_blur.py +0 -0
  49. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/noise.py +0 -0
  50. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/normalize.py +0 -0
  51. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/random_erase.py +0 -0
  52. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/resize.py +0 -0
  53. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/rotation.py +0 -0
  54. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/salt_pepper.py +0 -0
  55. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/sharpening.py +0 -0
  56. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/shearing.py +0 -0
  57. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/threshold.py +0 -0
  58. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/translation.py +0 -0
  59. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image/zoom.py +0 -0
  60. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image_augmentation.py +0 -0
  61. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/image_preprocess.py +0 -0
  62. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/prosthesis_detection.py +0 -0
  63. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/data_processing/technique.py +0 -0
  64. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/dataset/__init__.py +0 -0
  65. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/dataset/dataset.py +0 -0
  66. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/dataset/dataset_loader.py +0 -0
  67. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/dataset/grouping_strategy.py +0 -0
  68. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/dataset/image_loader.py +0 -0
  69. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/dataset/xy_tuple.py +0 -0
  70. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/metric_dictionnary.py +0 -0
  71. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/metric_utils.py +0 -0
  72. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/mlflow_utils.py +0 -0
  73. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/abstract_model.py +0 -0
  74. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/all.py +0 -0
  75. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/all.py +0 -0
  76. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/convnext.py +0 -0
  77. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/densenet.py +0 -0
  78. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/efficientnet.py +0 -0
  79. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/mobilenet.py +0 -0
  80. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/resnet.py +0 -0
  81. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/squeezenet.py +0 -0
  82. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/vgg.py +0 -0
  83. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras/xception.py +0 -0
  84. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/callbacks/__init__.py +0 -0
  85. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +0 -0
  86. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +0 -0
  87. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +0 -0
  88. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +0 -0
  89. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +0 -0
  90. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/losses/__init__.py +0 -0
  91. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +0 -0
  92. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/keras_utils/visualizations.py +0 -0
  93. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/model_interface.py +0 -0
  94. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/models/sandbox.py +0 -0
  95. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/range_tuple.py +0 -0
  96. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/scripts/augment_dataset.py +0 -0
  97. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/scripts/exhaustive_process.py +0 -0
  98. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/scripts/preprocess_dataset.py +0 -0
  99. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/scripts/routine.py +0 -0
  100. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/data_science/utils.py +0 -0
  101. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/installer/__init__.py +0 -0
  102. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/installer/common.py +0 -0
  103. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/installer/downloader.py +0 -0
  104. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/installer/linux.py +0 -0
  105. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/installer/main.py +0 -0
  106. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/installer/windows.py +0 -0
  107. {stouputils-1.4.0 → stouputils-1.4.1}/stouputils/py.typed +0 -0
@@ -7,6 +7,7 @@ uv.lock
7
7
  __temporary__/
8
8
  output.log
9
9
  logfile.txt
10
+ .vscode
10
11
 
11
12
  # Documentation
12
13
  docs/source/_static/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stouputils
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Stouputils is a collection of utility modules designed to simplify and enhance the development process. It includes a range of tools for tasks such as execution of doctests, display utilities, decorators, as well as context managers, and many more.
5
5
  Project-URL: Homepage, https://github.com/Stoupy51/stouputils
6
6
  Project-URL: Issues, https://github.com/Stoupy51/stouputils/issues
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "stouputils"
8
- version = "1.4.0"
8
+ version = "1.4.1"
9
9
  description = "Stouputils is a collection of utility modules designed to simplify and enhance the development process. It includes a range of tools for tasks such as execution of doctests, display utilities, decorators, as well as context managers, and many more."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -23,6 +23,9 @@ email = "stoupy51@gmail.com"
23
23
  Homepage = "https://github.com/Stoupy51/stouputils"
24
24
  Issues = "https://github.com/Stoupy51/stouputils/issues"
25
25
 
26
+ [project.scripts]
27
+ stouputils = "stouputils.__main__:main"
28
+
26
29
  [tool.pyright]
27
30
  typeCheckingMode = "strict"
28
31
 
@@ -16,6 +16,9 @@ Key Features:
16
16
  """
17
17
  # ruff: noqa: F403
18
18
 
19
+ # Version (handle case where the package is not installed)
20
+ from importlib.metadata import PackageNotFoundError, version
21
+
19
22
  # Imports
20
23
  from .all_doctests import *
21
24
  from .archive import *
@@ -31,11 +34,8 @@ from .io import *
31
34
  from .parallel import *
32
35
  from .print import *
33
36
 
34
- # Version (handle case where the package is not installed)
35
- import importlib.metadata
36
-
37
37
  try:
38
- __version__: str = importlib.metadata.version("stouputils")
39
- except importlib.metadata.PackageNotFoundError:
38
+ __version__: str = version("stouputils")
39
+ except PackageNotFoundError:
40
40
  __version__: str = "0.0.0-dev"
41
41
 
@@ -0,0 +1,35 @@
1
+
2
+
3
+ # Imports
4
+ import sys
5
+
6
+ from .all_doctests import launch_tests
7
+ from .decorators import handle_error
8
+ from .print import show_version
9
+
10
+
11
+ @handle_error(message="Error while running 'stouputils'")
12
+ def main():
13
+ second_arg: str = sys.argv[1].lower() if len(sys.argv) >= 2 else ""
14
+ if not second_arg:
15
+ # TODO
16
+ return
17
+
18
+ # Print the version of stouputils and its dependencies
19
+ if second_arg in ("--version","-v"):
20
+ return show_version()
21
+
22
+ # Handle "all_doctests" command
23
+ if second_arg == "all_doctests":
24
+ if launch_tests("." if len(sys.argv) == 2 else sys.argv[2]) > 0:
25
+ sys.exit(1)
26
+ return
27
+
28
+ # Check if the command is any package name
29
+ if second_arg in (): # type: ignore
30
+ return
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
35
+
@@ -1,6 +1,9 @@
1
1
  """
2
2
  This module is used to run all the doctests for all the modules in a given directory.
3
3
 
4
+ - launch_tests: Main function to launch tests for all modules in the given directory.
5
+ - test_module_with_progress: Test a module with testmod and measure the time taken with progress printing.
6
+
4
7
  .. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/all_doctests_module.gif
5
8
  :alt: stouputils all_doctests examples
6
9
  """
@@ -14,24 +17,17 @@ from doctest import TestResults, testmod
14
17
  from types import ModuleType
15
18
 
16
19
  from . import decorators
17
- from .decorators import LogLevels, measure_time
18
- from .print import error, info, progress, warning
20
+ from .decorators import measure_time
19
21
  from .io import clean_path, relative_path
22
+ from .print import error, info, progress, warning
20
23
 
21
24
 
22
- def test_module_with_progress(module: ModuleType, separator: str) -> TestResults:
23
- @measure_time(progress, message=f"Testing module '{module.__name__}' {separator}took")
24
- def internal() -> TestResults:
25
- return testmod(m=module)
26
- return internal()
27
-
28
25
  # Main program
29
- def launch_tests(root_dir: str, importing_errors: LogLevels = LogLevels.WARNING_TRACEBACK, strict: bool = True) -> int:
26
+ def launch_tests(root_dir: str, strict: bool = True) -> int:
30
27
  """ Main function to launch tests for all modules in the given directory.
31
28
 
32
29
  Args:
33
30
  root_dir (str): Root directory to search for modules
34
- importing_errors (LogLevels): Log level for the errors when importing modules
35
31
  strict (bool): Modify the force_raise_exception variable to True in the decorators module
36
32
 
37
33
  Returns:
@@ -61,7 +57,7 @@ def launch_tests(root_dir: str, importing_errors: LogLevels = LogLevels.WARNING_
61
57
  old_value: bool = strict
62
58
  decorators.force_raise_exception = True
63
59
  strict = old_value
64
-
60
+
65
61
  # Get the path of the directory to check modules from
66
62
  working_dir: str = clean_path(os.getcwd())
67
63
  root_dir = clean_path(os.path.abspath(root_dir))
@@ -108,7 +104,7 @@ def launch_tests(root_dir: str, importing_errors: LogLevels = LogLevels.WARNING_
108
104
  def internal(a: str = module_path, b: str = separator) -> None:
109
105
  modules.append(importlib.import_module(a))
110
106
  separators.append(b)
111
-
107
+
112
108
  try:
113
109
  internal()
114
110
  except Exception as e:
@@ -135,3 +131,18 @@ def launch_tests(root_dir: str, importing_errors: LogLevels = LogLevels.WARNING_
135
131
  # Return the number of failed tests
136
132
  return nb_failed_tests
137
133
 
134
+
135
+ def test_module_with_progress(module: ModuleType, separator: str) -> TestResults:
136
+ """ Test a module with testmod and measure the time taken with progress printing.
137
+
138
+ Args:
139
+ module (ModuleType): Module to test
140
+ separator (str): Separator string for alignment in output
141
+ Returns:
142
+ TestResults: The results of the tests
143
+ """
144
+ @measure_time(progress, message=f"Testing module '{module.__name__}' {separator}took")
145
+ def internal() -> TestResults:
146
+ return testmod(m=module)
147
+ return internal()
148
+
@@ -36,8 +36,8 @@ from PIL import Image
36
36
 
37
37
  from ...installer import check_executable
38
38
  from ...io import clean_path
39
- from ...parallel import colored_for_loop, multithreading
40
- from ...print import debug, info
39
+ from ...parallel import multithreading
40
+ from ...print import colored_for_loop, debug, info
41
41
  from .config import WAIFU2X_NCNN_VULKAN_RELEASES, Config
42
42
 
43
43
 
@@ -46,8 +46,8 @@ from PIL import Image
46
46
 
47
47
  from ...installer import check_executable
48
48
  from ...io import clean_path
49
- from ...parallel import colored_for_loop, multithreading
50
- from ...print import debug, error, info, warning
49
+ from ...parallel import multithreading
50
+ from ...print import colored_for_loop, debug, error, info, warning
51
51
  from .config import FFMPEG_RELEASES, YOUTUBE_BITRATE_RECOMMENDATIONS, Config
52
52
  from .image import convert_frame, get_all_files, upscale_folder
53
53
 
@@ -1,8 +1,9 @@
1
1
  """
2
2
  This module provides functions for creating and managing archives.
3
3
 
4
- - make_archive: Create a zip archive from a source directory with consistent file timestamps.
5
4
  - repair_zip_file: Try to repair a corrupted zip file by ignoring some of the errors
5
+ - make_archive: Create a zip archive from a source directory with consistent file timestamps.
6
+ - archive_cli: Main entry point for command line usage
6
7
 
7
8
  .. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/archive_module.gif
8
9
  :alt: stouputils archive examples
@@ -16,107 +17,7 @@ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
16
17
 
17
18
  from .decorators import LogLevels, handle_error
18
19
  from .io import clean_path, super_copy
19
-
20
-
21
- # Function that makes an archive with consistency (same zip file each time)
22
- @handle_error()
23
- def make_archive(
24
- source: str,
25
- destinations: list[str] | str | None = None,
26
- override_time: None | tuple[int, int, int, int, int, int] = None,
27
- create_dir: bool = False,
28
- ignore_patterns: str | None = None,
29
- ) -> bool:
30
- """ Create a zip archive from a source directory with consistent file timestamps.
31
- (Meaning deterministic zip file each time)
32
-
33
- Creates a zip archive from the source directory and copies it to one or more destinations.
34
- The archive will have consistent file timestamps across runs if override_time is specified.
35
- Uses maximum compression level (9) with ZIP_DEFLATED algorithm.
36
-
37
- Args:
38
- source (str): The source folder to archive
39
- destinations (list[str]|str): The destination folder(s) or file(s) to copy the archive to
40
- override_time (None | tuple[int, ...]): The constant time to use for the archive
41
- (e.g. (2024, 1, 1, 0, 0, 0) for 2024-01-01 00:00:00)
42
- create_dir (bool): Whether to create the destination directory if it doesn't exist
43
- ignore_patterns (str | None): Glob pattern(s) to ignore files. Can be a single pattern or comma-separated patterns (e.g. "*.pyc" or "*.pyc,__pycache__,*.log")
44
- Returns:
45
- bool: Always returns True unless any strong error
46
- Examples:
47
-
48
- .. code-block:: python
49
-
50
- > make_archive("/path/to/source", "/path/to/destination.zip")
51
- > make_archive("/path/to/source", ["/path/to/destination.zip", "/path/to/destination2.zip"])
52
- > make_archive("src", "hello_from_year_2085.zip", override_time=(2085,1,1,0,0,0))
53
- > make_archive("src", "output.zip", ignore_patterns="*.pyc")
54
- > make_archive("src", "output.zip", ignore_patterns="__pycache__")
55
- > make_archive("src", "output.zip", ignore_patterns="*.pyc,__pycache__,*.log")
56
- """
57
- # Fix copy_destinations type if needed
58
- if destinations is None:
59
- destinations = []
60
- if destinations and isinstance(destinations, str):
61
- destinations = [destinations]
62
- if not destinations:
63
- raise ValueError("destinations must be a list of at least one destination")
64
-
65
- # Create directories if needed
66
- if create_dir:
67
- for dest_file in destinations:
68
- dest_file = clean_path(dest_file)
69
- parent_dir = os.path.dirname(dest_file)
70
- if parent_dir and not os.path.exists(parent_dir):
71
- os.makedirs(parent_dir, exist_ok=True)
72
-
73
- # Create the archive
74
- destination: str = clean_path(destinations[0])
75
- destination = destination if ".zip" in destination else destination + ".zip"
76
-
77
- # Parse ignore patterns (can be a single pattern or comma-separated patterns)
78
- ignore_pattern_list: list[str] = []
79
- if ignore_patterns:
80
- ignore_pattern_list = [pattern.strip() for pattern in ignore_patterns.split(',')]
81
-
82
- def should_ignore(path: str) -> bool:
83
- """Check if a file or directory path should be ignored based on patterns."""
84
- if not ignore_pattern_list:
85
- return False
86
- for pattern in ignore_pattern_list:
87
- if fnmatch.fnmatch(os.path.basename(path), pattern) or fnmatch.fnmatch(path, pattern):
88
- return True
89
- return False
90
-
91
- with ZipFile(destination, "w", compression=ZIP_DEFLATED, compresslevel=9) as zip:
92
- for root, dirs, files in os.walk(source):
93
- # Filter out ignored directories in-place to prevent walking into them
94
- dirs[:] = [d for d in dirs if not should_ignore(d)]
95
-
96
- for file in files:
97
- file_path: str = clean_path(os.path.join(root, file))
98
- rel_path = os.path.relpath(file_path, source)
99
-
100
- # Skip files that match any ignore pattern
101
- if should_ignore(file) or should_ignore(rel_path):
102
- continue
103
-
104
- info: ZipInfo = ZipInfo(rel_path)
105
- info.compress_type = ZIP_DEFLATED
106
- if override_time:
107
- info.date_time = override_time
108
- with open(file_path, "rb") as f:
109
- zip.writestr(info, f.read())
110
-
111
- # Copy the archive to the destination(s)
112
- for dest_file in destinations[1:]:
113
- @handle_error(Exception, message=f"Unable to copy '{destination}' to '{dest_file}'", error_log=LogLevels.WARNING)
114
- def internal(src: str, dest: str) -> None:
115
- super_copy(src, dest, create_dir=create_dir)
116
- internal(destination, clean_path(dest_file))
117
-
118
- return True
119
-
20
+ from .print import debug, error, info
120
21
 
121
22
 
122
23
  # Function that repair a corrupted zip file (ignoring some of the errors)
@@ -246,9 +147,123 @@ def repair_zip_file(file_path: str, destination: str) -> bool:
246
147
 
247
148
  return True
248
149
 
150
+ # Function that makes an archive with consistency (same zip file each time)
151
+ @handle_error()
152
+ def make_archive(
153
+ source: str,
154
+ destinations: list[str] | str | None = None,
155
+ override_time: None | tuple[int, int, int, int, int, int] = None,
156
+ create_dir: bool = False,
157
+ ignore_patterns: str | None = None,
158
+ ) -> bool:
159
+ """ Create a zip archive from a source directory with consistent file timestamps.
160
+ (Meaning deterministic zip file each time)
249
161
 
250
- # CLI functionality
251
- if __name__ == "__main__":
162
+ Creates a zip archive from the source directory and copies it to one or more destinations.
163
+ The archive will have consistent file timestamps across runs if override_time is specified.
164
+ Uses maximum compression level (9) with ZIP_DEFLATED algorithm.
165
+
166
+ Args:
167
+ source (str): The source folder to archive
168
+ destinations (list[str]|str): The destination folder(s) or file(s) to copy the archive to
169
+ override_time (None | tuple[int, ...]): The constant time to use for the archive
170
+ (e.g. (2024, 1, 1, 0, 0, 0) for 2024-01-01 00:00:00)
171
+ create_dir (bool): Whether to create the destination directory if it doesn't exist
172
+ ignore_patterns (str | None): Glob pattern(s) to ignore files. Can be a single pattern or comma-separated patterns (e.g. "*.pyc" or "*.pyc,__pycache__,*.log")
173
+ Returns:
174
+ bool: Always returns True unless any strong error
175
+ Examples:
176
+
177
+ .. code-block:: python
178
+
179
+ > make_archive("/path/to/source", "/path/to/destination.zip")
180
+ > make_archive("/path/to/source", ["/path/to/destination.zip", "/path/to/destination2.zip"])
181
+ > make_archive("src", "hello_from_year_2085.zip", override_time=(2085,1,1,0,0,0))
182
+ > make_archive("src", "output.zip", ignore_patterns="*.pyc")
183
+ > make_archive("src", "output.zip", ignore_patterns="__pycache__")
184
+ > make_archive("src", "output.zip", ignore_patterns="*.pyc,__pycache__,*.log")
185
+ """
186
+ # Fix copy_destinations type if needed
187
+ if destinations is None:
188
+ destinations = []
189
+ if destinations and isinstance(destinations, str):
190
+ destinations = [destinations]
191
+ if not destinations:
192
+ raise ValueError("destinations must be a list of at least one destination")
193
+
194
+ # Create directories if needed
195
+ if create_dir:
196
+ for dest_file in destinations:
197
+ dest_file = clean_path(dest_file)
198
+ parent_dir = os.path.dirname(dest_file)
199
+ if parent_dir and not os.path.exists(parent_dir):
200
+ os.makedirs(parent_dir, exist_ok=True)
201
+
202
+ # Create the archive
203
+ destination: str = clean_path(destinations[0])
204
+ destination = destination if ".zip" in destination else destination + ".zip"
205
+
206
+ # Parse ignore patterns (can be a single pattern or comma-separated patterns)
207
+ ignore_pattern_list: list[str] = []
208
+ if ignore_patterns:
209
+ ignore_pattern_list = [pattern.strip() for pattern in ignore_patterns.split(',')]
210
+
211
+ def should_ignore(path: str) -> bool:
212
+ """Check if a file or directory path should be ignored based on patterns."""
213
+ if not ignore_pattern_list:
214
+ return False
215
+ for pattern in ignore_pattern_list:
216
+ if fnmatch.fnmatch(os.path.basename(path), pattern) or fnmatch.fnmatch(path, pattern):
217
+ return True
218
+ return False
219
+
220
+ with ZipFile(destination, "w", compression=ZIP_DEFLATED, compresslevel=9) as zip:
221
+ for root, dirs, files in os.walk(source):
222
+ # Filter out ignored directories in-place to prevent walking into them
223
+ dirs[:] = [d for d in dirs if not should_ignore(d)]
224
+
225
+ for file in files:
226
+ file_path: str = clean_path(os.path.join(root, file))
227
+ rel_path = os.path.relpath(file_path, source)
228
+
229
+ # Skip files that match any ignore pattern
230
+ if should_ignore(file) or should_ignore(rel_path):
231
+ continue
232
+
233
+ info: ZipInfo = ZipInfo(rel_path)
234
+ info.compress_type = ZIP_DEFLATED
235
+ if override_time:
236
+ info.date_time = override_time
237
+ with open(file_path, "rb") as f:
238
+ zip.writestr(info, f.read())
239
+
240
+ # Copy the archive to the destination(s)
241
+ for dest_file in destinations[1:]:
242
+ @handle_error(Exception, message=f"Unable to copy '{destination}' to '{dest_file}'", error_log=LogLevels.WARNING)
243
+ def internal(src: str, dest: str) -> None:
244
+ super_copy(src, dest, create_dir=create_dir)
245
+ internal(destination, clean_path(dest_file))
246
+
247
+ return True
248
+
249
+
250
+ # Main entry point for command line usage
251
+ def archive_cli():
252
+ """ Main entry point for command line usage.
253
+
254
+ Examples:
255
+
256
+ .. code-block:: bash
257
+
258
+ # Repair a corrupted zip file
259
+ python -m stouputils.archive repair /path/to/corrupted.zip /path/to/repaired.zip
260
+
261
+ # Create a zip archive
262
+ python -m stouputils.archive make /path/to/source /path/to/destination.zip
263
+
264
+ # Create a zip archive with ignore patterns
265
+ python -m stouputils.archive make /path/to/source /path/to/destination.zip --ignore "*.pyc,__pycache__"
266
+ """
252
267
  import argparse
253
268
  import sys
254
269
 
@@ -278,16 +293,16 @@ if __name__ == "__main__":
278
293
  base, ext = os.path.splitext(input_file)
279
294
  output_file = f"{base}_repaired{ext}"
280
295
 
281
- print(f"Repairing '{input_file}' to '{output_file}'...")
296
+ debug(f"Repairing '{input_file}' to '{output_file}'...")
282
297
  try:
283
298
  repair_zip_file(input_file, output_file)
284
- print(f"Successfully repaired zip file: {output_file}")
299
+ info(f"Successfully repaired zip file: {output_file}")
285
300
  except Exception as e:
286
- print(f"Error repairing zip file: {e}", file=sys.stderr)
301
+ error(f"Error repairing zip file: {e}", exit=False)
287
302
  sys.exit(1)
288
303
 
289
304
  elif args.command == "make":
290
- print(f"Creating archive from '{args.source}' to '{args.destination}'...")
305
+ debug(f"Creating archive from '{args.source}' to '{args.destination}'...")
291
306
  try:
292
307
  make_archive(
293
308
  source=args.source,
@@ -295,9 +310,9 @@ if __name__ == "__main__":
295
310
  create_dir=args.create_dir,
296
311
  ignore_patterns=args.ignore
297
312
  )
298
- print(f"Successfully created archive: {args.destination}")
313
+ info(f"Successfully created archive: {args.destination}")
299
314
  except Exception as e:
300
- print(f"Error creating archive: {e}", file=sys.stderr)
315
+ error(f"Error creating archive: {e}", exit=False)
301
316
  sys.exit(1)
302
317
 
303
318
  else:
@@ -305,3 +320,7 @@ if __name__ == "__main__":
305
320
  sys.exit(1)
306
321
 
307
322
 
323
+ if __name__ == "__main__":
324
+ archive_cli()
325
+
326
+