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