TSVZ 3.37__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 3.37
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 3.37
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
@@ -10,7 +10,6 @@ import functools
10
10
  import io
11
11
  import os
12
12
  import re
13
- from tabnanny import verbose
14
13
  import threading
15
14
  import time
16
15
  import sys
@@ -27,23 +26,42 @@ if os.name == 'nt':
27
26
  elif os.name == 'posix':
28
27
  import fcntl
29
28
 
30
- version = '3.37'
29
+ version = '3.38'
31
30
  __version__ = version
32
31
  author = 'pan@zopyr.us'
33
- COMMIT_DATE = '2026-04-21'
32
+ COMMIT_DATE = '2026-06-04'
34
33
 
35
34
  DEFAULT_DELIMITER = '\t'
36
35
  DEFAULTS_INDICATOR_KEY = '#_defaults_#'
37
36
 
38
37
  COMPRESSED_FILE_EXTENSIONS = ['gz','gzip','bz2','bzip2','xz','lzma','zst','zstd']
39
38
 
40
- def get_delimiter(delimiter,file_name = ''):
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
+ """
41
60
  global DEFAULT_DELIMITER
42
- if not delimiter:
43
- return DEFAULT_DELIMITER
44
- elif delimiter == ...:
61
+ if delimiter is ...:
45
62
  if not file_name:
46
- rtn = '\t'
63
+ # Nothing to infer from; reuse the last-determined default.
64
+ return DEFAULT_DELIMITER
47
65
  elif file_name.endswith('.csv'):
48
66
  rtn = ','
49
67
  elif file_name.endswith('.nsv'):
@@ -52,6 +70,8 @@ def get_delimiter(delimiter,file_name = ''):
52
70
  rtn = '|'
53
71
  else:
54
72
  rtn = '\t'
73
+ elif not delimiter:
74
+ return DEFAULT_DELIMITER
55
75
  elif delimiter == 'comma':
56
76
  rtn = ','
57
77
  elif delimiter == 'tab':
@@ -338,7 +358,7 @@ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False,str_forma
338
358
  power = 2**10
339
359
  n = 0
340
360
  power_labels = {0 : '', 1: 'Ki', 2: 'Mi', 3: 'Gi', 4: 'Ti', 5: 'Pi'}
341
- while size > power:
361
+ while size >= power:
342
362
  size /= power
343
363
  n += 1
344
364
  return f"{size:{str_format}}{' '}{power_labels[n]}"
@@ -346,7 +366,7 @@ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False,str_forma
346
366
  power = 10**3
347
367
  n = 0
348
368
  power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T', 5: 'P'}
349
- while size > power:
369
+ while size >= power:
350
370
  size /= power
351
371
  n += 1
352
372
  return f"{size:{str_format}}{' '}{power_labels[n]}"
@@ -413,7 +433,7 @@ def __teePrintOrNot(message,level = 'info',teeLogger = None):
413
433
  except Exception:
414
434
  print(message,flush=True)
415
435
 
416
- def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT_DELIMITER,defaults = ...,
436
+ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,defaults = ...,
417
437
  storeOffset = False, offset = -1):
418
438
  """
419
439
  Process a line of text and update the task dictionary.
@@ -433,6 +453,7 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT
433
453
  """
434
454
  if defaults is ...:
435
455
  defaults = []
456
+ delimiter = get_delimiter(delimiter)
436
457
  line = line.strip('\x00').rstrip('\r\n')
437
458
  if not line or (line.startswith('#') and not line.startswith(DEFAULTS_INDICATOR_KEY)):
438
459
  # if verbose:
@@ -456,7 +477,7 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT
456
477
  # if verbose:
457
478
  # __teePrintOrNot(f"Empty defaults line found: {line}",teeLogger=teeLogger)
458
479
  defaults.clear()
459
- defaults[0] = DEFAULTS_INDICATOR_KEY
480
+ defaults[:] = [DEFAULTS_INDICATOR_KEY]
460
481
  else:
461
482
  # if verbose:
462
483
  # __teePrintOrNot(f"Key {lineCache[0]} found with empty value, deleting such key's representaion",teeLogger=teeLogger)
@@ -491,6 +512,44 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = DEFAULT
491
512
  # __teePrintOrNot(f"Key {lineCache[0]} added",teeLogger=teeLogger)
492
513
  return correctColumnNum, lineCache
493
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
+
494
553
  def read_last_valid_line(fileName, taskDic, correctColumnNum, verbose=False, teeLogger=None, strict=False,
495
554
  encoding = 'utf8',delimiter = ...,defaults = ...,storeOffset = False ):
496
555
  """
