Typer
Typer is a modern CLI framework for building command-line applications with Python. It is lightweight, highly efficient, and easy to use.
Typer allows you to define command-line interfaces using Python's type annotations. It automatically generates help messages, argument parsing, and command completion. This makes it easy to create complex command-line applications with minimal code.
Key features
- Automatic Help Generation: Typer automatically generates help messages based on the definition of your command-line application. It provides a detailed and easy-to-understand help message to assist users with your application's functionality.
- Argument Parsing: Typer takes care of parsing command-line arguments and options, allowing you to focus on the logic of your application. It provides a simple and intuitive syntax for defining arguments and options, and supports various types, including integers, floats, booleans, and more.
- Type Annotations: Typer leverages Python's type hinting system to define command-line interfaces. This means you can use type annotations to define the expected types of arguments and options. Typer then automatically performs type checking and validation, ensuring that the input provided by the user is of the correct type.
- Interactive Completion: Typer provides interactive completion for commands, arguments, and options. This allows users to easily explore your application's functionality and available options by pressing the Tab key.
- Shell Completion: Typer can automatically generate shell completion scripts for popular shells like Bash, Zsh, and Fish. This makes it easier for users to interact with your command-line application by providing auto-completion support within their preferred shell environment.
Hax
Hax CLI is a simple CLI interface for the functionality of the hax project.
src/cli/ask.py
"""CLI command to ask a question."""from hax import aifrom langchain.prompts import PromptTemplatefrom typer import Typerapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command("q", hidden=True)@app.command(rich_help_panel="LLM", help="Ask OpenAI a question [[cyan1]q[/]]")def question():"""Ask OpenAI a question."""question_prompt = input("Question: ")answer = ai.openai_llm_predict(question=question_prompt)print(answer)@app.command("qt", hidden=True)@app.command(rich_help_panel="LLM", help="Ask OpenAI a question with a template [[cyan1]qt[/]]")def question_with_template(template_name: str):"""Ask OpenAI a question with a template."""match template_name:case "companyname":prompt = PromptTemplate.from_template("What is a good name for a company that makes {product}?")product_name = input("Product name: ")question_prompt = prompt.format(product=product_name)answer = ai.openai_llm_predict(question=question_prompt)print(answer)@app.command("c", hidden=True)@app.command(rich_help_panel="Chat", help="Chat with OpenAI. Quit with 'CTRL-c'. [[cyan1]c[/]]")def chat():"""Chat with OpenAI."""messages = []while True:message = input("Message: ")messages.append(message)answer = ai.openai_chat_predict(messages=messages)print(answer)
src/cli/config.py
"""CLI module for configuration."""import subprocess as supfrom hax import logfrom hax.conf import Depsfrom hax.file import Filefrom hax.cbs import Azure, CBSFilesfrom rich.prompt import Promptfrom typer import Argument, Typerfrom typing_extensions import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}class Completion:"""Provide completion of CLI command values."""@staticmethoddef az():"""Completion for az."""return ["acrlogin", "cli", "login", "node"]@app.command(rich_help_panel="Configure")def az(name: Annotated[str, Argument(help="Configure: 'cli'")] = "cli"):"""Azure related configuration."""match name:case "acrlogin":sup.run("az acr login --name crcapdevaksclu01", shell=True)case "cli":cbs_cert_file = CBSFiles.ot / "build" / "ca-certificates.pem"az_cert_file = (File.hbrew_cellar/ "azure-cli/"/ Deps.az_cli_version/ "libexec"/ "lib"/ "python3.11"/ "site-packages"/ "certifi"/ "cacert.pem")cbs_certs = cbs_cert_file.read_text()log.info(f"Adding self-signed certs to az certifi:\n\n{az_cert_file.read_text()}")with open(az_cert_file, "a") as f:f.write(cbs_certs)log.debug(f"Updated cacert.pm file contents:\n\n{az_cert_file.read_text()}")case "login":cluster = Prompt.ask("Cluster name:", default="crcapdevaksclu01")Azure.login(cluster=cluster)case "node":cbs_cert_file = CBSFiles.ot / "build" / "ca-certificates.pem"sup.run(f"npm config set cafile {cbs_cert_file}", shell=True)case _:log.warning(f'Setup argument not found: "{name}".')
src/cli/copy.py
"""CLI command to copy."""from hax import logfrom hax.cbs import CBSFilesfrom typer import Argument, Typerfrom typing_extensions import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command(rich_help_panel="Copy")def cbs(name: Annotated[str, Argument(help="Files to copy.")] = "prod"):"""Copy CBS files. Options are "prod"."""if name == "prod":CBSFiles.copy_prod()else:log.warning(f"Unknown argument: {name}")
src/cli/get.py
"""CLI command to get stuff."""import subprocess as supfrom typer import Typerapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command("yt", hidden=True)@app.command(rich_help_panel="Get", help="YouTube video [[cyan1]yt[/]]")def youtube(video: str):"""Download YouTube video"""sup.run(f"youtube-dl -x \--no-check-certificate \--embed-thumbnail \--add-metadata \--audio-format m4a \--audio-quality 0 \-o '~/Desktop/%(title)s.%(ext)s' {video}", shell=True)
src/cli/info.py
"""CLI command to copy."""from bacore.domain import filesfrom bacore.interactors import retrievefrom hax import console_handlerfrom pathlib import Pathfrom typer import Typerapp = Typer(rich_markup_mode="rich")state = {"verbose": False}pyproject_file = Path.home() / "Repos" / "hax" / "pyproject.toml"pyproject_toml_object = files.TOML(path=pyproject_file)pyproject_file_class_object = retrieve.File(file=pyproject_toml_object)project_info = retrieve.ProjectInfo(project_file=pyproject_file_class_object)@app.command(rich_help_panel="Info about")def project():"""Show information about project."""if console_handler.level >= 30: # WARNINGprint(f"Project version: {project_info.version}")elif console_handler.level >= 20: # INFOprint(f"Project version: {project_info.version}, Project name: {project_info.name}")else: # DEBUGprint(project_info.name, project_info.version, project_info.__mro__)
src/cli/install.py
"""CLI command to copy."""import subprocess as supfrom hax import logfrom hax.cli import upgradefrom hax.conf import Projectfrom os import getenvfrom shutil import whichfrom typer import Abort, Argument, Exit, Typerfrom typing import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command(rich_help_panel="Bootstrap", help="Flux in given directory with GitHub as remote")def flux(repo: Annotated[str, Argument(help="Flux repository name")],user: Annotated[str, Argument(help="GitHub user")] = getenv('GITHUB_USER'),branch: Annotated[str, Argument(help="Branch name")] = "main"):"""Bootstrap [Flux](https://fluxcd.io) in given directory using GitHub as remote.The flux bootstrap script is idempotent ("can be run many times without causing any harm - probably")."""assert getenv('GITHUB_USER') is not None, "Value of env var GITHUB_USER should not be Null."assert getenv('GITHUB_TOKEN') is not None, "Value of env var GITHUB_TOKEN should not be Null."if which("flux") is None:log.info("Flux is not installed. Installing...")if Project.os == "Darwin":log.debug("Installing fluxcd with Homebrew.")sup.run("brew install fluxcd/tap/flux")assert which("flux") is not None, "Flux is (still) not installed."else:log.warning("Only MacOS has install method for 'fluxcd' set up at the moment.")raise Abort() # Could do this: sup.run("curl -s https://fluxcd.io/install.sh | sudo bash")flux_prerequisities_are_met = sup.run("flux check --pre", shell=True, capture_output=True, check=True)if flux_prerequisities_are_met.returncode != 0:log.error("Prerequisites for running flux are not met. Please fix the errors and try again.")raise Exit(1)else:log.info("Prerequisites for running flux are met. Continuing...")sup.run(f"flux bootstrap github \--owner={user} \--repository={repo} \--branch={branch} \--path=./clusters/{repo} \--personal", shell=True, check=True)@app.command(rich_help_panel="Install", help="Homebrew on a Mac")def homebrew():"""[Homebrew](https://brew.sh)"""sup.run("/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")upgrade.homebrew()@app.command(rich_help_panel="Install", help="TailwindCSS in current directory")def tailwindcss():"""Install [TailwindCSS](https://tailwindcss.com) in the current directory.These are configurations which I'm not using right now: @latest postcss@latest autoprefixer@latest.If ssl error: npm config set strict-ssl falsePerhaps running with "-p" flag for `postcss` is a good idea."""log.info("Installing TailwindCSS in the current directory.")sup.run("npm install -D tailwindcss", shell=True)sup.run("npx tailwindcss init", shell=True)
src/cli/main.py
The main file contains the main() function which is used for callback to provide the verbose and very_verbose command options.
The app.addtyper() is used to group the commands into "verbs".
"""Provide CLI for all hax functionality."""from hax import console_handlerfrom hax.cli import ask, config, copy, get, info, install, mount, publish, start, test, upgrade, watchfrom typer import Option, Typerfrom typing import Annotateddef main(verbose: Annotated[bool, Option("--verbose", "-v", help="Verbose output ([blue]INFO[/] level)")] = False,very_verbose: Annotated[bool, Option("--very-verbose", "-vv", help="Very verbose output ([green]DEBUG[/] level)",),]= False,):"""Entry point for CLI."""if verbose:console_handler.setLevel(20) # INFOelif very_verbose:console_handler.setLevel(10) # DEBUGelse:console_handler.setLevel(30) # WARNINGapp = Typer(rich_markup_mode="rich", add_completion=False, callback=main, help="Enter command...")app.add_typer(ask.app, name="a", callback=main, help="Ask")app.add_typer(config.app, name="co", callback=main, help="Configure")app.add_typer(copy.app, name="c", callback=main, help="Copy")app.add_typer(get.app, name="g", callback=main, help="Get")app.add_typer(info.app, name="i", callback=main, help="Info")app.add_typer(install.app, name="in", callback=main, help="Install")app.add_typer(mount.app, name="m", callback=main, help="Mount")app.add_typer(publish.app, name="p", callback=main, help="Publish")app.add_typer(start.app, name="s", callback=main, help="Start")app.add_typer(test.app, name="t", callback=main, help="Test")app.add_typer(upgrade.app, name="u", callback=main, help="Upgrade")app.add_typer(watch.app, name="w", callback=main, help="Watch")state = {"verbose": False}
src/cli/mount.py
"""CLI command for mounting."""from hax import logfrom hax.cbs import CBSFilesfrom typer import Argument, Typerfrom typing_extensions import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command(rich_help_panel="Mount")def cbs(name: Annotated[str, Argument(help="Mount CBS smb shares.")] = "prod"):"""Mount files and folders."""if name == "prod":CBSFiles.mount()else:log.warning(f"Unknown argument: {name}")
src/cli/start.py
"""CLI command to run things."""import subprocess as supfrom hax import logfrom hax.cli import publish, testfrom hax.conf import Bojortfrom hax.docker import Containerfrom hax.cbs import CBSFilesfrom typer import Argument, Typerfrom typing import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}class Completion:"""Provide completion of CLI start commands.Completions should be provided in a function. The function should return a list of tuples with short name anddescription. (The description is not used in the completion, but is used in the help text in the CLI.)Todo:- Rewrite to make the class callable."""@staticmethoddef items(function):"""Return completion items without description. Used for auto-completion."""return ', '.join([item for (item, description) in function()])@staticmethoddef bojort():"""Completion for Bojort."""return [("main", "Start site, open pages, start tailwindcss, run tests."),("tailwind", "Start tailwindcss")]@staticmethoddef container():"""Completion for container."""return [("bdev", "Bojort dev"),("otdev", "OneTester dev"),("otdevb", "OneTester dev with bash"),("otdevp", "OneTester dev with ipython"),("otdevt", "OneTester dev with command 'ot test'"),("otj", "OneTester dev with Jupyter Lab"),("otmkdocs", "OneTester mkdocs"),("otmkdocsb", "OneTester mkdocs with bash")]@staticmethoddef tailwindcss():"""Completion for tailwindcss."""return [("bojort", "Bojort.com"),("gorg", "Grönberg.org"),("onefront", "OneFront.crosskey.fi")]@staticmethoddef website():"""Completion for site."""return [("gorg", "Grönberg.org")]@app.command(rich_help_panel="Start")def bojort(name: Annotated[str, Argument(help=f"Service: [blue]{Completion.items(Completion.bojort)}[/blue]",rich_help_panel="Start Bojort",autocompletion=Completion.bojort)] = "main"):"""Bojort application."""match name:case "main":Container.run(repo=Bojort.path, container="app", ports=(8010, 8010), command="b start site", detached=True)sup.run("open http://127.0.0.1:8010", shell=True)sup.run("open http://127.0.0.1:8010/docs", shell=True)test.bojort()case "tailwind":breakpoint()sup.run("cd ~/Repos/bojort/tailwindcss && \npx tailwindcss -i ./styles/input.css -o ../site/static/css/main.css --watch", shell=True)@app.command(rich_help_panel="Start")def cbs():"""Crosskey workflows"""# sup.run("say 'Starting Crosskey workflows'", shell=True)log.info("Starting Crosskey workflows")sup.run("xhost +localhost", shell=True) # Allow X11 forwarding# CBSFiles.mount()sup.run("docker container prune -f", shell=True) # Prune containerssup.run("docker image prune -f", shell=True) # Prune imagessup.run("docker volume prune -f", shell=True) # Prune volumesCBSFiles.copy_prod()# Container.run(repo=CBSFiles.otof, container="dev", command="ot test all")Container.run(repo=CBSFiles.ot, container="docs", command="ot publish documentation")# Container.run(repo=CBSFiles.otof, container="docs", command="ot publish documentation")container(name="otdev")sup.run("open http://127.0.0.1:8000", shell=True)container(name="otdocs")sup.run("open http://127.0.0.1:8041", shell=True)container(name="ottestreport")sup.run("open http://127.0.0.1:8042", shell=True)publish.onetester()# container(name="ottestreport")test.onetester()@app.command("c", rich_help_panel="Start")def container(name: Annotated[str, Argument(help=f"Name of container: [blue]{Completion.items(Completion.container)}[/blue]",rich_help_panel="Start container",autocompletion=Completion.container)]):"""Container..."""match name:case "bdev":Container.run(repo=Bojort.path, container="dev", command="bash")case "otapp":Container.run(repo=CBSFiles.ot, container="app")case "otdev":Container.run(repo=CBSFiles.ot, container="dev")case "otdevb":Container.run(repo=CBSFiles.ot, container="dev", command="bash")case "otdevp":Container.run(repo=CBSFiles.ot, container="dev", command="ipython")case "otdocs":Container.run(repo=CBSFiles.ot, container="docs")case "ottestreport":Container.run(repo=CBSFiles.ot, container="testreport")case _:log.error(f"Unknown container: {name}")@app.command("j", rich_help_panel="Start")def jupyter():"""Jupyter Lab"""log.info("Starting Jupyter Lab")sup.run("cd ~/Documents && jupyter-lab --port=9036 &", shell=True)@app.command(rich_help_panel="Start")def site():"""Local site..."""sup.run("uvicorn bojort.main:app --host 0.0.0.0 --port 8010 --reload", shell=True)@app.command(rich_help_panel="Start")def tailwindcss(site: Annotated[str, Argument(help=f"Site name: [blue]{Completion.items(Completion.tailwindcss)}[/]",rich_help_panel="Start tailwindcss",autocompletion=Completion.tailwindcss)]):"""TailwindCSS"""match site:case "bojort":sup.run(f"cd {Bojort.path}/tailwindcss && \npx tailwindcss -i ./styles/input.css -o ../site/static/css/main.css --watch", shell=True)case "gorg":sup.run(f"cd ~/Documents/.style/ && \npx tailwindcss -i input.css -o ../../Public/gorg/style/main.css --watch", shell=True)case "onefront":sup.run(f"cd {CBSFiles.ot}/tailwindcss && \npx tailwindcss -i ./styles/input.css -o ../site/static/css/main.css --watch", shell=True)case _:log.error(f"Unknown site: {site}")@app.command(rich_help_panel="Start")def website(name: Annotated[str,Argument(help=f"Name of site: [blue]{Completion.items(Completion.website)}[/blue]",rich_help_panel="Start site",autocompletion=Completion.website)]):"""Local site..."""match name:case "gorg":sup.run("cd ~/Documents && mkdocs serve -a 127.0.0.1:8001 &", shell=True)case _:log.error(f"Unknown site: {name}")
src/cli/test.py
"""CLI command to run tests."""import subprocess as supfrom hax import logfrom hax.conf import Bojortfrom hax.docker import Containerfrom hax.cbs import CBSFilesfrom typer import Option, Typerfrom typing_extensions import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command(rich_help_panel="Test")def bacore():"""Run pytest for bacore."""sup.run("cd ~/Repos/bacore && source venv/bin/activate && ptw -cw src tests -- -s", shell=True)@app.command(rich_help_panel="Test")def bojort():"""Run test suit Bojort."""Container.run(repo=Bojort.path, container="dev", command="b test")@app.command(rich_help_panel="Test")def hax(headed: Annotated[bool, Option(help="Run in headed mode.")] = False,memray: Annotated[bool, Option(help="Inspect load on memory usage while running tests.")] = False):"""Run pytest for hax.Possibility to run "headed" UI tests in a browser.Tools:- [pytest-testmon](https://pypi.org/project/pytest-testmon/)- [pytest-watch](https://pypi.org/project/pytest-watch/)"""log.debug("Running pytest with headed={headed} and memray={memray}.")if headed:if memray:sup.run("cd ~/Documents && ptw -cw src tests -- --headed --memray -s", shell=True)else:sup.run("cd ~/Documents && ptw -cw src tests -- --headed -s", shell=True)else:if memray:sup.run("cd ~/Documents && ptw -cw src tests -- --memray -s", shell=True)else:sup.run("cd ~/Documents && ptw -cw src tests -- -s", shell=True)@app.command(rich_help_panel="Test")def onetester():"""Run tests for OneTester inside of Docker container (locally)."""Container.run(repo=CBSFiles.ot, container="dev", command="ot test onetester")Container.run(repo=CBSFiles.otof, container="dev", command="ot test ci --headed")
How do I run pytest from code and then specify a particular repository to test in? Below is the code I have so far.
@app.command(richhelppanel="Test") def testbacore(): """Run pytest for bacore.""" retcode = pytest.main(["-s", "src", "tests"]) return retcode
[AI]: To run pytest from code and specify a particular repository to test, you can update your code as follows:
```python import pytest
@app.command(richhelppanel="Test") def testbacore(): """Run pytest for bacore.""" repository = "path/to/repository" # specify the repository directory retcode = pytest.main(["-s", f"{repository}/src", f"{repository}/tests"]) return retcode ```
By specifying the `repository` variable with the path to the desired repository, you can pass that path to pytest as the directory for both the source code (`src`) and the tests directory (`tests`).
[ME]:
src/cli/upgrade.py
"""CLI test commands."""import subprocess as supfrom typer import Argument, Typerfrom typing_extensions import Annotatedapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command(rich_help_panel="Upgrade")def homebrew():"""Upgrade Homebrew and all packages installed with it."""sup.run("brew update && brew upgrade && brew cleanup && brew doctor", shell=True)@app.command(rich_help_panel="Upgrade")def emacs(uninstall: Annotated[int, Argument(help="Emacs version to uninstall.")] = 29,install: Annotated[int, Argument(help="Emacs version to install.")] = 29):"""Emacs"""sup.run(f"brew uninstall emacs-plus@{uninstall}", shell=True)sup.run(f"brew install emacs-plus@{install} \--with-dbus \--with-mailutils \--with-no-frame-refocus \--with-imagemagic \--with-native-comp", shell=True)
src/cli/watch.py
"""CLI command to get stuff."""import subprocess as supfrom typer import Typerapp = Typer(rich_markup_mode="rich")state = {"verbose": False}@app.command(rich_help_panel="Watch")def bojort():"""Bojort kustomizations"""sup.run("cd ~/Repos/bojort-flux && \watch -n 10 flux get kustomizations", shell=True)