Module osbot_utils.helpers.Print_Table
Expand source code
from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self
from osbot_utils.utils.Misc import ansi_text_visible_length
raw_data = """|-------------------------------------------------------------------------------------|
| BOTO3 REST calls (via BaseClient._make_api_call) |
|-------------------------------------------------------------------------------------|
| # | Method | Duration | Params | Return Value |
|-------------------------------------------------------------------------------------|
| 0 | GetCallerIdentity | 412 ms | ('GetCallerIdentity', {}) | {'UserId': 'AIDAW3B45JBMJ7OKHCQZL', 'Account': '470426667096', 'Arn': 'arn:aws:iam::470426667096:user/OSBot-AWS-Dev__Only-IAM'} |
| 1 | GetCallerIdentity | 97 ms | ('GetCallerIdentity', {}) | {'UserId': 'AIDAW3B45JBMJ7OKHCQZL', 'Account': '470426667096', 'Arn': 'arn:aws:iam::470426667096:user/OSBot-AWS-Dev__Only-IAM'} |
| 2 | GetCallerIdentity | 96 ms | ('GetCallerIdentity', {}) | {'UserId': 'AIDAW3B45JBMJ7OKHCQZL', 'Account': '470426667096', 'Arn': 'arn:aws:iam::470426667096:user/OSBot-AWS-Dev__Only-IAM'} |
|-------------------------------------------------------------------------------------|
| Total Duration: 0.73 secs | Total calls: 3 |
|-------------------------------------------------------------------------------------|
"""
CHAR_TABLE_HORIZONTAL = "─"
CHAR_TABLE_BOTTOM_LEFT = "└"
CHAR_TABLE_BOTTOM_RIGHT = "┘"
CHAR_TABLE_MIDDLE_LEFT = "├"
CHAR_TABLE_MIDDLE_RIGHT = "┤"
CHAR_TABLE_MIDDLE = "┼"
CHAR_TABLE_VERTICAL = "│"
CHAR_TABLE_TOP_LEFT = "┌"
CHAR_TABLE_TOP_RIGHT = "┐"
MAX_CELL_SIZE = 200
class Print_Table(Kwargs_To_Self):
title : str
headers : list
headers_by_index : dict
footer : str
headers_size : list
headers_to_hide : list
max_cell_size : int = MAX_CELL_SIZE
rows : list
rows_texts : list
table_width : int
text__all : list
text__footer : str
text__headers : str
text__table_bottom : str
text__table_middle : str
text__table_top : str
text__title : str
text__width : int
def __init__(self, **kwargs):
super().__init__(**kwargs)
def add_column(self, header, cells:list):
self.fix_table()
columns_count = len(self.headers)
self.add_header(header)
for index, cell in enumerate(cells):
if len(self.rows) <= index:
new_row = ['' for _ in range(columns_count)] + [cell]
self.rows.append(new_row)
else:
self.rows[index].append(cell)
return self
def add_data(self, data):
if type(data) is dict:
self.add_dict(data)
elif type(data) is list:
for item in data:
self.add_data(item)
else:
self.add_row(data)
return self
def add_dict(self, data:dict):
self.fix_table() # makes sure the number of headers and rows are the same
all_headers = set(self.headers) | set(data.keys()) # get all headers from the table and the data
for header in sorted(all_headers): # sorted to have consistent order of new headers (since without it the order is pseudo random)
if header not in self.headers: # to make sure the table headers and new data keys match
self.add_header(header) # add any new headers not already present
row_raw = {header: '' for header in all_headers} # Create a raw row with empty values for all headers
row_raw.update(data) # Update the raw row with values from data
row_by_header = [row_raw[header] for header in self.headers] # create a new row object, ensuring headers order
self.add_row(row_by_header) # add the new row to the table
return self
def add_header(self, header:str):
self.headers.append(header)
return self
def add_headers(self, *headers:list):
for header in headers:
self.add_header(header)
return self
def add_row(self, row:list):
if type(row) is not list:
self.rows.append([row])
else:
self.rows.append(row)
return self
def add_rows(self, rows:list):
for row in rows:
self.add_row(row)
return self
def calculate_max_cell_size(self, cell):
lines_len = []
for line in str(cell).split('\n'): # Split the cell into lines and find the maximum length of any line
line_len_ansi_visible = ansi_text_visible_length(line) # add support for the use of ansi chars (which impact the len calculations)
lines_len.append(line_len_ansi_visible)
max_cell_line_length = max(lines_len)
if max_cell_line_length > self.max_cell_size:
max_cell_line_length = self.max_cell_size
return max_cell_line_length
def fix_table(self):
if self.rows:
max_cells = max(len(row) for row in self.rows) # get max number of cells in any row
else:
max_cells = 0
extra_header_count = len(self.headers) + 1 # Start counting extra headers from the current number of headers
while len(self.headers) < max_cells: # Extend headers if necessary
self.headers.append(f"Header #{extra_header_count}") # headers cannot have empty values
extra_header_count += 1
for row in self.rows: # Ensure each row has the same number of cells as there are headers
while len(row) < len(self.headers):
row.append("")
for index, header in enumerate(self.headers): # capture the index of the headers
self.headers_by_index[index] = header
def hide_headers(self, headers):
self.headers_to_hide = headers
return self
def map_headers_size(self):
self.headers_size = [] # initialize the headers size with the size of each header
for header in self.headers:
header_len_ansi_visible = ansi_text_visible_length(header)
self.headers_size.append(header_len_ansi_visible)
for row in self.rows: # iterate over each row and update the headers size with the size of the largest cell
for index, cell in enumerate(row): # for each row
if cell: # Check if the cell is not empty or None
max_cell_line_length = self.calculate_max_cell_size(cell)
self.headers_size[index] = max(self.headers_size[index], max_cell_line_length) # Update the corresponding header size if this line is longer than the current max
# fix edge case that happens when the title or footer is longer than the table width
if len(self.headers_size):
last_header = len(self.headers_size) - 1 # get the index of the last header
last_header_size = self.headers_size[last_header] # get the size of the last header
all_headers_size = sum(self.headers_size) # get the size of all headers
all_headers_size_minus_last = all_headers_size - last_header_size # get the size of all headers minus the last header
if sum(self.headers_size) < len(self.title): # if the title is longer than the headers, update the last header size
title_size = len(self.title) # get the size of the title
new_last_header_size = title_size - all_headers_size_minus_last # calculate the new size of the last header
self.headers_size[last_header] = new_last_header_size # update the last header size
if sum(self.headers_size) < len(self.footer): # if the footer is longer than the headers, update the last header size
footer_size = len(self.footer) # get the size of the footer
new_last_header_size = footer_size - all_headers_size_minus_last # calculate the new size of the last header
self.headers_size[last_header] = new_last_header_size # update the last header size
return self
def map_table_width(self):
self.table_width = len(self.text__headers)
if len(self.footer) > self.table_width:
self.table_width = len(self.footer) + 4
if len(self.title) > self.table_width:
self.table_width = len(self.title) + 4
# def map_rows_texts(self):
# self.rows_texts = []
# if not self.rows:
# self.rows_texts = [f"{CHAR_TABLE_VERTICAL} {CHAR_TABLE_VERTICAL}"]
# else:
# for row in self.rows:
# row_text = CHAR_TABLE_VERTICAL
# for index, cell in enumerate(row):
# size = self.headers_size[index]
# row_text += f" {str(cell):{size}} {CHAR_TABLE_VERTICAL}"
# self.rows_texts.append(row_text)
# return self
def cell_value(self, cell_value):
cell_value = str(cell_value)
if len(cell_value) > self.max_cell_size:
return cell_value[:self.max_cell_size - 3] + '...'
return cell_value
def map_rows_texts(self):
self.rows_texts = []
#if not self.rows:
# self.rows_texts = [f"{CHAR_TABLE_VERTICAL}aaa{CHAR_TABLE_VERTICAL}"]
if self.rows:
for row in self.rows:
row_text = CHAR_TABLE_VERTICAL
additional_lines = [[] for _ in row] # Prepare to hold additional lines from multiline cells
for index, cell in enumerate(row):
if self.should_show_header(index):
size = self.headers_size[index]
cell_lines = str(cell).split('\n') # Split the cell text by newlines
cell_value = self.cell_value(cell_lines[0])
extra_padding = ' ' * (size - ansi_text_visible_length(cell_value))
row_text += f" {cell_value}{extra_padding} {CHAR_TABLE_VERTICAL}" # Add the first line of the cell
for i, line in enumerate(cell_lines[1:], start=1):
additional_lines[index].append(line) # Store additional lines
self.rows_texts.append(row_text)
# Handle additional lines by creating new row_texts for them
max_additional_lines = max(len(lines) for lines in additional_lines)
for depth in range(max_additional_lines):
extra_row_text = CHAR_TABLE_VERTICAL
for index, column in enumerate(additional_lines):
cell_data = column[depth] if len(column) > depth else ''
size = self.headers_size[index]
cell_value = self.cell_value(cell_data)
extra_padding = ' ' * (size - ansi_text_visible_length(cell_value))
extra_row_text += f" {cell_value}{extra_padding} {CHAR_TABLE_VERTICAL}"
self.rows_texts.append(extra_row_text)
return self
def map_text__all(self):
self.text__all = [ self.text__table_top ]
if self.title : self.text__all += [ self.text__title , self.text__table_middle ]
if self.headers : self.text__all += [ self.text__headers , self.text__table_middle ]
if self.rows : self.text__all += [ *self.rows_texts ]
if self.footer : self.text__all += [ self.text__table_middle , self.text__footer ]
self.text__all += [ self.text__table_bottom ]
def map_text__footer(self):
self.text__footer = f"{CHAR_TABLE_VERTICAL} {self.footer:{self.text__width}} {CHAR_TABLE_VERTICAL}"
def map_text__headers(self):
self.text__headers = CHAR_TABLE_VERTICAL
if not self.headers:
self.text__headers += f" {CHAR_TABLE_VERTICAL}"
else:
for header, size in zip(self.headers, self.headers_size):
if self.should_show_header(header):
self.text__headers += f" {header:{size}} {CHAR_TABLE_VERTICAL}"
return self
def map_text__table_bottom(self): self.text__table_bottom = f"{CHAR_TABLE_BOTTOM_LEFT}" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_BOTTOM_RIGHT }"
def map_text__table_middle(self): self.text__table_middle = f"{CHAR_TABLE_MIDDLE_LEFT}" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_MIDDLE_RIGHT }"
def map_text__table_top (self): self.text__table_top = f"{CHAR_TABLE_TOP_LEFT }" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_TOP_RIGHT }"
def map_text__title(self):
self.text__title = f"{CHAR_TABLE_VERTICAL} {self.title:{self.text__width}} {CHAR_TABLE_VERTICAL}"
def map_text__width(self):
self.text__width = self.table_width - 4
# if self.table_width > 3: # there is no use case that that needs this check
# self.text__width = self.table_width - 4
# else:
# self.text__width = 0
def map_texts(self):
self.fix_table ()
self.map_headers_size ()
self.map_text__headers ()
self.map_rows_texts ()
self.map_table_width ()
self.map_text__width ()
self.map_text__footer ()
self.map_text__title ()
self.map_text__table_bottom ()
self.map_text__table_middle ()
self.map_text__table_top ()
self.map_text__all ()
def print(self, data=None, order=None):
if data:
self.add_data(data)
if order:
self.reorder_columns(order)
print()
self.map_texts()
for text in self.text__all:
print(text)
return self
def should_show_header(self, header):
if self.headers_to_hide:
if type(header) is int:
header_name = self.headers_by_index[header]
else:
header_name = str(header)
return header_name not in self.headers_to_hide
return True
def remove_columns(self,column_names):
if type (column_names) is str:
column_names = [column_names]
if type(column_names) is list:
for column_name in column_names:
if column_name in self.headers:
column_index = self.headers.index(column_name)
del self.headers[column_index]
for row in self.rows:
del row[column_index]
return self
def reorder_columns(self, new_order: list):
if set(new_order) != set(self.headers): # Check if the new_order list has the same headers as the current table
missing = set(self.headers) - set(new_order) or {}
extra = set(new_order) - set(self.headers) or {}
raise ValueError("New order must contain the same headers as the current table.\n"
f" - Missing headers: {missing}\n"
f" - Extra headers: {extra}")
index_map = {old_header: new_order.index(old_header) for old_header in self.headers} # Create a mapping from old index to new index
new_rows = [] # Reorder each row according to the new header order
for row in self.rows:
new_row = [None] * len(row) # Initialize a new row with placeholders
for old_index, cell in enumerate(row):
new_index = index_map[self.headers[old_index]]
new_row[new_index] = cell
new_rows.append(new_row)
self.headers = list(new_order) # Reorder the headers
self.rows = new_rows # Reorder the rows
return self
def set_footer(self, footer):
self.footer = footer
return self
def set_headers(self, headers):
self.headers = headers
return self
def set_order(self, *new_order):
return self.reorder_columns(new_order)
def set_title(self, title):
self.title = title
return self
def to_csv(self):
csv_content = ','.join(self.to_csv__escape_cell(header) for header in self.headers) + '\n' # Create a CSV string from the headers and rows
for row in self.rows:
csv_content += ','.join(self.to_csv__escape_cell(cell) if cell is not None else '' for cell in row) + '\n'
return csv_content
def to_csv__escape_cell(self, cell):
if cell and any(c in cell for c in [',', '"', '\n']):
cell = cell.replace('"', '""') # Escape double quotes
cell = cell.replace('\n', '\\n') # escape new lines
return f'"{cell}"' # Enclose the cell in double quotes
return cell
def to_dict(self):
table_dict = {header: [] for header in self.headers} # Initialize the dictionary with empty lists for each header
for row in self.rows: # Iterate over each row and append the cell to the corresponding header's list
for header, cell in zip(self.headers, row):
table_dict[header].append(cell)
return table_dict
Classes
class Print_Table (**kwargs)
-
A mixin class to strictly assign keyword arguments to pre-defined instance attributes during initialization.
This base class provides an init method that assigns values from keyword arguments to instance attributes. If an attribute with the same name as a key from the kwargs is defined in the class, it will be set to the value from kwargs. If the key does not match any predefined attribute names, an exception is raised.
This behavior enforces strict control over the attributes of instances, ensuring that only predefined attributes can be set at the time of instantiation and avoids silent attribute creation which can lead to bugs in the code.
Usage
class MyConfigurableClass(Kwargs_To_Self): attribute1 = 'default_value' attribute2 = True attribute3 : str attribute4 : list attribute4 : int = 42
# Other methods can be added here
Correctly override default values by passing keyword arguments
instance = MyConfigurableClass(attribute1='new_value', attribute2=False)
This will raise an exception as 'attribute3' is not predefined
instance = MyConfigurableClass(attribute3='invalid_attribute')
this will also assign the default value to any variable that has a type defined. In the example above the default values (mapped by default__kwargs and locals) will be: attribute1 = 'default_value' attribute2 = True attribute3 = '' # default value of str attribute4 = [] # default value of list attribute4 = 42 # defined value in the class
Note
It is important that all attributes which may be set at instantiation are predefined in the class. Failure to do so will result in an exception being raised.
Methods
init(**kwargs): The initializer that handles the assignment of keyword arguments to instance attributes. It enforces strict attribute assignment rules, only allowing attributes that are already defined in the class to be set.
Initialize an instance of the derived class, strictly assigning provided keyword arguments to corresponding instance attributes.
Parameters
**kwargs: Variable length keyword arguments.
Raises
Exception
- If a key from kwargs does not correspond to any attribute pre-defined in the class, an exception is raised to prevent setting an undefined attribute.
Expand source code
class Print_Table(Kwargs_To_Self): title : str headers : list headers_by_index : dict footer : str headers_size : list headers_to_hide : list max_cell_size : int = MAX_CELL_SIZE rows : list rows_texts : list table_width : int text__all : list text__footer : str text__headers : str text__table_bottom : str text__table_middle : str text__table_top : str text__title : str text__width : int def __init__(self, **kwargs): super().__init__(**kwargs) def add_column(self, header, cells:list): self.fix_table() columns_count = len(self.headers) self.add_header(header) for index, cell in enumerate(cells): if len(self.rows) <= index: new_row = ['' for _ in range(columns_count)] + [cell] self.rows.append(new_row) else: self.rows[index].append(cell) return self def add_data(self, data): if type(data) is dict: self.add_dict(data) elif type(data) is list: for item in data: self.add_data(item) else: self.add_row(data) return self def add_dict(self, data:dict): self.fix_table() # makes sure the number of headers and rows are the same all_headers = set(self.headers) | set(data.keys()) # get all headers from the table and the data for header in sorted(all_headers): # sorted to have consistent order of new headers (since without it the order is pseudo random) if header not in self.headers: # to make sure the table headers and new data keys match self.add_header(header) # add any new headers not already present row_raw = {header: '' for header in all_headers} # Create a raw row with empty values for all headers row_raw.update(data) # Update the raw row with values from data row_by_header = [row_raw[header] for header in self.headers] # create a new row object, ensuring headers order self.add_row(row_by_header) # add the new row to the table return self def add_header(self, header:str): self.headers.append(header) return self def add_headers(self, *headers:list): for header in headers: self.add_header(header) return self def add_row(self, row:list): if type(row) is not list: self.rows.append([row]) else: self.rows.append(row) return self def add_rows(self, rows:list): for row in rows: self.add_row(row) return self def calculate_max_cell_size(self, cell): lines_len = [] for line in str(cell).split('\n'): # Split the cell into lines and find the maximum length of any line line_len_ansi_visible = ansi_text_visible_length(line) # add support for the use of ansi chars (which impact the len calculations) lines_len.append(line_len_ansi_visible) max_cell_line_length = max(lines_len) if max_cell_line_length > self.max_cell_size: max_cell_line_length = self.max_cell_size return max_cell_line_length def fix_table(self): if self.rows: max_cells = max(len(row) for row in self.rows) # get max number of cells in any row else: max_cells = 0 extra_header_count = len(self.headers) + 1 # Start counting extra headers from the current number of headers while len(self.headers) < max_cells: # Extend headers if necessary self.headers.append(f"Header #{extra_header_count}") # headers cannot have empty values extra_header_count += 1 for row in self.rows: # Ensure each row has the same number of cells as there are headers while len(row) < len(self.headers): row.append("") for index, header in enumerate(self.headers): # capture the index of the headers self.headers_by_index[index] = header def hide_headers(self, headers): self.headers_to_hide = headers return self def map_headers_size(self): self.headers_size = [] # initialize the headers size with the size of each header for header in self.headers: header_len_ansi_visible = ansi_text_visible_length(header) self.headers_size.append(header_len_ansi_visible) for row in self.rows: # iterate over each row and update the headers size with the size of the largest cell for index, cell in enumerate(row): # for each row if cell: # Check if the cell is not empty or None max_cell_line_length = self.calculate_max_cell_size(cell) self.headers_size[index] = max(self.headers_size[index], max_cell_line_length) # Update the corresponding header size if this line is longer than the current max # fix edge case that happens when the title or footer is longer than the table width if len(self.headers_size): last_header = len(self.headers_size) - 1 # get the index of the last header last_header_size = self.headers_size[last_header] # get the size of the last header all_headers_size = sum(self.headers_size) # get the size of all headers all_headers_size_minus_last = all_headers_size - last_header_size # get the size of all headers minus the last header if sum(self.headers_size) < len(self.title): # if the title is longer than the headers, update the last header size title_size = len(self.title) # get the size of the title new_last_header_size = title_size - all_headers_size_minus_last # calculate the new size of the last header self.headers_size[last_header] = new_last_header_size # update the last header size if sum(self.headers_size) < len(self.footer): # if the footer is longer than the headers, update the last header size footer_size = len(self.footer) # get the size of the footer new_last_header_size = footer_size - all_headers_size_minus_last # calculate the new size of the last header self.headers_size[last_header] = new_last_header_size # update the last header size return self def map_table_width(self): self.table_width = len(self.text__headers) if len(self.footer) > self.table_width: self.table_width = len(self.footer) + 4 if len(self.title) > self.table_width: self.table_width = len(self.title) + 4 # def map_rows_texts(self): # self.rows_texts = [] # if not self.rows: # self.rows_texts = [f"{CHAR_TABLE_VERTICAL} {CHAR_TABLE_VERTICAL}"] # else: # for row in self.rows: # row_text = CHAR_TABLE_VERTICAL # for index, cell in enumerate(row): # size = self.headers_size[index] # row_text += f" {str(cell):{size}} {CHAR_TABLE_VERTICAL}" # self.rows_texts.append(row_text) # return self def cell_value(self, cell_value): cell_value = str(cell_value) if len(cell_value) > self.max_cell_size: return cell_value[:self.max_cell_size - 3] + '...' return cell_value def map_rows_texts(self): self.rows_texts = [] #if not self.rows: # self.rows_texts = [f"{CHAR_TABLE_VERTICAL}aaa{CHAR_TABLE_VERTICAL}"] if self.rows: for row in self.rows: row_text = CHAR_TABLE_VERTICAL additional_lines = [[] for _ in row] # Prepare to hold additional lines from multiline cells for index, cell in enumerate(row): if self.should_show_header(index): size = self.headers_size[index] cell_lines = str(cell).split('\n') # Split the cell text by newlines cell_value = self.cell_value(cell_lines[0]) extra_padding = ' ' * (size - ansi_text_visible_length(cell_value)) row_text += f" {cell_value}{extra_padding} {CHAR_TABLE_VERTICAL}" # Add the first line of the cell for i, line in enumerate(cell_lines[1:], start=1): additional_lines[index].append(line) # Store additional lines self.rows_texts.append(row_text) # Handle additional lines by creating new row_texts for them max_additional_lines = max(len(lines) for lines in additional_lines) for depth in range(max_additional_lines): extra_row_text = CHAR_TABLE_VERTICAL for index, column in enumerate(additional_lines): cell_data = column[depth] if len(column) > depth else '' size = self.headers_size[index] cell_value = self.cell_value(cell_data) extra_padding = ' ' * (size - ansi_text_visible_length(cell_value)) extra_row_text += f" {cell_value}{extra_padding} {CHAR_TABLE_VERTICAL}" self.rows_texts.append(extra_row_text) return self def map_text__all(self): self.text__all = [ self.text__table_top ] if self.title : self.text__all += [ self.text__title , self.text__table_middle ] if self.headers : self.text__all += [ self.text__headers , self.text__table_middle ] if self.rows : self.text__all += [ *self.rows_texts ] if self.footer : self.text__all += [ self.text__table_middle , self.text__footer ] self.text__all += [ self.text__table_bottom ] def map_text__footer(self): self.text__footer = f"{CHAR_TABLE_VERTICAL} {self.footer:{self.text__width}} {CHAR_TABLE_VERTICAL}" def map_text__headers(self): self.text__headers = CHAR_TABLE_VERTICAL if not self.headers: self.text__headers += f" {CHAR_TABLE_VERTICAL}" else: for header, size in zip(self.headers, self.headers_size): if self.should_show_header(header): self.text__headers += f" {header:{size}} {CHAR_TABLE_VERTICAL}" return self def map_text__table_bottom(self): self.text__table_bottom = f"{CHAR_TABLE_BOTTOM_LEFT}" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_BOTTOM_RIGHT }" def map_text__table_middle(self): self.text__table_middle = f"{CHAR_TABLE_MIDDLE_LEFT}" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_MIDDLE_RIGHT }" def map_text__table_top (self): self.text__table_top = f"{CHAR_TABLE_TOP_LEFT }" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_TOP_RIGHT }" def map_text__title(self): self.text__title = f"{CHAR_TABLE_VERTICAL} {self.title:{self.text__width}} {CHAR_TABLE_VERTICAL}" def map_text__width(self): self.text__width = self.table_width - 4 # if self.table_width > 3: # there is no use case that that needs this check # self.text__width = self.table_width - 4 # else: # self.text__width = 0 def map_texts(self): self.fix_table () self.map_headers_size () self.map_text__headers () self.map_rows_texts () self.map_table_width () self.map_text__width () self.map_text__footer () self.map_text__title () self.map_text__table_bottom () self.map_text__table_middle () self.map_text__table_top () self.map_text__all () def print(self, data=None, order=None): if data: self.add_data(data) if order: self.reorder_columns(order) print() self.map_texts() for text in self.text__all: print(text) return self def should_show_header(self, header): if self.headers_to_hide: if type(header) is int: header_name = self.headers_by_index[header] else: header_name = str(header) return header_name not in self.headers_to_hide return True def remove_columns(self,column_names): if type (column_names) is str: column_names = [column_names] if type(column_names) is list: for column_name in column_names: if column_name in self.headers: column_index = self.headers.index(column_name) del self.headers[column_index] for row in self.rows: del row[column_index] return self def reorder_columns(self, new_order: list): if set(new_order) != set(self.headers): # Check if the new_order list has the same headers as the current table missing = set(self.headers) - set(new_order) or {} extra = set(new_order) - set(self.headers) or {} raise ValueError("New order must contain the same headers as the current table.\n" f" - Missing headers: {missing}\n" f" - Extra headers: {extra}") index_map = {old_header: new_order.index(old_header) for old_header in self.headers} # Create a mapping from old index to new index new_rows = [] # Reorder each row according to the new header order for row in self.rows: new_row = [None] * len(row) # Initialize a new row with placeholders for old_index, cell in enumerate(row): new_index = index_map[self.headers[old_index]] new_row[new_index] = cell new_rows.append(new_row) self.headers = list(new_order) # Reorder the headers self.rows = new_rows # Reorder the rows return self def set_footer(self, footer): self.footer = footer return self def set_headers(self, headers): self.headers = headers return self def set_order(self, *new_order): return self.reorder_columns(new_order) def set_title(self, title): self.title = title return self def to_csv(self): csv_content = ','.join(self.to_csv__escape_cell(header) for header in self.headers) + '\n' # Create a CSV string from the headers and rows for row in self.rows: csv_content += ','.join(self.to_csv__escape_cell(cell) if cell is not None else '' for cell in row) + '\n' return csv_content def to_csv__escape_cell(self, cell): if cell and any(c in cell for c in [',', '"', '\n']): cell = cell.replace('"', '""') # Escape double quotes cell = cell.replace('\n', '\\n') # escape new lines return f'"{cell}"' # Enclose the cell in double quotes return cell def to_dict(self): table_dict = {header: [] for header in self.headers} # Initialize the dictionary with empty lists for each header for row in self.rows: # Iterate over each row and append the cell to the corresponding header's list for header, cell in zip(self.headers, row): table_dict[header].append(cell) return table_dict
Ancestors
Class variables
var headers : list
var headers_by_index : dict
var headers_size : list
var headers_to_hide : list
var max_cell_size : int
var rows : list
var rows_texts : list
var table_width : int
var text__all : list
var text__headers : str
var text__table_bottom : str
var text__table_middle : str
var text__table_top : str
var text__title : str
var text__width : int
var title : str
Methods
def add_column(self, header, cells: list)
-
Expand source code
def add_column(self, header, cells:list): self.fix_table() columns_count = len(self.headers) self.add_header(header) for index, cell in enumerate(cells): if len(self.rows) <= index: new_row = ['' for _ in range(columns_count)] + [cell] self.rows.append(new_row) else: self.rows[index].append(cell) return self
def add_data(self, data)
-
Expand source code
def add_data(self, data): if type(data) is dict: self.add_dict(data) elif type(data) is list: for item in data: self.add_data(item) else: self.add_row(data) return self
def add_dict(self, data: dict)
-
Expand source code
def add_dict(self, data:dict): self.fix_table() # makes sure the number of headers and rows are the same all_headers = set(self.headers) | set(data.keys()) # get all headers from the table and the data for header in sorted(all_headers): # sorted to have consistent order of new headers (since without it the order is pseudo random) if header not in self.headers: # to make sure the table headers and new data keys match self.add_header(header) # add any new headers not already present row_raw = {header: '' for header in all_headers} # Create a raw row with empty values for all headers row_raw.update(data) # Update the raw row with values from data row_by_header = [row_raw[header] for header in self.headers] # create a new row object, ensuring headers order self.add_row(row_by_header) # add the new row to the table return self
def add_header(self, header: str)
-
Expand source code
def add_header(self, header:str): self.headers.append(header) return self
def add_headers(self, *headers: list)
-
Expand source code
def add_headers(self, *headers:list): for header in headers: self.add_header(header) return self
def add_row(self, row: list)
-
Expand source code
def add_row(self, row:list): if type(row) is not list: self.rows.append([row]) else: self.rows.append(row) return self
def add_rows(self, rows: list)
-
Expand source code
def add_rows(self, rows:list): for row in rows: self.add_row(row) return self
def calculate_max_cell_size(self, cell)
-
Expand source code
def calculate_max_cell_size(self, cell): lines_len = [] for line in str(cell).split('\n'): # Split the cell into lines and find the maximum length of any line line_len_ansi_visible = ansi_text_visible_length(line) # add support for the use of ansi chars (which impact the len calculations) lines_len.append(line_len_ansi_visible) max_cell_line_length = max(lines_len) if max_cell_line_length > self.max_cell_size: max_cell_line_length = self.max_cell_size return max_cell_line_length
def cell_value(self, cell_value)
-
Expand source code
def cell_value(self, cell_value): cell_value = str(cell_value) if len(cell_value) > self.max_cell_size: return cell_value[:self.max_cell_size - 3] + '...' return cell_value
def fix_table(self)
-
Expand source code
def fix_table(self): if self.rows: max_cells = max(len(row) for row in self.rows) # get max number of cells in any row else: max_cells = 0 extra_header_count = len(self.headers) + 1 # Start counting extra headers from the current number of headers while len(self.headers) < max_cells: # Extend headers if necessary self.headers.append(f"Header #{extra_header_count}") # headers cannot have empty values extra_header_count += 1 for row in self.rows: # Ensure each row has the same number of cells as there are headers while len(row) < len(self.headers): row.append("") for index, header in enumerate(self.headers): # capture the index of the headers self.headers_by_index[index] = header
def hide_headers(self, headers)
-
Expand source code
def hide_headers(self, headers): self.headers_to_hide = headers return self
def map_headers_size(self)
-
Expand source code
def map_headers_size(self): self.headers_size = [] # initialize the headers size with the size of each header for header in self.headers: header_len_ansi_visible = ansi_text_visible_length(header) self.headers_size.append(header_len_ansi_visible) for row in self.rows: # iterate over each row and update the headers size with the size of the largest cell for index, cell in enumerate(row): # for each row if cell: # Check if the cell is not empty or None max_cell_line_length = self.calculate_max_cell_size(cell) self.headers_size[index] = max(self.headers_size[index], max_cell_line_length) # Update the corresponding header size if this line is longer than the current max # fix edge case that happens when the title or footer is longer than the table width if len(self.headers_size): last_header = len(self.headers_size) - 1 # get the index of the last header last_header_size = self.headers_size[last_header] # get the size of the last header all_headers_size = sum(self.headers_size) # get the size of all headers all_headers_size_minus_last = all_headers_size - last_header_size # get the size of all headers minus the last header if sum(self.headers_size) < len(self.title): # if the title is longer than the headers, update the last header size title_size = len(self.title) # get the size of the title new_last_header_size = title_size - all_headers_size_minus_last # calculate the new size of the last header self.headers_size[last_header] = new_last_header_size # update the last header size if sum(self.headers_size) < len(self.footer): # if the footer is longer than the headers, update the last header size footer_size = len(self.footer) # get the size of the footer new_last_header_size = footer_size - all_headers_size_minus_last # calculate the new size of the last header self.headers_size[last_header] = new_last_header_size # update the last header size return self
def map_rows_texts(self)
-
Expand source code
def map_rows_texts(self): self.rows_texts = [] #if not self.rows: # self.rows_texts = [f"{CHAR_TABLE_VERTICAL}aaa{CHAR_TABLE_VERTICAL}"] if self.rows: for row in self.rows: row_text = CHAR_TABLE_VERTICAL additional_lines = [[] for _ in row] # Prepare to hold additional lines from multiline cells for index, cell in enumerate(row): if self.should_show_header(index): size = self.headers_size[index] cell_lines = str(cell).split('\n') # Split the cell text by newlines cell_value = self.cell_value(cell_lines[0]) extra_padding = ' ' * (size - ansi_text_visible_length(cell_value)) row_text += f" {cell_value}{extra_padding} {CHAR_TABLE_VERTICAL}" # Add the first line of the cell for i, line in enumerate(cell_lines[1:], start=1): additional_lines[index].append(line) # Store additional lines self.rows_texts.append(row_text) # Handle additional lines by creating new row_texts for them max_additional_lines = max(len(lines) for lines in additional_lines) for depth in range(max_additional_lines): extra_row_text = CHAR_TABLE_VERTICAL for index, column in enumerate(additional_lines): cell_data = column[depth] if len(column) > depth else '' size = self.headers_size[index] cell_value = self.cell_value(cell_data) extra_padding = ' ' * (size - ansi_text_visible_length(cell_value)) extra_row_text += f" {cell_value}{extra_padding} {CHAR_TABLE_VERTICAL}" self.rows_texts.append(extra_row_text) return self
def map_table_width(self)
-
Expand source code
def map_table_width(self): self.table_width = len(self.text__headers) if len(self.footer) > self.table_width: self.table_width = len(self.footer) + 4 if len(self.title) > self.table_width: self.table_width = len(self.title) + 4
def map_text__all(self)
-
Expand source code
def map_text__all(self): self.text__all = [ self.text__table_top ] if self.title : self.text__all += [ self.text__title , self.text__table_middle ] if self.headers : self.text__all += [ self.text__headers , self.text__table_middle ] if self.rows : self.text__all += [ *self.rows_texts ] if self.footer : self.text__all += [ self.text__table_middle , self.text__footer ] self.text__all += [ self.text__table_bottom ]
-
Expand source code
def map_text__footer(self): self.text__footer = f"{CHAR_TABLE_VERTICAL} {self.footer:{self.text__width}} {CHAR_TABLE_VERTICAL}"
def map_text__headers(self)
-
Expand source code
def map_text__headers(self): self.text__headers = CHAR_TABLE_VERTICAL if not self.headers: self.text__headers += f" {CHAR_TABLE_VERTICAL}" else: for header, size in zip(self.headers, self.headers_size): if self.should_show_header(header): self.text__headers += f" {header:{size}} {CHAR_TABLE_VERTICAL}" return self
def map_text__table_bottom(self)
-
Expand source code
def map_text__table_bottom(self): self.text__table_bottom = f"{CHAR_TABLE_BOTTOM_LEFT}" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_BOTTOM_RIGHT }"
def map_text__table_middle(self)
-
Expand source code
def map_text__table_middle(self): self.text__table_middle = f"{CHAR_TABLE_MIDDLE_LEFT}" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_MIDDLE_RIGHT }"
def map_text__table_top(self)
-
Expand source code
def map_text__table_top (self): self.text__table_top = f"{CHAR_TABLE_TOP_LEFT }" + CHAR_TABLE_HORIZONTAL * (self.text__width + 2) + f"{CHAR_TABLE_TOP_RIGHT }"
def map_text__title(self)
-
Expand source code
def map_text__title(self): self.text__title = f"{CHAR_TABLE_VERTICAL} {self.title:{self.text__width}} {CHAR_TABLE_VERTICAL}"
def map_text__width(self)
-
Expand source code
def map_text__width(self): self.text__width = self.table_width - 4 # if self.table_width > 3: # there is no use case that that needs this check # self.text__width = self.table_width - 4 # else: # self.text__width = 0
def map_texts(self)
-
Expand source code
def map_texts(self): self.fix_table () self.map_headers_size () self.map_text__headers () self.map_rows_texts () self.map_table_width () self.map_text__width () self.map_text__footer () self.map_text__title () self.map_text__table_bottom () self.map_text__table_middle () self.map_text__table_top () self.map_text__all ()
def print(self, data=None, order=None)
-
Expand source code
def print(self, data=None, order=None): if data: self.add_data(data) if order: self.reorder_columns(order) print() self.map_texts() for text in self.text__all: print(text) return self
def remove_columns(self, column_names)
-
Expand source code
def remove_columns(self,column_names): if type (column_names) is str: column_names = [column_names] if type(column_names) is list: for column_name in column_names: if column_name in self.headers: column_index = self.headers.index(column_name) del self.headers[column_index] for row in self.rows: del row[column_index] return self
def reorder_columns(self, new_order: list)
-
Expand source code
def reorder_columns(self, new_order: list): if set(new_order) != set(self.headers): # Check if the new_order list has the same headers as the current table missing = set(self.headers) - set(new_order) or {} extra = set(new_order) - set(self.headers) or {} raise ValueError("New order must contain the same headers as the current table.\n" f" - Missing headers: {missing}\n" f" - Extra headers: {extra}") index_map = {old_header: new_order.index(old_header) for old_header in self.headers} # Create a mapping from old index to new index new_rows = [] # Reorder each row according to the new header order for row in self.rows: new_row = [None] * len(row) # Initialize a new row with placeholders for old_index, cell in enumerate(row): new_index = index_map[self.headers[old_index]] new_row[new_index] = cell new_rows.append(new_row) self.headers = list(new_order) # Reorder the headers self.rows = new_rows # Reorder the rows return self
-
Expand source code
def set_footer(self, footer): self.footer = footer return self
def set_headers(self, headers)
-
Expand source code
def set_headers(self, headers): self.headers = headers return self
def set_order(self, *new_order)
-
Expand source code
def set_order(self, *new_order): return self.reorder_columns(new_order)
def set_title(self, title)
-
Expand source code
def set_title(self, title): self.title = title return self
def should_show_header(self, header)
-
Expand source code
def should_show_header(self, header): if self.headers_to_hide: if type(header) is int: header_name = self.headers_by_index[header] else: header_name = str(header) return header_name not in self.headers_to_hide return True
def to_csv(self)
-
Expand source code
def to_csv(self): csv_content = ','.join(self.to_csv__escape_cell(header) for header in self.headers) + '\n' # Create a CSV string from the headers and rows for row in self.rows: csv_content += ','.join(self.to_csv__escape_cell(cell) if cell is not None else '' for cell in row) + '\n' return csv_content
def to_csv__escape_cell(self, cell)
-
Expand source code
def to_csv__escape_cell(self, cell): if cell and any(c in cell for c in [',', '"', '\n']): cell = cell.replace('"', '""') # Escape double quotes cell = cell.replace('\n', '\\n') # escape new lines return f'"{cell}"' # Enclose the cell in double quotes return cell
def to_dict(self)
-
Expand source code
def to_dict(self): table_dict = {header: [] for header in self.headers} # Initialize the dictionary with empty lists for each header for row in self.rows: # Iterate over each row and append the cell to the corresponding header's list for header, cell in zip(self.headers, row): table_dict[header].append(cell) return table_dict
Inherited members