splurge-dsv 2025.1.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.
- splurge_dsv/__init__.py +0 -0
- splurge_dsv/__main__.py +0 -0
- splurge_dsv/dsv_helper.py +263 -0
- splurge_dsv/exceptions.py +123 -0
- splurge_dsv/path_validator.py +262 -0
- splurge_dsv/resource_manager.py +432 -0
- splurge_dsv/string_tokenizer.py +136 -0
- splurge_dsv/text_file_helper.py +343 -0
- splurge_dsv-2025.1.0.dist-info/METADATA +292 -0
- splurge_dsv-2025.1.0.dist-info/RECORD +13 -0
- splurge_dsv-2025.1.0.dist-info/WHEEL +5 -0
- splurge_dsv-2025.1.0.dist-info/licenses/LICENSE +21 -0
- splurge_dsv-2025.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,432 @@
|
|
1
|
+
"""
|
2
|
+
Resource management utilities with context managers.
|
3
|
+
|
4
|
+
This module provides context managers and resource management utilities
|
5
|
+
for safe handling of file operations, streams, and other resources.
|
6
|
+
|
7
|
+
Copyright (c) 2025 Jim Schilling
|
8
|
+
|
9
|
+
Please preserve this header and all related material when sharing!
|
10
|
+
|
11
|
+
This module is licensed under the MIT License.
|
12
|
+
"""
|
13
|
+
|
14
|
+
from contextlib import contextmanager
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Iterator, Any, IO
|
17
|
+
|
18
|
+
from splurge_dsv.exceptions import (
|
19
|
+
SplurgeResourceAcquisitionError,
|
20
|
+
SplurgeResourceReleaseError,
|
21
|
+
SplurgeFileNotFoundError,
|
22
|
+
SplurgeFilePermissionError,
|
23
|
+
SplurgeFileEncodingError
|
24
|
+
)
|
25
|
+
from splurge_dsv.path_validator import PathValidator
|
26
|
+
|
27
|
+
|
28
|
+
# Module-level constants for resource management
|
29
|
+
DEFAULT_BUFFERING = -1 # Default buffering for file operations
|
30
|
+
DEFAULT_ENCODING = "utf-8" # Default text encoding
|
31
|
+
DEFAULT_MODE = "r" # Default file mode for reading
|
32
|
+
|
33
|
+
|
34
|
+
def _safe_open_file(
|
35
|
+
file_path: Path,
|
36
|
+
*,
|
37
|
+
mode: str,
|
38
|
+
encoding: str | None = None,
|
39
|
+
errors: str | None = None,
|
40
|
+
newline: str | None = None,
|
41
|
+
buffering: int = DEFAULT_BUFFERING
|
42
|
+
) -> IO[Any]:
|
43
|
+
"""
|
44
|
+
Safely open a file with proper error handling.
|
45
|
+
|
46
|
+
This function provides centralized file opening with consistent error handling
|
47
|
+
that converts standard file operation exceptions to custom exceptions.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
file_path: Path to the file
|
51
|
+
mode: File open mode
|
52
|
+
encoding: Text encoding (for text mode)
|
53
|
+
errors: Error handling for encoding
|
54
|
+
newline: Newline handling
|
55
|
+
buffering: Buffer size
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
File handle
|
59
|
+
|
60
|
+
Raises:
|
61
|
+
SplurgeFileNotFoundError: If file is not found
|
62
|
+
SplurgeFilePermissionError: If permission is denied
|
63
|
+
SplurgeFileEncodingError: If encoding error occurs
|
64
|
+
SplurgeResourceAcquisitionError: If other file operation fails
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
if 'b' in mode:
|
68
|
+
# Binary mode
|
69
|
+
return open(
|
70
|
+
file_path,
|
71
|
+
mode=mode,
|
72
|
+
buffering=buffering
|
73
|
+
)
|
74
|
+
else:
|
75
|
+
# Text mode
|
76
|
+
return open(
|
77
|
+
file_path,
|
78
|
+
mode=mode,
|
79
|
+
encoding=encoding,
|
80
|
+
errors=errors,
|
81
|
+
newline=newline,
|
82
|
+
buffering=buffering
|
83
|
+
)
|
84
|
+
except FileNotFoundError as e:
|
85
|
+
raise SplurgeFileNotFoundError(
|
86
|
+
f"File not found: {file_path}",
|
87
|
+
details=str(e)
|
88
|
+
)
|
89
|
+
except PermissionError as e:
|
90
|
+
raise SplurgeFilePermissionError(
|
91
|
+
f"Permission denied: {file_path}",
|
92
|
+
details=str(e)
|
93
|
+
)
|
94
|
+
except UnicodeDecodeError as e:
|
95
|
+
raise SplurgeFileEncodingError(
|
96
|
+
f"Encoding error reading file: {file_path}",
|
97
|
+
details=str(e)
|
98
|
+
)
|
99
|
+
except OSError as e:
|
100
|
+
raise SplurgeResourceAcquisitionError(
|
101
|
+
f"Failed to open file: {file_path}",
|
102
|
+
details=str(e)
|
103
|
+
)
|
104
|
+
|
105
|
+
|
106
|
+
class ResourceManager:
|
107
|
+
"""
|
108
|
+
Generic resource manager that implements the ResourceManagerProtocol.
|
109
|
+
|
110
|
+
This class provides a base implementation for resource management
|
111
|
+
with acquire/release semantics.
|
112
|
+
"""
|
113
|
+
|
114
|
+
def __init__(self) -> None:
|
115
|
+
"""Initialize the resource manager."""
|
116
|
+
self._resource: Any | None = None
|
117
|
+
self._is_acquired_flag: bool = False
|
118
|
+
|
119
|
+
def acquire(self) -> Any:
|
120
|
+
"""
|
121
|
+
Acquire the managed resource.
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
The acquired resource
|
125
|
+
|
126
|
+
Raises:
|
127
|
+
NotImplementedError: If _create_resource is not implemented by subclass
|
128
|
+
SplurgeResourceAcquisitionError: If resource cannot be acquired
|
129
|
+
"""
|
130
|
+
if self._is_acquired_flag:
|
131
|
+
raise SplurgeResourceAcquisitionError(
|
132
|
+
"Resource is already acquired",
|
133
|
+
details="Cannot acquire resource that is already in use"
|
134
|
+
)
|
135
|
+
|
136
|
+
try:
|
137
|
+
self._resource = self._create_resource()
|
138
|
+
self._is_acquired_flag = True
|
139
|
+
return self._resource
|
140
|
+
except NotImplementedError:
|
141
|
+
# Re-raise NotImplementedError without wrapping it
|
142
|
+
raise
|
143
|
+
except Exception as e:
|
144
|
+
raise SplurgeResourceAcquisitionError(
|
145
|
+
"Failed to acquire resource",
|
146
|
+
details=str(e)
|
147
|
+
)
|
148
|
+
|
149
|
+
def release(self) -> None:
|
150
|
+
"""
|
151
|
+
Release the managed resource.
|
152
|
+
|
153
|
+
Raises:
|
154
|
+
SplurgeResourceReleaseError: If resource cannot be released
|
155
|
+
"""
|
156
|
+
if not self._is_acquired_flag:
|
157
|
+
return # Nothing to release
|
158
|
+
|
159
|
+
try:
|
160
|
+
self._cleanup_resource()
|
161
|
+
self._resource = None
|
162
|
+
self._is_acquired_flag = False
|
163
|
+
except Exception as e:
|
164
|
+
raise SplurgeResourceReleaseError(
|
165
|
+
"Failed to release resource",
|
166
|
+
details=str(e)
|
167
|
+
)
|
168
|
+
|
169
|
+
def is_acquired(self) -> bool:
|
170
|
+
"""
|
171
|
+
Check if the resource is currently acquired.
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
True if resource is acquired, False otherwise
|
175
|
+
"""
|
176
|
+
return self._is_acquired_flag
|
177
|
+
|
178
|
+
def _create_resource(self) -> Any:
|
179
|
+
"""
|
180
|
+
Create the resource to be managed.
|
181
|
+
|
182
|
+
This method should be overridden by subclasses to provide
|
183
|
+
specific resource creation logic.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
The created resource
|
187
|
+
|
188
|
+
Raises:
|
189
|
+
NotImplementedError: If not overridden by subclass
|
190
|
+
"""
|
191
|
+
raise NotImplementedError("Subclasses must implement _create_resource")
|
192
|
+
|
193
|
+
def _cleanup_resource(self) -> None:
|
194
|
+
"""
|
195
|
+
Clean up the managed resource.
|
196
|
+
|
197
|
+
This method should be overridden by subclasses to provide
|
198
|
+
specific resource cleanup logic.
|
199
|
+
"""
|
200
|
+
if self._resource is not None and hasattr(self._resource, 'close'):
|
201
|
+
self._resource.close()
|
202
|
+
|
203
|
+
|
204
|
+
class FileResourceManager:
|
205
|
+
"""
|
206
|
+
Context manager for safe file operations with automatic cleanup.
|
207
|
+
|
208
|
+
This class provides context managers for reading and writing files
|
209
|
+
with proper error handling and resource cleanup.
|
210
|
+
"""
|
211
|
+
|
212
|
+
def __init__(
|
213
|
+
self,
|
214
|
+
file_path: str | Path,
|
215
|
+
*,
|
216
|
+
mode: str = DEFAULT_MODE,
|
217
|
+
encoding: str | None = DEFAULT_ENCODING,
|
218
|
+
errors: str | None = None,
|
219
|
+
newline: str | None = None,
|
220
|
+
buffering: int = DEFAULT_BUFFERING
|
221
|
+
) -> None:
|
222
|
+
"""
|
223
|
+
Initialize FileResourceManager.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
file_path: Path to the file
|
227
|
+
mode: File open mode ('r', 'w', 'a', etc.)
|
228
|
+
encoding: Text encoding (for text mode)
|
229
|
+
errors: Error handling for encoding
|
230
|
+
newline: Newline handling
|
231
|
+
buffering: Buffer size
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
SplurgePathValidationError: If file path is invalid
|
235
|
+
SplurgeResourceAcquisitionError: If file cannot be opened
|
236
|
+
"""
|
237
|
+
self._file_path = PathValidator.validate_path(
|
238
|
+
file_path,
|
239
|
+
must_exist=(mode in ['r', 'rb']),
|
240
|
+
must_be_file=True,
|
241
|
+
must_be_readable=(mode in ['r', 'rb'])
|
242
|
+
)
|
243
|
+
self.mode = mode
|
244
|
+
self.encoding = encoding
|
245
|
+
self.errors = errors
|
246
|
+
self.newline = newline
|
247
|
+
self.buffering = buffering
|
248
|
+
self._file_handle: IO[Any] | None = None
|
249
|
+
|
250
|
+
def __enter__(self) -> IO[Any]:
|
251
|
+
"""
|
252
|
+
Open the file and return the file handle.
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
File handle
|
256
|
+
|
257
|
+
Raises:
|
258
|
+
SplurgeFileNotFoundError: If file is not found
|
259
|
+
SplurgeFilePermissionError: If permission is denied
|
260
|
+
SplurgeFileEncodingError: If encoding error occurs
|
261
|
+
SplurgeResourceAcquisitionError: If other file operation fails
|
262
|
+
"""
|
263
|
+
self._file_handle = _safe_open_file(
|
264
|
+
self.file_path,
|
265
|
+
mode=self.mode,
|
266
|
+
encoding=self.encoding,
|
267
|
+
errors=self.errors,
|
268
|
+
newline=self.newline,
|
269
|
+
buffering=self.buffering
|
270
|
+
)
|
271
|
+
return self._file_handle
|
272
|
+
|
273
|
+
def __exit__(
|
274
|
+
self,
|
275
|
+
exc_type: type | None,
|
276
|
+
exc_val: Exception | None,
|
277
|
+
exc_tb: Any | None
|
278
|
+
) -> None:
|
279
|
+
"""
|
280
|
+
Close the file handle and cleanup resources.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
exc_type: Exception type if an exception occurred
|
284
|
+
exc_val: Exception value if an exception occurred
|
285
|
+
exc_tb: Exception traceback if an exception occurred
|
286
|
+
"""
|
287
|
+
if self._file_handle is not None:
|
288
|
+
try:
|
289
|
+
self._file_handle.close()
|
290
|
+
except OSError as e:
|
291
|
+
raise SplurgeResourceReleaseError(
|
292
|
+
f"Failed to close file: {self.file_path}",
|
293
|
+
details=str(e)
|
294
|
+
)
|
295
|
+
finally:
|
296
|
+
self._file_handle = None
|
297
|
+
|
298
|
+
@property
|
299
|
+
def file_path(self) -> Path | None:
|
300
|
+
"""Get the path of the temporary file."""
|
301
|
+
return self._file_path
|
302
|
+
|
303
|
+
|
304
|
+
class StreamResourceManager:
|
305
|
+
"""
|
306
|
+
Context manager for stream operations.
|
307
|
+
|
308
|
+
This class provides context managers for managing data streams
|
309
|
+
with proper cleanup and error handling.
|
310
|
+
"""
|
311
|
+
|
312
|
+
def __init__(
|
313
|
+
self,
|
314
|
+
stream: Iterator[Any],
|
315
|
+
*,
|
316
|
+
auto_close: bool = True
|
317
|
+
) -> None:
|
318
|
+
"""
|
319
|
+
Initialize StreamResourceManager.
|
320
|
+
|
321
|
+
Args:
|
322
|
+
stream: Iterator to manage
|
323
|
+
auto_close: Whether to automatically close the stream
|
324
|
+
"""
|
325
|
+
self.stream = stream
|
326
|
+
self.auto_close = auto_close
|
327
|
+
self._is_closed = False
|
328
|
+
|
329
|
+
def __enter__(self) -> Iterator[Any]:
|
330
|
+
"""
|
331
|
+
Return the stream.
|
332
|
+
|
333
|
+
Returns:
|
334
|
+
Stream iterator
|
335
|
+
"""
|
336
|
+
return self.stream
|
337
|
+
|
338
|
+
def __exit__(
|
339
|
+
self,
|
340
|
+
exc_type: type | None,
|
341
|
+
exc_val: Exception | None,
|
342
|
+
exc_tb: Any | None
|
343
|
+
) -> None:
|
344
|
+
"""
|
345
|
+
Clean up the stream.
|
346
|
+
|
347
|
+
Args:
|
348
|
+
exc_type: Exception type if an exception occurred
|
349
|
+
exc_val: Exception value if an exception occurred
|
350
|
+
exc_tb: Exception traceback if an exception occurred
|
351
|
+
"""
|
352
|
+
if self.auto_close and hasattr(self.stream, 'close'):
|
353
|
+
try:
|
354
|
+
self.stream.close()
|
355
|
+
except Exception as e:
|
356
|
+
raise SplurgeResourceReleaseError(
|
357
|
+
"Failed to close stream",
|
358
|
+
details=str(e)
|
359
|
+
)
|
360
|
+
|
361
|
+
# Mark as closed after context manager exits, regardless of close method
|
362
|
+
self._is_closed = True
|
363
|
+
|
364
|
+
@property
|
365
|
+
def is_closed(self) -> bool:
|
366
|
+
"""Check if the stream is closed."""
|
367
|
+
return self._is_closed
|
368
|
+
|
369
|
+
|
370
|
+
@contextmanager
|
371
|
+
def safe_file_operation(
|
372
|
+
file_path: str | Path,
|
373
|
+
*,
|
374
|
+
mode: str = DEFAULT_MODE,
|
375
|
+
encoding: str | None = DEFAULT_ENCODING,
|
376
|
+
errors: str | None = None,
|
377
|
+
newline: str | None = None,
|
378
|
+
buffering: int = DEFAULT_BUFFERING
|
379
|
+
) -> Iterator[IO[Any]]:
|
380
|
+
"""
|
381
|
+
Context manager for safe file operations.
|
382
|
+
|
383
|
+
Args:
|
384
|
+
file_path: Path to the file
|
385
|
+
mode: File open mode
|
386
|
+
encoding: Text encoding (for text mode)
|
387
|
+
errors: Error handling for encoding
|
388
|
+
newline: Newline handling
|
389
|
+
buffering: Buffer size
|
390
|
+
|
391
|
+
Yields:
|
392
|
+
File handle
|
393
|
+
|
394
|
+
Raises:
|
395
|
+
SplurgePathValidationError: If file path is invalid
|
396
|
+
SplurgeResourceAcquisitionError: If file cannot be opened
|
397
|
+
SplurgeResourceReleaseError: If file cannot be closed
|
398
|
+
"""
|
399
|
+
manager = FileResourceManager(
|
400
|
+
file_path,
|
401
|
+
mode=mode,
|
402
|
+
encoding=encoding,
|
403
|
+
errors=errors,
|
404
|
+
newline=newline,
|
405
|
+
buffering=buffering
|
406
|
+
)
|
407
|
+
with manager as file_handle:
|
408
|
+
yield file_handle
|
409
|
+
|
410
|
+
|
411
|
+
@contextmanager
|
412
|
+
def safe_stream_operation(
|
413
|
+
stream: Iterator[Any],
|
414
|
+
*,
|
415
|
+
auto_close: bool = True
|
416
|
+
) -> Iterator[Iterator[Any]]:
|
417
|
+
"""
|
418
|
+
Context manager for safe stream operations.
|
419
|
+
|
420
|
+
Args:
|
421
|
+
stream: Iterator to manage
|
422
|
+
auto_close: Whether to automatically close the stream
|
423
|
+
|
424
|
+
Yields:
|
425
|
+
Stream iterator
|
426
|
+
|
427
|
+
Raises:
|
428
|
+
SplurgeResourceReleaseError: If stream cannot be closed
|
429
|
+
"""
|
430
|
+
manager = StreamResourceManager(stream, auto_close=auto_close)
|
431
|
+
with manager as stream_handle:
|
432
|
+
yield stream_handle
|
@@ -0,0 +1,136 @@
|
|
1
|
+
"""
|
2
|
+
string_tokenizer.py
|
3
|
+
|
4
|
+
A utility module for string tokenization operations.
|
5
|
+
Provides methods to split strings into tokens and manipulate string boundaries.
|
6
|
+
|
7
|
+
Copyright (c) 2025 Jim Schilling
|
8
|
+
|
9
|
+
Please preserve this header and all related material when sharing!
|
10
|
+
|
11
|
+
This module is licensed under the MIT License.
|
12
|
+
"""
|
13
|
+
|
14
|
+
from splurge_dsv.exceptions import SplurgeParameterError
|
15
|
+
|
16
|
+
|
17
|
+
class StringTokenizer:
|
18
|
+
"""
|
19
|
+
Utility class for string tokenization operations.
|
20
|
+
|
21
|
+
This class provides methods to:
|
22
|
+
- Split strings into tokens based on delimiters
|
23
|
+
- Process multiple strings into token lists
|
24
|
+
- Remove matching characters from string boundaries
|
25
|
+
"""
|
26
|
+
|
27
|
+
DEFAULT_STRIP = True
|
28
|
+
|
29
|
+
@staticmethod
|
30
|
+
def parse(
|
31
|
+
content: str | None,
|
32
|
+
*,
|
33
|
+
delimiter: str,
|
34
|
+
strip: bool = DEFAULT_STRIP
|
35
|
+
) -> list[str]:
|
36
|
+
"""
|
37
|
+
Split a string into tokens based on a delimiter.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
content (str | None): The input string to tokenize
|
41
|
+
delimiter (str): The character(s) to split the string on
|
42
|
+
strip (bool, optional): Whether to strip whitespace from tokens. Defaults to True.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
list[str]: List of tokens, preserving empty tokens
|
46
|
+
|
47
|
+
Raises:
|
48
|
+
SplurgeParameterError: If delimiter is empty or None
|
49
|
+
|
50
|
+
Example:
|
51
|
+
>>> StringTokenizer.parse("a,b,c", delimiter=",")
|
52
|
+
['a', 'b', 'c']
|
53
|
+
>>> StringTokenizer.parse("a,,c", delimiter=",")
|
54
|
+
['a', '', 'c']
|
55
|
+
"""
|
56
|
+
if content is None:
|
57
|
+
return []
|
58
|
+
|
59
|
+
if delimiter is None or delimiter == "":
|
60
|
+
raise SplurgeParameterError("delimiter cannot be empty or None")
|
61
|
+
|
62
|
+
if strip and not content.strip():
|
63
|
+
return []
|
64
|
+
|
65
|
+
result: list[str] = content.split(delimiter)
|
66
|
+
if strip:
|
67
|
+
result = [token.strip() for token in result]
|
68
|
+
return result
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def parses(
|
72
|
+
cls,
|
73
|
+
content: list[str],
|
74
|
+
*,
|
75
|
+
delimiter: str,
|
76
|
+
strip: bool = DEFAULT_STRIP
|
77
|
+
) -> list[list[str]]:
|
78
|
+
"""
|
79
|
+
Process multiple strings into lists of tokens.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
content (list[str]): List of strings to tokenize
|
83
|
+
delimiter (str): The character(s) to split each string on
|
84
|
+
strip (bool, optional): Whether to strip whitespace from tokens. Defaults to True.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
list[list[str]]: List of token lists, one for each input string
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
SplurgeParameterError: If delimiter is empty or None
|
91
|
+
|
92
|
+
Example:
|
93
|
+
>>> StringTokenizer.parses(["a,b", "c,d"], delimiter=",")
|
94
|
+
[['a', 'b'], ['c', 'd']]
|
95
|
+
"""
|
96
|
+
if delimiter is None or delimiter == "":
|
97
|
+
raise SplurgeParameterError("delimiter cannot be empty or None")
|
98
|
+
|
99
|
+
return [cls.parse(text, delimiter=delimiter, strip=strip) for text in content]
|
100
|
+
|
101
|
+
@staticmethod
|
102
|
+
def remove_bookends(
|
103
|
+
content: str,
|
104
|
+
*,
|
105
|
+
bookend: str,
|
106
|
+
strip: bool = DEFAULT_STRIP
|
107
|
+
) -> str:
|
108
|
+
"""
|
109
|
+
Remove matching characters from both ends of a string.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
content (str): The input string to process
|
113
|
+
bookend (str): The character(s) to remove from both ends
|
114
|
+
strip (bool, optional): Whether to strip whitespace first. Defaults to True.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
str: The string with matching bookends removed
|
118
|
+
|
119
|
+
Raises:
|
120
|
+
SplurgeParameterError: If bookend is empty or None
|
121
|
+
|
122
|
+
Example:
|
123
|
+
>>> StringTokenizer.remove_bookends("'hello'", bookend="'")
|
124
|
+
'hello'
|
125
|
+
"""
|
126
|
+
if bookend is None or bookend == "":
|
127
|
+
raise SplurgeParameterError("bookend cannot be empty or None")
|
128
|
+
|
129
|
+
value: str = content.strip() if strip else content
|
130
|
+
if (
|
131
|
+
value.startswith(bookend)
|
132
|
+
and value.endswith(bookend)
|
133
|
+
and len(value) > 2 * len(bookend) - 1
|
134
|
+
):
|
135
|
+
return value[len(bookend) : -len(bookend)]
|
136
|
+
return value
|