@@ -518,6 +577,12 @@ def read_last_valid_line(fileName, taskDic, correctColumnNum, verbose=False, tee
518
577
  delimiter = get_delimiter(delimiter,file_name=fileName)
519
578
  if verbose:
520
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)
521
586
  with openFileAsCompressed(fileName, 'rb',encoding=encoding, teeLogger=teeLogger) as file:
522
587
  file.seek(0, os.SEEK_END)
523
588
  file_size = file.tell()
@@ -568,7 +633,8 @@ def read_last_valid_line(fileName, taskDic, correctColumnNum, verbose=False, tee
568
633
  return last_valid_line
569
634
 
570
635
  @functools.lru_cache(maxsize=None)
571
- def _get_sanitization_re(delimiter = DEFAULT_DELIMITER):
636
+ def _get_sanitization_re(delimiter = ...):
637
+ delimiter = get_delimiter(delimiter)
572
638
  return re.compile(r"(</sep/>|</LF/>|<sep>|<LF>|\n|" + re.escape(delimiter) + r")")
573
639
 
574
640
  _sanitize_replacements = {
@@ -578,9 +644,10 @@ _sanitize_replacements = {
578
644
  }
579
645
  _inverse_sanitize_replacements = {v: k for k, v in _sanitize_replacements.items()}
580
646
 
581
- def _sanitize(data,delimiter = DEFAULT_DELIMITER):
647
+ def _sanitize(data,delimiter = ...):
582
648
  if not data:
583
649
  return data
650
+ delimiter = get_delimiter(delimiter)
584
651
  def repl(m):
585
652
  tok = m.group(0)
586
653
  if tok == delimiter:
@@ -594,9 +661,10 @@ def _sanitize(data,delimiter = DEFAULT_DELIMITER):
594
661
  else:
595
662
  return [pattern.sub(repl,str(segment)) if segment else '' for segment in data]
596
663
 
597
- def _unsanitize(data,delimiter = DEFAULT_DELIMITER):
664
+ def _unsanitize(data,delimiter = ...):
598
665
  if not data:
599
666
  return data
667
+ delimiter = get_delimiter(delimiter)
600
668
  def repl(m):
601
669
  tok = m.group(0)
602
670
  if tok == "<sep>":
@@ -608,7 +676,7 @@ def _unsanitize(data,delimiter = DEFAULT_DELIMITER):
608
676
  else:
609
677
  return [pattern.sub(repl,str(segment).rstrip()) if segment else '' for segment in data]
610
678
 
611
- def _formatHeader(header,verbose = False,teeLogger = None,delimiter = DEFAULT_DELIMITER):
679
+ def _formatHeader(header,verbose = False,teeLogger = None,delimiter = ...):
612
680
  """
613
681
  Format the header string.
614
682
 
@@ -621,7 +689,7 @@ def _formatHeader(header,verbose = False,teeLogger = None,delimiter = DEFAULT_DE
621
689
  list: The formatted header list of string.
622
690
  """
623
691
  if isinstance(header,str):
624
- header = header.split(delimiter)
692
+ header = header.split(get_delimiter(delimiter))
625
693
  else:
626
694
  try:
627
695
  header = [str(s) for s in header]
@@ -631,7 +699,7 @@ def _formatHeader(header,verbose = False,teeLogger = None,delimiter = DEFAULT_DE
631
699
  header = []
632
700
  return [s.rstrip() for s in header]
633
701
 
634
- def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = False,delimiter = DEFAULT_DELIMITER):
702
+ def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = False,delimiter = ...):
635
703
  """
636
704
  Verify if a line contains the header.
637
705
 
@@ -645,6 +713,7 @@ def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = Fal
645
713
  Returns:
646
714
  bool: True if the header matches the line, False otherwise.
647
715
  """
716
+ delimiter = get_delimiter(delimiter)
648
717
  line = _formatHeader(line,verbose=verbose,teeLogger=teeLogger,delimiter=delimiter)
649
718
  if verbose:
650
719
  __teePrintOrNot(f"Header: \n{header}",teeLogger=teeLogger)
@@ -656,7 +725,7 @@ def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = Fal
656
725
  return False
657
726
  return True
658
727
 
659
- def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,header = [],encoding = 'utf8',strict = True,delimiter = DEFAULT_DELIMITER):
728
+ def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,header = [],encoding = 'utf8',strict = True,delimiter = ...):
660
729
  """
661
730
  Verify the existence of the tabular file.
662
731
 
@@ -671,6 +740,7 @@ def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,heade
671
740
  Returns:
672
741
  bool: True if the file exists, False otherwise.
673
742
  """
