Aller au contenu

Paramètres et variables d'environnement

🌐 Traduction par IA et humains

Cette traduction a été réalisée par une IA guidée par des humains. 🤝

Elle peut contenir des erreurs d'interprétation du sens original, ou paraître peu naturelle, etc. 🤖

Vous pouvez améliorer cette traduction en nous aidant à mieux guider le LLM d'IA.

Version anglaise

Dans de nombreux cas, votre application peut avoir besoin de paramètres ou de configurations externes, par exemple des clés secrètes, des identifiants de base de données, des identifiants pour des services d'e-mail, etc.

La plupart de ces paramètres sont variables (peuvent changer), comme les URL de base de données. Et beaucoup peuvent être sensibles, comme les secrets.

C'est pourquoi il est courant de les fournir via des variables d'environnement lues par l'application.

Astuce

Pour comprendre les variables d'environnement, vous pouvez lire Variables d'environnement.

Types et validation

Ces variables d'environnement ne gèrent que des chaînes de texte, car elles sont externes à Python et doivent être compatibles avec d'autres programmes et le reste du système (et même avec différents systèmes d'exploitation, comme Linux, Windows, macOS).

Cela signifie que toute valeur lue en Python depuis une variable d'environnement sera une str, et toute conversion vers un autre type ou toute validation doit être effectuée dans le code.

Pydantic Settings

Heureusement, Pydantic fournit un excellent utilitaire pour gérer ces paramètres provenant des variables d'environnement avec Pydantic : gestion des paramètres.

Installer pydantic-settings

D'abord, vous devez créer votre environnement virtuel, l'activer, puis installer le paquet pydantic-settings :

$ pip install pydantic-settings
---> 100%

Il est également inclus lorsque vous installez les extras all avec :

$ pip install "fastapi[all]"
---> 100%

Créer l'objet Settings

Importez BaseSettings depuis Pydantic et créez une sous-classe, comme pour un modèle Pydantic.

De la même manière qu'avec les modèles Pydantic, vous déclarez des attributs de classe avec des annotations de type, et éventuellement des valeurs par défaut.

Vous pouvez utiliser toutes les mêmes fonctionnalités et outils de validation que pour les modèles Pydantic, comme différents types de données et des validations supplémentaires avec Field().

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Astuce

Si vous voulez quelque chose à copier-coller rapidement, n'utilisez pas cet exemple, utilisez le dernier ci-dessous.

Ensuite, lorsque vous créez une instance de cette classe Settings (dans ce cas, l'objet settings), Pydantic lira les variables d'environnement de manière insensible à la casse, donc une variable en majuscules APP_NAME sera tout de même lue pour l'attribut app_name.

Il convertira ensuite et validera les données. Ainsi, lorsque vous utilisez cet objet settings, vous aurez des données des types que vous avez déclarés (par exemple, items_per_user sera un int).

Utiliser settings

Vous pouvez ensuite utiliser le nouvel objet settings dans votre application :

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Exécuter le serveur

Ensuite, vous exécutez le serveur en passant les configurations comme variables d'environnement ; par exemple, vous pouvez définir un ADMIN_EMAIL et APP_NAME avec :

$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Astuce

Pour définir plusieurs variables d'environnement pour une seule commande, séparez-les simplement par un espace et placez-les toutes avant la commande.

Ainsi, le paramètre admin_email sera défini sur « deadpool@example.com ».

Le app_name sera « ChimichangApp ».

Et items_per_user conservera sa valeur par défaut de 50.

Paramètres dans un autre module

Vous pouvez placer ces paramètres dans un autre module comme vous l'avez vu dans Applications plus grandes - Plusieurs fichiers.

Par exemple, vous pourriez avoir un fichier config.py avec :

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()

Puis l'utiliser dans un fichier main.py :

from fastapi import FastAPI

from .config import settings

app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Astuce

Vous aurez également besoin d'un fichier __init__.py comme vous l'avez vu dans Applications plus grandes - Plusieurs fichiers.

Paramètres dans une dépendance

Dans certains cas, il peut être utile de fournir les paramètres via une dépendance, au lieu d'avoir un objet global settings utilisé partout.

Cela peut être particulièrement utile pendant les tests, car il est très facile de surcharger une dépendance avec vos propres paramètres personnalisés.

Le fichier de configuration

En repartant de l'exemple précédent, votre fichier config.py pourrait ressembler à :

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

Notez que maintenant, nous ne créons pas d'instance par défaut settings = Settings().

Le fichier principal de l'application

Nous créons maintenant une dépendance qui renvoie un nouveau config.Settings().

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Astuce

Nous parlerons de @lru_cache dans un instant.

Pour l'instant, vous pouvez supposer que get_settings() est une fonction normale.

Nous pouvons ensuite l'exiger depuis la fonction de chemin d'accès comme dépendance et l'utiliser où nous en avons besoin.

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Paramètres et tests

Il devient alors très simple de fournir un autre objet de paramètres pendant les tests en créant une surcharge de dépendance pour get_settings :

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }

Dans la surcharge de dépendance, nous définissons une nouvelle valeur pour admin_email lors de la création du nouvel objet Settings, puis nous renvoyons ce nouvel objet.

Nous pouvons ensuite tester qu'il est bien utilisé.

Lire un fichier .env

Si vous avez de nombreux paramètres susceptibles de beaucoup changer, peut-être selon les environnements, il peut être utile de les placer dans un fichier, puis de les lire comme s'il s'agissait de variables d'environnement.

Cette pratique est suffisamment courante pour avoir un nom ; ces variables d'environnement sont fréquemment placées dans un fichier .env, et le fichier est appelé un « dotenv ».

Astuce

Un fichier commençant par un point (.) est un fichier caché dans les systèmes de type Unix, comme Linux et macOS.

Mais un fichier dotenv n'a pas forcément exactement ce nom de fichier.

Pydantic prend en charge la lecture depuis ce type de fichiers en utilisant une bibliothèque externe. Vous pouvez en lire davantage ici : Pydantic Settings : prise en charge de Dotenv (.env).

Astuce

Pour que cela fonctionne, vous devez exécuter pip install python-dotenv.

Le fichier .env

Vous pouvez avoir un fichier .env avec :

ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"

Lire les paramètres depuis .env

Puis mettre à jour votre config.py avec :

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")

Astuce

L'attribut model_config est utilisé uniquement pour la configuration Pydantic. Vous pouvez en lire davantage ici : Pydantic : Concepts : Configuration.

Ici, nous définissons la configuration env_file à l'intérieur de votre classe Pydantic Settings et lui attribuons le nom du fichier dotenv que nous voulons utiliser.

Créer Settings une seule fois avec lru_cache

Lire un fichier depuis le disque est normalement une opération coûteuse (lente), vous voudrez donc probablement le faire une seule fois puis réutiliser le même objet de paramètres, au lieu de le lire à chaque requête.

Mais chaque fois que nous faisons :

Settings()

un nouvel objet Settings serait créé, et à sa création il lirait à nouveau le fichier .env.

Si la fonction de dépendance était simplement :

def get_settings():
    return Settings()

nous créerions cet objet pour chaque requête, et nous lirions le fichier .env pour chaque requête. ⚠️

Mais comme nous utilisons le décorateur @lru_cache au-dessus, l'objet Settings sera créé une seule fois, la première fois qu'il est appelé. ✔️

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: config.Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Ensuite, pour tout appel ultérieur de get_settings() dans les dépendances pour les requêtes suivantes, au lieu d'exécuter le code interne de get_settings() et de créer un nouvel objet Settings, il renverra le même objet que celui retourné au premier appel, encore et encore.

Détails techniques de lru_cache

@lru_cache modifie la fonction qu'il décore pour renvoyer la même valeur que celle qui a été retournée la première fois, au lieu de la recalculer en exécutant le code de la fonction à chaque fois.

Ainsi, la fonction située en dessous sera exécutée une fois pour chaque combinaison d'arguments. Ensuite, les valeurs renvoyées par chacune de ces combinaisons d'arguments seront réutilisées à chaque fois que la fonction sera appelée avec exactement la même combinaison d'arguments.

Par exemple, si vous avez une fonction :

@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
    return f"Hello {salutation} {name}"

votre programme pourrait s'exécuter comme ceci :

sequenceDiagram

participant code as Code
participant function as say_hi()
participant execute as Execute function

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Camila")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick", salutation="Mr.")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Rick")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

Dans le cas de notre dépendance get_settings(), la fonction ne prend même aucun argument, elle renvoie donc toujours la même valeur.

De cette façon, elle se comporte presque comme s'il s'agissait simplement d'une variable globale. Mais comme elle utilise une fonction de dépendance, nous pouvons alors la surcharger facilement pour les tests.

@lru_cache fait partie de functools qui fait partie de la bibliothèque standard de Python, vous pouvez en lire davantage dans la documentation Python pour @lru_cache.

Récapitulatif

Vous pouvez utiliser Pydantic Settings pour gérer les paramètres ou configurations de votre application, avec toute la puissance des modèles Pydantic.

  • En utilisant une dépendance, vous pouvez simplifier les tests.
  • Vous pouvez utiliser des fichiers .env.
  • Utiliser @lru_cache vous permet d'éviter de relire le fichier dotenv à chaque requête, tout en vous permettant de le surcharger pendant les tests.