stouputils 1.16.3__py3-none-any.whl → 1.18.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 (42) hide show
  1. stouputils/__init__.py +1 -0
  2. stouputils/__init__.pyi +1 -0
  3. stouputils/all_doctests.py +1 -1
  4. stouputils/collections.py +2 -5
  5. stouputils/collections.pyi +2 -4
  6. stouputils/continuous_delivery/stubs.py +1 -1
  7. stouputils/ctx.py +1 -3
  8. stouputils/ctx.pyi +1 -3
  9. stouputils/decorators.py +1 -1
  10. stouputils/image.py +8 -10
  11. stouputils/image.pyi +4 -6
  12. stouputils/io.py +22 -1
  13. stouputils/io.pyi +7 -1
  14. stouputils/lock/__init__.py +36 -0
  15. stouputils/lock/__init__.pyi +5 -0
  16. stouputils/lock/base.py +536 -0
  17. stouputils/lock/base.pyi +169 -0
  18. stouputils/lock/queue.py +377 -0
  19. stouputils/lock/queue.pyi +131 -0
  20. stouputils/lock/re_entrant.py +115 -0
  21. stouputils/lock/re_entrant.pyi +81 -0
  22. stouputils/lock/redis_fifo.py +299 -0
  23. stouputils/lock/redis_fifo.pyi +123 -0
  24. stouputils/lock/shared.py +30 -0
  25. stouputils/lock/shared.pyi +16 -0
  26. stouputils/parallel/__init__.py +29 -0
  27. stouputils/parallel/__init__.pyi +4 -0
  28. stouputils/parallel/capturer.py +133 -0
  29. stouputils/parallel/capturer.pyi +38 -0
  30. stouputils/parallel/common.py +134 -0
  31. stouputils/parallel/common.pyi +53 -0
  32. stouputils/parallel/multi.py +309 -0
  33. stouputils/{parallel.pyi → parallel/multi.pyi} +14 -112
  34. stouputils/parallel/subprocess.py +163 -0
  35. stouputils/parallel/subprocess.pyi +64 -0
  36. stouputils/print.py +2 -3
  37. stouputils/print.pyi +1 -2
  38. {stouputils-1.16.3.dist-info → stouputils-1.18.0.dist-info}/METADATA +4 -1
  39. {stouputils-1.16.3.dist-info → stouputils-1.18.0.dist-info}/RECORD +41 -21
  40. stouputils/parallel.py +0 -556
  41. {stouputils-1.16.3.dist-info → stouputils-1.18.0.dist-info}/WHEEL +0 -0
  42. {stouputils-1.16.3.dist-info → stouputils-1.18.0.dist-info}/entry_points.txt +0 -0
stouputils/__init__.py CHANGED
@@ -18,6 +18,7 @@ from .ctx import *
18
18
  from .decorators import *
19
19
  from .image import *
20
20
  from .io import *
21
+ from .lock import *
21
22
  from .parallel import *
22
23
  from .print import *
23
24
  from .typing import *
stouputils/__init__.pyi CHANGED
@@ -8,6 +8,7 @@ from .ctx import *
8
8
  from .decorators import *
9
9
  from .image import *
10
10
  from .io import *
11
+ from .lock import *
11
12
  from .parallel import *
12
13
  from .print import *
13
14
  from .typing import *
@@ -99,7 +99,7 @@ def launch_tests(root_dir: str, strict: bool = True, pattern: str = "*") -> int:
99
99
  import fnmatch
100
100
  modules_file_paths = [
101
101
  path for path in modules_file_paths
102
- if fnmatch.fnmatch(path.split(".")[-1], pattern)
102
+ if fnmatch.fnmatch(path, pattern)
103
103
  ]
104
104
  if not modules_file_paths:
105
105
  raise ValueError(f"No modules matching pattern '{pattern}' found in '{relative_path(root_dir)}'")
stouputils/collections.py CHANGED
@@ -17,7 +17,7 @@ import os
17
17
  import shutil
18
18
  import tempfile
19
19
  from collections.abc import Callable, Iterable
20
- from typing import TYPE_CHECKING, Any, Literal, TypeVar
20
+ from typing import TYPE_CHECKING, Any, Literal
21
21
 
22
22
  # Lazy imports for typing
23
23
  if TYPE_CHECKING:
@@ -26,9 +26,6 @@ if TYPE_CHECKING:
26
26
  import zarr # pyright: ignore[reportMissingTypeStubs]
27
27
  from numpy.typing import NDArray
28
28
 
29
- # Typing
30
- T = TypeVar("T")
31
-
32
29
  # Functions
33
30
  def unique_list[T](list_to_clean: Iterable[T], method: Literal["id", "hash", "str"] = "str") -> list[T]:
