Python Packaging

Package structure, pyproject.toml configuration, and distribution.

pyproject.toml Structure

pyproject.toml replaces setup.py + setup.cfg — single source of truth for project metadata
[project]
name = "domus-api"
version = "0.3.0"
description = "Infrastructure API for Domus Digitalis"
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
authors = [{name = "Evan", email = "evan@domusdigitalis.dev"}]
dependencies = [
    "fastapi>=0.115.0",
    "httpx>=0.28.0",
    "pydantic>=2.10.0",
    "pydantic-settings>=2.7.0",
    "uvicorn[standard]>=0.34.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-cov>=6.0",
    "ruff>=0.9.0",
]

[project.scripts]
domus-api = "domus_api.main:cli"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

src Layout

src/ layout prevents accidental imports from project root — the correct default
domus-api/
├── pyproject.toml
├── src/
│   └── domus_api/
│       ├── __init__.py      # contains __version__
│       ├── main.py          # FastAPI app + CLI entry point
│       ├── models.py        # Pydantic models
│       ├── routes/
│       │   ├── __init__.py
│       │   └── devices.py
│       └── dependencies.py
├── tests/
│   ├── conftest.py
│   └── test_devices.py
└── README.md

Version Management

Single source of version — init.py or dynamic from pyproject.toml
# src/domus_api/__init__.py
__version__ = "0.3.0"

# Access at runtime
from domus_api import __version__

# Or read from installed package metadata (no hardcoded version)
from importlib.metadata import version
__version__ = version("domus-api")

Entry Points (console_scripts)

console_scripts create CLI commands — installed into PATH automatically
# In pyproject.toml
[project.scripts]
domus-api = "domus_api.main:cli"        # calls cli() function in main.py
domus-check = "domus_api.health:check"  # separate health check command
# src/domus_api/main.py
import uvicorn

def cli():
    """Entry point for `domus-api` command."""
    uvicorn.run("domus_api.main:app", host="0.0.0.0", port=8000, reload=True)

Building with uv

uv build creates wheel + sdist — faster than pip, lockfile-aware
# Install uv (if not already)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Build wheel and sdist
uv build
# Output:
#   dist/domus_api-0.3.0-py3-none-any.whl
#   dist/domus_api-0.3.0.tar.gz

# Build wheel only
uv build --wheel

Editable Install

Editable install links to source — changes take effect without reinstalling
# Install in editable mode with dev dependencies
uv pip install -e ".[dev]"

# Or with pip
pip install -e ".[dev]"

# Verify installation
python -c "import domus_api; print(domus_api.__version__)"
domus-api  # CLI entry point works immediately

Publishing with uv

Publish to PyPI or private index — uv handles authentication and upload
# Build first
uv build

# Publish to PyPI (requires API token)
uv publish

# Publish to private index
uv publish --index-url https://pypi.domusdigitalis.dev/simple/

# Test on TestPyPI first
uv publish --index-url https://test.pypi.org/simple/

Wheel vs Sdist

Wheel is the install format, sdist is the source format — always ship both
dist/
├── domus_api-0.3.0-py3-none-any.whl   # wheel: pre-built, fast install
│                                        #   py3 = Python 3
│                                        #   none = no ABI dependency
│                                        #   any = platform-independent
└── domus_api-0.3.0.tar.gz             # sdist: source archive, needs build step

# Wheel internals (it's a zip file):
domus_api/
├── __init__.py
├── main.py
└── ...
domus_api-0.3.0.dist-info/
├── METADATA
├── WHEEL
├── RECORD        # checksums for every file
└── entry_points.txt

Tool Configuration in pyproject.toml

Keep all tool config in pyproject.toml — no more scattered .ini and .cfg files
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
markers = [
    "slow: marks tests as slow",
    "integration: marks integration tests",
]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP"]

[tool.coverage.run]
source = ["src/domus_api"]
omit = ["*/tests/*"]

[tool.coverage.report]
fail_under = 80
show_missing = true

Dependency Management with uv

uv manages virtualenvs and lockfiles — replaces pip + venv + pip-tools
# Create project with uv
uv init domus-api
cd domus-api

# Add dependencies
uv add fastapi httpx pydantic

# Add dev dependencies
uv add --dev pytest pytest-cov ruff

# Lock dependencies (creates uv.lock)
uv lock

# Sync environment to match lockfile
uv sync

# Run command in project environment
uv run pytest
uv run domus-api