Packages

All our packages should be distributed on PyPI.

Use the Pypackage Cookiecutter template, and add the repository to pre-commit.ci:

uv tool install cookiecutter
cookiecutter gh:open-contracting/software-development-handbook --directory cookiecutter-pypackage

uv venv -p 3.9
uv pip install -e .[test]

uv tool install pre-commit
pre-commit install

See also

Package-rated content in Directory layout, Testing and Linting

Metadata

If the package is distributed on PyPI, use this template for the pyproject.toml file, adding options like entry-points as needed, and removing the Jinja syntax if not using the Cookiecutter template:

[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"

[project]
name = "{{ cookiecutter.package_name }}"
version = "0.0.1"
authors = [{name = "Open Contracting Partnership", email = "data@open-contracting.org"}]
description = "{{ cookiecutter.short_description }}"
readme = "README.rst"
license = {text = "BSD"}
urls = {Homepage = "https://github.com/open-contracting/{{ cookiecutter.repository_name }}"}
classifiers = [
    "License :: OSI Approved :: BSD License",
{%- if cookiecutter.os_independent == "y" %}
    "Operating System :: OS Independent",
{%- else %}
    "Operating System :: POSIX :: Linux",
{%- endif %}
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: Implementation :: CPython",
{%- if cookiecutter.pypy == "y" %}
    "Programming Language :: Python :: Implementation :: PyPy",
{%- endif %}
]
dependencies = []

[project.optional-dependencies]
test = [
    "coverage",
    "pytest",
]

[tool.setuptools.packages.find]
exclude = [
    "tests",
    "tests.*",
]

[tool.ruff]
line-length = 119
target-version = "py39"

[tool.ruff.lint]
select = ["ALL"]
ignore = [
    "ANN", "C901", "COM812", "D203", "D212", "D415", "EM", "ISC001", "PERF203", "PLR091", "Q000",
]

[tool.ruff.lint.flake8-builtins]
builtins-ignorelist = ["copyright"]

[tool.ruff.lint.flake8-unused-arguments]
ignore-variadic-names = true

[tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "INP001"]
"tests/*" = [
    "ARG001", "D", "FBT003", "INP001", "PLR2004", "S", "TRY003",
]

If the package isn’t distributed on PyPI, use this template pyproject.toml:

[project]
name = "NAME"
version = "0.0.0"

[tool.setuptools.packages.find]
exclude = ["tests", "tests.*"]

Reference: Packaging and distributing projects

Requirements

  • Use dependencies and optional-dependencies in the pyproject.toml file

  • Do not use a requirements.txt file

  • Sort requirements alphabetically

Classifiers

"Operating System :: OS Independent"

The package is tested on macOS, Windows and Ubuntu.

"Operating System :: POSIX :: Linux"

The package is tested on Ubuntu only.

"Programming Language :: Python :: Implementation :: PyPy"

The package is tested on PyPy.

Documentation

The template reads the documentation from a README.rst file. To convert a README.md file, install pandoc and run:

pandoc --from=markdown --to=rst --output=README.rst README.md

Publish releases

Continuous integration

To publish tagged releases to PyPI, create a .github/workflows/pypi.yml file:

name: Publish to PyPI
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - run: pip install --upgrade build
      - run: python -m build --sdist --wheel
      - uses: actions/upload-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
  test:
    needs: build
    permissions:
      id-token: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1
        with:
          repository-url: https://test.pypi.org/legacy/
          skip-existing: true
  publish:
    if: startsWith(github.ref, 'refs/tags/')
    needs: test
    permissions:
      id-token: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1

The open-contracting organization sets the PYPI_API_TOKEN organization secret to the API token of the opencontracting PyPI user, and TEST_PYPI_API_TOKEN to that of the opencontracting Test PyPI user.

After publishing the first release to PyPI, add additional owners.

Release process

  1. Ensure that you are on an up-to-date main branch:

    git checkout main
    git pull --rebase
    
  2. Ensure that the package is ready for release:

    • All tests pass on continuous integration

    • The version number is correct in pyproject.toml and docs/conf.py (if present)

    • The changelog is up-to-date and dated

  3. Tag the release, replacing x.y.z twice:

    git tag -a x.y.z -m 'x.y.z release.'
    
  4. Push the release:

    git push --follow-tags
    
  5. Announce on the discussion group if relevant

Reference: Publishing package distribution releases using GitHub Actions CI/CD workflows