743
+ delimiter = get_delimiter(delimiter, file_name=fileName)
674
744
  remainingFileName, _ ,extenstionName = fileName.rpartition('.')
675
745
  if extenstionName in COMPRESSED_FILE_EXTENSIONS:
676
746
  remainingFileName, _ ,extenstionName = remainingFileName.rpartition('.')
@@ -687,7 +757,6 @@ def _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = None,heade
687
757
  try:
688
758
  with openFileAsCompressed(fileName, mode ='wb',encoding=encoding,teeLogger=teeLogger)as file:
689
759
  header = delimiter.join(_sanitize(_formatHeader(header,
690
- verbose=verbose,
691
760
  teeLogger=teeLogger,
692
761
  delimiter=delimiter,
693
762
  ),delimiter=delimiter))
@@ -780,7 +849,7 @@ def readTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fal
780
849
  with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger)as file:
781
850
  if any(header) and verifyHeader:
782
851
  line = file.readline().decode(encoding=encoding,errors='replace')
783
- 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:
784
853
  correctColumnNum = len(header)
785
854
  if verbose:
786
855
  __teePrintOrNot(f"correctColumnNum: {correctColumnNum}",teeLogger=teeLogger)
@@ -882,7 +951,7 @@ def appendLinesTabularFile(fileName,linesToAppend,teeLogger = None,header = '',c
882
951
  if any(header) and verifyHeader:
883
952
  with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger)as file:
884
953
  line = file.readline().decode(encoding=encoding,errors='replace')
885
- if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict):
954
+ if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
886
955
  correctColumnNum = len(header)
887
956
  if verbose:
888
957
  __teePrintOrNot(f"correctColumnNum: {correctColumnNum}",teeLogger=teeLogger)
@@ -936,7 +1005,7 @@ def clearTabularFile(fileName,teeLogger = None,header = '',verifyHeader = False,
936
1005
  with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger)as file:
937
1006
  if any(header) and verifyHeader:
938
1007
  line = file.readline().decode(encoding=encoding,errors='replace')
939
- 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):
940
1009
  __teePrintOrNot(f'Warning: Header mismatch in {fileName}. Keeping original header in file...','warning',teeLogger)
941
1010
  header = _formatHeader(line,verbose = verbose,teeLogger = teeLogger,delimiter=delimiter)
942
1011
  with openFileAsCompressed(fileName, mode ='wb',encoding=encoding,teeLogger=teeLogger)as file:
@@ -1029,10 +1098,10 @@ def scrubTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fa
1029
1098
  appendLinesTabularFile(fileName,file,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,verifyHeader = verifyHeader,verbose = verbose,encoding = encoding,strict = strict,delimiter = delimiter)
