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