Building and Releasing a Python CLI
Tags: opensource, cli, python, tutorial
Published: 2023-11-25 04:30:17

So this week I packaged and released the final version of my CLI, [Tiller](https://github.com/Namatuzio/tiller). It is now officially downloadable through [PyPI](https://test.pypi.org/project/namatuzio-tiller/) as well!

This was something totally new to me so there was lots to be learned!

## Package and Release method

One of the first things to consider when releasing any kind of project is *where* users will be able to find it. Considering I used Python, I decided to go with a popular package hosting site called [PyPi](https://test.pypi.org/). The best part is that there's a very useful [guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/) on how to do just what I wanted to do!

This guide helped tremendously with getting everything uploaded and working, but don't worry if you decide to go with other package distribution sites or platforms, there are bound to be many tutorials on how to use it!

So to get started, I refactored my file structure within my repo to separate test and functional code. I also added an empty `__init__.py` file, which I was unsure of its use until today. `__init__.py` is a handy file that tells Python what to do when a package is first installed. I also learned that without an init file, Python packages are treated as namespace packages, which for 99% of use cases, is completely unnecessary, especially for my CLI which should be run in a self-contained package. 

Another concept I learned was about [build backends](https://packaging.python.org/en/latest/glossary/#term-Build-Backend), an import step which is used to initialize and install any dependencies of the app you're packaging. Since the tutorial went with using [Hatch](https://hatch.pypa.io/latest/) that is also what I went with, though it didn't provide a lot of useful details especially because it didn't show how to add any dependencies, so I took a look at the docs which were very nice and simple to follow. 

Then I set all the metadata required for my app such as version numbers, names, authors, etc. and moved on to building my application. This was done using `py -m build` which generated 2 types of files; a tar.gz which is a [source distribution file](https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist) and a .whl which is a [built distribution file](https://packaging.python.org/en/latest/glossary/#term-Built-Distribution). 

A source distribution file is a file that contains raw information about a package and contains only the essential files required for installing from tools like pip or being turned into a built distribution.

A built distribution file is a pre-built package file that simply needs to find the right home for the package to run as expected. Both were concepts that were somewhat new to me. I've seen built distribution files previously but had no idea what the file extension really meant. 

Anyways, I then installed [twine](https://twine.readthedocs.io/en/latest/) which would allow me to finally upload my CLI to PyPi. Running `py -m twine upload --repository testpypi dist/*` took everything from the previously made `dist` file and uploaded it onto PyPi to be openly downloaded. After some bug fixing and a few patch release later, everything was running smoothly, and [tiller](https://github.com/Namatuzio/tiller) was finally ready to be used.

## Problems

During the packaging process, I ran into a few problems, it's the reason why my [tags](https://github.com/Namatuzio/tiller/tags) are all over the place in terms of versioning. Some of the changes were so minuscule I overlooked adding a new patch version.

A lot of my problems came from my `pyproject.toml` file which after uploading everything to PyPi, would not properly load everything which made me lost on how to run my app. It was at this point that I actually decided to take a deeper look at the hatchling docs to find out I was missing the dependencies part. I also decided to add an entry point into my app, allowing users to simply the call to running tiller by adding the following to my `pyproject.toml`

```toml
[project.scripts]
tiller = "src.namatuzio_tiller_package.main:app"
```

This lets users call all my functionality by calling `tiller` as opposed to `./main.py` when the app is installed.

Another issue was forgetting to update certain aspects of my code. For example, when I tested my -v functionality, I had the check set to a default string, when I could've simply imported my `__version__` variable from the main file to have it be the same in multiple locations. 

## Testing other CLIs

The CLI I decided to test was [Txt2StaticHTML](https://github.com/Amir-Helali/Txt2StaticHtml) a package of similar functionality, though done in C#. It was interesting testing it mainly because of how different NuGet packages are from Python packages. I had to open up VS2022 and install the NuGet package. Everything was inside of the namespace the repo owner had developed using the namespace allowed all of the functionality to become available. Calling 
```c#
Converter.Run(<DIR>);
```
Ran the default functionality, and even more could be called using a string array. I didn't have much feedback to give especially since the README drew everything out for me. The functionality was all there and it was pretty simple to use and a great refresher on C#. 

## Having my CLI tested

The owner of Txt2StaticHtml also tested my package out. He installed everything from the PyPi page using pip and followed the rest of my README to test the functionality. There wasn't a lot of feedback from the tester because I tried to make my README as descriptive as possible. 

## Try it for yourself!

Did you want to try out Tiller? Well if that's a yes then worry not! It's extremely simple to download and use. 

### Installation

Please note that Tiller requires Python 3.11 or higher to run due to the use of TOMLib.

```cmd
pip install -i https://test.pypi.org/simple/ namatuzio-tiller
``` 

### Package Usage:

    tiller [OPTIONS] DIR

### Options:

    --version, -v  Print the current version of Tiller.
    --help, -h     Print the help message.
    --config, -c   Specify the path to a TOML config file to be used.
    --output, -o   Change the output directory of the .html files.
    --lang, -l    Specify the language of the generated HTML file.

#### Display the current version of Tiller:
  
```
.\main.py --version (or -v)

Tiller Version: 1.1.0 
```

#### Transform a file through a relative path:

```
tiller .\example.txt
Converted example.txt to example.html
```

```
.\example.txt

Hello

World\\

How are you?
```

```html
.\til\example.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <h1>Hello</h1>
    <body>
        <p>Hello</p>
		<p>World</p>
		<p>How are you?</p>
    </body>
</html>
```

#### Generate HTML file with a different language:
```
tiller --lang fr .\example.txt

Converted example.txt to example.html
```
```html
.\example.txt

Hello

World

How are you?
```
```html
.\til\example.html
<!DOCTYPE html>
<html lang="fr">
    <style>
    body {
        background-color: rgb(0, 116, 145);
        text-align: center;
        color: white;
        font-family: Arial, Helvetica, sans-serif;
        font-size: xx-large;
    }
    </style>
    <head>
        <meta charset="UTF-8">
        <title>example</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <h1>example</h1>
    <body>
        <p>Hello</p>
		<p>World</p>
		<p>How are you?</p>
    </body>
</html>
```

#### Transform file(s) using a TOML config file
```
tiller --config (or -c) .\examples\TOMLExample.toml .\examples

Converted example.txt to example.html
Converted example2.txt to example2.html
Converted example3.md to example3.html
example4.html is not a .txt file or a .md file. Skipping...

TOMLExample.toml is not a .txt file or a .md file. Skipping...
```