1030
1099
  return file
1031
1100
 
1032
- def getListView(tsvzDic,header = [],delimiter = DEFAULT_DELIMITER):
1101
+ def getListView(tsvzDic,header = [],delimiter = ...):
1033
1102
  if header:
1034
1103
  if isinstance(header,str):
1035
- header = header.split(delimiter)
1104
+ header = header.split(get_delimiter(delimiter))
1036
1105
  elif not isinstance(header,list):
1037
1106
  try:
1038
1107
  header = list(header)
@@ -1076,7 +1145,7 @@ class TSVZed(OrderedDict):
1076
1145
  rewrite_on_exit : bool, default=False
1077
1146
  If True, rewrites the entire file when closing/exiting.
1078
1147
  rewrite_interval : float, default=0
1079
- Minimum time interval (in seconds) between full file rewrites. 0 means no limit.
1148
+ Minimum time interval (in seconds) between full file rewrites. 0 means do not rewrite.
1080
1149
  append_check_delay : float, default=0.01
1081
1150
  Time delay (in seconds) between checks of the append queue by the worker thread.
1082
1151
  monitor_external_changes : bool, default=True
@@ -1605,7 +1674,7 @@ memoryOnly:{self.memoryOnly}
1605
1674
  if self.header:
1606
1675
  line = file.readline().decode(self.encoding,errors='replace')
1607
1676
  aftPos = file.tell()
1608
- 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):
1609
1678
  header = self.delimiter.join(_sanitize(self.header,delimiter=self.delimiter))
1610
1679
  file.seek(0)
1611
1680
  file.write(f'{header}\n'.encode(encoding=self.encoding,errors='replace'))
@@ -1736,6 +1805,7 @@ memoryOnly:{self.memoryOnly}
1736
1805
 
1737
1806
  def get_file_obj(self,modes = 'ab'):
1738
1807
  self.writeLock.acquire()
1808
+ file = None
1739
1809
  try:
1740
1810
  if not self.encoding:
1741
1811
  self.encoding = 'utf8'
@@ -1762,6 +1832,8 @@ memoryOnly:{self.memoryOnly}
1762
1832
  # if write lock is already released, return
1763
1833
  if not self.writeLock.locked():
1764
1834
  return
1835
+ if file is None:
1836
+ return
1765
1837
  try:
1766
1838
  file.flush() # Ensure the file is flushed before unlocking
1767
1839
  os.fsync(file.fileno()) # Ensure the file is synced to disk before unlocking
@@ -2034,13 +2106,18 @@ class TSVZedLite(MutableMapping):
2034
2106
  )
2035
2107
  if self.verbose:
2036
2108
  eprint(f"Read at position {pos}: {segments}")
2037
- if key is not ... and segments[0] != key:
2109
+ if key is not ...:
2110
+ if not segments:
2111
+ eprint(f"Error: No segments found at position {pos}")
2112
+ elif segments[0] != key:
2038
2113
  eprint(f"Warning: Key mismatch at position {pos}: expected {key}, got {segments[0]}")
2039
- if self.strict:
2040
- eprint("Error: Key mismatch and strict mode enabled. Raising KeyError.")
2041
- raise KeyError(key)
2042
- else :
2043
- eprint("Continuing despite key mismatch due to non-strict mode. Expect errors!")
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!")
2044
2121
  return segments
2045
2122
 
2046
2123
  # Implement basic __getitem__, __setitem__, __delitem__, __iter__, and __len__. needed for MutableMapping
@@ -2093,6 +2170,7 @@ class TSVZedLite(MutableMapping):
2093
2170
  self.defaults = value
2094
2171
  if self.verbose:
2095
2172
  eprint(f"Defaults set to {value}")
2173
+ return
2096
2174
  elif key.startswith('#'):
2097
2175
  if self.verbose:
2098
2176
  eprint(f"Key {key} updated in memory (data in index) as it starts with #")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes