From Dev to Deployment: How I Published 'gh-echo v1.0.0' to PyPI

From Dev to Deployment: How I Published 'gh-echo v1.0.0' to PyPI

An End-to-End Guide on Building, Testing, and Publishing a Python CLI Tool with Poetry

Introduction

Hello Everyone! I’m excited to announce that GitHub Echo Version 1.0.0 is finally out! 🎉 This release comes packed with features and improvements compared to the previous version available on PyPI.

Here’s what’s new:

  • Improved Config File Support: Streamlined usage for better customization.

  • Enhanced Error Handling: Covering more edge cases to ensure a seamless experience.

  • Better Documentation: Comprehensive examples and detailed guides to get you started quickly.

To make this release as robust as possible, I had to:

  • Test all aspects of the application extensively.

  • Add error handling for numerous use cases.

  • Write additional tests to ensure reliability.

  • Enhance the documentation with clear examples.

I’ve built this tool using Poetry for dependency management and publishing, and it's available on PyPI. In this blog, I’ll walk you through my release process, the tools I used, the commands I ran, and the actions I took to publish GitHub Echo 1.0.0 successfully.

The pyproject.toml file

If you're using Poetry for dependency management and packaging like I am, you already have a pyproject.toml file. This file is how Poetry tracks project configurations, dependencies, and metadata. However, to package and publish your project successfully, additional setup is required in this file.

Below is a complete example of my pyproject.toml file for GitHub Echo, and I’ll walk you through what each line does and why it’s important.

[tool.poetry]
name = "gh-echo"
version = "1.0.0"
description = "A command-line tool built to obtain in-depth, actionable information about GitHub repositories."
authors = ["Aryan Khurana <aryankhurana1511@gmail.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://pypi.org/project/gh-echo/o"
repository = "https://github.com/AryanK1511/github-echo"
documentation = "https://github.com/AryanK1511/github-echo/blob/main/README.md"
keywords = ["GitHub", "GenAI", "Python3", "CLI"]
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent"
]
packages = [{include = "application"}]
include = ["_main.py", "_config.py", "_constants.py", "pyproject.toml"]

[tool.poetry.scripts]
lint = "_scripts:lint"
format = "_scripts:format_code"
lint-and-format = "_scripts:lint_and_format"
run-tests = "_scripts:run_tests"
run-tests-on-files = "_scripts:run_tests_on_files"
run-tests-on-classes = "_scripts:run_tests_on_classes"
run-coverage = "_scripts:run_coverage"
run-coverage-report = "_scripts:run_coverage_report"
run-coverage-html = "_scripts:run_coverage_html"
watch-tests = "_scripts:watch_tests"
watch-tests-coverage = "_scripts:watch_tests_with_coverage"
gh-echo = "_main:app"

[tool.poetry.dependencies]
python = "^3.9"
typer = "^0.12.5"
google-generativeai = "^0.8.0"
python-dotenv = "^1.0.1"
httpx = "^0.27.2"
pytest = "^8.3.3"
single-source = "^0.4.0"
groq = "^0.11.0"
toml = "^0.10.2"
pre-commit = "^4.0.1"
coverage = "^7.6.4"
pytest-watch = "^4.2.0"

[tool.poetry.group.dev.dependencies]
ruff = "^0.6.4"

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

[tool.poetry]

The [tool.poetry] section contains metadata about my project that is crucial for publishing and distributing it on platforms like PyPI.

[tool.poetry]
name = "gh-echo"  
version = "1.0.0"  
description = "A command-line tool built to obtain in-depth, actionable information about GitHub repositories."  
authors = ["Aryan Khurana <aryankhurana1511@gmail.com>"]  
license = "MIT"  
readme = "README.md"  
homepage = "https://pypi.org/project/gh-echo/"  
repository = "https://github.com/AryanK1511/github-echo"  
documentation = "https://github.com/AryanK1511/github-echo/blob/main/README.md"  
keywords = ["GitHub", "GenAI", "Python3", "CLI"]  
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent"
]
  1. name: The package name as it will appear on PyPI.

  2. version: Version number following Semantic Versioning.

  3. description: A brief description of what the package does.

  4. authors: List of authors in the format Name <email> for credit and contact.

  5. license: Indicates the licensing model; in this case, it's MIT.

  6. readme: The file containing the long description of your project (usually Markdown).

  7. homepage: Link to the project’s PyPI page.

  8. repository: URL of the project’s source code repository.

  9. documentation: Link to the documentation, often the README.md on GitHub.

  10. keywords: Tags to make your project searchable.

  11. classifiers: PyPI Classifiers to categorize the package.

Packages and Include

packages = [{include = "application"}]  
include = ["_main.py", "_config.py", "_constants.py", "pyproject.toml"]
  1. packages: Specifies the directory to include as part of the package (e.g., the application folder).

  2. include: Ensures additional files outside the package (like config files) are included when the package is distributed.

[tool.poetry.scripts]

[tool.poetry.scripts]
gh-echo = "_main:app"

This section creates CLI commands for my project.

  1. gh-echo: The command users will type in the terminal to run my application.

  2. _main:app: Points to the entry point of my application, where app is defined with Typer.

Dependencies

These are the dependencies and the development dependencies that the project needs to run.

