diff --git a/odxtools/cli/_print_utils.py b/odxtools/cli/_print_utils.py index 4050a1e0..84f58fd1 100644 --- a/odxtools/cli/_print_utils.py +++ b/odxtools/cli/_print_utils.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MIT import re import textwrap -from typing import Any, Callable, Dict, List, Optional, Union +from typing import List, Optional, Tuple, Union import markdownify -from rich.table import Table -from rich.padding import Padding +from rich import print as rich_print +from rich.padding import Padding as RichPadding +from rich.table import Table as RichTable from ..description import Description from ..diaglayers.diaglayer import DiagLayer @@ -37,110 +38,129 @@ def print_diagnostic_service(service: DiagService, print_pre_condition_states: bool = False, print_state_transitions: bool = False, print_audiences: bool = False, - allow_unknown_bit_lengths: bool = False, - print_fn: Callable[..., Any] = print) -> None: + allow_unknown_bit_lengths: bool = False) -> None: - print_fn(f" Service '{service.short_name}':") + rich_print(f" Service '{service.short_name}':") if service.description: desc = format_desc(service.description, indent=3) - print_fn(f" Description: " + desc) + rich_print(f" Description: " + desc) if print_pre_condition_states and len(service.pre_condition_states) > 0: pre_condition_states_short_names = [ pre_condition_state.short_name for pre_condition_state in service.pre_condition_states ] - print_fn(f" Pre-Condition States: {', '.join(pre_condition_states_short_names)}") + rich_print(f" Pre-Condition States: {', '.join(pre_condition_states_short_names)}") if print_state_transitions and len(service.state_transitions) > 0: state_transitions = [ f"{state_transition.source_snref} -> {state_transition.target_snref}" for state_transition in service.state_transitions ] - print_fn(f" State Transitions: {', '.join(state_transitions)}") + rich_print(f" State Transitions: {', '.join(state_transitions)}") if print_audiences and service.audience: enabled_audiences_short_names = [ enabled_audience.short_name for enabled_audience in service.audience.enabled_audiences ] - print_fn(f" Enabled Audiences: {', '.join(enabled_audiences_short_names)}") + rich_print(f" Enabled Audiences: {', '.join(enabled_audiences_short_names)}") if print_params: - print_service_parameters( - service, allow_unknown_bit_lengths=allow_unknown_bit_lengths, print_fn=print_fn) + print_service_parameters(service, allow_unknown_bit_lengths=allow_unknown_bit_lengths) def print_service_parameters(service: DiagService, - allow_unknown_bit_lengths: bool = False, - print_fn: Callable[..., Any] = print) -> None: - # prints parameter details of request, positive response and negative response of diagnostic service + *, + allow_unknown_bit_lengths: bool = False) -> None: + # prints parameter details of request, positive response and + # negative response of diagnostic service # Request if service.request: - print_fn(f" Request '{service.request.short_name}':") + rich_print(f" Request '{service.request.short_name}':") const_prefix = service.request.coded_const_prefix() - print_fn( + rich_print( f" Identifying Prefix: 0x{const_prefix.hex().upper()} ({bytes(const_prefix)!r})") - print_fn(f" Parameters:") - table = extract_parameter_tabulation_data(list(service.request.parameters)) - print_fn(Padding(table, pad=(0, 0, 0, 4))) - print_fn() + rich_print(f" Parameters:") + param_table = extract_parameter_tabulation_data(service.request.parameters) + rich_print(RichPadding(param_table, pad=(0, 0, 0, 4))) + rich_print() else: - print_fn(f" No Request!") + rich_print(f" No Request!") # Positive Responses if not service.positive_responses: - print_fn(f" No positive responses") + rich_print(f" No positive responses") for resp in service.positive_responses: - print_fn(f" Positive Response '{resp.short_name}':") - print_fn(f" Parameters:\n") + rich_print(f" Positive Response '{resp.short_name}':") + rich_print(f" Parameters:\n") table = extract_parameter_tabulation_data(list(resp.parameters)) - print_fn(Padding(table, pad=(0, 0, 0, 4))) - print_fn() + rich_print(RichPadding(table, pad=(0, 0, 0, 4))) + rich_print() # Negative Response if not service.negative_responses: - print_fn(f" No negative responses") + rich_print(f" No negative responses") for resp in service.negative_responses: - print_fn(f" Negative Response '{resp.short_name}':") - print_fn(f" Parameters:\n") + rich_print(f" Negative Response '{resp.short_name}':") + rich_print(f" Parameters:\n") table = extract_parameter_tabulation_data(list(resp.parameters)) - print_fn(Padding(table, pad=(0, 0, 0, 4))) - print_fn() + rich_print(RichPadding(table, pad=(0, 0, 0, 4))) + rich_print() - print_fn("\n") + rich_print("\n") -def extract_service_tabulation_data(services: List[DiagService]) -> Dict[str, Any]: - # extracts data of diagnostic services into Dictionary which can be printed by tabulate module - # TODO: consider indentation +def extract_service_tabulation_data(services: List[DiagService], + *, + additional_columns: Optional[List[Tuple[str, List[str]]]] = None + ) -> RichTable: + """Extracts data of diagnostic services into Dictionary which can + be printed by tabulate module + """ - name = [] - semantic = [] - request: List[Optional[str]] = [] + # Create Rich table + table = RichTable( + title="", show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) + + name_column: List[str] = [] + semantic_column: List[str] = [] + request_column: List[str] = [] for service in services: - name.append(service.short_name) - semantic.append(service.semantic) + name_column.append(service.short_name) + semantic_column.append(service.semantic or "") if service.request: prefix = service.request.coded_const_prefix() - request.append(f"0x{str(prefix.hex().upper())[:32]}...") if len( - prefix) > 32 else request.append(f"0x{str(prefix.hex().upper())}") + request_column.append(f"0x{str(prefix.hex().upper())[:32]}...") if len( + prefix) > 32 else request_column.append(f"0x{str(prefix.hex().upper())}") else: - request.append(None) + request_column.append("") - return {'Name': name, 'Semantic': semantic, 'Hex-Request': request} + table.add_column("Name", style="green") + table.add_column("Semantic", justify="left", style="white") + table.add_column("Request", justify="left", style="white") + if additional_columns is not None: + for ac_title, _ in additional_columns: + table.add_column(ac_title, justify="left", style="white") + + rows = zip(name_column, semantic_column, request_column, + *[ac[1] for ac in additional_columns]) + for row in rows: + table.add_row(*map(str, row)) + + return table -def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Table: - # extracts data of parameters of diagnostic services into Dictionary which can be printed by tabulate module - # TODO: consider indentation +def extract_parameter_tabulation_data(parameters: List[Parameter]) -> RichTable: + # extracts data of parameters of diagnostic services into + # a RichTable object that can be printed # Create Rich table - table = Table( + table = RichTable( title="", show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) # Add columns with appropriate styling @@ -154,42 +174,43 @@ def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Table: table.add_column("Value Type", justify="left", style="white") table.add_column("Linked DOP", justify="left", style="white") - name: List[str] = [] - byte: List[Optional[int]] = [] - bit_length: List[Optional[int]] = [] - semantic: List[Optional[str]] = [] - param_type: List[Optional[str]] = [] - value: List[Optional[str]] = [] - value_type: List[Optional[str]] = [] - data_type: List[Optional[str]] = [] - dop: List[Optional[str]] = [] + name_column: List[str] = [] + byte_column: List[str] = [] + bit_length_column: List[str] = [] + semantic_column: List[str] = [] + param_type_column: List[str] = [] + value_column: List[str] = [] + value_type_column: List[str] = [] + data_type_column: List[str] = [] + dop_column: List[str] = [] for param in parameters: - name.append(param.short_name) - byte.append(param.byte_position) - semantic.append(param.semantic) - param_type.append(param.parameter_type) + name_column.append(param.short_name) + byte_column.append("" if param.byte_position is None else str(param.byte_position)) + semantic_column.append(param.semantic or "") + param_type_column.append(param.parameter_type) length = 0 if param.get_static_bit_length() is not None: - bit_length.append(param.get_static_bit_length()) - length = (param.get_static_bit_length() or 0) // 4 + n = param.get_static_bit_length() + bit_length_column.append("" if n is None else str(n)) + length = (n or 0) // 4 else: - bit_length.append(None) + bit_length_column.append("") if isinstance(param, CodedConstParameter): if isinstance(param.coded_value, int): - value.append(f"0x{param.coded_value:0{length}X}") + value_column.append(f"0x{param.coded_value:0{length}X}") elif isinstance(param.coded_value, bytes) or isinstance(param.coded_value, bytearray): - value.append(f"0x{param.coded_value.hex().upper()}") + value_column.append(f"0x{param.coded_value.hex().upper()}") else: - value.append(f"{param.coded_value!r}") - data_type.append(param.diag_coded_type.base_data_type.name) - value_type.append('coded value') - dop.append(None) + value_column.append(f"{param.coded_value!r}") + data_type_column.append(param.diag_coded_type.base_data_type.name) + value_type_column.append('coded value') + dop_column.append("") elif isinstance(param, NrcConstParameter): - data_type.append(param.diag_coded_type.base_data_type.name) - value.append(str(param.coded_values)) - value_type.append('coded values') - dop.append(None) + data_type_column.append(param.diag_coded_type.base_data_type.name) + value_column.append(str(param.coded_values)) + value_type_column.append('coded values') + dop_column.append("") elif isinstance(param, (PhysicalConstantParameter, SystemParameter, ValueParameter)): # this is a hack to make this routine work for parameters # which reference DOPs of a type that a is not yet @@ -198,55 +219,53 @@ def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Table: param_dop = getattr(param, "_dop", None) if param_dop is not None: - dop.append(param_dop.short_name) + dop_column.append(param_dop.short_name) if param_dop is not None and (phys_type := getattr(param, "physical_type", None)) is not None: - data_type.append(phys_type.base_data_type.name) + data_type_column.append(phys_type.base_data_type.name) else: - data_type.append(None) + data_type_column.append("") if isinstance(param, PhysicalConstantParameter): if isinstance(param.physical_constant_value, bytes) or isinstance( param.physical_constant_value, bytearray): - value.append(f"0x{param.physical_constant_value.hex().upper()}") + value_column.append(f"0x{param.physical_constant_value.hex().upper()}") else: - value.append(f"{param.physical_constant_value!r}") - value_type.append('constant value') + value_column.append(f"{param.physical_constant_value!r}") + value_type_column.append('constant value') elif isinstance(param, ValueParameter) and param.physical_default_value is not None: if isinstance(param.physical_default_value, bytes) or isinstance( param.physical_default_value, bytearray): - value.append(f"0x{param.physical_default_value.hex().upper()}") + value_column.append(f"0x{param.physical_default_value.hex().upper()}") else: - value.append(f"{param.physical_default_value!r}") - value_type.append('default value') + value_column.append(f"{param.physical_default_value!r}") + value_type_column.append('default value') else: - value.append(None) - value_type.append(None) + value_column.append("") + value_type_column.append("") else: - value.append(None) - data_type.append(None) - value_type.append(None) - dop.append(None) + value_column.append("") + data_type_column.append("") + value_type_column.append("") + dop_column.append("") - for lst in [byte, semantic, bit_length, value, value_type, data_type, dop]: - lst[:] = ["" if x is None else x for x in lst] # type: ignore[attr-defined, index] # Add all rows at once by zipping dictionary values - rows = zip(name, byte, bit_length, semantic, param_type, data_type, value, value_type, dop) + rows = zip(name_column, byte_column, bit_length_column, semantic_column, param_type_column, + data_type_column, value_column, value_type_column, dop_column) for row in rows: table.add_row(*map(str, row)) return table -def print_dl_metrics(variants: List[DiagLayer], print_fn: Callable[..., Any] = print) -> None: +def print_dl_metrics(variants: List[DiagLayer]) -> None: """ Print diagnostic layer metrics using Rich tables. Args: variants: List of diagnostic layer variants to analyze - print_fn: Optional callable for custom print handling (defaults to built-in print) """ # Create Rich table - table = Table( + table = RichTable( title="", show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) # Add columns with appropriate styling @@ -267,4 +286,4 @@ def print_dl_metrics(variants: List[DiagLayer], print_fn: Callable[..., Any] = p table.add_row(variant.short_name, variant.variant_type.value, str(len(all_services)), str(len(ddds.data_object_props)), str(len(getattr(variant, "comparams_refs", [])))) - print_fn(table) + rich_print(table) diff --git a/odxtools/cli/compare.py b/odxtools/cli/compare.py index a9b689f0..609344c9 100644 --- a/odxtools/cli/compare.py +++ b/odxtools/cli/compare.py @@ -5,8 +5,9 @@ import os from typing import Any, Dict, List, Optional, Set, Union, cast -from rich import print -from tabulate import tabulate # TODO: switch to rich tables +from rich import print as rich_print +from rich.padding import Padding as RichPadding +from rich.table import Table as RichTable from ..database import Database from ..diaglayers.diaglayer import DiagLayer @@ -46,11 +47,14 @@ class Display: # class with variables and functions to display the result of the comparison # TODO - # Idea: results as json export - # - write results of comparison in json structure - # - use odxlinks to refer to dignostic services / objects if changes have already been detected (e.g. in another ecu variant / diagnostic layer) - - # print all information about parameter properties (request, pos. response & neg. response parameters) for changed diagnostic services + # - Idea: results as json export + # - write results of comparison in json structure + # - use odxlinks to refer to dignostic services / objects if + # changes have already been detected (e.g. in another ecu + # variant / diagnostic layer) + # - print all information about parameter properties (request, + # pos. response & neg. response parameters) for changed diagnostic + # services param_detailed: bool obj_detailed: bool @@ -62,46 +66,45 @@ def print_dl_changes(self, service_dict: SpecsServiceDict) -> None: if service_dict["new_services"] or service_dict["deleted_services"] or service_dict[ "changed_name_of_service"][0] or service_dict["changed_parameters_of_service"][0]: assert isinstance(service_dict["diag_layer"], str) - print() - print( + rich_print() + rich_print( f"Changed diagnostic services for diagnostic layer '{service_dict['diag_layer']}' ({service_dict['diag_layer_type']}):" ) if service_dict["new_services"]: assert isinstance(service_dict["new_services"], List) - print() - print(" [blue]New services[/blue]") - table = extract_service_tabulation_data( - service_dict["new_services"]) # type: ignore[arg-type] - print(tabulate(table, headers="keys", tablefmt="presto")) + rich_print() + rich_print(" [blue]New services[/blue]") + rich_print(extract_service_tabulation_data( + service_dict["new_services"])) # type: ignore[arg-type] if service_dict["deleted_services"]: assert isinstance(service_dict["deleted_services"], List) - print() - print(" [blue]Deleted services[/blue]") - table = extract_service_tabulation_data( - service_dict["deleted_services"]) # type: ignore[arg-type] - print(tabulate(table, headers="keys", tablefmt="presto")) + rich_print() + rich_print(" [blue]Deleted services[/blue]") + rich_print(extract_service_tabulation_data( + service_dict["deleted_services"])) # type: ignore[arg-type] if service_dict["changed_name_of_service"][0]: - print() - print(" [blue]Renamed services[/blue]") - table = extract_service_tabulation_data( - service_dict["changed_name_of_service"][0]) # type: ignore[arg-type] - table["Old service name"] = service_dict["changed_name_of_service"][1] - print(tabulate(table, headers="keys", tablefmt="presto")) + rich_print() + rich_print(" [blue]Renamed services[/blue]") + rich_print(extract_service_tabulation_data( + service_dict["changed_name_of_service"][0])) # type: ignore[arg-type] if service_dict["changed_parameters_of_service"][0]: - print() - print(" [blue]Services with parameter changes[/blue]") + rich_print() + rich_print(" [blue]Services with parameter changes[/blue]") # create table with information about services with parameter changes + changed_param_column = [ + str(x) for x in service_dict["changed_parameters_of_service"][ + 1] # type: ignore[union-attr] + ] table = extract_service_tabulation_data( - service_dict["changed_parameters_of_service"][0]) # type: ignore[arg-type] - # add information about changed parameters - table["Changed parameters"] = service_dict["changed_parameters_of_service"][1] - print(tabulate(table, headers="keys", tablefmt="presto")) + service_dict["changed_parameters_of_service"][0], # type: ignore[arg-type] + additional_columns=[("Changed Parameters", changed_param_column)]) + rich_print(table) for service_idx, service in enumerate( service_dict["changed_parameters_of_service"][0]): # type: ignore[arg-type] assert isinstance(service, DiagService) - print() - print( + rich_print() + rich_print( f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]" ) # detailed_info in [infotext1, dict1, infotext2, dict2, ...] @@ -110,10 +113,22 @@ def print_dl_changes(self, service_dict: SpecsServiceDict) -> None: service_dict["changed_parameters_of_service"])[2][service_idx] for detailed_info in info_list: if isinstance(detailed_info, str): - print() - print(detailed_info) + rich_print() + rich_print(detailed_info) elif isinstance(detailed_info, dict): - print(tabulate(detailed_info, headers="keys", tablefmt="presto")) + table = RichTable( + show_header=True, + header_style="bold cyan", + border_style="blue", + show_lines=True) + for header in detailed_info: + table.add_column(header) + rows = zip(*detailed_info.values()) + for row in rows: + table.add_row(*map(str, row)) + + rich_print(RichPadding(table, pad=(0, 0, 0, 4))) + rich_print() if self.param_detailed: # print all parameter details of diagnostic service print_service_parameters(service, allow_unknown_bit_lengths=True) @@ -124,16 +139,18 @@ def print_database_changes(self, changes_variants: SpecsChangesVariants) -> None # diagnostic layers if changes_variants["new_diagnostic_layers"] or changes_variants[ "deleted_diagnostic_layers"]: - print() - print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") - print(" New diagnostic layers: ") + rich_print() + rich_print("[bright_blue]Changed diagnostic layers[/bright_blue]: ") + rich_print(" New diagnostic layers: ") for variant in changes_variants["new_diagnostic_layers"]: assert isinstance(variant, DiagLayer) - print(f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") - print(" Deleted diagnostic layers: ") + rich_print( + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + rich_print(" Deleted diagnostic layers: ") for variant in changes_variants["deleted_diagnostic_layers"]: assert isinstance(variant, DiagLayer) - print(f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") + rich_print( + f" [magenta]{variant.short_name}[/magenta] ({variant.variant_type.value})") # diagnostic services for _, value in changes_variants.items(): @@ -275,8 +292,8 @@ def compare_services(self, service1: DiagService, if res1_idx == res2_idx: # find changed request parameter properties table = self.compare_parameters(param1, param2) - infotext = (f" Properties of request parameter '{param2.short_name}'" - f" have changed\n") + infotext = (f" Properties of request parameter '{param2.short_name}' " + f"that have changed:\n") # array index starts with 0 -> param[0] is 1. service parameter if table["Property"]: @@ -311,8 +328,8 @@ def compare_services(self, service1: DiagService, # find changed positive response parameter properties table = self.compare_parameters(param1, param2) infotext = ( - f" Properties of positive response parameter '{param2.short_name}'" - f"have changed\n") + f" Properties of positive response parameter '{param2.short_name}' that " + f"have changed:\n") # array index starts with 0 -> param[0] is first service parameter if table["Property"]: @@ -354,7 +371,7 @@ def compare_services(self, service1: DiagService, if param1_idx == param2_idx: # find changed negative response parameter properties table = self.compare_parameters(param1, param2) - infotext = f" Properties of response parameter '{param2.short_name}' have changed\n" + infotext = f" Properties of response parameter '{param2.short_name}' that have changed:\n" # array index starts with 0 -> param[0] is 1. service parameter if table["Property"]: @@ -622,7 +639,7 @@ def run(args: argparse.Namespace) -> None: for name in args.variants: if name not in task.diagnostic_layer_names: - print(f"The variant '{name}' could not be found!") + rich_print(f"The variant '{name}' could not be found!") return task.db_indicator_1 = 0 @@ -632,19 +649,20 @@ def run(args: argparse.Namespace) -> None: break task.db_indicator_2 = db_idx + 1 - print() - print(f"Changes in file '{os.path.basename(db_names[0])}'") - print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") + rich_print() + rich_print(f"Changes in file '{os.path.basename(db_names[0])}'") + rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") - print() - print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") + rich_print() + rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") print_dl_metrics([ variant for variant in task.databases[0].diag_layers if variant.short_name in task.diagnostic_layer_names ]) - print() - print(f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + rich_print() + rich_print( + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") print_dl_metrics([ variant for variant in task.databases[db_idx + 1].diag_layers if variant.short_name in task.diagnostic_layer_names @@ -673,16 +691,17 @@ def run(args: argparse.Namespace) -> None: break task.db_indicator_2 = db_idx + 1 - print() - print(f"Changes in file '{os.path.basename(db_names[0])}") - print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") + rich_print() + rich_print(f"Changes in file '{os.path.basename(db_names[0])}") + rich_print(f" (compared to '{os.path.basename(db_names[db_idx + 1])}')") - print() - print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") + rich_print() + rich_print(f"Overview of diagnostic layers (for {os.path.basename(db_names[0])})") print_dl_metrics(list(task.databases[0].diag_layers)) - print() - print(f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") + rich_print() + rich_print( + f"Overview of diagnostic layers (for {os.path.basename(db_names[db_idx+1])})") print_dl_metrics(list(task.databases[db_idx + 1].diag_layers)) task.print_database_changes( @@ -704,20 +723,20 @@ def run(args: argparse.Namespace) -> None: for name in args.variants: if name not in task.diagnostic_layer_names: - print(f"The variant '{name}' could not be found!") + rich_print(f"The variant '{name}' could not be found!") return - print() - print(f"Overview of diagnostic layers: ") + rich_print() + rich_print(f"Overview of diagnostic layers: ") print_dl_metrics(task.diagnostic_layers) for db_idx, dl in enumerate(task.diagnostic_layers): if db_idx + 1 >= len(task.diagnostic_layers): break - print() - print(f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})") - print( + rich_print() + rich_print(f"Changes in diagnostic layer '{dl.short_name}' ({dl.variant_type.value})") + rich_print( f" (compared to '{task.diagnostic_layers[db_idx + 1].short_name}' ({task.diagnostic_layers[db_idx + 1].variant_type.value}))" ) task.print_dl_changes( @@ -725,4 +744,4 @@ def run(args: argparse.Namespace) -> None: else: # no databases & no variants specified - print("Please specify either a database or variant for a comparison") + rich_print("Please specify either a database or variant for a comparison") diff --git a/odxtools/cli/list.py b/odxtools/cli/list.py index 7d1b6fc4..e92707f6 100644 --- a/odxtools/cli/list.py +++ b/odxtools/cli/list.py @@ -52,7 +52,7 @@ def print_summary(odxdb: Database, if diag_layers: rich.print("\n") rich.print(f"Overview of diagnostic layers: ") - print_dl_metrics(diag_layers, print_fn=rich.print) + print_dl_metrics(diag_layers) for dl in diag_layers: rich.print("\n") @@ -93,8 +93,7 @@ def print_summary(odxdb: Database, print_pre_condition_states=print_pre_condition_states, print_state_transitions=print_state_transitions, print_audiences=print_audiences, - allow_unknown_bit_lengths=allow_unknown_bit_lengths, - print_fn=rich.print) + allow_unknown_bit_lengths=allow_unknown_bit_lengths) elif isinstance(service, SingleEcuJob): rich.print(f" Single ECU job: {service.odx_id}") else: diff --git a/pyproject.toml b/pyproject.toml index 1fafeda5..f839a195 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ dependencies = [ "markdownify >= 0.11", "deprecation >= 2.1", "packaging", - "tabulate >= 0.9", "rich >= 13.7", "typing_extensions >= 4.9", ]