TSVZ 3.36__tar.gz → 3.38__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.
- {tsvz-3.36 → tsvz-3.38}/PKG-INFO +6 -1
- {tsvz-3.36 → tsvz-3.38}/TSVZ.egg-info/PKG-INFO +6 -1
- {tsvz-3.36 → tsvz-3.38}/TSVZ.egg-info/SOURCES.txt +3 -1
- tsvz-3.38/TSVZ.egg-info/requires.txt +4 -0
- {tsvz-3.36 → tsvz-3.38}/TSVZ.py +132 -39
- {tsvz-3.36 → tsvz-3.38}/setup.py +8 -0
- tsvz-3.38/tests/test_regression.py +104 -0
- {tsvz-3.36 → tsvz-3.38}/README.md +0 -0
- {tsvz-3.36 → tsvz-3.38}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-3.36 → tsvz-3.38}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-3.36 → tsvz-3.38}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-3.36 → tsvz-3.38}/setup.cfg +0 -0
{tsvz-3.36 → tsvz-3.38}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.38
|
|
4
4
|
Summary: An simple in memory wrapper around a TSV file to function as a database
|
|
5
5
|
Home-page: https://github.com/yufei-pan/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -11,6 +11,9 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Requires-Python: >=3.6
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: argparse
|
|
15
|
+
Provides-Extra: completion
|
|
16
|
+
Requires-Dist: argcomplete; extra == "completion"
|
|
14
17
|
Dynamic: author
|
|
15
18
|
Dynamic: author-email
|
|
16
19
|
Dynamic: classifier
|
|
@@ -18,6 +21,8 @@ Dynamic: description
|
|
|
18
21
|
Dynamic: description-content-type
|
|
19
22
|
Dynamic: home-page
|
|
20
23
|
Dynamic: license
|
|
24
|
+
Dynamic: provides-extra
|
|
25
|
+
Dynamic: requires-dist
|
|
21
26
|
Dynamic: requires-python
|
|
22
27
|
Dynamic: summary
|
|
23
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.38
|
|
4
4
|
Summary: An simple in memory wrapper around a TSV file to function as a database
|
|
5
5
|
Home-page: https://github.com/yufei-pan/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -11,6 +11,9 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Requires-Python: >=3.6
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: argparse
|
|
15
|
+
Provides-Extra: completion
|
|
16
|
+
Requires-Dist: argcomplete; extra == "completion"
|
|
14
17
|
Dynamic: author
|
|
15
18
|
Dynamic: author-email
|
|
16
19
|
Dynamic: classifier
|
|
@@ -18,6 +21,8 @@ Dynamic: description
|
|
|
18
21
|
Dynamic: description-content-type
|
|
19
22
|
Dynamic: home-page
|
|
20
23
|
Dynamic: license
|
|
24
|
+
Dynamic: provides-extra
|
|
25
|
+
Dynamic: requires-dist
|
|
21
26
|
Dynamic: requires-python
|
|
22
27
|
Dynamic: summary
|
|
23
28
|
|
{tsvz-3.36 → tsvz-3.38}/TSVZ.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
2
3
|
# /// script
|
|
3
4
|
# requires-python = ">=3.6"
|
|
4
5
|
# dependencies = [
|
|
@@ -9,7 +10,6 @@ import functools
|
|
|
9
10
|
import io
|
|
10
11
|
import os
|
|
11
12
|
import re
|
|
12
|
-
from tabnanny import verbose
|
|
13
13
|
import threading
|
|
14
14
|
import time
|
|
15
15
|
import sys
|
|
@@ -26,23 +26,42 @@ if os.name == 'nt':
|
|
|
26
26
|
elif os.name == 'posix':
|
|
27
27
|
import fcntl
|
|
28
28
|
|
|
29
|
-
version = '3.
|
|
29
|
+
version = '3.38'
|
|
30
30
|
__version__ = version
|
|
31
31
|
author = 'pan@zopyr.us'
|
|
32
|
-
COMMIT_DATE = '2026-
|
|
32
|
+
COMMIT_DATE = '2026-06-04'
|
|
33
33
|
|
|
34
34
|
DEFAULT_DELIMITER = '\t'
|
|
35
35
|
DEFAULTS_INDICATOR_KEY = '#_defaults_#'
|
|
36
36
|
|
|
37
|
-
COMPRESSED_FILE_EXTENSIONS = ['gz','gzip','bz2','bzip2','xz','lzma']
|
|
37
|
+
COMPRESSED_FILE_EXTENSIONS = ['gz','gzip','bz2','bzip2','xz','lzma','zst','zstd']
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def _isCompressedFile(fileName):
|
|
40
|
+
return fileName.rpartition('.')[2].lower() in COMPRESSED_FILE_EXTENSIONS
|
|
41
|
+
|
|
42
|
+
def get_delimiter(delimiter = ...,file_name = ''):
|
|
43
|
+
"""
|
|
44
|
+
Resolve the delimiter to use for an operation.
|
|
45
|
+
|
|
46
|
+
When the delimiter is not provided (the ``...`` sentinel or a falsy value),
|
|
47
|
+
it is determined automatically: inferred from the file name when one is
|
|
48
|
+
available, otherwise falling back to the last-determined default. Explicit
|
|
49
|
+
delimiters (including the aliases 'comma', 'tab', 'pipe', 'null' and escape
|
|
50
|
+
sequences) are normalized. The resolved value is remembered as the new
|
|
51
|
+
module-level default.
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
- delimiter: The delimiter or alias, or ``...``/``None`` to auto-determine.
|
|
55
|
+
- file_name (str, optional): The file name to infer the delimiter from.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: The resolved delimiter.
|
|
59
|
+
"""
|
|
40
60
|
global DEFAULT_DELIMITER
|
|
41
|
-
if
|
|
42
|
-
return DEFAULT_DELIMITER
|
|
43
|
-
elif delimiter == ...:
|
|
61
|
+
if delimiter is ...:
|
|
44
62
|
if not file_name:
|
|
45
|
-
|
|
63
|
+
# Nothing to infer from; reuse the last-determined default.
|
|
64
|
+
return DEFAULT_DELIMITER
|
|
46
65
|
elif file_name.endswith('.csv'):
|
|
47
66
|
rtn = ','
|
|
48
67
|
elif file_name.endswith('.nsv'):
|
|
@@ -51,6 +70,8 @@ def get_delimiter(delimiter,file_name = ''):
|
|
|
51
70
|
rtn = '|'
|
|
52
71
|
else:
|
|
53
72
|
rtn = '\t'
|
|
73
|
+
elif not delimiter:
|
|
74
|
+
return DEFAULT_DELIMITER
|
|
54
75
|
elif delimiter == 'comma':
|
|
55
76
|
rtn = ','
|
|
56
77
|
elif delimiter == 'tab':
|
|
@@ -75,34 +96,43 @@ def eprint(*args, **kwargs):
|
|
|
75
96
|
print(*args, **kwargs)
|
|
76
97
|
|
|
77
98
|
def openFileAsCompressed(fileName,mode = 'rb',encoding = 'utf8',teeLogger = None,compressLevel = 1):
|
|
99
|
+
lowerFileName = fileName.lower()
|
|
78
100
|
if 'b' not in mode:
|
|
79
101
|
mode += 't'
|
|
80
102
|
kwargs = {}
|
|
81
103
|
if 'r' not in mode:
|
|
82
|
-
if
|
|
104
|
+
if lowerFileName.endswith('.xz'):
|
|
83
105
|
kwargs['preset'] = compressLevel
|
|
106
|
+
elif lowerFileName.endswith('.zst') or lowerFileName.endswith('.zstd'):
|
|
107
|
+
kwargs['level'] = compressLevel
|
|
84
108
|
else:
|
|
85
109
|
kwargs['compresslevel'] = compressLevel
|
|
86
110
|
if 'b' not in mode:
|
|
87
111
|
kwargs['encoding'] = encoding
|
|
88
|
-
if
|
|
112
|
+
if lowerFileName.endswith('.xz') or lowerFileName.endswith('.lzma'):
|
|
89
113
|
try:
|
|
90
114
|
import lzma
|
|
91
115
|
return lzma.open(fileName, mode, **kwargs)
|
|
92
116
|
except Exception:
|
|
93
117
|
__teePrintOrNot(f"Failed to open {fileName} with lzma, trying bin",teeLogger=teeLogger)
|
|
94
|
-
elif
|
|
118
|
+
elif lowerFileName.endswith('.gz') or lowerFileName.endswith('.gzip'):
|
|
95
119
|
try:
|
|
96
120
|
import gzip
|
|
97
121
|
return gzip.open(fileName, mode, **kwargs)
|
|
98
122
|
except Exception:
|
|
99
123
|
__teePrintOrNot(f"Failed to open {fileName} with gzip, trying bin",teeLogger=teeLogger)
|
|
100
|
-
elif
|
|
124
|
+
elif lowerFileName.endswith('.bz2') or lowerFileName.endswith('.bzip2'):
|
|
101
125
|
try:
|
|
102
126
|
import bz2
|
|
103
127
|
return bz2.open(fileName, mode, **kwargs)
|
|
104
128
|
except Exception:
|
|
105
129
|
__teePrintOrNot(f"Failed to open {fileName} with bz2, trying bin",teeLogger=teeLogger)
|
|
130
|
+
elif lowerFileName.endswith('.zst') or lowerFileName.endswith('.zstd'):
|
|
131
|
+
try:
|
|
132
|
+
from compression import zstd
|
|
133
|
+
return zstd.open(fileName, mode, **kwargs)
|
|
134
|
+
except Exception:
|
|
135
|
+
__teePrintOrNot(f"Failed to open {fileName} with zstd, trying bin",teeLogger=teeLogger)
|
|
106
136
|
if 't' in mode:
|
|
107
137
|
mode = mode.replace('t','')
|
|
108
138
|
return open(fileName, mode, encoding=encoding)
|
|
@@ -328,7 +358,7 @@ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False,str_forma
|
|
|
328
358
|
power = 2**10
|
|
329
359
|
n = 0
|
|
330
360
|
power_labels = {0 : '', 1: 'Ki', 2: 'Mi', 3: 'Gi', 4: 'Ti', 5: 'Pi'}
|
|
331
|
-
while size
|
|
361
|
+
while size >= power:
|
|
332
362
|
size /= power
|
|
333
363
|
n += 1
|
|
334
364
|
return f"{size:{str_format}}{' '}{power_labels[n]}"
|
|
@@ -336,7 +366,7 @@ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False,str_forma
|
|
|
336
366
|
power = 10**3
|
|
337
367
|
n = 0
|
|
338
368
|
power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T', 5: 'P'}
|
|
339
|
-
while size
|
|
369
|
+
while size >= power:
|
|
340
370
|
size /= power
|
|
341
371
|
n += 1
|
|
342
372
|
return f"{size:{str_format}}{' '}{power_labels[n]}"
|
|
@@ -403,7 +433,7 @@ def __teePrintOrNot(message,level = 'info',teeLogger = None):
|
|
|
403
433
|
except Exception:
|
|
404
434
|
print(message,flush=True)
|
|
405
435
|
|
|
406
|
-
def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter =
|
|
436
|
+
def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,defaults = ...,
|
|
407
437
|
storeOffset = False, offset = -1):
|
|
408
438
|
"""
|
|
409
439
|
Process a line of text and update the task dictionary.
|
|
@@ -423,6 +453,7 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT
|
|
|
423
453
|
"""
|
|
424
454
|
if defaults is ...:
|
|
425
455
|
defaults = []
|
|
456
|
+
delimiter = get_delimiter(delimiter)
|
|
426
457
|
line = line.strip('\x00').rstrip('\r\n')
|
|
427
458
|
if not line or (line.startswith('#') and not line.startswith(DEFAULTS_INDICATOR_KEY)):
|
|
428
459
|
# if verbose:
|
|
@@ -446,7 +477,7 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT
|
|
|
446
477
|
# if verbose:
|
|
447
478
|
# __teePrintOrNot(f"Empty defaults line found: {line}",teeLogger=teeLogger)
|
|
448
479
|
defaults.clear()
|
|
449
|
-
defaults[
|
|
480
|
+
defaults[:] = [DEFAULTS_INDICATOR_KEY]
|
|
450
481
|
else:
|
|
451
482
|
# if verbose:
|
|
452
483
|
# __teePrintOrNot(f"Key {lineCache[0]} found with empty value, deleting such key's representaion",teeLogger=teeLogger)
|
|
@@ -481,6 +512,44 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT
|
|
|
481
512
|
# __teePrintOrNot(f"Key {lineCache[0]} added",teeLogger=teeLogger)
|
|
482
513
|
return correctColumnNum, lineCache
|
|
483
514
|
|
|
515
|
+
def _read_last_valid_line_forward(fileName, taskDic, correctColumnNum, verbose=False, teeLogger=None,
|
|
516
|
+
strict=False, encoding='utf8', delimiter=...,
|
|
517
|
+
defaults=None, storeOffset=False):
|
|
518
|
+
"""
|
|
519
|
+
Forward, single-pass variant of read_last_valid_line for compressed files.
|
|
520
|
+
|
|
521
|
+
Backward chunked seeking forces a full re-decompress on every seek (and is not
|
|
522
|
+
reliably supported for some codecs e.g. zstd), so for compressed files we decompress
|
|
523
|
+
once, streaming forward, and keep only the last valid line / offset. A scratch dict is
|
|
524
|
+
used so memory stays flat instead of accumulating the whole file into taskDic.
|
|
525
|
+
"""
|
|
526
|
+
if defaults is None:
|
|
527
|
+
defaults = []
|
|
528
|
+
delimiter = get_delimiter(delimiter, file_name=fileName)
|
|
529
|
+
last_valid = -1 if storeOffset else []
|
|
530
|
+
scratch = {}
|
|
531
|
+
with openFileAsCompressed(fileName, 'rb', encoding=encoding, teeLogger=teeLogger) as file:
|
|
532
|
+
while True:
|
|
533
|
+
offset = file.tell()
|
|
534
|
+
line = file.readline()
|
|
535
|
+
if not line:
|
|
536
|
+
break
|
|
537
|
+
if not line.strip():
|
|
538
|
+
continue
|
|
539
|
+
correctColumnNum, lineCache = _processLine(
|
|
540
|
+
line=line.decode(encoding=encoding, errors='replace'),
|
|
541
|
+
taskDic=scratch,
|
|
542
|
+
correctColumnNum=correctColumnNum,
|
|
543
|
+
strict=strict,
|
|
544
|
+
delimiter=delimiter,
|
|
545
|
+
defaults=defaults,
|
|
546
|
+
storeOffset=storeOffset,
|
|
547
|
+
offset=offset,
|
|
548
|
+
)
|
|
549
|
+
if lineCache:
|
|
550
|
+
last_valid = offset if storeOffset else lineCache
|
|
551
|
+
return last_valid
|
|
552
|
+
|
|
484
553
|
def read_last_valid_line(fileName, taskDic, correctColumnNum, verbose=False, teeLogger=None, strict=False,
|
|
485
554
|
encoding = 'utf8',delimiter = ...,defaults = ...,storeOffset = False ):
|
|
486
555
|
"""
|
|
@@ -508,6 +577,12 @@ def read_last_valid_line(fileName, taskDic, correctColumnNum, verbose=False, tee
|
|
|
508
577
|
delimiter = get_delimiter(delimiter,file_name=fileName)
|
|
509
578
|
if verbose:
|
|
510
579
|
__teePrintOrNot(f"Reading last line only from {fileName}",teeLogger=teeLogger)
|
|
580
|
+
if _isCompressedFile(fileName):
|
|
581
|
+
# Backward seeking through a codec is slow/unsupported; decompress once, forward.
|
|
582
|
+
return _read_last_valid_line_forward(
|
|
583
|
+
fileName, taskDic, correctColumnNum, verbose=verbose, teeLogger=teeLogger,
|
|
584
|
+
strict=strict, encoding=encoding, delimiter=delimiter, defaults=defaults,
|
|
585
|
+
storeOffset=storeOffset)
|
|
511
586
|
with openFileAsCompressed(fileName, 'rb',encoding=encoding, teeLogger=teeLogger) as file:
|
|
512
587
|
file.seek(0, os.SEEK_END)
|
|
513
588
|
file_size = file.tell()
|
|
@@ -558,7 +633,8 @@ def read_last_valid_line(fileName, taskDic, correctColumnNum, verbose=False, tee
|
|
|
558
633
|
return last_valid_line
|
|
559
634
|
|
|
560
635
|
@functools.lru_cache(maxsize=None)
|
|
561
|
-
def _get_sanitization_re(delimiter =
|
|
636
|
+
def _get_sanitization_re(delimiter = ...):
|
|
637
|
+
delimiter = get_delimiter(delimiter)
|
|
562
638
|
return re.compile(r"(</sep/>|</LF/>|<sep>|<LF>|\n|" + re.escape(delimiter) + r")")
|
|
563
639
|
|
|
564
640
|
_sanitize_replacements = {
|
|
@@ -568,9 +644,10 @@ _sanitize_replacements = {
|
|
|
568
644
|
}
|
|
569
645
|
_inverse_sanitize_replacements = {v: k for k, v in _sanitize_replacements.items()}
|
|
570
646
|
|
|
571
|
-
def _sanitize(data,delimiter =
|
|
647
|
+
def _sanitize(data,delimiter = ...):
|
|
572
648
|
if not data:
|
|
573
649
|
return data
|
|
650
|
+
delimiter = get_delimiter(delimiter)
|
|
574
651
|
def repl(m):
|
|
575
652
|
tok = m.group(0)
|
|
576
653
|
if tok == delimiter:
|
|
@@ -584,9 +661,10 @@ def _sanitize(data,delimiter = DEFAULT_DELIMITER):
|
|
|
584
661
|
else:
|
|
585
662
|
return [pattern.sub(repl,str(segment)) if segment else '' for segment in data]
|
|
586
663
|
|
|
587
|
-
def _unsanitize(data,delimiter =
|
|
664
|
+
def _unsanitize(data,delimiter = ...):
|
|
588
665
|
if not data:
|
|
589
666
|
return data
|
|
667
|
+
delimiter = get_delimiter(delimiter)
|
|
590
668
|
def repl(m):
|
|
591
669
|
tok = m.group(0)
|
|
592
670
|
if tok == "<sep>":
|
|
@@ -598,7 +676,7 @@ def _unsanitize(data,delimiter = DEFAULT_DELIMITER):
|
|
|
598
676
|
else:
|
|
599
677
|
return [pattern.sub(repl,str(segment).rstrip()) if segment else '' for segment in data]
|
|
600
678
|
|
|
601
|
-
def _formatHeader(header,verbose = False,teeLogger = None,delimiter =
|
|
679
|
+
def _formatHeader(header,verbose = False,teeLogger = None,delimiter = ...):
|
|
602
680
|
"""
|
|
603
681
|
Format the header string.
|
|
604
682
|
|
|
@@ -611,7 +689,7 @@ def _formatHeader(header,verbose = False,teeLogger = None,delimiter = DEFAULT_DE
|
|
|
611
689
|
list: The formatted header list of string.
|
|
612
690
|
"""
|
|
613
691
|
if isinstance(header,str):
|
|
614
|
-
header = header.split(delimiter)
|
|
692
|
+
header = header.split(get_delimiter(delimiter))
|
|
615
693
|
else:
|
|
616
694
|
try:
|
|
617
695
|
header = [str(s) for s in header]
|
|
@@ -621,7 +699,7 @@ def _formatHeader(header,verbose = False,teeLogger = None,delimiter = DEFAULT_DE
|
|
|
621
699
|
header = []
|
|
622
700
|
return [s.rstrip() for s in header]
|
|
623
701
|
|
|
624
|
-
def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = False,delimiter =
|
|
702
|
+
def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = False,delimiter = ...):
|
|
625
703
|
"""
|
|
626
704
|
Verify if a line contains the header.
|
|
627
705
|
|
|
@@ -635,6 +713,7 @@ def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = Fal
|
|
|
635
713
|
Returns:
|
|
636
714
|
bool: True if the header matches the line, False otherwise.
|
|
637
715
|
"""
|
|
716
|
+
delimiter = get_delimiter(delimiter)
|
|
638
717
|
line = _formatHeader(line,verbose=verbose,teeLogger=teeLogger,delimiter=delimiter)
|
|
639
718
|
if verbose:
|
|
640
719
|
__teePrintOrNot(f"Header: \n{header}",teeLogger=teeLogger)
|
|
@@ -646,7 +725,7 @@ def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = Fal
|
|
|
646
725
|
return False
|
|
647
726
|
return True
|
|
648
727
|
|
|
649
|
-
def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,header = [],encoding = 'utf8',strict = True,delimiter =
|
|
728
|
+
def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,header = [],encoding = 'utf8',strict = True,delimiter = ...):
|
|
650
729
|
"""
|
|
651
730
|
Verify the existence of the tabular file.
|
|
652
731
|
|
|
@@ -661,6 +740,7 @@ def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,heade
|
|
|
661
740
|
Returns:
|
|
662
741
|
bool: True if the file exists, False otherwise.
|
|
663
742
|
"""
|
|
743
|
+
delimiter = get_delimiter(delimiter, file_name=fileName)
|
|
664
744
|
remainingFileName, _ ,extenstionName = fileName.rpartition('.')
|
|
665
745
|
if extenstionName in COMPRESSED_FILE_EXTENSIONS:
|
|
666
746
|
remainingFileName, _ ,extenstionName = remainingFileName.rpartition('.')
|
|
@@ -677,7 +757,6 @@ def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,heade
|
|
|
677
757
|
try:
|
|
678
758
|
with openFileAsCompressed(fileName, mode ='wb',encoding=encoding,teeLogger=teeLogger)as file:
|
|
679
759
|
header = delimiter.join(_sanitize(_formatHeader(header,
|
|
680
|
-
verbose=verbose,
|
|
681
760
|
teeLogger=teeLogger,
|
|
682
761
|
delimiter=delimiter,
|
|
683
762
|
),delimiter=delimiter))
|
|
@@ -770,7 +849,7 @@ def readTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fal
|
|
|
770
849
|
with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger)as file:
|
|
771
850
|
if any(header) and verifyHeader:
|
|
772
851
|
line = file.readline().decode(encoding=encoding,errors='replace')
|
|
773
|
-
if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict) and correctColumnNum == -1:
|
|
852
|
+
if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter) and correctColumnNum == -1:
|
|
774
853
|
correctColumnNum = len(header)
|
|
775
854
|
if verbose:
|
|
776
855
|
__teePrintOrNot(f"correctColumnNum: {correctColumnNum}",teeLogger=teeLogger)
|
|
@@ -872,7 +951,7 @@ def appendLinesTabularFile(fileName,linesToAppend,teeLogger = None,header = '',c
|
|
|
872
951
|
if any(header) and verifyHeader:
|
|
873
952
|
with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger)as file:
|
|
874
953
|
line = file.readline().decode(encoding=encoding,errors='replace')
|
|
875
|
-
if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict):
|
|
954
|
+
if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
|
|
876
955
|
correctColumnNum = len(header)
|
|
877
956
|
if verbose:
|
|
878
957
|
__teePrintOrNot(f"correctColumnNum: {correctColumnNum}",teeLogger=teeLogger)
|
|
@@ -926,7 +1005,7 @@ def clearTabularFile(fileName,teeLogger = None,header = '',verifyHeader = False,
|
|
|
926
1005
|
with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger)as file:
|
|
927
1006
|
if any(header) and verifyHeader:
|
|
928
1007
|
line = file.readline().decode(encoding=encoding,errors='replace')
|
|
929
|
-
if not _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict):
|
|
1008
|
+
if not _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
|
|
930
1009
|
__teePrintOrNot(f'Warning: Header mismatch in {fileName}. Keeping original header in file...','warning',teeLogger)
|
|
931
1010
|
header = _formatHeader(line,verbose = verbose,teeLogger = teeLogger,delimiter=delimiter)
|
|
932
1011
|
with openFileAsCompressed(fileName, mode ='wb',encoding=encoding,teeLogger=teeLogger)as file:
|
|
@@ -1019,10 +1098,10 @@ def scrubTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fa
|
|
|
1019
1098
|
appendLinesTabularFile(fileName,file,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,verifyHeader = verifyHeader,verbose = verbose,encoding = encoding,strict = strict,delimiter = delimiter)
|
|
1020
1099
|
return file
|
|
1021
1100
|
|
|
1022
|
-
def getListView(tsvzDic,header = [],delimiter =
|
|
1101
|
+
def getListView(tsvzDic,header = [],delimiter = ...):
|
|
1023
1102
|
if header:
|
|
1024
1103
|
if isinstance(header,str):
|
|
1025
|
-
header = header.split(delimiter)
|
|
1104
|
+
header = header.split(get_delimiter(delimiter))
|
|
1026
1105
|
elif not isinstance(header,list):
|
|
1027
1106
|
try:
|
|
1028
1107
|
header = list(header)
|
|
@@ -1066,7 +1145,7 @@ class TSVZed(OrderedDict):
|
|
|
1066
1145
|
rewrite_on_exit : bool, default=False
|
|
1067
1146
|
If True, rewrites the entire file when closing/exiting.
|
|
1068
1147
|
rewrite_interval : float, default=0
|
|
1069
|
-
Minimum time interval (in seconds) between full file rewrites. 0 means
|
|
1148
|
+
Minimum time interval (in seconds) between full file rewrites. 0 means do not rewrite.
|
|
1070
1149
|
append_check_delay : float, default=0.01
|
|
1071
1150
|
Time delay (in seconds) between checks of the append queue by the worker thread.
|
|
1072
1151
|
monitor_external_changes : bool, default=True
|
|
@@ -1595,7 +1674,7 @@ memoryOnly:{self.memoryOnly}
|
|
|
1595
1674
|
if self.header:
|
|
1596
1675
|
line = file.readline().decode(self.encoding,errors='replace')
|
|
1597
1676
|
aftPos = file.tell()
|
|
1598
|
-
if not _lineContainHeader(self.header,line,verbose = self.verbose,teeLogger = self.teeLogger,strict = self.strict):
|
|
1677
|
+
if not _lineContainHeader(self.header,line,verbose = self.verbose,teeLogger = self.teeLogger,strict = self.strict,delimiter = self.delimiter):
|
|
1599
1678
|
header = self.delimiter.join(_sanitize(self.header,delimiter=self.delimiter))
|
|
1600
1679
|
file.seek(0)
|
|
1601
1680
|
file.write(f'{header}\n'.encode(encoding=self.encoding,errors='replace'))
|
|
@@ -1726,6 +1805,7 @@ memoryOnly:{self.memoryOnly}
|
|
|
1726
1805
|
|
|
1727
1806
|
def get_file_obj(self,modes = 'ab'):
|
|
1728
1807
|
self.writeLock.acquire()
|
|
1808
|
+
file = None
|
|
1729
1809
|
try:
|
|
1730
1810
|
if not self.encoding:
|
|
1731
1811
|
self.encoding = 'utf8'
|
|
@@ -1752,6 +1832,8 @@ memoryOnly:{self.memoryOnly}
|
|
|
1752
1832
|
# if write lock is already released, return
|
|
1753
1833
|
if not self.writeLock.locked():
|
|
1754
1834
|
return
|
|
1835
|
+
if file is None:
|
|
1836
|
+
return
|
|
1755
1837
|
try:
|
|
1756
1838
|
file.flush() # Ensure the file is flushed before unlocking
|
|
1757
1839
|
os.fsync(file.fileno()) # Ensure the file is synced to disk before unlocking
|
|
@@ -2024,13 +2106,18 @@ class TSVZedLite(MutableMapping):
|
|
|
2024
2106
|
)
|
|
2025
2107
|
if self.verbose:
|
|
2026
2108
|
eprint(f"Read at position {pos}: {segments}")
|
|
2027
|
-
if key is not
|
|
2109
|
+
if key is not ...:
|
|
2110
|
+
if not segments:
|
|
2111
|
+
eprint(f"Error: No segments found at position {pos}")
|
|
2112
|
+
elif segments[0] != key:
|
|
2028
2113
|
eprint(f"Warning: Key mismatch at position {pos}: expected {key}, got {segments[0]}")
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2114
|
+
else:
|
|
2115
|
+
return segments
|
|
2116
|
+
if self.strict:
|
|
2117
|
+
eprint("Error: Key mismatch and strict mode enabled. Raising KeyError.")
|
|
2118
|
+
raise KeyError(key)
|
|
2119
|
+
else :
|
|
2120
|
+
eprint("Continuing despite key mismatch due to non-strict mode. Expect errors!")
|
|
2034
2121
|
return segments
|
|
2035
2122
|
|
|
2036
2123
|
# Implement basic __getitem__, __setitem__, __delitem__, __iter__, and __len__. needed for MutableMapping
|
|
@@ -2083,6 +2170,7 @@ class TSVZedLite(MutableMapping):
|
|
|
2083
2170
|
self.defaults = value
|
|
2084
2171
|
if self.verbose:
|
|
2085
2172
|
eprint(f"Defaults set to {value}")
|
|
2173
|
+
return
|
|
2086
2174
|
elif key.startswith('#'):
|
|
2087
2175
|
if self.verbose:
|
|
2088
2176
|
eprint(f"Key {key} updated in memory (data in index) as it starts with #")
|
|
@@ -2261,6 +2349,11 @@ def __main__():
|
|
|
2261
2349
|
strictMode.add_argument('-f', '--force', dest = 'strict',action='store_false', help='Force the operation. Ignore checks for column numbers / headers')
|
|
2262
2350
|
parser.add_argument('-v', '--verbose', action='store_true', help='Print verbose output')
|
|
2263
2351
|
parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} by {author}')
|
|
2352
|
+
try:
|
|
2353
|
+
import argcomplete
|
|
2354
|
+
argcomplete.autocomplete(parser,always_complete_options='long')
|
|
2355
|
+
except ImportError:
|
|
2356
|
+
pass
|
|
2264
2357
|
args = parser.parse_args()
|
|
2265
2358
|
args.delimiter = get_delimiter(delimiter=args.delimiter,file_name=args.filename)
|
|
2266
2359
|
if args.header and args.header.endswith('\\'):
|
{tsvz-3.36 → tsvz-3.38}/setup.py
RENAMED
|
@@ -15,6 +15,14 @@ setup(
|
|
|
15
15
|
'tsvz = TSVZ:__main__',
|
|
16
16
|
],
|
|
17
17
|
},
|
|
18
|
+
install_requires=[
|
|
19
|
+
'argparse',
|
|
20
|
+
],
|
|
21
|
+
extras_require={
|
|
22
|
+
'completion': [
|
|
23
|
+
'argcomplete',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
18
26
|
classifiers=[
|
|
19
27
|
'Programming Language :: Python :: 3',
|
|
20
28
|
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
7
|
+
import TSVZ
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestTSVZRegressions(unittest.TestCase):
|
|
11
|
+
def test_append_and_read_preserves_embedded_tab_and_newline(self):
|
|
12
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
13
|
+
path = os.path.join(temp_dir, "embedded.tsv")
|
|
14
|
+
header = ["key", "payload"]
|
|
15
|
+
row = ["row1", "hello\tworld\nline2"]
|
|
16
|
+
|
|
17
|
+
TSVZ.appendTabularFile(
|
|
18
|
+
path,
|
|
19
|
+
row,
|
|
20
|
+
header=header,
|
|
21
|
+
createIfNotExist=True,
|
|
22
|
+
verifyHeader=True,
|
|
23
|
+
strict=True,
|
|
24
|
+
delimiter="\t",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
data = TSVZ.readTabularFile(
|
|
28
|
+
path,
|
|
29
|
+
header=header,
|
|
30
|
+
verifyHeader=True,
|
|
31
|
+
strict=True,
|
|
32
|
+
delimiter="\t",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self.assertIn("row1", data)
|
|
36
|
+
self.assertEqual(data["row1"], row)
|
|
37
|
+
|
|
38
|
+
def test_read_uses_defaults_row_for_missing_values(self):
|
|
39
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
40
|
+
path = os.path.join(temp_dir, "defaults.tsv")
|
|
41
|
+
with open(path, "w", encoding="utf8") as handle:
|
|
42
|
+
handle.write("key\tcpu\tmem\n")
|
|
43
|
+
handle.write(f"{TSVZ.DEFAULTS_INDICATOR_KEY}\t4\t8G\n")
|
|
44
|
+
handle.write("job1\t2\t16G\n")
|
|
45
|
+
handle.write("job2\t\t32G\n")
|
|
46
|
+
|
|
47
|
+
data = TSVZ.readTabularFile(
|
|
48
|
+
path,
|
|
49
|
+
header=["key", "cpu", "mem"],
|
|
50
|
+
verifyHeader=True,
|
|
51
|
+
strict=True,
|
|
52
|
+
delimiter="\t",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.assertEqual(data["job1"], ["job1", "2", "16G"])
|
|
56
|
+
self.assertEqual(data["job2"], ["job2", "4", "32G"])
|
|
57
|
+
|
|
58
|
+
def test_last_line_only_ignores_delete_marker_line(self):
|
|
59
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
60
|
+
path = os.path.join(temp_dir, "lastline.tsv")
|
|
61
|
+
with open(path, "w", encoding="utf8") as handle:
|
|
62
|
+
handle.write("key\tvalue\n")
|
|
63
|
+
handle.write("alive\t42\n")
|
|
64
|
+
handle.write("alive\n")
|
|
65
|
+
|
|
66
|
+
last_valid = TSVZ.readTabularFile(
|
|
67
|
+
path,
|
|
68
|
+
header=["key", "value"],
|
|
69
|
+
verifyHeader=True,
|
|
70
|
+
lastLineOnly=True,
|
|
71
|
+
strict=False,
|
|
72
|
+
delimiter="\t",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self.assertEqual(last_valid, ["alive", "42"])
|
|
76
|
+
|
|
77
|
+
def test_append_and_read_gzip_file(self):
|
|
78
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
79
|
+
path = os.path.join(temp_dir, "compressed.tsv.gz")
|
|
80
|
+
header = ["key", "value"]
|
|
81
|
+
|
|
82
|
+
TSVZ.appendTabularFile(
|
|
83
|
+
path,
|
|
84
|
+
["gk", "gv"],
|
|
85
|
+
header=header,
|
|
86
|
+
createIfNotExist=True,
|
|
87
|
+
verifyHeader=True,
|
|
88
|
+
strict=True,
|
|
89
|
+
delimiter="\t",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
data = TSVZ.readTabularFile(
|
|
93
|
+
path,
|
|
94
|
+
header=header,
|
|
95
|
+
verifyHeader=True,
|
|
96
|
+
strict=True,
|
|
97
|
+
delimiter="\t",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self.assertEqual(data["gk"], ["gk", "gv"])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|