[tool.poetry.dependencies]
python = "^3.9"
typer = "^0.12.5"
google-generativeai = "^0.8.0"
python-dotenv = "^1.0.1"
httpx = "^0.27.2"
pytest = "^8.3.3"
single-source = "^0.4.0"
groq = "^0.11.0"
toml = "^0.10.2"
pre-commit = "^4.0.1"
coverage = "^7.6.4"
pytest-watch = "^4.2.0"

[tool.poetry.group.dev.dependencies]
ruff = "^0.6.4"

[build-system]

[build-system]
requires = ["poetry-core>=1.0.0"]  
build-backend = "poetry.core.masonry.api"
  1. requires: Specifies the required build backend (Poetry).

  2. build-backend: Points to the Poetry core system for handling builds.

Why Is This File Important?

This file is critical for:

  • Dependency management: Poetry ensures all dependencies are properly tracked.

  • Publishing: The metadata is required for uploading your package to PyPI.

  • Automation: The scripts and include sections allow for custom workflows.

Building and Publishing

In this section, we’ll walk through the entire process of building and publishing a Python package to both TestPyPI and PyPI using Poetry. This will allow others to pip install gh-echo and start using your tool effortlessly. Before proceeding, ensure you’ve configured your pyproject.toml file as detailed in the previous section.

What Happens When You Publish a Package?

When you run pip install, Python fetches the package from the PyPI repository. To make your package available for this process, you need to upload it to PyPI.

However, once a specific version is published on PyPI, you cannot overwrite it. Therefore, we first use TestPyPI, a testing environment, to ensure everything works as expected before publishing to the main repository.

Here’s a complete step-by-step guide to achieve this.

1. Create an Account on TestPyPI and PyPI

  1. Visit TestPyPI and PyPI to create accounts if you don’t already have them.

  2. Once registered, generate an API token on each platform:

    • For TestPyPI: Go to your account settings and generate a new API token here.

    • For PyPI: Generate an API token from your account settings here.

Save these tokens securely, as they will be used for authentication.

2. Configure Poetry to Use the Repositories

You need to tell Poetry where to publish your package. Add TestPyPI and PyPI as repositories in your Poetry configuration:

poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry config repositories.pypi https://upload.pypi.org/legacy/

Next, set up authentication credentials for both repositories using your API tokens:

poetry config pypi-token.testpypi <your_testpypi_token>
poetry config pypi-token.pypi <your_pypi_token>

These commands save your credentials securely in Poetry’s configuration file.

📖 Learn more about adding repositories here.

3. Build Your Package

Once your pyproject.toml is ready, build your package into a distributable format:

poetry build

This will create the following files in the dist/ directory:

  • A .tar.gz file: Source distribution (sdist).

  • A .whl file: Built distribution (wheel).

4. Publish to TestPyPI

Before publishing to PyPI, test your package on TestPyPI:

poetry publish -r testpypi

This command uploads your package to TestPyPI. Ensure everything works as expected by installing your package from TestPyPI in a virtual environment:

pip install -i https://test.pypi.org/simple/ gh-echo

Test your package thoroughly after installation.

5. Publish to PyPI

Once satisfied with your package’s functionality on TestPyPI, publish it to the main PyPI repository:

poetry publish --no-interaction --verbose

This command uploads your package to PyPI, making it publicly available.

6. Verify and Install

After publishing, verify that your package is live on PyPI by visiting the homepage (e.g., https://pypi.org/project/gh-echo/).

To test installation from PyPI, run:

pip install gh-echo

Completing the Release Process: Versioning, Tagging, and Pushing to GitHub

After publishing the package to PyPI, the next step is to ensure your repository is up-to-date and reflects the changes you’ve made. This includes committing all changes, creating a release tag, and pushing everything to GitHub.

Here’s how you can do it step by step:

1. Commit All Changes

First, ensure all your changes, including the updates to the pyproject.toml, README.md, or any other files, are committed.

Run the following commands:

# Stage all changes
git add .

# Commit changes with a meaningful message
git commit -m "Prepare release 1.0.0: Update pyproject.toml, add docs, and publish to PyPI"

2. Create a Git Tag

Tags are essential for marking specific points in your repository's history as release versions. You can use a lightweight or annotated tag. Here, we’ll create an annotated tag for version 1.0.0:

git tag -a v1.0.0 -m "Release version 1.0.0: First stable release of gh-echo"

This command:

  • -a: Creates an annotated tag.

  • -m: Adds a message describing the tag.

3. Push Changes to GitHub

Push all your committed changes along with the newly created tag to GitHub:

# Push all commits
git push origin main

# Push the tag
git push origin v1.0.0

4. Create a Release on GitHub

To make your release official, create a release on GitHub. Here’s how I did it:

  1. Go to the repository’s Releases page.

  2. Click "Draft a new release".

  3. Select the tag v1.0.0 from the dropdown menu or type it in.

  4. Add a release title like "gh-echo v1.0.0: First Stable Release".

  5. Provide a summary of what’s new in this version or let GitHub generate the automated change log.

  6. Attach any relevant release artifacts if needed (e.g., .tar.gz, .whl files from the dist/ folder).

  7. Click "Publish Release".

Final Verification and Conclusion

To ensure everything is correctly set up:

  • Check your GitHub repository for the new tag and release.

  • Verify the PyPI page for gh-echo to confirm the latest version is live.

  • Install your tool using:

pip install gh-echo

Now your package is ready for the world to use! 🎉

You can also check out my Release on Github and Package on PyPI.