diff --git a/tdp/cli/commands/browse.py b/tdp/cli/commands/browse.py index ba69dafb..1dd34307 100644 --- a/tdp/cli/commands/browse.py +++ b/tdp/cli/commands/browse.py @@ -6,14 +6,16 @@ import click from sqlalchemy import Engine +from tabulate import tabulate from tdp.cli.params import database_dsn_option from tdp.cli.utils import ( print_deployment, - print_object, - print_table, + print_operations, ) -from tdp.core.models import DeploymentModel, OperationModel +from tdp.core.entities.operation import OperationName +from tdp.core.models.deployment_model import DeploymentModel +from tdp.core.models.operation_model import OperationModel from tdp.dao import Dao @@ -58,79 +60,123 @@ def browse( ): """Browse deployments.""" with Dao(db_engine) as dao: - # Print last deployment plan + if plan and last: + raise click.BadOptionUsage( + "--plan", "Cannot use --plan and --last together." + ) + if deployment_id and (plan or last): + raise click.BadOptionUsage( + "deployment_id", "Cannot use deployment_id with --plan or --last." + ) + # Print the planned deployment if plan: - deployment_plan = dao.get_planned_deployment() - if deployment_plan: - _print_deployment(deployment_plan) - else: - click.echo("No planned deployment found") - click.echo("Create a deployment plan using the `tdp plan` command") + browse_plan(dao) return + # Print the last deployment elif last: - deployment = dao.get_last_deployment() - if deployment: - _print_deployment(deployment) - else: - click.echo("No deployment found") - click.echo("Create a deployment plan using the `tdp plan` command") + browse_last(dao) return - - # Print a specific operation - if deployment_id and operation: - _print_operations(dao.get_operation(deployment_id, operation)) - return - # Print a specific deployment - if deployment_id: - deployment = dao.get_deployment(deployment_id) - if deployment: - _print_deployment(deployment) + elif deployment_id and not operation: + browse_deployment(dao, deployment_id) + return + # Print a specific operation + elif deployment_id and operation: + browse_operation(dao, deployment_id, operation) return - # Print all deployments - _print_deployments(dao.get_last_deployments(limit=limit, offset=offset)) + else: + browse_deployments(dao, limit, offset) -def _print_deployments(deployments: Iterable[DeploymentModel]) -> None: - """Print a list of deployments in a human readable format. - - Args: - deployments: List of deployments to print. - """ - if not deployments: - click.echo("No deployment found") +def browse_plan(dao: Dao) -> None: + if deployment_plan := dao.get_planned_deployment(): + print_deployment(deployment_plan, filter_out=["logs"]) + else: + click.echo("No planned deployment found") click.echo("Create a deployment plan using the `tdp plan` command") - print_table( - [d.to_dict(filter_out=["options"]) for d in deployments], - ) - - -def _print_deployment(deployment: DeploymentModel) -> None: - """Print a deployment in a human readable format. - Args: - deployment: Deployment to print. - """ - if not deployment: - click.echo("Deployment does not exist.") - return +def browse_last(dao: Dao) -> None: + if last_deployment := dao.get_last_deployment(): + print_deployment(last_deployment, filter_out=["logs"]) + else: + click.echo("No deployment found") + click.echo("Create a deployment plan using the `tdp plan` command") - print_deployment(deployment, filter_out=["logs"]) +def browse_deployment(dao: Dao, deployment_id: int) -> None: + if deployment := dao.get_deployment(deployment_id): + print_deployment(deployment, filter_out=["logs"]) + else: + click.echo(f"Deployment {deployment_id} does not exist.") + + +def browse_operation(dao: Dao, deployment_id: int, operation: str) -> None: + record = None + # Try to parse the operation argument as an integer + try: + operation_order = int(operation) + record = dao.get_operation(deployment_id, operation_order) + # If the operation argument is not an integer, consider that it is an + # operation name + except ValueError: + # Check if the operation name is valid + try: + OperationName.from_str(operation) + except ValueError as e: + raise click.BadParameter( + f"Operation {operation} is not a valid operation name." + ) from e + operations = dao.get_operations_by_name(deployment_id, operation) + if len(operations) == 1: + record = operations[0] + # If there are multiple operations with the given name, print them asking for a + # specific operation order + elif len(operations) > 1: + click.secho( + f'Multiple operations "{operations[0].operation}" found for ' + + f"deployment {deployment_id}:", + bold=True, + ) + print_operations(operations, filter_out=["logs"]) + click.echo("\nUse the operation order to print a specific operation.") + return + if record: + _print_operation(record) + else: + click.echo(f'Operation "{operation}" not found for deployment {deployment_id}') + click.echo( + "Either the deployment does not exist or the operation is not" + + " found for the deployment." + ) + + +def browse_deployments(dao: Dao, limit: int, offset: int) -> None: + deployments = dao.get_last_deployments(limit=limit, offset=offset) + if len(deployments) > 0: + _print_deployments(deployments) + else: + click.echo("No deployments found.") + click.echo("Create a deployment plan using the `tdp plan` command.") + + +def _print_operation(operation: OperationModel) -> None: + click.echo( + tabulate( + operation.to_dict(filter_out=["logs"]).items(), + tablefmt="plain", + ) + ) + if operation.logs: + click.secho("\nLogs", bold=True) + click.echo(str(operation.logs, "utf-8")) -def _print_operations(operations: list[OperationModel]) -> None: - """Print a list of operations in a human readable format. - Args: - operation: Operation to print. - """ - # Print general operation infos - click.secho("Operation(s) details", bold=True) - for operation in operations: - click.echo(print_object(operation.to_dict())) - # Print operation records - if operation.logs: - click.secho("\nOperations", bold=True) - click.echo(str(operation.logs, "utf-8")) +def _print_deployments(deployments: Iterable[DeploymentModel]) -> None: + click.echo( + tabulate( + [d.to_dict(filter_out=["options"]) for d in deployments], + headers="keys", + ) + ) diff --git a/tdp/cli/commands/status/show.py b/tdp/cli/commands/status/show.py index 8b03f4fc..031a6ccb 100644 --- a/tdp/cli/commands/status/show.py +++ b/tdp/cli/commands/status/show.py @@ -7,6 +7,7 @@ import click from sqlalchemy import Engine +from tabulate import tabulate from tdp.cli.params import ( collections_option, @@ -22,7 +23,6 @@ from tdp.cli.utils import ( check_services_cleanliness, print_hosted_entity_status_log, - print_table, ) from tdp.core.models.sch_status_log_model import SCHStatusLogModel from tdp.core.variables import ClusterVariables @@ -119,6 +119,9 @@ def show( def _print_sch_status_logs(sch_status: Iterable[SCHStatusLogModel]) -> None: - print_table( - [status.to_dict(filter_out=["id", "timestamp"]) for status in sch_status], + click.echo( + tabulate( + [status.to_dict(filter_out=["id", "timestamp"]) for status in sch_status], + headers="keys", + ) ) diff --git a/tdp/cli/utils.py b/tdp/cli/utils.py index 51597098..b56b53ea 100644 --- a/tdp/cli/utils.py +++ b/tdp/cli/utils.py @@ -11,6 +11,7 @@ from tabulate import tabulate from tdp.core.entities.hosted_entity_status import HostedEntityStatus +from tdp.core.models.operation_model import OperationModel if TYPE_CHECKING: from tdp.core.models import DeploymentModel @@ -47,47 +48,45 @@ def print_deployment( ) -> None: # Print general deployment infos click.secho("Deployment details", bold=True) - click.echo(print_object(deployment.to_dict(filter_out=filter_out))) + click.echo( + tabulate( + deployment.to_dict(filter_out=filter_out).items(), + tablefmt="plain", + ) + ) # Print deployment operations click.secho("\nOperations", bold=True) - print_table( - [o.to_dict(filter_out=filter_out) for o in deployment.operations], + print_operations( + deployment.operations, filter_out=[*(filter_out or []), "deployment_id"] ) -def print_object(obj: dict) -> None: - """Print an object in a human readable format. +def print_operations( + operations: Iterable[OperationModel], /, *, filter_out: Optional[list[str]] = None +) -> None: + """Print a list of operations in a human readable format. Args: - obj: Object to print. + operations: List of operations to print. """ click.echo( tabulate( - obj.items(), - tablefmt="plain", + [o.to_dict(filter_out=filter_out) for o in operations], + headers="keys", ) ) -def print_table(rows) -> None: - """Print a list of rows in a human readable format. - - Args: - rows: List of rows to print. - """ +def print_hosted_entity_status_log(sch_status: Iterable[HostedEntityStatus]) -> None: click.echo( tabulate( - rows, + [status.export_tabulate() for status in sch_status], headers="keys", ) ) -def print_hosted_entity_status_log(sch_status: Iterable[HostedEntityStatus]) -> None: - print_table([status.export_tabulate() for status in sch_status]) - - def _parse_line(line: str) -> tuple[str, Optional[str], Optional[list[str]]]: """Parses a line which contains an operation, and eventually a host and extra vars. diff --git a/tdp/dao.py b/tdp/dao.py index e5c2ebf1..a74c0bbf 100644 --- a/tdp/dao.py +++ b/tdp/dao.py @@ -240,10 +240,10 @@ def get_deployment(self, id: int) -> Optional[DeploymentModel]: self._check_session() return self.session.get(DeploymentModel, id) - def get_operation( + def get_operations_by_name( self, deployment_id: int, operation_name: str ) -> list[OperationModel]: - """Get an operation. + """Get all operations for a deployment from their name. Args: deployment_id: The deployment ID. @@ -255,6 +255,21 @@ def get_operation( .all() ) + def get_operation( + self, deployment_id: int, operation_order: int + ) -> Optional[OperationModel]: + """Get an operation by deployment ID and operation order. + + Args: + deployment_id: The deployment ID. + operation_order: The operation order. + """ + return ( + self.session.query(OperationModel) + .filter_by(deployment_id=deployment_id, operation_order=operation_order) + .one_or_none() + ) + def get_planned_deployment(self) -> Optional[DeploymentModel]: self._check_session() return ( @@ -272,7 +287,7 @@ def get_last_deployment(self) -> Optional[DeploymentModel]: def get_last_deployments( self, limit: Optional[int] = None, offset: Optional[int] = None - ) -> Iterable[DeploymentModel]: + ) -> list[DeploymentModel]: """Get last deployments in ascending order. Use limit and offset to paginate the results. diff --git a/tests/unit/test_dao.py b/tests/unit/test_dao.py index 681172b8..c94299b9 100644 --- a/tests/unit/test_dao.py +++ b/tests/unit/test_dao.py @@ -271,7 +271,9 @@ def test_operation(db_engine): session.commit() with Dao(db_engine) as dao: assert assert_equal_values_in_model( - dao.get_operation(deployment_id=1, operation_name="test_operation")[0], + dao.get_operations_by_name( + deployment_id=1, operation_name="test_operation" + )[0], OperationModel( deployment_id=1, operation_order=1,