multiCMD 1.34__py3-none-any.whl → 1.37__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.
multiCMD.py CHANGED
@@ -20,9 +20,9 @@ import itertools
20
20
  import signal
21
21
 
22
22
  #%% global vars
23
- version = '1.34'
23
+ version = '1.37'
24
24
  __version__ = version
25
- COMMIT_DATE = '2025-08-25'
25
+ COMMIT_DATE = '2025-09-11'
26
26
  __running_threads = set()
27
27
  __variables = {}
28
28
 
@@ -621,7 +621,7 @@ def run_commands(commands, timeout=0,max_threads=1,quiet=False,dry_run=False,wit
621
621
  else:
622
622
  return [task.stdout for task in tasks]
623
623
 
624
- def join_threads(threads=__running_threads,timeout=None):
624
+ def join_threads(threads=...,timeout=None):
625
625
  '''
626
626
  Join threads
627
627
 
@@ -633,6 +633,8 @@ def join_threads(threads=__running_threads,timeout=None):
633
633
  None
634
634
  '''
635
635
  global __running_threads
636
+ if threads is ...:
637
+ threads = __running_threads
636
638
  for thread in threads:
637
639
  thread.join(timeout=timeout)
638
640
  if threads is __running_threads:
@@ -679,73 +681,119 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
679
681
  # If there is no input, return None
680
682
  return None
681
683
 
682
- def pretty_format_table(data, delimiter = '\t',header = None):
683
- version = 1.11
684
+ def pretty_format_table(data, delimiter="\t", header=None, full=False):
685
+ import re
686
+ version = 1.12
684
687
  _ = version
688
+ def visible_len(s):
689
+ return len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", s))
690
+ def table_width(col_widths, sep_len):
691
+ # total width = sum of column widths + separators between columns
692
+ return sum(col_widths) + sep_len * (len(col_widths) - 1)
693
+ def truncate_to_width(s, width):
694
+ # If fits, leave as is. If too long and width >= 1, keep width-1 chars + "."
695
+ # If width == 0, nothing fits; return empty string.
696
+ if visible_len(s) <= width:
697
+ return s
698
+ if width <= 0:
699
+ return ""
700
+ # Build a truncated plain string based on visible chars (no ANSI awareness for slicing)
701
+ # For simplicity, slice the raw string. This may cut ANSI; best to avoid ANSI in data if truncation occurs.
702
+ return s[: max(width - 2, 0)] + ".."
685
703
  if not data:
686
- return ''
704
+ return ""
705
+ # Normalize input data structure
687
706
  if isinstance(data, str):
688
- data = data.strip('\n').split('\n')
707
+ data = data.strip("\n").split("\n")
689
708
  data = [line.split(delimiter) for line in data]
690
709
  elif isinstance(data, dict):
691
- # flatten the 2D dict to a list of lists
692
710
  if isinstance(next(iter(data.values())), dict):
693
- tempData = [['key'] + list(next(iter(data.values())).keys())]
694
- tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
711
+ tempData = [["key"] + list(next(iter(data.values())).keys())]
712
+ tempData.extend([[key] + list(value.values()) for key, value in data.items()])
695
713
  data = tempData
696
714
  else:
697
- # it is a dict of lists
698
715
  data = [[key] + list(value) for key, value in data.items()]
699
716
  elif not isinstance(data, list):
700
717
  data = list(data)
701
- # format the list into 2d list of list of strings
702
718
  if isinstance(data[0], dict):
703
- tempData = [data[0].keys()]
719
+ tempData = [list(data[0].keys())]
704
720
  tempData.extend([list(item.values()) for item in data])
705
721
  data = tempData
706
722
  data = [[str(item) for item in row] for row in data]
707
723
  num_cols = len(data[0])
708
- col_widths = [0] * num_cols
709
- # Calculate the maximum width of each column
710
- for c in range(num_cols):
711
- #col_widths[c] = max(len(row[c]) for row in data)
712
- # handle ansii escape sequences
713
- col_widths[c] = max(len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]','',row[c])) for row in data)
714
- if header:
715
- header_widths = [len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', col)) for col in header]
716
- col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
717
- # Build the row format string
718
- row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
719
- # Print the header
720
- if not header:
724
+ # Resolve header and rows
725
+ using_provided_header = header is not None
726
+ if not using_provided_header:
721
727
  header = data[0]
722
- outTable = []
723
- outTable.append(row_format.format(*header))
724
- outTable.append('-+-'.join('-' * width for width in col_widths))
725
- for row in data[1:]:
726
- # if the row is empty, print an divider
727
- if not any(row):
728
- outTable.append('-+-'.join('-' * width for width in col_widths))
729
- else:
730
- outTable.append(row_format.format(*row))
728
+ rows = data[1:]
731
729
  else:
732
- # pad / truncate header to appropriate length
733
- if isinstance(header,str):
730
+ if isinstance(header, str):
734
731
  header = header.split(delimiter)
732
+ # Pad/trim header to match num_cols
735
733
  if len(header) < num_cols:
736
- header += ['']*(num_cols-len(header))
734
+ header = header + [""] * (num_cols - len(header))
737
735
  elif len(header) > num_cols:
738
736
  header = header[:num_cols]
739
- outTable = []
740
- outTable.append(row_format.format(*header))
741
- outTable.append('-+-'.join('-' * width for width in col_widths))
742
- for row in data:
743
- # if the row is empty, print an divider
737
+ rows = data
738
+ # Compute initial column widths based on data and header
739
+ def compute_col_widths(hdr, rows_):
740
+ col_w = [0] * len(hdr)
741
+ for i in range(len(hdr)):
742
+ col_w[i] = max(0, visible_len(hdr[i]), *(visible_len(r[i]) for r in rows_ if i < len(r)))
743
+ return col_w
744
+ # Ensure all rows have the same number of columns
745
+ normalized_rows = []
746
+ for r in rows:
747
+ if len(r) < num_cols:
748
+ r = r + [""] * (num_cols - len(r))
749
+ elif len(r) > num_cols:
750
+ r = r[:num_cols]
751
+ normalized_rows.append(r)
752
+ rows = normalized_rows
753
+ col_widths = compute_col_widths(header, rows)
754
+ # If full=True, keep existing formatting
755
+ # Else try to fit within the terminal width by:
756
+ # 1) Switching to compressed separators if needed
757
+ # 2) Recursively compressing columns (truncating with ".")
758
+ sep = " | "
759
+ hsep = "-+-"
760
+ cols = get_terminal_size()[0]
761
+ def render(hdr, rows, col_w, sep_str, hsep_str):
762
+ row_fmt = sep_str.join("{{:<{}}}".format(w) for w in col_w)
763
+ out = []
764
+ out.append(row_fmt.format(*hdr))
765
+ out.append(hsep_str.join("-" * w for w in col_w))
766
+ for row in rows:
744
767
  if not any(row):
745
- outTable.append('-+-'.join('-' * width for width in col_widths))
768
+ out.append(hsep_str.join("-" * w for w in col_w))
746
769
  else:
747
- outTable.append(row_format.format(*row))
748
- return '\n'.join(outTable) + '\n'
770
+ row = [truncate_to_width(row[i], col_w[i]) for i in range(len(row))]
771
+ out.append(row_fmt.format(*row))
772
+ return "\n".join(out) + "\n"
773
+ if full:
774
+ return render(header, rows, col_widths, sep, hsep)
775
+ # Try default separators first
776
+ if table_width(col_widths, len(sep)) <= cols:
777
+ return render(header, rows, col_widths, sep, hsep)
778
+ # Use compressed separators (no spaces)
779
+ sep = "|"
780
+ hsep = "+"
781
+ if table_width(col_widths, len(sep)) <= cols:
782
+ return render(header, rows, col_widths, sep, hsep)
783
+ # Begin column compression
784
+ # Track which columns have been compressed already to header width
785
+ header_widths = [visible_len(h) for h in header]
786
+ width_diff = [max(col_widths[i] - header_widths[i],0) for i in range(num_cols)]
787
+ total_overflow_width = table_width(col_widths, len(sep)) - cols
788
+ for i, diff in sorted(enumerate(width_diff), key=lambda x: -x[1]):
789
+ if total_overflow_width <= 0:
790
+ break
791
+ if diff <= 0:
792
+ continue
793
+ reduce_by = min(diff, total_overflow_width)
794
+ col_widths[i] -= reduce_by
795
+ total_overflow_width -= reduce_by
796
+ return render(header, rows, col_widths, sep, hsep)
749
797
 
750
798
  def parseTable(data,sort=False):
751
799
  if isinstance(data, str):
@@ -903,3 +951,109 @@ def print_progress_bar(iteration, total, prefix='', suffix=''):
903
951
  except:
904
952
  if iteration % 5 == 0:
905
953
  print(_genrate_progress_bar(iteration, total, prefix, suffix))
954
+
955
+ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False, str_format=".2f"):
956
+ if to_int or isinstance(size, str):
957
+ if isinstance(size, int):
958
+ return size
959
+ elif isinstance(size, str):
960
+ match = re.match(r"(\d+(\.\d+)?)\s*([a-zA-Z]*)", size)
961
+ if not match:
962
+ if to_str:
963
+ return size
964
+ print(
965
+ "Invalid size format. Expected format: 'number [unit]', "
966
+ "e.g., '1.5 GiB' or '1.5GiB'"
967
+ )
968
+ print(f"Got: {size}")
969
+ return 0
970
+ number, _, unit = match.groups()
971
+ number = float(number)
972
+ unit = unit.strip().lower().rstrip("b")
973
+ if unit.endswith("i"):
974
+ use_1024_bytes = True
975
+ elif use_1024_bytes is None:
976
+ use_1024_bytes = False
977
+ unit = unit.rstrip("i")
978
+ if use_1024_bytes:
979
+ power = 2**10
980
+ else:
981
+ power = 10**3
982
+ unit_labels = {
983
+ "": 0,
984
+ "k": 1,
985
+ "m": 2,
986
+ "g": 3,
987
+ "t": 4,
988
+ "p": 5,
989
+ "e": 6,
990
+ "z": 7,
991
+ "y": 8,
992
+ }
993
+ if unit not in unit_labels:
994
+ if to_str:
995
+ return size
996
+ else:
997
+ if to_str:
998
+ return format_bytes(
999
+ size=int(number * (power**unit_labels[unit])),
1000
+ use_1024_bytes=use_1024_bytes,
1001
+ to_str=True,
1002
+ str_format=str_format,
1003
+ )
1004
+ return int(number * (power**unit_labels[unit]))
1005
+ else:
1006
+ try:
1007
+ return int(size)
1008
+ except Exception:
1009
+ return 0
1010
+ elif to_str or isinstance(size, int) or isinstance(size, float):
1011
+ if isinstance(size, str):
1012
+ try:
1013
+ size = size.rstrip("B").rstrip("b")
1014
+ size = float(size.lower().strip())
1015
+ except Exception:
1016
+ return size
1017
+ if use_1024_bytes or use_1024_bytes is None:
1018
+ power = 2**10
1019
+ n = 0
1020
+ power_labels = {
1021
+ 0: "",
1022
+ 1: "Ki",
1023
+ 2: "Mi",
1024
+ 3: "Gi",
1025
+ 4: "Ti",
1026
+ 5: "Pi",
1027
+ 6: "Ei",
1028
+ 7: "Zi",
1029
+ 8: "Yi",
1030
+ }
1031
+ while size > power:
1032
+ size /= power
1033
+ n += 1
1034
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
1035
+ else:
1036
+ power = 10**3
1037
+ n = 0
1038
+ power_labels = {
1039
+ 0: "",
1040
+ 1: "K",
1041
+ 2: "M",
1042
+ 3: "G",
1043
+ 4: "T",
1044
+ 5: "P",
1045
+ 6: "E",
1046
+ 7: "Z",
1047
+ 8: "Y",
1048
+ }
1049
+ while size > power:
1050
+ size /= power
1051
+ n += 1
1052
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
1053
+ else:
1054
+ try:
1055
+ return format_bytes(float(size), use_1024_bytes)
1056
+ except Exception:
1057
+ pass
1058
+ return 0
1059
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.34
3
+ Version: 1.37
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -0,0 +1,6 @@
1
+ multiCMD.py,sha256=4niC5CAyZd9TQaxVBzftEoJUjCyQTrJa1IPm6uJbCyE,34260
2
+ multicmd-1.37.dist-info/METADATA,sha256=5lQgwCy5ZFlRQcBINnmPdGBF0StovsYOMK-HTVvdor0,5640
3
+ multicmd-1.37.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multicmd-1.37.dist-info/entry_points.txt,sha256=nSLBkYrcUCQxt1w3LIJkvgOhpRYEC0xAPqNG7u4OYs8,89
5
+ multicmd-1.37.dist-info/top_level.txt,sha256=DSqgftD40G09F3qEjpHRCUNUsGUvGZZG69Sm3YEUiWI,9
6
+ multicmd-1.37.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- multiCMD.py,sha256=GDefhp2vHOBFgiBmgIFa9Xls7EK95fDtuKWNW-tdopE,30274
2
- multicmd-1.34.dist-info/METADATA,sha256=AaZBveq1NASBsdNBiUSSJFVAZ6ddb06MYM26C6t8NUY,5640
3
- multicmd-1.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multicmd-1.34.dist-info/entry_points.txt,sha256=nSLBkYrcUCQxt1w3LIJkvgOhpRYEC0xAPqNG7u4OYs8,89
5
- multicmd-1.34.dist-info/top_level.txt,sha256=DSqgftD40G09F3qEjpHRCUNUsGUvGZZG69Sm3YEUiWI,9
6
- multicmd-1.34.dist-info/RECORD,,