mirror of
https://github.com/rendercv/rendercv.git
synced 2026-03-13 04:11:41 -04:00
cli: refactor cli
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
The `rendercv.cli` package contains the functions and classes that handle RenderCV's
|
||||
command-line interface (CLI). It uses [Typer](https://typer.tiangolo.com/) to create the
|
||||
CLI and [Rich](https://rich.readthedocs.io/en/latest/) to provide a nice-looking
|
||||
terminal output.
|
||||
"""
|
||||
|
||||
__all__ = ["commands", "printer", "utilities"]
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
`rendercv.cli.commands` module contains all the command-line interface (CLI) commands of
|
||||
RenderCV.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
@@ -7,6 +12,22 @@ import pydantic
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from .. import __version__
|
||||
from .. import data_models as dm
|
||||
from .. import renderer as r
|
||||
from .printer import (
|
||||
LiveProgressReporter,
|
||||
error,
|
||||
information,
|
||||
warn_if_new_version_is_available,
|
||||
warning,
|
||||
welcome,
|
||||
)
|
||||
from .utilities import (
|
||||
copy_templates,
|
||||
handle_exceptions,
|
||||
parse_render_command_override_arguments,
|
||||
)
|
||||
|
||||
app = typer.Typer(
|
||||
rich_markup_mode="rich",
|
||||
@@ -127,7 +148,7 @@ def cli_command_render(
|
||||
] = None,
|
||||
extra_data_model_override_argumets: typer.Context = None,
|
||||
):
|
||||
"""Generate a $\\LaTeX$ CV from a YAML input file."""
|
||||
"""Render a CV from a YAML input file."""
|
||||
welcome()
|
||||
|
||||
input_file_path = pathlib.Path(input_file_name).absolute()
|
||||
@@ -161,7 +182,7 @@ def cli_command_render(
|
||||
key_and_values = dict()
|
||||
|
||||
if extra_data_model_override_argumets:
|
||||
key_and_values = parse_data_model_override_arguments(
|
||||
key_and_values = parse_render_command_override_arguments(
|
||||
extra_data_model_override_argumets
|
||||
)
|
||||
for key, value in key_and_values.items():
|
||||
@@ -269,7 +290,7 @@ def cli_command_new(
|
||||
),
|
||||
] = False,
|
||||
):
|
||||
"""Generate a YAML input file to get started."""
|
||||
"""Generate a YAML input file and the LaTeX and Markdown source files."""
|
||||
created_files_and_folders = []
|
||||
|
||||
input_file_name = f"{full_name.replace(' ', '_')}_CV.yaml"
|
||||
@@ -333,7 +354,7 @@ def cli_command_create_theme(
|
||||
),
|
||||
] = "classic",
|
||||
):
|
||||
"""Create a custom theme folder based on an existing theme."""
|
||||
"""Create a custom theme based on an existing theme."""
|
||||
if based_on not in dm.available_themes:
|
||||
error(
|
||||
f'The theme "{based_on}" is not in the list of available themes:'
|
||||
@@ -378,6 +399,8 @@ def main(
|
||||
Optional[bool], typer.Option("--version", "-v", help="Show the version.")
|
||||
] = None,
|
||||
):
|
||||
"""If the `--version` option is used, then show the version. Otherwise, show the
|
||||
help message (see `no_args_is_help` argument of `typer.Typer` object)."""
|
||||
if version_requested:
|
||||
there_is_a_new_version = warn_if_new_version_is_available()
|
||||
if not there_is_a_new_version:
|
||||
|
||||
184
rendercv/cli/printer.py
Normal file
184
rendercv/cli/printer.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
`rendercv.cli.printer` module contains all the functions and classes that are used to
|
||||
print nice-looking messages to the terminal.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import rich
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from .. import __version__
|
||||
from .utilities import get_latest_version_number_from_pypi
|
||||
|
||||
|
||||
class LiveProgressReporter(rich.live.Live):
|
||||
"""This class is a wrapper around `rich.live.Live` that provides the live progress
|
||||
reporting functionality.
|
||||
|
||||
Args:
|
||||
number_of_steps (int): The number of steps to be finished.
|
||||
"""
|
||||
|
||||
def __init__(self, number_of_steps: int, end_message: str = "Your CV is rendered!"):
|
||||
class TimeElapsedColumn(rich.progress.ProgressColumn):
|
||||
def render(self, task: "rich.progress.Task") -> rich.text.Text:
|
||||
elapsed = task.finished_time if task.finished else task.elapsed
|
||||
delta = f"{elapsed:.1f} s"
|
||||
return rich.text.Text(str(delta), style="progress.elapsed")
|
||||
|
||||
self.step_progress = rich.progress.Progress(
|
||||
TimeElapsedColumn(), rich.progress.TextColumn("{task.description}")
|
||||
)
|
||||
|
||||
self.overall_progress = rich.progress.Progress(
|
||||
TimeElapsedColumn(),
|
||||
rich.progress.BarColumn(),
|
||||
rich.progress.TextColumn("{task.description}"),
|
||||
)
|
||||
|
||||
self.group = rich.console.Group(
|
||||
rich.panel.Panel(rich.console.Group(self.step_progress)),
|
||||
self.overall_progress,
|
||||
)
|
||||
|
||||
self.overall_task_id = self.overall_progress.add_task("", total=number_of_steps)
|
||||
self.number_of_steps = number_of_steps
|
||||
self.end_message = end_message
|
||||
self.current_step = 0
|
||||
self.overall_progress.update(
|
||||
self.overall_task_id,
|
||||
description=(
|
||||
f"[bold #AAAAAA]({self.current_step} out of"
|
||||
f" {self.number_of_steps} steps finished)"
|
||||
),
|
||||
)
|
||||
super().__init__(self.group)
|
||||
|
||||
def __enter__(self) -> "LiveProgressReporter":
|
||||
"""Overwrite the `__enter__` method for the correct return type."""
|
||||
self.start(refresh=self._renderable is not None)
|
||||
return self
|
||||
|
||||
def start_a_step(self, step_name: str):
|
||||
"""Start a step and update the progress bars."""
|
||||
self.current_step_name = step_name
|
||||
self.current_step_id = self.step_progress.add_task(
|
||||
f"{self.current_step_name} has started."
|
||||
)
|
||||
|
||||
def finish_the_current_step(self):
|
||||
"""Finish the current step and update the progress bars."""
|
||||
self.step_progress.stop_task(self.current_step_id)
|
||||
self.step_progress.update(
|
||||
self.current_step_id, description=f"{self.current_step_name} has finished."
|
||||
)
|
||||
self.current_step += 1
|
||||
self.overall_progress.update(
|
||||
self.overall_task_id,
|
||||
description=(
|
||||
f"[bold #AAAAAA]({self.current_step} out of"
|
||||
f" {self.number_of_steps} steps finished)"
|
||||
),
|
||||
advance=1,
|
||||
)
|
||||
if self.current_step == self.number_of_steps:
|
||||
self.end()
|
||||
|
||||
def end(self):
|
||||
"""End the live progress reporting."""
|
||||
self.overall_progress.update(
|
||||
self.overall_task_id,
|
||||
description=f"[yellow]{self.end_message}",
|
||||
)
|
||||
|
||||
|
||||
def warn_if_new_version_is_available() -> bool:
|
||||
"""Check if a new version of RenderCV is available and print a warning message if
|
||||
there is a new version. Also, return True if there is a new version, and False
|
||||
otherwise.
|
||||
|
||||
Returns:
|
||||
bool: True if there is a new version, and False otherwise.
|
||||
"""
|
||||
latest_version = get_latest_version_number_from_pypi()
|
||||
if latest_version is not None and __version__ != latest_version:
|
||||
warning(
|
||||
f"A new version of RenderCV is available! You are using v{__version__},"
|
||||
f" and the latest version is v{latest_version}."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def welcome():
|
||||
"""Print a welcome message to the terminal."""
|
||||
warn_if_new_version_is_available()
|
||||
|
||||
table = rich.table.Table(
|
||||
title=(
|
||||
"\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some"
|
||||
" useful links:"
|
||||
),
|
||||
title_justify="left",
|
||||
)
|
||||
|
||||
table.add_column("Title", style="magenta", justify="left")
|
||||
table.add_column("Link", style="cyan", justify="right", no_wrap=True)
|
||||
|
||||
table.add_row("Documentation", "https://docs.rendercv.com")
|
||||
table.add_row("Source code", "https://github.com/sinaatalay/rendercv/")
|
||||
table.add_row("Bug reports", "https://github.com/sinaatalay/rendercv/issues/")
|
||||
table.add_row("Feature requests", "https://github.com/sinaatalay/rendercv/issues/")
|
||||
table.add_row("Discussions", "https://github.com/sinaatalay/rendercv/discussions/")
|
||||
table.add_row(
|
||||
"RenderCV Pipeline", "https://github.com/sinaatalay/rendercv-pipeline/"
|
||||
)
|
||||
|
||||
print(table)
|
||||
|
||||
|
||||
def warning(text: str):
|
||||
"""Print a warning message to the terminal.
|
||||
|
||||
Args:
|
||||
text (str): The text of the warning message.
|
||||
"""
|
||||
print(f"[bold yellow]{text}")
|
||||
|
||||
|
||||
def error(text: Optional[str] = None, exception: Optional[Exception] = None):
|
||||
"""Print an error message to the terminal and exit the program. If an exception is
|
||||
given, then print the exception's message as well. If neither text nor exception is
|
||||
given, then print an empty line and exit the program.
|
||||
|
||||
Args:
|
||||
text (str): The text of the error message.
|
||||
exception (Exception, optional): An exception object. Defaults to None.
|
||||
"""
|
||||
if exception is not None:
|
||||
exception_messages = [str(arg) for arg in exception.args]
|
||||
exception_message = "\n\n".join(exception_messages)
|
||||
if text is None:
|
||||
text = "An error occurred:"
|
||||
|
||||
print(
|
||||
f"\n[bold red]{text}[/bold red]\n\n[orange4]{exception_message}[/orange4]\n"
|
||||
)
|
||||
elif text is not None:
|
||||
print(f"\n[bold red]{text}\n")
|
||||
else:
|
||||
print()
|
||||
|
||||
raise typer.Exit(code=4)
|
||||
|
||||
|
||||
def information(text: str):
|
||||
"""Print an information message to the terminal.
|
||||
|
||||
Args:
|
||||
text (str): The text of the information message.
|
||||
"""
|
||||
print(f"[yellow]{text}")
|
||||
@@ -1,18 +1,14 @@
|
||||
"""
|
||||
This module contains the functions and classes that handle the command line interface
|
||||
(CLI) of RenderCV. It uses [Typer](https://typer.tiangolo.com/) to create the CLI and
|
||||
[Rich](https://rich.readthedocs.io/en/latest/) to provide a nice looking terminal
|
||||
output.
|
||||
`rendercv.cli.utilities` module contains utility functions that are required by CLI.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import urllib.request
|
||||
from typing import Annotated, Callable, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
import jinja2
|
||||
import pydantic
|
||||
@@ -27,9 +23,8 @@ import ruamel.yaml.parser
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from . import __version__
|
||||
from . import data_models as dm
|
||||
from . import renderer as r
|
||||
from .printer import error, warning
|
||||
|
||||
|
||||
def get_latest_version_number_from_pypi() -> Optional[str]:
|
||||
"""Get the latest version number of RenderCV from PyPI.
|
||||
@@ -59,96 +54,6 @@ def get_latest_version_number_from_pypi() -> Optional[str]:
|
||||
return version
|
||||
|
||||
|
||||
def warn_if_new_version_is_available() -> bool:
|
||||
"""Check if a new version of RenderCV is available and print a warning message if
|
||||
there is a new version. Also, return True if there is a new version, and False
|
||||
otherwise.
|
||||
|
||||
Returns:
|
||||
bool: True if there is a new version, and False otherwise.
|
||||
"""
|
||||
latest_version = get_latest_version_number_from_pypi()
|
||||
if latest_version is not None and __version__ != latest_version:
|
||||
warning(
|
||||
f"A new version of RenderCV is available! You are using v{__version__},"
|
||||
f" and the latest version is v{latest_version}."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def welcome():
|
||||
"""Print a welcome message to the terminal."""
|
||||
warn_if_new_version_is_available()
|
||||
|
||||
table = rich.table.Table(
|
||||
title=(
|
||||
"\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some"
|
||||
" useful links:"
|
||||
),
|
||||
title_justify="left",
|
||||
)
|
||||
|
||||
table.add_column("Title", style="magenta", justify="left")
|
||||
table.add_column("Link", style="cyan", justify="right", no_wrap=True)
|
||||
|
||||
table.add_row("Documentation", "https://docs.rendercv.com")
|
||||
table.add_row("Source code", "https://github.com/sinaatalay/rendercv/")
|
||||
table.add_row("Bug reports", "https://github.com/sinaatalay/rendercv/issues/")
|
||||
table.add_row("Feature requests", "https://github.com/sinaatalay/rendercv/issues/")
|
||||
table.add_row("Discussions", "https://github.com/sinaatalay/rendercv/discussions/")
|
||||
table.add_row(
|
||||
"RenderCV Pipeline", "https://github.com/sinaatalay/rendercv-pipeline/"
|
||||
)
|
||||
|
||||
print(table)
|
||||
|
||||
|
||||
def warning(text: str):
|
||||
"""Print a warning message to the terminal.
|
||||
|
||||
Args:
|
||||
text (str): The text of the warning message.
|
||||
"""
|
||||
print(f"[bold yellow]{text}")
|
||||
|
||||
|
||||
def error(text: Optional[str] = None, exception: Optional[Exception] = None):
|
||||
"""Print an error message to the terminal and exit the program. If an exception is
|
||||
given, then print the exception's message as well. If neither text nor exception is
|
||||
given, then print an empty line and exit the program.
|
||||
|
||||
Args:
|
||||
text (str): The text of the error message.
|
||||
exception (Exception, optional): An exception object. Defaults to None.
|
||||
"""
|
||||
if exception is not None:
|
||||
exception_messages = [str(arg) for arg in exception.args]
|
||||
exception_message = "\n\n".join(exception_messages)
|
||||
if text is None:
|
||||
text = "An error occurred:"
|
||||
|
||||
print(
|
||||
f"\n[bold red]{text}[/bold red]\n\n[orange4]{exception_message}[/orange4]\n"
|
||||
)
|
||||
elif text is not None:
|
||||
print(f"\n[bold red]{text}\n")
|
||||
else:
|
||||
print()
|
||||
|
||||
raise typer.Exit(code=4)
|
||||
|
||||
|
||||
def information(text: str):
|
||||
"""Print an information message to the terminal.
|
||||
|
||||
Args:
|
||||
text (str): The text of the information message.
|
||||
"""
|
||||
print(f"[yellow]{text}")
|
||||
|
||||
|
||||
def get_error_message_and_location_and_value_from_a_custom_error(
|
||||
error_string: str,
|
||||
) -> tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
@@ -397,87 +302,6 @@ def handle_exceptions(function: Callable) -> Callable:
|
||||
return wrapper
|
||||
|
||||
|
||||
class LiveProgressReporter(rich.live.Live):
|
||||
"""This class is a wrapper around `rich.live.Live` that provides the live progress
|
||||
reporting functionality.
|
||||
|
||||
Args:
|
||||
number_of_steps (int): The number of steps to be finished.
|
||||
"""
|
||||
|
||||
def __init__(self, number_of_steps: int, end_message: str = "Your CV is rendered!"):
|
||||
class TimeElapsedColumn(rich.progress.ProgressColumn):
|
||||
def render(self, task: "rich.progress.Task") -> rich.text.Text:
|
||||
elapsed = task.finished_time if task.finished else task.elapsed
|
||||
delta = f"{elapsed:.1f} s"
|
||||
return rich.text.Text(str(delta), style="progress.elapsed")
|
||||
|
||||
self.step_progress = rich.progress.Progress(
|
||||
TimeElapsedColumn(), rich.progress.TextColumn("{task.description}")
|
||||
)
|
||||
|
||||
self.overall_progress = rich.progress.Progress(
|
||||
TimeElapsedColumn(),
|
||||
rich.progress.BarColumn(),
|
||||
rich.progress.TextColumn("{task.description}"),
|
||||
)
|
||||
|
||||
self.group = rich.console.Group(
|
||||
rich.panel.Panel(rich.console.Group(self.step_progress)),
|
||||
self.overall_progress,
|
||||
)
|
||||
|
||||
self.overall_task_id = self.overall_progress.add_task("", total=number_of_steps)
|
||||
self.number_of_steps = number_of_steps
|
||||
self.end_message = end_message
|
||||
self.current_step = 0
|
||||
self.overall_progress.update(
|
||||
self.overall_task_id,
|
||||
description=(
|
||||
f"[bold #AAAAAA]({self.current_step} out of"
|
||||
f" {self.number_of_steps} steps finished)"
|
||||
),
|
||||
)
|
||||
super().__init__(self.group)
|
||||
|
||||
def __enter__(self) -> "LiveProgressReporter":
|
||||
"""Overwrite the `__enter__` method for the correct return type."""
|
||||
self.start(refresh=self._renderable is not None)
|
||||
return self
|
||||
|
||||
def start_a_step(self, step_name: str):
|
||||
"""Start a step and update the progress bars."""
|
||||
self.current_step_name = step_name
|
||||
self.current_step_id = self.step_progress.add_task(
|
||||
f"{self.current_step_name} has started."
|
||||
)
|
||||
|
||||
def finish_the_current_step(self):
|
||||
"""Finish the current step and update the progress bars."""
|
||||
self.step_progress.stop_task(self.current_step_id)
|
||||
self.step_progress.update(
|
||||
self.current_step_id, description=f"{self.current_step_name} has finished."
|
||||
)
|
||||
self.current_step += 1
|
||||
self.overall_progress.update(
|
||||
self.overall_task_id,
|
||||
description=(
|
||||
f"[bold #AAAAAA]({self.current_step} out of"
|
||||
f" {self.number_of_steps} steps finished)"
|
||||
),
|
||||
advance=1,
|
||||
)
|
||||
if self.current_step == self.number_of_steps:
|
||||
self.end()
|
||||
|
||||
def end(self):
|
||||
"""End the live progress reporting."""
|
||||
self.overall_progress.update(
|
||||
self.overall_task_id,
|
||||
description=f"[yellow]{self.end_message}",
|
||||
)
|
||||
|
||||
|
||||
def copy_templates(
|
||||
folder_name: str,
|
||||
copy_to: pathlib.Path,
|
||||
@@ -524,11 +348,11 @@ def copy_templates(
|
||||
return destination
|
||||
|
||||
|
||||
def parse_data_model_override_arguments(
|
||||
def parse_render_command_override_arguments(
|
||||
extra_arguments: typer.Context,
|
||||
) -> dict["str", "str"]:
|
||||
"""Parse extra arguments as data model key and value pairs and return them as a
|
||||
dictionary.
|
||||
"""Parse extra arguments given to the `render` command as data model key and value
|
||||
pairs and return them as a dictionary.
|
||||
|
||||
Args:
|
||||
extra_arguments (typer.Context): The extra arguments context.
|
||||
@@ -562,4 +386,3 @@ def parse_data_model_override_arguments(
|
||||
key_and_values[key] = value
|
||||
|
||||
return key_and_values
|
||||
|
||||
Reference in New Issue
Block a user