# Build Your First CLI Application With Typer

Howdy everyone, In this article, we will be building a CLI application with Typer and Python. we will build a to-do application with CRUD(Create Read Update Delete) functionality.

Before building our to-do CLI first let's discuss what is typer and its features.

# What is Typer?

[Typer](https://typer.tiangolo.com/) is a python library for building CLI applications with python and type hints. it provides a lot of features and some of them are

* Simple to start
    
* Easy to use
    
* Easy to debug
    
* Scalable
    
* Less code
    
* Easy to follow docs
    

Let's build our to-do CLI

# Project Setup

### Create a project folder and give it a name

```typescript
> mkdir todo_cli
```

### Navigate to the folder

```typescript
> cd todo_cli
```

### Create a virtual environment and activate it

> Virtual environment is an isolated environment that keeps your dependencies isolated so your global environment does not affect it.

We will be using `venv` to create a virtual environment. `venv` is a python module for creating lightweight "virtual environments".

```typescript
> python -m venv env
> source env/bin/activate
```

Now, we have the virtual environment activated let's install the required dependencies

### Install dependencies

This command will install the `type` and additional dependencies like `Colorama`, `shellingham` and `click`.

Typer uses `Click` internally. it is a command-line creation toolkit.

* **Colorama** - to make sure your terminal colour work correctly.
    
* **shellingham** - to detect current shell
    

```typescript
> pip install typer[all]
```

### Create a README file for your project

This will help end-users to read the project description and instructions to use it.

```typescript
> touch README.md
```

### Create `requirements.txt` and add your project dependencies to it.

```typescript
> touch requirements.txt
```

After creating the requirements.txt file add the below content to it.

```typescript
typer==0.3.2
colorama==0.4.4
shellingham==1.4.0
```

### Create `.gitignore` file to tell git which file or folder to ignore in a project

```typescript
> touch .gitignore
```

After the `.gitignore` file is created add the below content to it.

```typescript
.pytest_cache
__pycache__

venv
env
*.pyc
```

Now our basic project structure is ready, we can move forward and create our `todo` package which will contain our to-do CLI business logic.

# To-do CLI Business Logic

> For brevity we will use dots ... to denote existing code that remains unchanged

### Our todo will have this data structure.

```python
{
   'Description': "Describe your todo",
   'Priority': "Priority of your todo" , 
   'Done': "Is completed or not"
}
```

### Create a folder and name it `todo`

```typescript
# todo_cli/todo
> mkdir todo_cli/todo
```

### Create a file named `__init__.py` inside `todo` folder

Python has a straightforward packaging system. you have to create the `__init__.py` file inside the folder and python will treat that folder as a package.

```shell
# todo_cli/todo/__inti__.py
> touch todo_cli/todo/__inti__.py
```

Now set up the `todo` package by adding some codes to the `__init__.py` file.

Here we are adding `__app_name__` and `__version__` for our `todo` package. also, we are adding an `ERRORS` dictionary with error codes like SUCCESS, ID\_ERROR, etc.

```python
# todo/__init__.py

__app_name__="todo"
__version__="0.1.0"

# this will assign numbers from 0 to 6
(
    SUCCESS,
    DIR_ERROR,
    FILE_ERROR,
    DB_READ_ERROR,
    DB_WRITE_ERROR,
    JSON_ERROR,
    ID_ERROR,
) = range(7)


# this will create errors dictionary with error code 
ERRORS = {
    DIR_ERROR: "config directory error",
    FILE_ERROR: "config file error",
    DB_READ_ERROR: "database read error",
    DB_WRITE_ERROR: "database write error",
    ID_ERROR: "to-do id error",
}
```

We will be going to use the `ERRORS` dictionary throughout the project to provide consistent error codes and messages.

> Want to read more about python dictionaries and structuring data, you can read it from here.

%[https://blog.sachinchaurasiya.dev/an-introduction-to-python-dictionary-and-structuring-data] 

To work with CLI applications we will need to set up some configs and in the next section, we will create a config module for our to-do CLI.

## Config Module

### Create a file named `config.py` inside `todo` folder

```shell
# todo_cli/todo/config.py
> touch todo_cli/todo/config.py
```

To store our to-dos we will need a database, for that we will be going to use a `JSON` file as a database to store our to-dos.

Also, we will need to create an application config file so that someone using it will get initial configs.

Open a `config.py` file and add the below code to it.

```python
import configparser
from pathlib import Path

import typer

from todo import (
 __app_name__
)

CONFIG_DIR_PATH = Path(typer.get_app_dir(__app_name__))
CONFIG_FILE_PATH = CONFIG_DIR_PATH / "config.ini"
```

Here we are importing `configparser`, `Path` from **pathlib** Package and `typer`.

* **Configparser** - to implement basic configuration language for python programs.
    
* **Path** - to work with system paths in a cross-platform way.
    
* **typer** - to work with typer helper functions and classes.
    

We have created `CONFIG_DIR_PATH` to hold our application config directory path using `__app_name__` that we have defined earlier in our project.

`CONFIG_FILE_PATH` to hold the project config file path.

Setting up the config is a one-time task and for that, we will create an initializer method to set up the initial config.

```python
...

from todo import (
 __app_name__,

DB_WRITE_ERROR, #new
DIR_ERROR, #new
FILE_ERROR, #new
SUCCESS, #new
)

...

def init_app(db_path: str) -> int:
    """Initialize the application."""
    config_code = _init_config_file()
    if config_code != SUCCESS:
        return config_code
    database_code = _create_database(db_path)
    if database_code != SUCCESS:
        return database_code
    return SUCCESS


def _init_config_file() -> int:
    try:
        CONFIG_DIR_PATH.mkdir(exist_ok=True)
    except OSError:
        return DIR_ERROR
    try:
        CONFIG_FILE_PATH.touch(exist_ok=True)
    except OSError:
        return FILE_ERROR
    return SUCCESS


def _create_database(db_path: str) -> int:
    config_parser = configparser.ConfigParser()
    config_parser["General"] = {"database": db_path}
    try:
        with CONFIG_FILE_PATH.open("w") as file:
            config_parser.write(file)
    except OSError:
        return DB_WRITE_ERROR
    return SUCCESS
```

We have created `init_app` function with two helper function `_init_config_file` and `_create_database`.

The **init\_app** method will initialize the application using two helper functions.

The first helper function **\_init\_config\_file** will create the project configuration file. If everything goes well it will return the **SUCCESS** code. and if not it will return the proper error code **DIR\_ERROR** or **FILE\_ERROR**.

The Second helper function **\_create\_database** will take the database location path string.

and create the database for our to-dos.

on success, it will return **SUCCESS** code and on error, it will return the **DB\_WRITE\_ERROR** code.

Our project config setup is complete.

Now let's code the `database.py` module for our to-do database functionality.

## Database Module

### Create a file named `database.py` inside `todo` folder

```shell
# todo_cli/todo/database.py
> touch todo_cli/todo/database.py
```

This module will contain **four** parts which are

* **Default database file path** - this will be used if no database file path is provided.
    
* **database initializer** - to initialize the database with some value for example **\[\]** Empty to-do list.
    
* **get database path helper** - to get the current path to the to-do database.
    
* **DatabaseHandler** - to read and write to-dos to the database.
    

We will code the first three parts in the next section and the last one in the coming section.

First, we will need to import some modules and classes.

```python
import configparser
from pathlib import Path
from todo import DB_WRITE_ERROR, SUCCESS, DB_READ_ERROR, JSON_ERROR
```

We will create a default database file path and use it if a custom path is not provided.

```python
DEFAULT_DB_FILE_PATH = Path.home().joinpath(
    "." + Path.home().stem + "_todo.json"
)
```

**database initializer** will try to initialize the database with an empty to-do list.

```python
def init_database(db_path: Path) -> int:
    """Create the to-do database."""
    try:
        db_path.write_text("[]")  # Empty to-do list
        return SUCCESS
    except OSError:
        return DB_WRITE_ERROR
```

**database path helper**, it will read the current path of the database from our project config file.

```python
def get_database_path(config_file: Path) -> Path:
    """Return the current path to the to-do database.""" 
    config_parser = configparser.ConfigParser()
    config_parser.read(config_file)
    return Path(config_parser["General"]["database"])
```

**DatabaseHandler** for reading and writing to the database.

```typescript
...
import json
from typing import Any, Dict, List, NamedTuple
...

class DBResponse(NamedTuple):
    todo_list: List[Dict[str, Any]]
    error: int


class DatabaseHandler:
    def __init__(self, db_path: Path) -> None:
        self._db_path = db_path

    def read_todos(self) -> DBResponse:
        try:
            with self._db_path.open("r") as db:
                try:
                    return DBResponse(json.load(db), SUCCESS)
                except json.JSONDecodeError:  # Catch wrong JSON format
                    return DBResponse([], JSON_ERROR)
        except OSError:  # Catch file IO problems
            return DBResponse([], DB_READ_ERROR)

    def write_todos(self, todo_list: List[Dict[str, Any]]) -> DBResponse:
        try:
            with self._db_path.open("w") as db:
                json.dump(todo_list, db, indent=4)
            return DBResponse(todo_list, SUCCESS)
        except OSError:  # Catch file IO problems
            return DBResponse(todo_list, DB_WRITE_ERROR)
```

Here we have imported a couple of things `JSON` to work with JSON data and type hints from the `typing` module.

**DBResponse class** to return named tuple in response. named tuples will allow us to create named tuple with type hints for their named fields. here we have to fields `todo_list` and `error` so it will return something like this,

```python
>>> from todo import database

>>> res = database.DBResponse(todo_list=[{'Description': 'Write a blog on creating your first CLI Application with typer.', 'Priority': 2, 'Done': False}], error=0)

# we can access tuple value for a specific field like this,
>>> res.todo_list
```

**DatabaseHandler class** have two methods,

1. read\_todos - to read to-dos from the database and return a response with type `DBResponse`
    
2. write\_todos - it will take a new to-do list and write it to the database.
    

The database module is completed. to communicate with the database we will need a controller and in the next section, we will code the todo controller.

## To-do Controller

### Create a file named `todo.py` inside `todo` folder

```shell
# todo_cli/todo/todo.py
> touch todo_cli/todo/todo.py
```

We will create one class with the name `Todoer` and add the following methods to it.

1. add - Create todo
    
2. get\_todo\_list- Read todo
    
3. set\_done - Update todo
    
4. remove - Delete todo
    

Let's start coding our controller by importing the required modules and classes.

```python
from pathlib import Path
from typing import Any, Dict, NamedTuple, List
from todo.database import DatabaseHandler
from todo import DB_READ_ERROR, ID_ERROR
```

As we have created `DBResponse class` for the database to return a consistent response, similarly we will need to create `CurrentTodo class` for todo controller response.

CurrentTodo will contain two fields named **todo** and **error**.

```python
...
class CurrentTodo(NamedTuple):
    todo: Dict[str, Any]
    error: int
```

Now let's write code for our **Todoer** class.

```python
...
class Todoer:
    def __init__(self, db_path: Path) -> None:
        self._db_handler = DatabaseHandler(db_path)
        
    def add(self, description: List[str], priority: int = 2) -> CurrentTodo:
        """Add a new to-do to the database."""
        pass

    def get_todo_list(self) -> List[Dict[str, Any]]:
        """Return the current to-do list."""
        pass

    def set_done(self, todo_id: int) -> CurrentTodo:
        """Set a to-do as done."""
        pass

    def remove(self, todo_id: int) -> CurrentTodo:
        """Remove a to-do from the database using its id or index."""
        pass
```

For now, we have added the **pass** keyword for each method, we will code each method business logic one by one.

**add method** it will accept 2 parameter namely `description` and `priority`. then we will join the description with a space character and check if the description is ending with `.` or not, if not will add `.` at the end.

Then we will create a `todo` object with the property "Description", "Priority" and "Done". we will set `Done` as false initially.

Next, we will read to-dos from the database and check for error handling, if everything goes well we will append the new todo to the existing todo list and write it back to the database.

```python
...
def add(self, description: List[str], priority: int = 2) -> CurrentTodo:
        """Add a new to-do to the database."""
        description_text = " ".join(description)
        if not description_text.endswith("."):
            description_text += "."
        todo = {
            "Description": description_text,
            "Priority": priority,
            "Done": False,
        }
        read = self._db_handler.read_todos()
        if read.error == DB_READ_ERROR:
            return CurrentTodo(todo, read.error)
        read.todo_list.append(todo)
        write = self._db_handler.write_todos(read.todo_list)
        return CurrentTodo(todo, write.error)
```

**get\_todo\_list** first it will read the to-dos from the database using the *read\_todos* method and store the response in the *read* variable then it will return the `read.todo_list`.

```python
...
def get_todo_list(self) -> List[Dict[str, Any]]:
        """Return the current to-do list."""
        read = self._db_handler.read_todos()
        return read.todo_list
```

**set\_done** will accept todo id and set corresponding todo as done if it exists, otherwise throw an error.

```python
...
def set_done(self, todo_id: int) -> CurrentTodo:
        """Set a to-do as done."""
        read = self._db_handler.read_todos()
        if read.error:
            return CurrentTodo({}, read.error)
        try:
            todo = read.todo_list[todo_id - 1]
        except IndexError:
            return CurrentTodo({}, ID_ERROR)
        todo["Done"] = True
        write = self._db_handler.write_todos(read.todo_list)
        return CurrentTodo(todo, write.error)
```

**remove** will accept todo id and delete the corresponding todo if it exists, otherwise throw an error.

```python
...
def remove(self, todo_id: int) -> CurrentTodo:
        """Remove a to-do from the database using its id or index."""
        read = self._db_handler.read_todos()
        if read.error:
            return CurrentTodo({}, read.error)
        try:
            todo = read.todo_list.pop(todo_id - 1)
        except IndexError:
            return CurrentTodo({}, ID_ERROR)
        write = self._db_handler.write_todos(read.todo_list)
        return CurrentTodo(todo, write.error)
```

If you have made till here, awesome 😍👨🏻‍💻.

Now we will code the CLI module, This module will contain CLI commands for our to-do application. it will also contain one helper function that will help our application to get a to-do controller.

## CLI Module

### Create a file named `cli.py` inside `todo` folder

```shell
# todo_cli/todo/cli.py
> touch todo_cli/todo/cli.py
```

Let's start with importing some modules and classes.

```python
from pathlib import Path

# Type hints from typing
from typing import Optional, List

# Typer module
import typer

# importing other modules config, database and todo
from todo import __app_name__, __version__, ERRORS, config, database, todo
```

To work with typer we will need to initialize the application using the `Typer` class from the typer module.

```python
...
# initializing todo_cli, you can give any name you like.
todo_cli = typer.Typer()
```

When we create a `todo_cli = typer.Typer()`, it will work as a group of commands. And you can create multiple commands with it.

Each of those commands can have its own CLI parameters.

But as those CLI parameters are handled by each of those commands, they don't allow us to create CLI parameters for the main CLI application itself.

But we can use `@todo_cli.callback()` for that. it declares the CLI parameters for the main CLI application.

```python
...
def callback(value: bool) -> None:
    if value:
        typer.echo(f"{__app_name__} v{__version__}")
        raise typer.Exit()


@todo_cli.callback()
def main(
    version: Optional[bool] = typer.Option(
        None,
        "--version",
        "-v",
        help="Show the application's version and exit.",
        callback=callback,
        is_eager=True,
    )
) -> None:
    return
```

Here we have two functions

* callback - It will get the boolean value and if the value is true it will print the **app name** with the **version**.
    
* main - Get executed when we run our CLI with some initial parameters like `-v`, `--version` and `--help`.
    

To see this thing in action let's create the `__main__.py` file in our todo folder and make our `todo` package an executable program.

### Create a file named `__main__.py` inside `todo` folder

```shell
# todo_cli/todo/__main__.py
> touch todo_cli/todo/__main__.py
```

Add this code to the `__main__.py` file.

```python
# importing CLI module and app name
from todo import cli, __app_name__

# calling typer app that is **todo_cli** with **prog_name** to ensure our application get's correct app name.
def main():
    cli.todo_cli(prog_name=__app_name__)

if __name__ == "__main__":
    main()
```

Our minimal CLI is ready and we can run these commands to see things in action.

#### Get application version

```shell
> python3 -m todo -v 
OR
> python3 -m todo --version

# Output - it will print the app name with the version.
todo v0.1.0
```

#### Get help

```shell
> python3 -m todo --help

# Output - it will print the available options and commands.
Usage: todo [OPTIONS] COMMAND [ARGS]...

Options:
  -v, --version         Show the application's version and exit.
  --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:
...
```

This is amazing right, so let's add some commands to **add**, **read**, **update** and **delete** to-dos.

Before adding commands we will need to add the helper function that will help us to communicate with our todo **database** using the **todo** controller.

```python
...
def get_todoer() -> todo.Todoer:
    if config.CONFIG_FILE_PATH.exists():
        db_path = database.get_database_path(config.CONFIG_FILE_PATH)
    else:
        typer.secho(
            'Config file not found. Please, run "todo init"',
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)
    if db_path.exists():
        return todo.Todoer(db_path)
    else:
        typer.secho(
            'Database not found. Please, run "todo init"',
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)
```

This helper will check if the application config is present or not. if present it will get the database path and return the `Todoer` controller instance.

If any of the checks fail it will echo the error using the `typer.secho()`.

**typer.secho()** - It is the same as an echo but it will also apply the style i.e style and echo.

### init command

```python
...
@todo_cli.command()
def init(
    db_path: str = typer.Option(
        str(database.DEFAULT_DB_FILE_PATH),
        "--db-path",
        "-db",
        prompt="to-do database location?",
    ),
) -> None:
    """Initialize the to-do database."""
    app_init_error = config.init_app(db_path)
    if app_init_error:
        typer.secho(
            f'Creating config file failed with "{ERRORS[app_init_error]}"',
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)
    db_init_error = database.init_database(Path(db_path))
    if db_init_error:
        typer.secho(
            f'Creating database failed with "{ERRORS[db_init_error]}"',
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)
    else:
        typer.secho(f"The to-do database is {db_path}", fg=typer.colors.GREEN)
```

Here we are using command decorator to make init function as **init** command that will accept database location.

**typer.Option()** method will provide an option to pass cli parameters. here we are passing four params,

* **str(database.DEFAULT\_DB\_FILE\_PATH)** - default database path
    
* **"--db-path"** - param to pass database location
    
* "-db" - param to pass database location
    
* **prompt="to-do database location?"** - prompt to ask for database location.
    

Let's initialize our application with the `init` command.

#### Example :

```python
> python3 -m todo init

# Output

# prompt for database location, if you leave it empty and `Enter` it will take the default path,
to-do database location? [/Users/username/.username_todo.json]: 

# after entering you will get output like this
The to-do database is /Users/username/.username_todo.json
```

### add command

```python
...
@todo_cli.command()
def add(
    description: List[str] = typer.Argument(...),
    priority: int = typer.Option(2, "--priority", "-p", min=1, max=3),
) -> None:
    """Add a new to-do with a DESCRIPTION."""
    todoer = get_todoer()
    todo, error = todoer.add(description, priority)
    if error:
        typer.secho(
            f'Adding to-do failed with "{ERRORS[error]}"', fg=typer.colors.RED
        )
        raise typer.Exit(1)
    else:
        typer.secho(
            f"""to-do: "{todo['Description']}" was added """
            f"""with priority: {priority}""",
            fg=typer.colors.GREEN,
        )
```

add command will accept two parameter

* description - description for the todo as an argument
    
* priority - as an option with default value 2.
    

**typer.Argument(...)** is used for required values. **typer.Option()** is used for Optional values.

**get\_todoer()** helper will return the controller instance and then we will call the `add` method of the controller with description and priority. it will return the named tuple with fields **todo** and **error**. if an error then we will print the error message using that error code with the corresponding text colour. else we will print the message that **to-do was added**.

#### Example :

```shell
> python3 -m todo add "write an article on building CLI application with typer " --priority 2

# Output
to-do: "write an article on building CLI application with typer." was added with priority: 2
```

### list command

This command will read all the to-dos from the database and print all the to-dos with some style.

```python
...
@todo_cli.command(name="list")
def list_all() -> None:
    """List all to-dos."""
    todoer = get_todoer()
    todo_list = todoer.get_todo_list()
    if len(todo_list) == 0:
        typer.secho(
            "There are no tasks in the to-do list yet", fg=typer.colors.RED
        )
        raise typer.Exit()
    typer.secho("\nto-do list:\n", fg=typer.colors.CYAN, bold=True)
    typer.secho(str(todo_list), fg=typer.colors.CYAN, bold=True)
```

We can pass the **name** param to the command decorator to specify the command name. it is optional but here we are passing **list** as a name.

#### Example :

```shell
> python3 -m todo list
```

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1642178730512/kf396dbiqM.png align="left")

### complete command

This command will take the **todo id** and mark it as complete.

```python
...
@todo_cli.command()
def complete(todo_id: int = typer.Argument(...)) -> None:
    """Complete a to-do by setting it as done using its TODO_ID."""
    todoer = get_todoer()
    todo, error = todoer.set_done(todo_id)
    if error:
        typer.secho(
            f'Completing to-do # "{todo_id}" failed with "{ERRORS[error]}"',
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)
    else:
        typer.secho(
            f"""to-do # {todo_id} "{todo['Description']}" completed!""",
            fg=typer.colors.GREEN,
        )
```

We will use this command to update our todo with todo\_id. here we have made **todo\_id** as the required argument and if we don't pass the id it will give an error **Missing argument 'TODO\_ID'**.

#### Example :

```shell
> python3 -m todo complete 1
 # Output
to-do # 1 "write an article on building CLI application with typer." completed!
```

Till now we have added commands for adding, reading and updating the todo. let's add the command to delete the todo.

### delete command

This command will take the **todo id** and the option **force** to delete the todo without confirmation.

```python
...
@todo_cli.command()
def delete(
    todo_id: int = typer.Argument(...),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force deletion without confirmation.",
    ),
) -> None:

    """Delete a to-do using its TODO_ID."""
    todoer = get_todoer()

    def _delete():
        todo, error = todoer.remove(todo_id)
        if error:
            typer.secho(
                f'Removing to-do # {todo_id} failed with "{ERRORS[error]}"',
                fg=typer.colors.RED,
            )
            raise typer.Exit(1)
        else:
            typer.secho(
                f"""to-do # {todo_id}: '{todo["Description"]}' was removed""",
                fg=typer.colors.GREEN,
            )

    if force:
        _delete()
    else:
        todo_list = todoer.get_todo_list()
        try:
            todo = todo_list[todo_id - 1]
        except IndexError:
            typer.secho("Invalid TODO_ID", fg=typer.colors.RED)
            raise typer.Exit(1)
        delete = typer.confirm(
            f"Delete to-do # {todo_id}: {todo['Description']}?"
        )

        if delete:
            _delete()
        else:
            typer.echo("Operation cancelled")
```

Here we have created the `_delete()` helper function that will use the controller instance remove method to execute the remove logic. It will return a named tuple with fields todo and error. If an error then it will print the error message with the error code else it will print the message \*\* todo was removed\*\*.

If **force** is true then we will directly call the `_delete()` helper and if not then we will ask for confirmation using **typer.confirm()** method.

If the confirmed response is true then we will call the `_delete()` helper and if not then we will print the message **Operation cancelled**

#### Example :

```shell
> python3 -m todo delete 1 
# Output 
Delete to-do # 1: write an article on building CLI application with typer.? [y/N]: N
Operation cancelled

> python3 -m todo delete 1 
# Output
Delete to-do # 1: write an article on building CLI application with typer.? [y/N]: y
to-do # 1: 'write an article on building CLI application with typer.' was removed
```

Congratulations 🎉😍, we have successfully built our first CLI application with Typer and Python.

# Summary

* We discussed what is typer and its feature.
    
* Did setup for todo CLI application
    
* Coded different modules for our application namely config, database, controller and CLI.
    
* We discussed the typer **callback()** decorator.
    
* We Discussed typer **Option()** and **Argument()** methods
    
* We Discussed typer command decorator with or without name param
    
* Added different commands to our CLI **init**, **add**, **list**, **complete** and **delete**.
    

# Conclusion

Typer is a great python library that allows us to build a CLI application with minimal effort and less code. we can use typer to build simple as well as complex CLI applications. give it a try and let me know in the comments what kind of CLI you have built with typer.

And that’s it for this topic. Thank you for reading.

# Connect with me

[LinkedIn](https://www.linkedin.com/in/sachin-chaurasiya) | [Twitter](https://twitter.com/sachindotcom)
