Aller au contenu

Construction et packaging

L'écosystème Python a profondement évolue ces dernières années avec l'émergence de nouveaux gestionnaires de paquets (poetry, uv) et la standardisation autour de pyproject.toml. Cette section couvre le packaging de librairies, la publication sur PyPI, la conteneurisation et les pipelines CI/CD.


Comparatif des gestionnaires de paquets

Outil Vitesse Lock file Virtualenv Publish PyPI Points forts
pip Lent Non natif Non Via twine Standard, toujours present
poetry Moyen Oui Oui Oui Tout-en-un, gestion des dépendances fine
uv Très rapide Oui Oui Non direct Remplacé pip/venv/pip-tools, écrit en Rust
pdm Rapide Oui Oui Oui Standards PEP 517/582, flexible

uv en 2025

uv (de Astral, créateurs de ruff) est devenu le gestionnaire le plus rapide disponible. Il remplacé pip, pip-tools et virtualenv avec une compatibilité quasi-totale. Recommande pour les nouveaux projets.


pyproject.toml — Le standard actuel

pyproject.toml est le fichier de configuration unifie défini par PEP 517/518/621. Il remplacé setup.py, setup.cfg et requirements.txt pour les projets packagables.

Exemple complet avec uv

# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mon-paquet"
version = "1.0.0"
description = "Un exemple de paquet Python"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.12"
authors = [{ name = "Fabien", email = "contact@nuevolia.dev" }]
keywords = ["api", "fastapi", "exemple"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3.12",
    "License :: OSI Approved :: MIT License",
]

dependencies = [
    "fastapi>=0.115",
    "sqlalchemy>=2.0",
    "pydantic>=2.0",
    "uvicorn[standard]>=0.30",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-asyncio>=0.23",
    "pytest-cov>=5.0",
    "httpx>=0.27",
    "hypothesis>=6.0",
]
lint = [
    "ruff>=0.4",
    "mypy>=1.10",
]

[project.scripts]
mon-api = "mon_paquet.main:run"

[project.urls]
Homepage = "https://nuevolia.dev"
Repository = "https://github.com/fabien/mon-paquet"

[tool.uv]
dev-dependencies = [
    "pytest>=8.0",
    "ruff>=0.4",
]

Commandes uv essentielles

# Installer uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# Creer un nouveau projet
uv init mon-projet
cd mon-projet

# Ajouter une dependance
uv add fastapi
uv add --dev pytest ruff

# Installer les dependances (genere uv.lock)
uv sync

# Lancer un script dans l'environnement
uv run python main.py
uv run pytest

# Mettre a jour toutes les dependances
uv lock --upgrade
uv sync

Workflow avec poetry

# Installer poetry
curl -sSL https://install.python-poetry.org | python3 -

# Creer un projet
poetry new mon-projet
cd mon-projet

# Ajouter des dependances
poetry add fastapi uvicorn
poetry add --group dev pytest ruff mypy

# Installer et activer l'environnement
poetry install
poetry shell

# Publier sur PyPI
poetry build       # Cree dist/mon_projet-1.0.0.tar.gz et .whl
poetry publish     # Publie sur PyPI (necessite un compte)

Build et publication sur PyPI

# Avec uv et hatch
uv build
# => dist/mon_paquet-1.0.0-py3-none-any.whl
# => dist/mon_paquet-1.0.0.tar.gz

# Publication via twine (compatible pip/uv)
pip install twine
twine upload dist/*

# Sur PyPI Test d'abord (recommande)
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ mon-paquet

Tokens PyPI

N'utilisez jamais votre mot de passe PyPI directement. Créez un token API sur pypi.org et stockez-le dans ~/.pypirc ou via la variable d'environnement TWINE_TOKEN.


Dockerfile optimise multi-stage

Un Dockerfile Python de production doit minimiser la taille de l'image finale et séparer les étapes de build et de runtime.

# Dockerfile
# ============================================================
# Stage 1 : builder — installe les dependances
# ============================================================
FROM python:3.12-slim AS builder

WORKDIR /build

# Copier uniquement les fichiers de dependances d'abord
# (optimise le cache Docker lors des modifications de code)
COPY pyproject.toml uv.lock ./

# Installer uv et les dependances dans un dossier isole
RUN pip install --no-cache-dir uv && \
    uv export --no-dev --format requirements-txt > requirements.txt && \
    pip install --no-cache-dir --target=/deps -r requirements.txt

# ============================================================
# Stage 2 : runtime — image minimale sans outils de build
# ============================================================
FROM python:3.12-slim AS runtime

# Utilisateur non-root pour la securite
RUN useradd --create-home --shell /bin/bash appuser

WORKDIR /app

# Copier les dependances depuis le builder
COPY --from=builder /deps /deps
ENV PYTHONPATH=/deps

# Copier le code applicatif
COPY --chown=appuser:appuser . .

USER appuser

# Variables d'environnement recommandees
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PORT=8000

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
# Construction et test de l'image
docker build -t mon-api:latest .
docker run -p 8000:8000 mon-api:latest

# Verification de la taille
docker images mon-api

.dockerignore

__pycache__/
*.pyc
*.pyo
.pytest_cache/
.mypy_cache/
.ruff_cache/
htmlcov/
dist/
*.egg-info/
.env
.env.*
tests/
docs/
*.md
.git/

Pipeline CI/CD — GitHub Actions

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  lint:
    name: Lint et formatage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Installer uv
        uses: astral-sh/setup-uv@v3

      - name: Installer Python
        run: uv python install 3.12

      - name: Installer les dependances
        run: uv sync --all-extras

      - name: Verifier le formatage (ruff)
        run: uv run ruff format --check .

      - name: Linter (ruff)
        run: uv run ruff check .

      - name: Verification des types (mypy)
        run: uv run mypy .

  test:
    name: Tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.12", "3.13"]
    steps:
      - uses: actions/checkout@v4

      - name: Installer uv
        uses: astral-sh/setup-uv@v3

      - name: Installer Python ${{ matrix.python-version }}
        run: uv python install ${{ matrix.python-version }}

      - name: Installer les dependances
        run: uv sync --all-extras

      - name: Executer les tests avec couverture
        run: uv run pytest --cov=. --cov-report=xml --cov-fail-under=75

      - name: Publier la couverture
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml

  build:
    name: Build Docker
    runs-on: ubuntu-latest
    needs: [lint, test]
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Build image Docker
        run: docker build -t mon-api:${{ github.sha }} .

      - name: Test de sante de l'image
        run: |
          docker run -d -p 8000:8000 --name test-container mon-api:${{ github.sha }}
          sleep 5
          curl -f http://localhost:8000/health || exit 1
          docker stop test-container