34
31
  """ Remove duplicates from the list while keeping the order using ids, hash, or str
@@ -79,7 +76,7 @@ def unique_list[T](list_to_clean: Iterable[T], method: Literal["id", "hash", "st
79
76
  return result
80
77
 
81
78
 
82
- def at_least_n(iterable: Iterable[T], predicate: Callable[[T], bool], n: int) -> bool:
79
+ def at_least_n[T](iterable: Iterable[T], predicate: Callable[[T], bool], n: int) -> bool:
83
80
  """ Return True if at least n elements in iterable satisfy predicate.
84
81
  It's like the built-in any() but for at least n matches.
85
82
 
@@ -2,9 +2,7 @@ import polars as pl
2
2
  import zarr
3
3
  from collections.abc import Callable as Callable, Iterable
4
4
  from numpy.typing import NDArray as NDArray
5
- from typing import Any, Literal, TypeVar
6
-
7
- T = TypeVar('T')
5
+ from typing import Any, Literal
8
6
 
9
7
  def unique_list[T](list_to_clean: Iterable[T], method: Literal['id', 'hash', 'str'] = 'str') -> list[T]:
10
8
  ''' Remove duplicates from the list while keeping the order using ids, hash, or str
@@ -31,7 +29,7 @@ def unique_list[T](list_to_clean: Iterable[T], method: Literal['id', 'hash', 'st
31
29
  \t\t>>> unique_list([s1, s2, s1, s1, s3, s2, s3], method="str")
32
30
  \t\t[{1, 2, 3}, {2, 3, 4}]
33
31
  \t'''
34
- def at_least_n(iterable: Iterable[T], predicate: Callable[[T], bool], n: int) -> bool:
32
+ def at_least_n[T](iterable: Iterable[T], predicate: Callable[[T], bool], n: int) -> bool:
35
33
  """ Return True if at least n elements in iterable satisfy predicate.
36
34
  \tIt's like the built-in any() but for at least n matches.
37
35
 
@@ -29,7 +29,7 @@ def generate_stubs(
29
29
  try:
30
30
  from mypy.stubgen import main as stubgen_main
31
31
  except ImportError as e:
32
- raise ImportError("mypy is required for array_to_disk function. Please install it via 'pip install mypy'.") from e
32
+ raise ImportError("mypy is required for generate_stubs function. Please install it via 'pip install mypy'.") from e
33
33
  try:
34
34
  stubgen_main(["-p", package_name, *extra_args.split()])
35
35
  return 0
stouputils/ctx.py CHANGED
@@ -20,13 +20,11 @@ import sys
20
20
  import time
21
21
  from collections.abc import Callable
22
22
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
23
- from typing import IO, Any, TextIO, TypeVar
23
+ from typing import IO, Any, TextIO
24
24
 
25
25
  from .io import super_open
26
26
  from .print import TeeMultiOutput, debug
27
27
 
28
- # Type variable for context managers
29
- T = TypeVar("T")
30
28
 
31
29
  # Abstract base class for context managers supporting both sync and async usage
32
30
  class AbstractBothContextManager[T](AbstractContextManager[T], AbstractAsyncContextManager[T]):
stouputils/ctx.pyi CHANGED
@@ -3,9 +3,7 @@ from .io import super_open as super_open
3
3
  from .print import TeeMultiOutput as TeeMultiOutput, debug as debug
4
4
  from collections.abc import Callable as Callable
5
5
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
6
- from typing import Any, IO, TextIO, TypeVar
7
-
8
- T = TypeVar('T')
6
+ from typing import Any, IO, TextIO
9
7
 
10
8
  class AbstractBothContextManager[T](AbstractContextManager[T], AbstractAsyncContextManager[T], metaclass=abc.ABCMeta):
11
9
  """ Abstract base class for context managers that support both synchronous and asynchronous usage. """
stouputils/decorators.py CHANGED
@@ -211,7 +211,7 @@ def timeout(
211
211
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
212
212
  # Check if we can use signal-based timeout (Unix only)
213
213
  import os
214
- use_signal: bool = os.name != 'nt' # Not Windows
214
+ use_signal: bool = os.name != "nt" # Not Windows
215
215
 
216
216
  if use_signal:
217
217
  try:
stouputils/image.py CHANGED
@@ -12,7 +12,7 @@ See stouputils.data_science.data_processing for lots more image processing utili
12
12
  # Imports
13
13
  import os
14
14
  from collections.abc import Callable
15
- from typing import TYPE_CHECKING, Any, TypeVar, cast
15
+ from typing import TYPE_CHECKING, Any, cast
16
16
 
17
17
  from .io import super_open
18
18
  from .print import debug, info
@@ -22,15 +22,13 @@ if TYPE_CHECKING:
22
22
  from numpy.typing import NDArray
23
23
  from PIL import Image
24
24
 
25
- PIL_Image_or_NDArray = TypeVar("PIL_Image_or_NDArray", bound="Image.Image | NDArray[np.number]")
26
-
27
25
  # Functions
28
- def image_resize[PIL_Image_or_NDArray](
29
- image: PIL_Image_or_NDArray,
26
+ def image_resize[T: "Image.Image | NDArray[np.number]"](
27
+ image: T,
30
28
  max_result_size: int,
31
29
  resampling: "Image.Resampling | None" = None,
32
30
  min_or_max: Callable[[int, int], int] = max,
33
- return_type: type[PIL_Image_or_NDArray] | str = "same",
31
+ return_type: type[T] | str = "same",
34
32
  keep_aspect_ratio: bool = True,
35
33
  ) -> Any:
36
34
  """ Resize an image while preserving its aspect ratio by default.
