stouputils 1.18.0__py3-none-any.whl → 1.18.2__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/collections.py CHANGED
@@ -132,16 +132,45 @@ def sort_dict_keys[T](dictionary: dict[T, Any], order: list[T], reverse: bool =
132
132
  def upsert_in_dataframe(
133
133
  df: "pl.DataFrame",
134
134
  new_entry: dict[str, Any],
135
- primary_keys: dict[str, Any] | None = None
135
+ primary_keys: list[str] | dict[str, Any] | None = None
136
136
  ) -> "pl.DataFrame":
137
137
  """ Insert or update a row in the Polars DataFrame based on primary keys.
138
138
 
139
139
  Args:
140
140
  df (pl.DataFrame): The Polars DataFrame to update.
141
141
  new_entry (dict[str, Any]): The new entry to insert or update.
142
- primary_keys (dict[str, Any]): The primary keys to identify the row (default: empty).
142
+ primary_keys (list[str] | dict[str, Any] | None): The primary keys to identify the row (for updates).
143
143
  Returns:
144
144
  pl.DataFrame: The updated Polars DataFrame.
145
+ Examples:
146
+ >>> import polars as pl
147
+ >>> df = pl.DataFrame({"id": [1, 2], "value": ["a", "b"]})
148
+ >>> new_entry = {"id": 2, "value": "updated"}
149
+ >>> updated_df = upsert_in_dataframe(df, new_entry, primary_keys=["id"])
150
+ >>> print(updated_df)
151
+ shape: (2, 2)
152
+ ┌─────┬─────────┐
153
+ │ id ┆ value │
154
+ │ --- ┆ --- │
155
+ │ i64 ┆ str │
156
+ ╞═════╪═════════╡
157
+ │ 1 ┆ a │
158
+ │ 2 ┆ updated │
159
+ └─────┴─────────┘
160
+
161
+ >>> new_entry = {"id": 3, "value": "new"}
162
+ >>> updated_df = upsert_in_dataframe(updated_df, new_entry, primary_keys=["id"])
163
+ >>> print(updated_df)
164
+ shape: (3, 2)
165
+ ┌─────┬─────────┐
166
+ │ id ┆ value │
167
+ │ --- ┆ --- │
168
+ │ i64 ┆ str │
169
+ ╞═════╪═════════╡
170
+ │ 1 ┆ a │
171
+ │ 2 ┆ updated │
172
+ │ 3 ┆ new │
173
+ └─────┴─────────┘
145
174
  """
146
175
  # Imports
147
176
  import polars as pl
@@ -155,6 +184,10 @@ def upsert_in_dataframe(
155
184
  new_row_df = pl.DataFrame([new_entry])
156
185
  return pl.concat([df, new_row_df], how="diagonal_relaxed")
157
186
 
187
+ # If primary keys are provided as a list, convert to dict with values from new_entry
188
+ if isinstance(primary_keys, list):
189
+ primary_keys = {key: new_entry[key] for key in primary_keys if key in new_entry}
190
+
158
191
  # Build mask based on primary keys
159
192
  mask: pl.Expr = pl.lit(True)
160
193
  for key, value in primary_keys.items():
@@ -69,16 +69,45 @@ def sort_dict_keys[T](dictionary: dict[T, Any], order: list[T], reverse: bool =
69
69
  \t\t>>> sort_dict_keys({\'b\': 2, \'a\': 1, \'c\': 3, \'d\': 4}, order=["c", "b"])
70
70
  \t\t{\'c\': 3, \'b\': 2, \'a\': 1, \'d\': 4}
71
71
  \t'''
72
- def upsert_in_dataframe(df: pl.DataFrame, new_entry: dict[str, Any], primary_keys: dict[str, Any] | None = None) -> pl.DataFrame:
73
- """ Insert or update a row in the Polars DataFrame based on primary keys.
72
+ def upsert_in_dataframe(df: pl.DataFrame, new_entry: dict[str, Any], primary_keys: list[str] | dict[str, Any] | None = None) -> pl.DataFrame:
73
+ ''' Insert or update a row in the Polars DataFrame based on primary keys.
74
74
 
75
75
  \tArgs:
76
76
  \t\tdf\t\t\t\t(pl.DataFrame):\t\tThe Polars DataFrame to update.
77
77
  \t\tnew_entry\t\t(dict[str, Any]):\tThe new entry to insert or update.
78
- \t\tprimary_keys\t(dict[str, Any]):\tThe primary keys to identify the row (default: empty).
78
+ \t\tprimary_keys\t(list[str] | dict[str, Any] | None):\tThe primary keys to identify the row (for updates).
79
79
  \tReturns:
80
80
  \t\tpl.DataFrame: The updated Polars DataFrame.
81
- \t"""
81
+ \tExamples:
82
+ \t\t>>> import polars as pl
83
+ \t\t>>> df = pl.DataFrame({"id": [1, 2], "value": ["a", "b"]})
84
+ \t\t>>> new_entry = {"id": 2, "value": "updated"}
85
+ \t\t>>> updated_df = upsert_in_dataframe(df, new_entry, primary_keys=["id"])
86
+ \t\t>>> print(updated_df)
87
+ \t\tshape: (2, 2)
88
+ \t\t┌─────┬─────────┐
89
+ \t\t│ id ┆ value │
90
+ \t\t│ --- ┆ --- │
91
+ \t\t│ i64 ┆ str │
92
+ \t\t╞═════╪═════════╡
93
+ \t\t│ 1 ┆ a │
94
+ \t\t│ 2 ┆ updated │
95
+ \t\t└─────┴─────────┘
96
+
97
+ \t\t>>> new_entry = {"id": 3, "value": "new"}
98
+ \t\t>>> updated_df = upsert_in_dataframe(updated_df, new_entry, primary_keys=["id"])
99
+ \t\t>>> print(updated_df)
100
+ \t\tshape: (3, 2)
101
+ \t\t┌─────┬─────────┐
102
+ \t\t│ id ┆ value │
103
+ \t\t│ --- ┆ --- │
104
+ \t\t│ i64 ┆ str │
105
+ \t\t╞═════╪═════════╡
106
+ \t\t│ 1 ┆ a │
107
+ \t\t│ 2 ┆ updated │
108
+ \t\t│ 3 ┆ new │
109
+ \t\t└─────┴─────────┘
110
+ \t'''
82
111
  def array_to_disk(data: NDArray[Any] | zarr.Array, delete_input: bool = True, more_data: NDArray[Any] | zarr.Array | None = None) -> tuple['zarr.Array', str, int]:
83
112
  """ Easily handle large numpy arrays on disk using zarr for efficient storage and access.
84
113
 
stouputils/io.py CHANGED
@@ -492,7 +492,7 @@ def clean_path(file_path: str, trailing_slash: bool = True) -> str:
492
492
  # Return the cleaned path
493
493
  return file_path if file_path != "." else ""
494
494
 
495
- def safe_close(file: IO[Any] | int | None) -> None:
495
+ def safe_close(file: IO[Any] | int | Any | None) -> None:
496
496
  """ Safely close a file object (or file descriptor) after flushing, ignoring any exceptions.
497
497
 
498
498
  Args:
@@ -506,9 +506,9 @@ def safe_close(file: IO[Any] | int | None) -> None:
506
506
  except Exception:
507
507
  pass
508
508
  elif file:
509
- for func in (file.flush, file.close):
509
+ for func in ("flush", "close"):
510
510
  try:
511
- func()
511
+ getattr(file, func)()
512
512
  except Exception:
513
513
  pass
514
514
 
stouputils/io.pyi CHANGED
@@ -211,7 +211,7 @@ 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:
214
+ def safe_close(file: IO[Any] | int | Any | None) -> None:
215
215
  """ Safely close a file object (or file descriptor) after flushing, ignoring any exceptions.
216
216
 
217
217
  \tArgs:
@@ -65,8 +65,7 @@ class CaptureOutput:
65
65
 
66
66
  def parent_close_write(self) -> None:
67
67
  """ Close the parent's copy of the write end; the child's copy remains. """
68
- safe_close(self.write_fd)
69
- self.write_conn.close()
68
+ safe_close(self.write_conn)
70
69
  self.write_fd = -1 # Prevent accidental reuse
71
70
 
72
71
  def start_listener(self) -> None:
@@ -110,8 +109,7 @@ class CaptureOutput:
110
109
  if len(buffer) > self.chunk_size * 4:
111
110
  _handle_buffer()
112
111
  finally:
113
- safe_close(self.read_fd)
114
- self.read_conn.close()
112
+ safe_close(self.read_conn)
115
113
  self.read_fd = -1
116
114
  self._thread = None # Mark thread as stopped so callers don't block unnecessarily
117
115
 
@@ -123,8 +121,9 @@ class CaptureOutput:
123
121
  def join_listener(self, timeout: float | None = None) -> None:
124
122
  """ Wait for the listener thread to finish (until EOF). """
125
123
  if self._thread is None:
126
- safe_close(self.read_fd)
127
- return self.read_conn.close()
124
+ safe_close(self.read_conn)
125
+ self.read_fd = -1
126
+ return
128
127
  self._thread.join(timeout)
129
128
 
130
129
  # If thread finished, ensure read fd is closed and clear thread
@@ -4,9 +4,20 @@ import time
4
4
  from collections.abc import Callable
5
5
  from typing import Any
6
6
 
7
+ from ..typing import JsonDict
7
8
  from .capturer import CaptureOutput
8
9
 
9
10
 
11
+ class RemoteSubprocessError(RuntimeError):
12
+ """ Raised in the parent when the child raised an exception - contains the child's formatted traceback. """
13
+ def __init__(self, exc_type: str, exc_repr: str, traceback_str: str):
14
+ msg = f"Exception in subprocess ({exc_type}): {exc_repr}\n\nRemote traceback:\n{traceback_str}"
15
+ super().__init__(msg)
16
+ self.remote_type = exc_type
17
+ self.remote_repr = exc_repr
18
+ self.remote_traceback = traceback_str
19
+
20
+
10
21
  def run_in_subprocess[R](
11
22
  func: Callable[..., R],
12
23
  *args: Any,
@@ -37,7 +48,8 @@ def run_in_subprocess[R](
37
48
  R: The return value of the function.
38
49
 
39
50
  Raises:
40
- RuntimeError: If the subprocess exits with a non-zero exit code or times out.
51
+ RemoteSubprocessError: If the child raised an exception - contains the child's formatted traceback.
52
+ RuntimeError: If the subprocess exits with a non-zero exit code or did not return a result.
41
53
  TimeoutError: If the subprocess exceeds the specified timeout.
42
54
 
43
55
  Examples:
@@ -66,7 +78,7 @@ def run_in_subprocess[R](
66
78
  from multiprocessing import Queue
67
79
 
68
80
  # Create a queue to get the result from the subprocess (only if we need to wait)
69
- result_queue: Queue[R | Exception] | None = None if no_join else Queue()
81
+ result_queue: Queue[JsonDict] | None = None if no_join else Queue()
70
82
 
71
83
  # Optionally setup output capture pipe and listener
72
84
  capturer: CaptureOutput | None = None
@@ -105,23 +117,22 @@ def run_in_subprocess[R](
105
117
  process.join()
106
118
  raise TimeoutError(f"Subprocess exceeded timeout of {timeout} seconds and was terminated")
107
119
 
108
- # Check exit code
120
+ # Retrieve the payload if present
121
+ result_payload: JsonDict | None = result_queue.get_nowait() if not result_queue.empty() else None
122
+
123
+ # If the child sent a structured exception, raise it with the formatted traceback
124
+ if isinstance(result_payload, dict):
125
+ if result_payload.pop("ok", False) is False:
126
+ raise RemoteSubprocessError(**result_payload)
127
+ else:
128
+ return result_payload["result"]
129
+
130
+ # Raise an error according to the exit code presence
109
131
  if process.exitcode != 0:
110
- # Try to get any exception from the queue (non-blocking)
111
- if not result_queue.empty():
112
- result_or_exception = result_queue.get_nowait()
113
- if isinstance(result_or_exception, Exception):
114
- raise result_or_exception
115
132
  raise RuntimeError(f"Subprocess failed with exit code {process.exitcode}")
133
+ raise RuntimeError("Subprocess did not return any result")
116
134
 
117
- # Retrieve the result
118
- try:
119
- result_or_exception = result_queue.get_nowait()
120
- if isinstance(result_or_exception, Exception):
121
- raise result_or_exception
122
- return result_or_exception
123
- except Exception as e:
124
- raise RuntimeError("Subprocess did not return any result") from e
135
+ # Finally, ensure we drain/join the listener if capturing output
125
136
  finally:
126
137
  if capturer is not None:
127
138
  capturer.join_listener(timeout=5.0)
@@ -154,10 +165,21 @@ def _subprocess_wrapper[R](
154
165
  # Execute the target function and put the result in the queue
155
166
  result: R = func(*args, **kwargs)
156
167
  if result_queue is not None:
157
- result_queue.put(result)
168
+ result_queue.put({"ok": True, "result": result})
158
169
 
159
170
  # Handle cleanup and exceptions
160
171
  except Exception as e:
161
172
  if result_queue is not None:
162
- result_queue.put(e)
173
+ try:
174
+ import traceback
175
+ tb = traceback.format_exc()
176
+ result_queue.put({
177
+ "ok": False,
178
+ "exc_type": e.__class__.__name__,
179
+ "exc_repr": repr(e),
180
+ "traceback_str": tb,
181
+ })
182
+ except Exception:
183
+ # Nothing we can do if even this fails
184
+ pass
163
185
 
@@ -1,7 +1,16 @@
1
+ from ..typing import JsonDict as JsonDict
1
2
  from .capturer import CaptureOutput as CaptureOutput
3
+ from _typeshed import Incomplete
2
4
  from collections.abc import Callable as Callable
3
5
  from typing import Any
4
6
 
7
+ class RemoteSubprocessError(RuntimeError):
8
+ """ Raised in the parent when the child raised an exception - contains the child's formatted traceback. """
9
+ remote_type: Incomplete
10
+ remote_repr: Incomplete
11
+ remote_traceback: Incomplete
12
+ def __init__(self, exc_type: str, exc_repr: str, traceback_str: str) -> None: ...
13
+
5
14
  def run_in_subprocess[R](func: Callable[..., R], *args: Any, timeout: float | None = None, no_join: bool = False, capture_output: bool = False, **kwargs: Any) -> R:
6
15
  ''' Execute a function in a subprocess with positional and keyword arguments.
7
16
 
@@ -25,7 +34,8 @@ def run_in_subprocess[R](func: Callable[..., R], *args: Any, timeout: float | No
25
34
  \t\tR: The return value of the function.
26
35
 
27
36
  \tRaises:
28
- \t\tRuntimeError: If the subprocess exits with a non-zero exit code or times out.
37
+ \t\tRemoteSubprocessError: If the child raised an exception - contains the child\'s formatted traceback.
38
+ \t\tRuntimeError: If the subprocess exits with a non-zero exit code or did not return a result.
29
39
  \t\tTimeoutError: If the subprocess exceeds the specified timeout.
30
40
 
31
41
  \tExamples:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: stouputils
3
- Version: 1.18.0
3
+ Version: 1.18.2
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
  Keywords: utilities,tools,helpers,development,python
6
6
  Author: Stoupy51
@@ -21,8 +21,8 @@ stouputils/archive.py,sha256=uDrPFxbY_C8SwUZRH4FWnYSoJKkFWynCx751zP9AHaY,12144
21
21
  stouputils/archive.pyi,sha256=Z2BbQAiErRYntv53QC9uf_XPw3tx3Oy73wB0Bbil11c,3246
22
22
  stouputils/backup.py,sha256=AE5WKMLiyk0VkRUfhmNfO2EUeUbZY5GTFVIuI5z7axA,20947
23
23
  stouputils/backup.pyi,sha256=-SLVykkR5U8479T84zjNPVBNnV193s0zyWjathY2DDA,4923
24
- stouputils/collections.py,sha256=jUNJQaMhmgLetVyZrYm0FFiaU0RkBfuXeJEvJ7wyGzU,9815
25
- stouputils/collections.pyi,sha256=naOr5vsvu2SOW-IUFLaoyxYXVv0Vk5pC9zD_mmT0dg8,4297
24
+ stouputils/collections.py,sha256=73799uJ5ryQoNBo7N4Cz41Q992QP_kSsw_hy33ZpDyw,11121
25
+ stouputils/collections.pyi,sha256=jPzyaeesz1IqutuT69Bt4DFOguURjXbcYO2l2sifXRA,5448
26
26
  stouputils/continuous_delivery/__init__.py,sha256=JqPww29xZ-pp6OJDGhUj2dxyV9rgTTMUz0YDDVr9RaA,731
27
27
  stouputils/continuous_delivery/__init__.pyi,sha256=_Sz2D10n1CDEyY8qDFwXNKdr01HVxanY4qdq9aN19cc,117
28
28
  stouputils/continuous_delivery/cd_utils.py,sha256=fkaHk2V3j66uFAUsM2c_UddNhXW2KAQcrh7jVsH79pU,8594
@@ -125,8 +125,8 @@ stouputils/installer/main.py,sha256=8wrx_cnQo1dFGRf6x8vtxh6-96tQ-AzMyvJ0S64j0io,
125
125
  stouputils/installer/main.pyi,sha256=r3j4GoMBpU06MpOqjSwoDTiSMOmbA3WWUA87970b6KE,3134
126
126
  stouputils/installer/windows.py,sha256=r2AIuoyAmtMEuoCtQBH9GWQWI-JUT2J9zoH28j9ruOU,4880
127
127
  stouputils/installer/windows.pyi,sha256=tHogIFhPVDQS0I10liLkAxnpaFFAvmFtEVMpPIae5LU,1616
128
- stouputils/io.py,sha256=Q0Erzy7k8MTEw50s1O_CbuP2bh7oxt42tbcXcUeX97E,17782
129
- stouputils/io.pyi,sha256=zsId531UOFs-P5kc5cCl3W6oA0X47T_RD2HPDpElZX8,9308
128
+ stouputils/io.py,sha256=3-n6RGItDn7mdHSkzK4oSUMeL-3VJ1k2cvoSfzBH_ak,17797
129
+ stouputils/io.pyi,sha256=n2o28-4BfhlEH1f-vCLU8YlaqzG-4qgrZ3p8jEnJFeE,9314
130
130
  stouputils/lock/__init__.py,sha256=8EvKPwnd5oHAWP-2vs6ULUDCSNyUh-mw12nYvBqgVAc,1029
131
131
  stouputils/lock/__init__.pyi,sha256=qcTm6JcGXfwQB2dUCa2wFEInSwJF2pOrYnejSpvGd7k,120
132
132
  stouputils/lock/base.py,sha256=hjSXRzOLVTMNrxpf4QcmfCfdlSRHFT9e130Zz_cqxY8,20483
@@ -141,14 +141,14 @@ stouputils/lock/shared.py,sha256=G8Mcy7dXtNESyU7hSaeihNrCU4l98VhyQyO_vQYPJ7g,788
141
141
  stouputils/lock/shared.pyi,sha256=0CV6TpTaDEkcGA35Q-ijp8ckImZ32umlMA4U-8C_O-I,545
142
142
  stouputils/parallel/__init__.py,sha256=myD8KiVfPPKF26Xu8Clu0p-VaYDK74loMUjUkl6-9XU,1013
143
143
  stouputils/parallel/__init__.pyi,sha256=UtZKtl9i__OH0Edypap9oZUcTF1h91qfpItG1-x7TfE,97
144
- stouputils/parallel/capturer.py,sha256=lo7D1x2RGo9SHkr2sIrY6wL4V5wbsxngnmBMbn5-o_I,4177
144
+ stouputils/parallel/capturer.py,sha256=1ON8QuMrk9B0WS5lCIKtItzKRmlddrHsJAhMHYvKFyE,4127
145
145
  stouputils/parallel/capturer.pyi,sha256=DWa3biPFzrGJBmkaFhAWwhbX4gbKQAipBAOJm4_XBy8,1665
146
146
  stouputils/parallel/common.py,sha256=niDcAiEX3flX0ow91gXOB4umlOrR8PIYvpcKPClJHfM,4910
147
147
  stouputils/parallel/common.pyi,sha256=jbyftOYHKP2qaA8YC1f1f12-BDBkhfsQsnPdsR4oet8,2493
148
148
  stouputils/parallel/multi.py,sha256=tHJgcQJwsI6QeKEHoGJC4tsVK_6t1Fazkb06i1u-W_8,12610
149
149
  stouputils/parallel/multi.pyi,sha256=DWolZn1UoXxOfuw7LqEJcU8aQJsN-_DRhPGJlJCA5pQ,8021
150
- stouputils/parallel/subprocess.py,sha256=YD9mda-zMRpudlby4cLwLJxIY5BjPwn8K11eJ-3k-6E,5790
151
- stouputils/parallel/subprocess.pyi,sha256=9g5FDYfcnIikd9OOtDP1u_NPT2elk5YjUsQN9eIMSso,3145
150
+ stouputils/parallel/subprocess.py,sha256=LWbwwAmnz54dCz9TAcKNg1TOMCVSP0C-0GIXaS5nVx0,6728
151
+ stouputils/parallel/subprocess.pyi,sha256=gzRtpTslvoENLtSNk79fe3Xz8lV3IwuopT9uMHW9BTU,3680
152
152
  stouputils/print.py,sha256=PNyKvKheI7ior_-jQQ0Xu8ym7tSctheyHXTFykw2MKc,24552
153
153
  stouputils/print.pyi,sha256=0z-BFpOEZ48GBGcD08C4Be67cPKX4ZxSHqKyOLRS_M8,10205
154
154
  stouputils/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
@@ -156,7 +156,7 @@ stouputils/typing.py,sha256=TwvxrvxhBRkyHkoOpfyXebN13M3xJb8MAjKXiNIWjew,2205
156
156
  stouputils/typing.pyi,sha256=U2UmFZausMYpnsUQROQE2JOwHcjx2hKV0rJuOdR57Ew,1341
157
157
  stouputils/version_pkg.py,sha256=Jsp-s03L14DkiZ94vQgrlQmaxApfn9DC8M_nzT1SJLk,7014
158
158
  stouputils/version_pkg.pyi,sha256=QPvqp1U3QA-9C_CC1dT9Vahv1hXEhstbM7x5uzMZSsQ,755
159
- stouputils-1.18.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
160
- stouputils-1.18.0.dist-info/entry_points.txt,sha256=tx0z9VOnE-sfkmbFbA93zaBMzV3XSsKEJa_BWIqUzxw,57
161
- stouputils-1.18.0.dist-info/METADATA,sha256=OrIKAYzFOAG8DIJjpNj2UQzc2QW7iRSH_PS3yc6EnMQ,14011
162
- stouputils-1.18.0.dist-info/RECORD,,
159
+ stouputils-1.18.2.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
160
+ stouputils-1.18.2.dist-info/entry_points.txt,sha256=tx0z9VOnE-sfkmbFbA93zaBMzV3XSsKEJa_BWIqUzxw,57
161
+ stouputils-1.18.2.dist-info/METADATA,sha256=74JWSrmZCgZa1xNxkPpitRGeL9SUre0dE8dFIY8BosA,14011
162
+ stouputils-1.18.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.27
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any