484 lines
21 KiB
Markdown
484 lines
21 KiB
Markdown
# TP : Filmothèque (API REST)
|
||
|
||
## Informations générales
|
||
|
||
**Cours** : Python Avancé > REST API > Fast API \
|
||
**Objectifs pédagogiques** :
|
||
- Créer une application backend basée sur FastAPI.
|
||
- Structurer une application en couches (API/Routeurs, BLL/Services, DAL/Repository).
|
||
- Définir des schémas de données avec Pydantic pour la validation et la sérialisation.
|
||
- Interagir avec une base de données en asynchrone grâce à SQLAlchemy.
|
||
- Mettre en place un système d'injection de dépendances (pour la session BDD).
|
||
- Gérer les erreurs de manière centralisée avec des gestionnaires d'exceptions personnalisés, en particulier pour DAL et BLL
|
||
- Tests : pytest
|
||
- Outils modernes et performants (poetry, PyCharm, FastAPI, SQLAlchemy 2.x)
|
||
- Bonnes pratiques de l'entreprise
|
||
|
||
---
|
||
|
||
## Prérequis
|
||
|
||
### Connaissances préalables
|
||
|
||
* Connaissances de base en programmation.
|
||
* Application du modèle en couches (API, BLL, DAL).
|
||
|
||
### Architecture en couches
|
||
|
||
La pile applicative que nous allons construire suit une séparation claire des responsabilités :
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[API / Routeurs] -- "Appelle le" --> B[Services / BLL];
|
||
B -- "Appelle le" --> C[Repository / DAL];
|
||
C -- "Utilise l'" --> D[ORM / SQLAlchemy];
|
||
D -- "Dialogue avec la" --> E[Base de donnees];
|
||
```
|
||
|
||
* **API / Routeurs** : la porte d'entrée HTTP.
|
||
* **Services / BLL** : la logique métier (Business Logic Layer).
|
||
* **Repository / DAL** : l'abstraction de la persistance (Data Access Layer).
|
||
* **ORM / SQLAlchemy** : les modèles et la session qui parlent à la BDD.
|
||
|
||
### Installation et configuration de l’environnement
|
||
|
||
- Installation des dépendances : faire `poetry install` (fichier lock déja présent)
|
||
- À ce stade, si vos imports ne sont pas reconnus dans l'IDE PyCharm à l'ouverture d'un fichier (par exemple `app/main.py`), marquer le répertoire `src` comme `Sources Root` (Clic droit sur répertoire `src`, puis `Mark Directory as ... > Sources Root`) ou redémarrer l'IDE (File > Invalidate Caches... > Just restart à gauche).
|
||
- Par défaut, vous travaillerez avec une base de données locale `sqlite`. C'est fortement recommandé pour gagner du temps !
|
||
|
||
### Lancement du projet
|
||
|
||
Une fois le projet cloné, se positionner dans le répertoire racine du projet (là où se trouve le fichier `pyproject.toml`).
|
||
|
||
Faire clic droit puis `Settings` sur l'onglet du terminal local Pycharm, puis définir la variable d'environnement `BACKEND_CORS_ORIGINS`, en prévision de l'accès à ce micro service depuis un front Angular :
|
||
- `BACKEND_CORS_ORIGINS=["http://localhost:4200"]`)
|
||
|
||
Ouvrir un (nouvel onglet) terminal local Pycharm, puis lancer le projet via :
|
||
|
||
- `cd src`
|
||
- `uvicorn app.main:app --host 0.0.0.0 --port 8000`
|
||
|
||
Les URLs suivantes devront alors être accessibles, selon vos avancées sur le projet :
|
||
|
||
- http://127.0.0.1:8000/ -> retourne "welcome to this fantastic API"
|
||
- http://127.0.0.1:8000/api/v1/movies/ -> retourne une liste de films, éventuellement vide
|
||
|
||
Notez qu'un pré-remplissage de la base est possible via le fichier `app/db/seeding.py`.
|
||
Si vous souhaitez réinitialiser la base, vous pouvez supprimer le fichier `local_dev.db` à la racine de votre projet, en prenant soin de décocher l'option `safe delete` (pas de refactor nécessaire à cet effet).
|
||
|
||
---
|
||
|
||
## Énoncé
|
||
|
||
### Modèle logique : schéma UML
|
||
|
||
Vous trouverez ci-dessous pour information le schéma UML de la base de données.
|
||
|
||

|
||
|
||
### Structure du projet
|
||
|
||
La structure du projet `tp_fastapi` est la suivante :
|
||
|
||
```
|
||
tp_fastapi/
|
||
src/
|
||
├── app/
|
||
│ ├── api/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── deps.py
|
||
│ │ ├── exception_handlers.py
|
||
│ │ └── routers/ # la porte d'entrée HTTP
|
||
│ │ ├── __init__.py
|
||
│ │ ├── genres.py
|
||
│ │ ├── movies.py
|
||
│ │ ├── opinions.py
|
||
│ │ └── participants.py
|
||
│ ├── core/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── config.py
|
||
│ │ └── exceptions.py
|
||
│ ├── db/
|
||
│ │ ├── __init__.py
|
||
│ │ └── session.py
|
||
│ ├── models/ # les Business Objects (BO), qui sont également des entités pour l'ORM SQLAlchemy
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base_class.py
|
||
│ │ ├── genre.py
|
||
│ │ ├── member.py
|
||
│ │ ├── movie.py
|
||
│ │ ├── movie_actors.py
|
||
│ │ ├── opinion.py
|
||
│ │ ├── participant.py
|
||
│ │ └── person.py
|
||
│ ├── repository/ # l'abstraction de la persistance (Data Access Layer)
|
||
│ │ ├── __init__.py
|
||
│ │ ├── genre.py
|
||
│ │ ├── movie.py
|
||
│ │ ├── opinion.py
|
||
│ │ └── participant.py
|
||
│ ├── schemas/ # objets manipulés lors des requêtes/réponses HTTP
|
||
│ │ ├── __init__.py
|
||
│ │ ├── genre.py
|
||
│ │ ├── movie.py
|
||
│ │ ├── opinion.py
|
||
│ │ ├── participant.py
|
||
│ │ └── person.py
|
||
│ ├── services/ # la logique métier (Business Logic Layer)
|
||
│ │ ├── __init__.py
|
||
│ │ ├── genre.py
|
||
│ │ ├── movie.py
|
||
│ │ ├── opinion.py
|
||
│ │ └── participant.py
|
||
│ ├── __init__.py
|
||
│ └── main.py
|
||
```
|
||
|
||
## Étape 1 : les fondations - modèles de données ORM
|
||
|
||
L'Object-Relational Mapping (ORM) est la couche qui traduit nos objets Python en tables de base de données.
|
||
C'est une partie importante, mais complexe.
|
||
Pour que vous puissiez vous concentrer sur l'architecture de l'API, **le code de ce répertoire `app/models/` vous est intégralement fourni**.
|
||
|
||
Votre mission est de **créer les fichiers et d'y copier le code ci-dessous**. Prenez le temps de lire et de comprendre les relations définies :
|
||
* `Person` est une classe de base utilisant l'**héritage** pour définir `Participant` (acteur/réalisateur) et `Member` (utilisateur).
|
||
* `Movie` a une relation **One-to-Many** avec `Opinion` (un film peut avoir plusieurs avis).
|
||
* `Movie` a une relation **Many-to-One** avec `Genre` et `Participant` (pour le réalisateur).
|
||
* `Movie` et `Participant` (pour les acteurs) ont une relation **Many-to-Many** via une table d'association.
|
||
|
||
#### Fichier : `app/models/base_class.py`
|
||
```python
|
||
from sqlalchemy.orm import declarative_base
|
||
Base = declarative_base()
|
||
```
|
||
|
||
#### Fichier : `app/models/person.py`
|
||
```python
|
||
from sqlalchemy import String
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
from .base_class import Base
|
||
|
||
class Person(Base):
|
||
__tablename__ = "persons"
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
last_name: Mapped[str] = mapped_column(String(255))
|
||
first_name: Mapped[str] = mapped_column(String(255))
|
||
# colonne discriminante pour la hiérarchie d'héritage
|
||
type: Mapped[str] = mapped_column(String(50))
|
||
|
||
__mapper_args__ = {
|
||
"polymorphic_identity": "person",
|
||
"polymorphic_on": "type",
|
||
}
|
||
```
|
||
|
||
#### Fichier : `app/models/participant.py`
|
||
```python
|
||
from sqlalchemy import ForeignKey
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
from .person import Person
|
||
|
||
class Participant(Person):
|
||
__tablename__ = "participants"
|
||
id: Mapped[int] = mapped_column(ForeignKey("persons.id"), primary_key=True)
|
||
__mapper_args__ = {"polymorphic_identity": "participant"}
|
||
```
|
||
|
||
#### Fichier : `app/models/genre.py`
|
||
```python
|
||
from sqlalchemy import String
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
from .base_class import Base
|
||
|
||
class Genre(Base):
|
||
__tablename__ = "genres"
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
label: Mapped[str] = mapped_column(String(255))
|
||
```
|
||
|
||
#### Fichier : `app/models/movie_actors.py`
|
||
```python
|
||
from sqlalchemy import Column, ForeignKey, Table
|
||
from .base_class import Base
|
||
|
||
movie_actors_association_table = Table(
|
||
"movie_actors_association",
|
||
Base.metadata,
|
||
Column("movie_id", ForeignKey("movies.id"), primary_key=True),
|
||
Column("participant_id", ForeignKey("participants.id"), primary_key=True),
|
||
)
|
||
```
|
||
|
||
#### Fichier : `app/models/movie.py`
|
||
```python
|
||
from sqlalchemy import Integer, String, ForeignKey, Text
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
from typing import List
|
||
from .base_class import Base
|
||
from .movie_actors import movie_actors_association_table
|
||
|
||
class Movie(Base):
|
||
__tablename__ = "movies"
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
title: Mapped[str] = mapped_column(String(250), nullable=False)
|
||
year: Mapped[int] = mapped_column(Integer)
|
||
duration: Mapped[int] = mapped_column(Integer)
|
||
synopsis: Mapped[str] = mapped_column(Text)
|
||
director_id: Mapped[int] = mapped_column(ForeignKey("participants.id"))
|
||
genre_id: Mapped[int] = mapped_column(ForeignKey("genres.id"))
|
||
|
||
director: Mapped["Participant"] = relationship(foreign_keys=[director_id])
|
||
genre: Mapped["Genre"] = relationship()
|
||
actors: Mapped[List["Participant"]] = relationship(secondary=movie_actors_association_table)
|
||
opinions: Mapped[List["Opinion"]] = relationship(back_populates="movie", cascade="all, delete-orphan")
|
||
```
|
||
|
||
#### Fichier : `app/models/opinion.py`
|
||
|
||
```python
|
||
from sqlalchemy import (
|
||
Integer,
|
||
ForeignKey,
|
||
Text
|
||
)
|
||
from sqlalchemy.orm import (
|
||
Mapped,
|
||
mapped_column,
|
||
relationship
|
||
)
|
||
from .base_class import Base
|
||
|
||
class Opinion(Base):
|
||
__tablename__ = "opinions"
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
note: Mapped[int] = mapped_column(Integer)
|
||
comment: Mapped[str] = mapped_column(Text)
|
||
|
||
member_id: Mapped[int] = mapped_column(ForeignKey("members.id"))
|
||
movie_id: Mapped[int] = mapped_column(ForeignKey("movies.id"))
|
||
|
||
member: Mapped["Member"] = relationship(back_populates="opinions")
|
||
movie: Mapped["Movie"] = relationship(back_populates="opinions")
|
||
```
|
||
|
||
#### Fichier : `app/models/member.py`
|
||
|
||
```python
|
||
from sqlalchemy import (
|
||
String,
|
||
Boolean,
|
||
ForeignKey,
|
||
)
|
||
from sqlalchemy.orm import (
|
||
Mapped,
|
||
mapped_column,
|
||
relationship
|
||
)
|
||
from typing import List
|
||
from .person import Person
|
||
|
||
class Member(Person):
|
||
__tablename__ = "members"
|
||
|
||
# La clé primaire est aussi une clé étrangère vers la table parente
|
||
id: Mapped[int] = mapped_column(ForeignKey("persons.id"), primary_key=True)
|
||
|
||
# Champs spécifiques à Member
|
||
login: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||
password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
|
||
# La relation vers Opinion
|
||
opinions: Mapped[List["Opinion"]] = relationship(back_populates="member")
|
||
|
||
__mapper_args__ = {
|
||
"polymorphic_identity": "member",
|
||
}
|
||
|
||
def __repr__(self) -> str:
|
||
return f"Member(id={self.id}, login='{self.login}')"
|
||
```
|
||
|
||
#### Fichier : `app/models/__init__.py`
|
||
|
||
Pensez également à **importer toutes les classes dans `app/models/__init__.py`** pour faciliter les imports ailleurs dans le projet :
|
||
|
||
```python
|
||
# app/models/__init__.py
|
||
|
||
# Eviter les erreurs liés à l'importation circulaire
|
||
# Exemple : sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[Movie(movies)], expression 'Genre' failed to locate a name ('Genre'). If this is a class name, consider adding this relationship() to the <class 'app.models.film.Film'> class after both dependent classes have been defined
|
||
|
||
from .base_class import Base
|
||
from .genre import Genre
|
||
from .member import Member
|
||
from .movie import Movie
|
||
from .opinion import Opinion
|
||
from .participant import Participant
|
||
from .person import Person
|
||
|
||
# L'import des associations (ex : personnefilm) n'est en général pas nécessaire ici car elles sont gérées dans les modèles eux-mêmes
|
||
```
|
||
|
||
**Astuce :** lorsque vos fichiers sont prêts, et que vous aurez lancé l'application une première fois, vous pourrez désactiver la drop/création automatique des tables dans `main.py` (mettre en commentaire `await conn.run_sync(Base.metadata.drop_all)`).
|
||
|
||
---
|
||
|
||
## Étape 2 : les schémas de données avec Pydantic
|
||
|
||
Les schémas Pydantic définissent la "forme" des données que notre API attend en entrée et renvoie en sortie. Ils assurent une validation robuste et automatique.
|
||
|
||
Votre mission : **Créez les fichiers dans `app/schemas/`** et écrivez les classes Pydantic. Une bonne pratique est de créer :
|
||
* Une classe `Base` (champs communs).
|
||
* Une classe `Create` (champs requis pour la création).
|
||
* Une classe `Read` (pour la lecture, avec les `id` et les relations).
|
||
|
||
**Exemple pour `genre.py` :**
|
||
```python
|
||
from pydantic import BaseModel, ConfigDict
|
||
|
||
class GenreBase(BaseModel):
|
||
label: str
|
||
|
||
class GenreCreate(GenreBase):
|
||
pass
|
||
|
||
class GenreRead(GenreBase):
|
||
id: int
|
||
model_config = ConfigDict(from_attributes=True)
|
||
```
|
||
Inspirez-vous de cet exemple et des modèles ORM pour créer les schémas pour `Person`, `Participant`, `Opinion` et `Movie`.
|
||
|
||
**Astuce pour `Movie` :** Le schéma de création `MovieCreate` attendra des IDs (`director_id`, `actors_ids`), tandis que le schéma de lecture `MovieRead` renverra les objets imbriqués (`director: Person`, `actors: List[Person]`).
|
||
|
||
Pour valider votre travail, vous pouvez lancer le test suivant, à la racine du projet : `poetry run pytest tests/enonce/etape2_test_schemas.py`.
|
||
|
||
---
|
||
|
||
## Étape 3 : la couche d'accès aux données (DAL / Repository)
|
||
|
||
Dans la couche DAL (Data Access Layer), le Repository est responsable de toutes les requêtes à la base de données. C'est ici que vous écrirez vos requêtes SQLAlchemy. L'objectif est de cacher la complexité de l'ORM au reste de l'application.
|
||
|
||
Votre mission : **Complétez les fonctions dans les fichiers du répertoire `app/repositories/`**.
|
||
Toutes les fonctions recevront une session `db: AsyncSession` en paramètre.
|
||
|
||
**Exemple pour `repositories\movie.py` :**
|
||
* **`get_movie(db, movie_id)`** : Doit récupérer un film par son ID. Pensez à utiliser `selectinload` pour charger ses relations (genre, réalisateur, acteurs) de manière efficace et éviter le problème N+1.
|
||
* **`get_movies(db, skip, limit)`** : Récupère une liste de films paginée.
|
||
* **`create_movie(db, movie)`** : Crée une nouvelle instance `Movie`, l'ajoute à la session et la commit.
|
||
|
||
**Important** : entourez vos requêtes d'un bloc `try...except SQLAlchemyError` et levez une `DALException` personnalisée en cas d'erreur.
|
||
|
||
Pour valider votre travail, vous pouvez lancer le test suivant, à la racine du projet : `poetry run pytest tests/enonce/etape3_test_repositories.py`.
|
||
|
||
---
|
||
|
||
## Étape 4 : la logique métier (BLL / Services)
|
||
|
||
Le Service est le cœur de votre logique. Il orchestre les appels au Repository et applique les règles métier. C'est lui qui décide si une action est valide ou non.
|
||
|
||
Votre mission : **Implémentez la logique dans les fichiers du répertoire `app/services/`**.
|
||
|
||
**Exemple pour `services\movie.py` :**
|
||
* Dans `create_movie`, avant d'appeler le repository, **ajoutez des vérifications** :
|
||
1. Le titre du film ne doit pas être vide.
|
||
2. L'année de sortie doit être réaliste (ex: entre 1888 et aujourd'hui + 5 ans).
|
||
3. Si une de ces règles n'est pas respectée, levez une `ValidationBLLException`.
|
||
* Dans `get_movie_by_id`, si le repository ne retourne aucun film, levez une `NotFoundBLLException`.
|
||
|
||
**Principe clé** : Un service ne doit jamais manipuler directement des exceptions HTTP (`HTTPException`). Il utilise des exceptions métier personnalisées (`BLLException`, `NotFoundBLLException`, etc.) pour rester indépendant du framework web.
|
||
|
||
Pour valider votre travail, vous pouvez lancer le test suivant, à la racine du projet : `poetry run pytest tests/enonce/etape4_test_services.py`.
|
||
|
||
---
|
||
|
||
## Étape 5 : la couche API (Routers)
|
||
|
||
C'est ici que tout se connecte ! Les routeurs FastAPI définissent les endpoints de votre API, reçoivent les requêtes HTTP, appellent les services appropriés et retournent les réponses.
|
||
|
||
Votre mission : **Créez les endpoints dans les fichiers du répertoire `app/api/routers/`**.
|
||
|
||
* Utilisez les décorateurs de FastAPI (`@router.get`, `@router.post`, etc.).
|
||
* Utilisez `response_model` pour spécifier le schéma Pydantic de la réponse.
|
||
* Utilisez `status_code` pour définir le code de statut HTTP approprié (ex: `201 CREATED`).
|
||
* Injectez la session de base de données en utilisant `db: AsyncSession = Depends(get_db)`.
|
||
* Appelez la fonction de service correspondante et retournez son résultat.
|
||
|
||
Pour valider votre travail, vous pouvez lancer le test suivant, à la racine du projet : `poetry run pytest tests/enonce/etape5_test_api.py`.
|
||
|
||
---
|
||
|
||
## Étape 6 : l'assemblage final et la gestion des erreurs
|
||
|
||
La dernière étape consiste à configurer l'application FastAPI principale, à connecter les routeurs et à mettre en place la gestion centralisée des exceptions.
|
||
|
||
Votre mission : **Complétez le fichier `app/main.py` et observez `app/api/exception_handlers.py`**.
|
||
|
||
1. Dans `main.py` :
|
||
* Créez l'instance `FastAPI`, si ce n'est pas déja fait.
|
||
* Utilisez `app.include_router()` pour ajouter chaque routeur que vous avez créé.
|
||
* Utilisez `@app.add_exception_handler()` pour lier vos exceptions métier personnalisées (`NotFoundBLLException`, `ValidationBLLException`, `DALException`, `BLLException`) à des fonctions de gestion.
|
||
|
||
2. Dans `app/api/exception_handlers.py`, observez les points suivants :
|
||
* Les fonctions `async def` (ex: `not_found_bll_exception_handler`) qui prennent `Request` et votre exception en paramètres. À quoi servent-elles ?
|
||
* Ces fonctions doivent retourner une `JSONResponse` avec le code de statut HTTP adéquat (`404`, `400`, `500`) et un message d'erreur clair dans le contenu. Pourquoi c'est une bonne pratique avec REST de retourner un code status HTTP approprié ? Est-ce que le code en question sépare les erreurs techniques des erreurs fonctionnelles ?
|
||
|
||
Le schéma ci-dessous illustre comment les exceptions personnalisées sont levées par les couches internes (DAL, BLL) et interceptées par les gestionnaires centraux (handlers) définis dans `main.py` pour produire une réponse HTTP propre.
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph "Flux de requete"
|
||
A[Requete HTTP] --> B[Endpoint API]
|
||
B -- Appelle --> C(Service BLL)
|
||
C -- Appelle --> D(Repository DAL)
|
||
end
|
||
|
||
subgraph "Flux d'exception"
|
||
D -- Leve --> E{DALException}
|
||
C -- Leve --> F{NotFoundBLLException}
|
||
C -- Leve --> G{ValidationBLLException}
|
||
|
||
H[main.py: @app.add_exception_handler] -- Enregistre --> I(handler_dal)
|
||
H -- Enregistre --> J(handler_not_found)
|
||
H -- Enregistre --> K(handler_validation)
|
||
|
||
E -.-> I
|
||
F -.-> J
|
||
G -.-> K
|
||
|
||
I -- Genere --> L[JSONResponse 500]
|
||
J -- Genere --> M[JSONResponse 404]
|
||
K -- Genere --> N[JSONResponse 400]
|
||
end
|
||
```
|
||
|
||
Une fois terminé, lancez votre application avec `uvicorn app.main:app --reload` (depuis le dossier `src/`) et explorez la documentation interactive sur `http://127.0.0.1:8000/docs`.
|
||
|
||
-----
|
||
|
||
## Aller plus loin (bonus)
|
||
|
||
* Vérifier que l'intégralité des tests passent. Se positionner à la racine du projet (avant le src), puis lancer les tests unitaires via :
|
||
- `poetry run pytest`
|
||
- `poetry run pytest -vv` (avec davantage d'éléments de debug)
|
||
* Implémentez-les endpoints `UPDATE` et `DELETE` pour les participants et les films.
|
||
* Ajoutez des filtres à la route `GET /movies/` (par année, par genre, etc.).
|
||
* Lancer les tests unitaires suivants : `poetry run pytest tests/api/test_movies_api.py` et `poetry run pytest tests/services/test_movie_service.py`
|
||
* Écrivez d'autres tests unitaires et d'intégration avec `pytest`.
|
||
* Mettre en œuvre une véritable base de données MySQL :
|
||
|
||
Si vous souhaitez établir une connexion avec une véritable base de données telle que MySQL, sachez que le package `asyncmy` sera requis (`poetry add asyncmy@^0.2.10`), ainsi qu'au préalable sous Windows, l'installation des redistributables Visual Studio 2022 (via l'outil vsBuildTools dont le binaire et la documentation sont transmis dans votre `00_Install_Pack.zip`).
|
||
|
||
```
|
||
La base doit être créée au préalable (exemple en root) :
|
||
|
||
CREATE DATABASE filmotheque CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
|
||
Un utilisateur filmotheque (mdp : filmotheque) doit être créé avec les droits sur cette base.
|
||
```
|
||
|
||
Faire clic droit puis Settings sur l'onglet du terminal local Pycharm, puis définir la variable d'environnement DATABASE_URL, contenant l'URL de connexion MySQL adéquate :
|
||
|
||
`DATABASE_URL=mysql+asyncmy://filmotheque:filmotheque@127.0.0.1:3306/filmotheque`
|
||
|
||
Fermez l'onglet du terminal, puis en ouvrir un autre, afin que les modifications soient effectives |