@@ -121,11 +119,11 @@ def image_resize[PIL_Image_or_NDArray](
121
119
  return new_image
122
120
 
123
121
 
124
- def auto_crop[PIL_Image_or_NDArray](
125
- image: PIL_Image_or_NDArray,
122
+ def auto_crop[T: "Image.Image | NDArray[np.number]"](
123
+ image: T,
126
124
  mask: "NDArray[np.bool_] | None" = None,
127
125
  threshold: int | float | Callable[["NDArray[np.number]"], int | float] | None = None,
128
- return_type: type[PIL_Image_or_NDArray] | str = "same",
126
+ return_type: type[T] | str = "same",
129
127
  contiguous: bool = True,
130
128
  ) -> Any:
131
129
  """ Automatically crop an image to remove zero or uniform regions.
@@ -401,7 +399,7 @@ def numpy_to_obj(
401
399
 
402
400
  # Apply marching cubes algorithm to extract mesh
403
401
  verts, faces, _, _ = cast(
404
- tuple[NDArray[np.floating], NDArray[np.integer], NDArray[np.floating], NDArray[np.floating]],
402
+ "tuple[NDArray[np.floating], NDArray[np.integer], NDArray[np.floating], NDArray[np.floating]]",
405
403
  measure.marching_cubes(volume, level=threshold, step_size=step_size, allow_degenerate=False) # type: ignore
406
404
  )
407
405
 
stouputils/image.pyi CHANGED
@@ -3,12 +3,10 @@ from .io import super_open as super_open
3
3
  from .print import debug as debug, info as info
4
4
  from PIL import Image
5
5
  from collections.abc import Callable
6
- from numpy.typing import NDArray
7
- from typing import Any, TypeVar
6
+ from numpy.typing import NDArray as NDArray
7
+ from typing import Any
8
8
 
9
- PIL_Image_or_NDArray = TypeVar('PIL_Image_or_NDArray', bound='Image.Image | NDArray[np.number]')
10
-
11
- def image_resize[PIL_Image_or_NDArray](image: PIL_Image_or_NDArray, max_result_size: int, resampling: Image.Resampling | None = None, min_or_max: Callable[[int, int], int] = ..., return_type: type[PIL_Image_or_NDArray] | str = 'same', keep_aspect_ratio: bool = True) -> Any:
9
+ def image_resize[T: Image.Image | NDArray[np.number]](image: T, max_result_size: int, resampling: Image.Resampling | None = None, min_or_max: Callable[[int, int], int] = ..., return_type: type[T] | str = 'same', keep_aspect_ratio: bool = True) -> Any:
12
10
  ''' Resize an image while preserving its aspect ratio by default.
13
11
  \tScales the image so that its largest dimension equals max_result_size.
14
12
 
@@ -47,7 +45,7 @@ def image_resize[PIL_Image_or_NDArray](image: PIL_Image_or_NDArray, max_result_s
47
45
  \t\t>>> image_resize(pil_image, 50, resampling=Image.Resampling.NEAREST).size
48
46
  \t\t(50, 25)
49
47
  \t'''
50
- def auto_crop[PIL_Image_or_NDArray](image: PIL_Image_or_NDArray, mask: NDArray[np.bool_] | None = None, threshold: int | float | Callable[[NDArray[np.number]], int | float] | None = None, return_type: type[PIL_Image_or_NDArray] | str = 'same', contiguous: bool = True) -> Any:
48
+ def auto_crop[T: Image.Image | NDArray[np.number]](image: T, mask: NDArray[np.bool_] | None = None, threshold: int | float | Callable[[NDArray[np.number]], int | float] | None = None, return_type: type[T] | str = 'same', contiguous: bool = True) -> Any:
51
49
  ''' Automatically crop an image to remove zero or uniform regions.
52
50
 
53
51
  \tThis function crops the image to keep only the region where pixels are non-zero
stouputils/io.py CHANGED
@@ -11,6 +11,7 @@ This module provides utilities for file management.
11
11
  - super_open: Open a file with the given mode, creating the directory if it doesn't exist (only if writing)
12
12
  - replace_tilde: Replace the "~" by the user's home directory
13
13
  - clean_path: Clean the path by replacing backslashes with forward slashes and simplifying the path
14
+ - safe_close: Safely close a file descriptor or file object after flushing, ignoring any exceptions
14
15
 
15
16
  .. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/io_module.gif
16
17
  :alt: stouputils io examples
@@ -338,7 +339,7 @@ def super_copy(src: str, dst: str, create_dir: bool = True, symlink: bool = Fals
338
339
  src (str): The source path
339
340
  dst (str): The destination path
340
341
  create_dir (bool): Whether to create the directory if it doesn't exist (default: True)
341
- symlink (bool): Whether to create a symlink instead of copying (Linux only, default: True)
342
+ symlink (bool): Whether to create a symlink instead of copying (Linux only)
342
343
  Returns:
343
344
  str: The destination path
344
345
  """
@@ -491,3 +492,23 @@ def clean_path(file_path: str, trailing_slash: bool = True) -> str:
491
492
  # Return the cleaned path
492
493
  return file_path if file_path != "." else ""
493
494
 
495
+ def safe_close(file: IO[Any] | int | None) -> None:
496
+ """ Safely close a file object (or file descriptor) after flushing, ignoring any exceptions.
497
+
498
+ Args:
499
+ file (IO[Any] | int | None): The file object or file descriptor to close
500
+ """
501
+ if isinstance(file, int):
502
+ if file != -1:
503
+ for func in (os.fsync, os.close):
504
+ try:
505
+ func(file)
506
+ except Exception:
507
+ pass
508
+ elif file:
509
+ for func in (file.flush, file.close):
510
+ try:
511
+ func()
512
+ except Exception:
513
+ pass
514
+
stouputils/io.pyi CHANGED
@@ -144,7 +144,7 @@ def super_copy(src: str, dst: str, create_dir: bool = True, symlink: bool = Fals
144
144
  \t\tsrc (str): The source path
145
145
  \t\tdst (str): The destination path
146
146
  \t\tcreate_dir (bool): Whether to create the directory if it doesn't exist (default: True)
147
- \t\tsymlink (bool): Whether to create a symlink instead of copying (Linux only, default: True)
147
+ \t\tsymlink (bool): Whether to create a symlink instead of copying (Linux only)
148
148
  \tReturns:
149
149
  \t\tstr: The destination path
150
150
  \t"""
@@ -211,3 +211,9 @@ def clean_path(file_path: str, trailing_slash: bool = True) -> str:
211
211
  \t\t>>> clean_path("C:/folder1\\\\folder2")
212
212
  \t\t\'C:/folder1/folder2\'
213
213
  \t'''
214
+ def safe_close(file: IO[Any] | int | None) -> None:
215
+ """ Safely close a file object (or file descriptor) after flushing, ignoring any exceptions.
216
+
217
+ \tArgs:
218
+ \t\tfile (IO[Any] | int | None): The file object or file descriptor to close
219
+ \t"""
@@ -0,0 +1,36 @@
1
+ """ Inter-process locks implementing First-In-First-Out (FIFO).
2
+
3
+ Source:
4
+ - https://en.wikipedia.org/wiki/File_locking
5
+ - https://en.wikipedia.org/wiki/Starvation_%28computer_science%29
6
+ - https://en.wikipedia.org/wiki/FIFO_and_LIFO_accounting
7
+
8
+ Provides three classes:
9
+
10
+ - LockFifo: basic cross-process lock using filesystem (POSIX via fcntl, Windows via msvcrt).
11
+ - RLockFifo: reentrant per-(process,thread) lock built on top of LockFifo.
12
+ - RedisLockFifo: distributed lock using redis (optional dependency).
13
+
14
+ Usage
15
+ -----
16
+ >>> import stouputils as stp
17
+ >>> with stp.LockFifo("some_directory/my.lock", timeout=5):
18
+ ... pass
19
+
20
+ >>> with stp.RLockFifo("some_directory/my_r.lock", timeout=5):
21
+ ... pass
22
+
23
+ >>> def _redis_example():
24
+ ... with stp.RedisLockFifo("my_redis_lock", timeout=5):
25
+ ... pass
26
+ >>> import os
27
+ >>> if os.name != "nt":
28
+ ... _redis_example()
29
+ """
30
+ # Imports
31
+ from .base import *
32
+ from .queue import *
33
+ from .re_entrant import *
34
+ from .redis_fifo import *
35
+ from .shared import *
36
+
@@ -0,0 +1,5 @@
1
+ from .base import *
2
+ from .queue import *
3
+ from .re_entrant import *
4
+ from .redis_fifo import *
5
+ from .shared import *