As programmers, we often want to create simple CLI tools to automate certain processes or facilitate our daily lives. In my case, it was an application that facilitates food ordering via Wolt (here you can find the code for my app).

The purpose of this post is to demonstrate how you can use the typer library to create a simple CLI application and how to publish it on PyPI using poetry. There is no doubt that most of the readers have already created simple CLI applications, but you most likely didn’t use typer for it, nor did they publish their applications on PyPI.

As a final example, I’d like to show you how you can automate the application publishing process using Github Actions, so that every time you push a new version to the repository, the application is automatically published on PyPI.

My hope is that you will find this post useful and that you will learn something from it. Let’s start!

Requirements:

Introduction

For this guide, we’ll use poetry, and you need to install it before you start. During the tutorial, we will create a very simple CLI application used to greet user. We will first create a poetry project, then create a CLI application with typer, and finally publish it on PyPI.

Create and setup project

Create a project with Poetry:

$ poetry new greet-app

and then enter the new project directory: cd greet-app. Next, add typer[all] to project dependencies:

$ poetry add "typer[all]"

The structure of the project was generated by poetry:

.
├── poetry.lock
├── pyproject.toml
├── README.md
├── greet_app
│   └── __init__.py
└── tests
    └── __init__.py

Now we will create a basic version of the CLI application. Create file greet_app/main.py:

$ touch greet_app/main.py

Open main.py file and create an extremely simple typer app**:**

# greet_app/main.py

import typer

app = typer.Typer()

@app.callback()
def callback() -> None:
    """My app description"""

@app.command()
def hello() -> None:
    """Say Hello World"""
    typer.echo("Hello World!")

Our application currently has one hello command, which prints Hello World to the screen.

Note: as we are creating an installable Python package, there’s no need to add a section with if __name__ == "__main__":.

Let’s now look at the pyproject.toml file’s project configuration. It should look something like this:

# pyproject.toml

[tool.poetry]
name = "greet-app"
version = "0.1.0"
description = ""
authors = ["Kamil Woźniak <info@kamilwozniak.com>"]
readme = "README.md"
packages = [{include = "greet_app"}]

[tool.poetry.dependencies]
python = "^3.10"
typer = {extras = ["all"], version = "^0.7.0"}

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Our goal is to create a Python package that can be installed with pip, this is done by adding a configuration to the section [tool.poetry.scripts] of the pyproject.toml:

# pyproject.toml

[tool.poetry]
name = "greet-app"
version = "0.1.0"
description = ""
authors = ["Kamil Woźniak <info@kamilwozniak.com>"]
readme = "README.md"
packages = [{include = "greet-app"}]

[tool.poetry.scripts]
greet-app = "greet-app.main:app"

[tool.poetry.dependencies]
python = "^3.10"
typer = {extras = ["all"], version = "^0.7.0"}

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Now, please activate poetry environment:

$ poetry shell

You can now install our app:

$ poetry install

Try it:

$ greet-app hello
Hello World!

By default, the typer adds the --help option:

$ greet-app --help
Usage: greet-app [OPTIONS] COMMAND [ARGS]...

 My app description

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --install-completion          Install completion for the current shell.      │
│ --show-completion             Show completion for the current shell, to copy │
│                               it or customize the installation.              │
│ --help                        Show this message and exit.                    │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ hello               Say Hello World.                                         │
╰──────────────────────────────────────────────────────────────────────────────╯

That’s awesome! We created a working tool with just a few lines of code! 👩‍💻

Modifications

Each CLI tools is versioned, now we will create an option to display the current version of our application by the command greet-app --version. We need to modify the main.py file a bit:

# greet_app/main.py

from importlib import metadata

import typer

app = typer.Typer()
__version__ = metadata.version(__package__)

def version_callback(value: bool) -> None:
    if value:
        print(__version__)
        raise typer.Exit()

@app.callback()
def callback(
    _: bool = typer.Option(None, "--version", "-v", callback=version_callback)
) -> None:
    """My app description"""

@app.command()
def hello() -> None:
    """Say Hello World"""
    typer.echo("Hello World!")

Try it:

$ greet-app -v
0.1.0

Awesome! 👏 Now we’ll try to modify the hello command a bit so that instead of printing Hello World it will print a hello for the given name:

@app.command()
def hello(
    name: str = typer.Argument(..., help="The person to greet.")
) -> None:
    """Say hello to NAME"""
    typer.echo(f"Hello {name}!")

Now we can add argument to command call:

$ greet-app hello Kamil
Hello Kamil!

We can also give a default value for the name argument:

@app.command()
def hello(
    name: str = typer.Argument(default="World", help="The person to greet.")
) -> None:
    """Say hello to NAME"""
    typer.echo(f"Hello {name}!")

Now, if you don’t specify a value for the name parameter, it will default to World:

$ greet-app hello
Hello World!

Publishing

Our simple application can now be published on PyPI. To do it, you first need to configure a PyPI auth token. First, create an account there. And then go to Add API token page to create a new token.

Now configure Poetry to use this token:

$ poetry config pypi-token.pypi my-pypi-token

Poetry now lets you publish your package:

$ poetry publish --build

Note: Poetry will give you an error here because there is a project by me with the same name.

Great job! 💡Now in Your projects section you can find your application. We can try to install it via pip:

$ pip install greet-app

Release new version

After each change in the application, in order to publish it, we need to update the application version in the pyproject.toml file. In the absence of this step, your application will not be updated. For example, let’s make a little modification in main.py:

@app.command()
def hello(
    name: str = typer.Argument(default="World", help="The person to greet.")
) -> None:
    """Say hello to NAME"""
    emoji = "🌎" if name == "World" else "👋"
    typer.echo(f"Hello {name}! {emoji}")

Now let’s update version in pyproject.toml:

version = "0.2.0"

Let’s publish a new version of our application now:

$ poetry publish --build

We can now check with pip whether the version of our application has changed:

$ pip show greet-app

Next steps

We can now try to create a repository on the GitHub platform and prepare an action that will automatically publish a new version of our application:

$ git init

Next, create a repository on GitHub and setup remote repository, example:

$ git remote add origin git@github.com:Valaraucoo/greet-app.git

Now let’s add all files:

$ git add --all

And commit all changes:

$ git commit -m "initial commit"

Then push to remote repository:

$ git push origin master

As a final step, I’d like to show you how you can automate the application publishing process using Github Actions. Let’s create .github/workflows/publish.yaml file:

name: Release to PyPI

on:
  push:
    branches:
      - master

jobs:
  release:
    name: Release
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Build and publish to pypi
        uses: JRubics/poetry-publish@v1.13
        with:
          pypi_token: ${{ secrets.PYPI_TOKEN }}

This action will attempt to publish a new version of the application in PyPI - as long as the version of the application in the pyproject.toml file is changed. Let’s try it out by updating the application version to 0.3.0:

version = "0.3.0"

Before pushing our code to repository, we need to add our PyPI token to GitHub Secrets, open:

https://github.com/your_name/repo_name/settings/secrets/actions/new

and add new secret PYPI_TOKEN to your repository. Now you can push your code to repository. Now check in PyPI if the version of your application has been updated.

If you want to see how to configure a larger CLI application, I recommend you to look at my repository for what-to-eat applications!