Created
May 16, 2024 14:12
-
-
Save FMeinicke/8e728241c9c12f9470cde530bef3a9d6 to your computer and use it in GitHub Desktop.
Python typer CLI application template
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| if sys.version_info.minor == 7: | |
| import importlib_metadata as metadata | |
| else: | |
| from importlib import metadata | |
| __version__ = metadata.version(__name__) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import logging | |
| import os | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import Optional | |
| import typer | |
| from PySide6.QtCore import QCoreApplication | |
| from qmixsdk.qmixbus import Bus | |
| from qmixsdk.qmixvalve import Valve as QmixValve | |
| from typing_extensions import Annotated | |
| from . import __name__ as application_name | |
| from . import __version__ | |
| _logger = logging.getLogger(__name__) | |
| app = typer.Typer() | |
| def show_version(show_version: bool): | |
| """ | |
| Callback function for the --version option of the application. | |
| Prints the version of the application and exits if the option was set. | |
| Parameters | |
| ---------- | |
| show_version : bool | |
| Whether the option was set or not | |
| """ | |
| if show_version: | |
| print(f"{application_name} v{__version__}") | |
| raise typer.Exit() | |
| _LOGGING_FORMAT = ( | |
| "%(asctime)s [%(threadName)-{thread_name_len}.{thread_name_len}s] %(levelname)-8s| " | |
| "%(name)s %(module)s.%(funcName)s (%(lineno)s): %(message)s" | |
| ) | |
| def set_logging_level(log_level: str): | |
| """ | |
| Callback function for the --log-level option of the application. | |
| Sets the logging level for the application. | |
| Parameters | |
| ---------- | |
| log_level : str | |
| The logging level from the command line option | |
| Returns | |
| ------- | |
| str | |
| The logging level that was set | |
| """ | |
| logging_level = log_level.upper() | |
| try: | |
| import coloredlogs | |
| coloredlogs.install( | |
| fmt=_LOGGING_FORMAT.format(thread_name_len=12), datefmt="%Y-%m-%d %H:%M:%S,%f", level=logging_level | |
| ) | |
| except ModuleNotFoundError: | |
| print("Cannot find coloredlogs! Please install coloredlogs, if you'd like to have nicer logging output:") | |
| print("`pip install coloredlogs`") | |
| logging.basicConfig(format=_LOGGING_FORMAT.format(thread_name_len=12), level=logging_level) | |
| return log_level | |
| def setup_logging(log_level: str, log_file_dir: Optional[Path] = None) -> None: | |
| """ | |
| Sets up logging for the application. | |
| Parameters | |
| ---------- | |
| log_level : str | |
| The logging level for the application from the command line option | |
| log_file_dir : Path, optional | |
| The directory to write log files to from the command line option | |
| """ | |
| def log_file_name(dir: Path, log_level: str) -> Path: | |
| return dir.joinpath(f"{application_name}-{log_level.lower()}-{time.strftime('%Y-%m-%d_%H-%M-%S')}.log") | |
| def make_log_file_handler(log_file_dir: Path, log_level: str) -> logging.FileHandler: | |
| os.makedirs(log_file_dir, exist_ok=True) | |
| log_file_handler = logging.FileHandler(log_file_name(log_file_dir, log_level)) | |
| log_file_handler.setFormatter(logging.Formatter(_LOGGING_FORMAT.format(thread_name_len=60))) | |
| return log_file_handler | |
| log_file_handler: Optional[logging.FileHandler] = None | |
| if log_file_dir is not None: | |
| log_file_handler = make_log_file_handler(log_file_dir, log_level) | |
| logging.getLogger().addHandler(log_file_handler) | |
| logging.info(f"Writing log to file {log_file_handler.baseFilename!r}") | |
| logging.info(f"Starting log for {sys.executable} with args {sys.argv}") | |
| # ----------------------------------------------------------------------------- | |
| # main program | |
| @app.command(no_args_is_help=False, context_settings={"help_option_names": ["-h", "--help"]}) | |
| def main( | |
| some_arg: Annotated[ | |
| Path, | |
| typer.Argument( | |
| metavar="SOME_ARG", | |
| help="An example argument.", | |
| exists=True, | |
| file_okay=False, | |
| dir_okay=True, | |
| writable=False, | |
| readable=True, | |
| resolve_path=True, | |
| ), | |
| ], | |
| version: Annotated[ | |
| Optional[bool], | |
| typer.Option( | |
| "--version", | |
| "-v", | |
| callback=show_version, | |
| is_eager=True, | |
| help="Show the application's version number and exit.", | |
| ), | |
| ] = None, | |
| log_level: Annotated[ | |
| str, | |
| typer.Option( | |
| "--log-level", | |
| "-l", | |
| callback=set_logging_level, | |
| metavar="LEVEL", | |
| help="Set the logging level for the application.", | |
| case_sensitive=False, | |
| formats=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], | |
| ), | |
| ] = "INFO", | |
| log_file_dir: Annotated[ | |
| Optional[Path], | |
| typer.Option( | |
| metavar="DIR", | |
| help="The directory to write log files to (if not given, log messages will only be printed to standard out)", | |
| exists=False, | |
| file_okay=False, | |
| dir_okay=True, | |
| readable=True, | |
| writable=True, | |
| resolve_path=True, | |
| ), | |
| ] = None, | |
| ) -> None: | |
| """ | |
| Typer application template. | |
| """ | |
| setup_logging(log_level, log_file_dir) | |
| qapp = QCoreApplication(sys.argv) | |
| _logger.info("Starting application!") | |
| exit_code = qapp.exec() | |
| _logger.debug(f"Application exited with exit code {exit_code}") | |
| if __name__ == "__main__": | |
| app() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment