AgentSkillsCN

Architecture Guardian

架构守护者

SKILL.md

🏛️ Architecture Guardian

Skill critique : Protège l'architecture globale du projet api.audace contre les violations structurelles.


📋 Contexte du Projet

Stack Technique

TechnologieVersionRôle
FastAPI0.109.0Framework web async
Python3.11+Langage principal
SQLAlchemy2.0ORM avec relations
PostgreSQL15Base de données
Pydanticv2Validation (model_dump())
Alembic1.13+Migrations DB
JWTpython-joseAuthentification
pytest8.0+Tests

Organisation Actuelle du Code

code
api.audace/
├── maintest.py              # Point d'entrée FastAPI
├── app/
│   ├── config/              # Configuration (settings)
│   ├── db/
│   │   ├── database.py      # Session DB, get_db()
│   │   └── crud/            # 26 fichiers CRUD
│   ├── models/              # 25 modèles SQLAlchemy
│   ├── schemas/             # 17 schémas Pydantic
│   ├── utils/               # Utilitaires (hash, tokens)
│   ├── middleware/          # LoggerMiddleware, APIVersionMiddleware
│   └── exceptions/          # Exceptions custom
├── core/
│   └── auth/                # OAuth2, JWT (oauth2.py)
├── routeur/                 # 14 fichiers de routes
│   └── search_route/        # Routes de recherche
├── alembic/
│   └── versions/            # 14 migrations
└── tests/                   # Tests pytest

Patterns Existants

  • Soft delete via BaseModel (is_deleted, deleted_at)
  • Relations SQLAlchemy avec cascade et back_populates
  • RBAC : 40+ permissions granulaires dans UserPermissions
  • Audit logging : Traçabilité des actions critiques

🎯 Objectif du Skill

Garantir que toute modification respecte la structure établie :

  1. Séparation stricte des couches (Routes → CRUD → Models → Schemas)
  2. Intégrité des relations SQLAlchemy
  3. Migrations Alembic pour tout changement DB
  4. Soft delete obligatoire

✅ Règles Obligatoires

1. Séparation des Couches (STRICTEMENT RESPECTER)

code
ROUTEUR (API Endpoints)
    │ importe uniquement
    ▼
CRUD (Logique métier)
    │ utilise
    ▼
MODELS (SQLAlchemy)
    │ valide avec
    ▼
SCHEMAS (Pydantic)

Règles d'import :

python
# ✅ Dans routeur/*.py
from app.db.crud.crud_users import get_user_or_404
from app.db.database import get_db
from core.auth.oauth2 import get_current_user

# ✅ Dans app/db/crud/*.py
from app.models.model_user import User
from app.schemas.schema_users import UserCreate

# ❌ INTERDIT
from routeur.users_route import ...  # Jamais CRUD → Routeur
from app.models.model_user import User  # Jamais Schema → Model

2. Nommage des Fichiers

TypePatternExemple
Route{entity}_route.pyshow_route.py
CRUDcrud_{entity}.pycrud_show.py
Modelmodel_{entity}.pymodel_show.py
Schemaschema_{entity}.pyschema_show.py

3. Structure d'un Routeur

python
# routeur/{entity}_route.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List

from app.db.database import get_db
from app.db.crud.crud_{entity} import (
    create_{entity}, get_{entity}s, get_{entity}_by_id,
    update_{entity}, delete_{entity}
)
from app.schemas.schema_{entity} import {Entity}Create, {Entity}Update, {Entity}Response
from core.auth.oauth2 import get_current_user
from app.models.model_user import User

router = APIRouter(
    prefix="/{entities}",
    tags=['{entities}']
)

@router.get("/", response_model=List[{Entity}Response])
def get_all_{entities}(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
    skip: int = 0,
    limit: int = 100
):
    return get_{entity}s(db, skip=skip, limit=limit)

4. Structure d'un CRUD

python
# app/db/crud/crud_{entity}.py
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from datetime import datetime

from app.models.model_{entity} import {Entity}
from app.schemas.schema_{entity} import {Entity}Create, {Entity}Update

def get_{entity}s(db: Session, skip: int = 0, limit: int = 100):
    """Récupère les {entity}s non supprimés avec pagination."""
    return db.query({Entity}).filter(
        {Entity}.is_deleted == False
    ).offset(skip).limit(limit).all()

def get_{entity}_by_id(db: Session, {entity}_id: int):
    """Récupère un {entity} par ID ou lève 404."""
    {entity} = db.query({Entity}).filter(
        {Entity}.id == {entity}_id,
        {Entity}.is_deleted == False
    ).first()
    if not {entity}:
        raise HTTPException(status_code=404, detail="{Entity} not found")
    return {entity}

def soft_delete_{entity}(db: Session, {entity}_id: int, deleted_by: int):
    """Suppression logique (soft delete)."""
    {entity} = get_{entity}_by_id(db, {entity}_id)
    {entity}.is_deleted = True
    {entity}.deleted_at = datetime.utcnow()
    db.commit()
    return {entity}

5. Soft Delete Obligatoire

python
# Tout modèle métier DOIT hériter de BaseModel
from app.models.base_model import BaseModel

class MyEntity(BaseModel):
    __tablename__ = "my_entities"
    # ... colonnes
python
# BaseModel fournit automatiquement :
is_deleted = Column(Boolean, default=False)
deleted_at = Column(DateTime, nullable=True)

6. Migrations Alembic Obligatoires

Pour TOUT changement de modèle :

bash
# 1. Modifier le modèle
# 2. Générer la migration
alembic revision --autogenerate -m "description_claire"

# 3. Vérifier le fichier généré
# 4. Appliquer
alembic upgrade head

# 5. Tester downgrade
alembic downgrade -1
alembic upgrade head

🚫 Interdictions Explicites

❌ Import Circulaire

python
# routeur/show_route.py
from app.db.crud.crud_show import create_show  # ✅

# app/db/crud/crud_show.py
from routeur.show_route import router  # ❌ INTERDIT !

❌ Accès Direct DB dans Routeur

python
# ❌ INTERDIT
@router.get("/shows")
def get_shows(db: Session = Depends(get_db)):
    return db.query(Show).all()  # ❌ Query direct !

# ✅ CORRECT
@router.get("/shows")
def get_shows(db: Session = Depends(get_db)):
    return get_shows_crud(db)  # Appel CRUD

❌ Hard Delete

python
# ❌ INTERDIT
db.delete(entity)
db.commit()

# ✅ CORRECT
entity.is_deleted = True
entity.deleted_at = datetime.utcnow()
db.commit()

❌ Modification Modèle sans Migration

python
# ❌ INTERDIT : Ajouter colonne sans migration
class User(BaseModel):
    new_column = Column(String)  # Sans alembic !

# ✅ CORRECT
# 1. Ajouter la colonne
# 2. alembic revision --autogenerate -m "add_new_column_to_user"
# 3. alembic upgrade head

❌ Ignorer les Exceptions

python
# ❌ INTERDIT
try:
    result = db.query(User).first()
except:
    pass  # Silencieux !

# ✅ CORRECT
try:
    result = db.query(User).first()
except SQLAlchemyError as e:
    logger.error(f"Database error: {e}")
    raise HTTPException(status_code=500, detail="Database error")

❌ dict() avec Pydantic v2

python
# ❌ INTERDIT (déprécié)
user_data = user_schema.dict()

# ✅ CORRECT
user_data = user_schema.model_dump()
user_data = user_schema.model_dump(exclude={"password"})

📝 Exemples Concrets du Projet

Exemple 1 : Route Show (Correct)

python
# routeur/show_route.py
router = APIRouter(prefix="/shows", tags=['shows'])

@router.post("/detail", status_code=status.HTTP_201_CREATED)
async def create_show_with_details_endpoint(
    show_data: ShowCreateWithDetail,
    db: Session = Depends(get_db),
    current_user: User = Depends(oauth2.get_current_user)
):
    show = create_show_with_details(
        db=db,
        show_data=show_data,
        curent_user_id=current_user.id
    )
    return {"message": "Show created successfully.", "show": show}

Exemple 2 : CRUD Users (Correct)

python
# app/db/crud/crud_users.py
def get_user_or_404_with_permissions(db: Session, user_id: int) -> dict:
    user = db.query(User).options(
        joinedload(User.permissions)
    ).filter(
        User.id == user_id,
        User.is_active == True
    ).first()
    
    if not user:
        raise NoResultFound("User not found or inactive")
    return user

Exemple 3 : BaseModel (Pattern Soft Delete)

python
# app/models/base_model.py
class BaseModel(Base):
    __abstract__ = True
    is_deleted = Column(Boolean, default=False)
    deleted_at = Column(DateTime, nullable=True)

✅ Checklist de Validation

Avant Commit

  • Structure : Fichier dans le bon dossier (routeur/, crud/, models/, schemas/)
  • Nommage : Pattern respecté ({type}_{entity}.py)
  • Imports : Aucun import circulaire (CRUD ↛ Routeur)
  • Soft Delete : Utilisation de is_deleted, pas de db.delete()
  • Migration : Si modèle modifié → alembic revision --autogenerate
  • Pydantic : model_dump() et non dict()

Avant PR

  • Tests passent : pytest
  • Pas de TODO/FIXME : Code propre
  • Documentation : Docstrings présentes
  • Logs : Exceptions loggées, pas ignorées
  • Migration testée : alembic upgrade head + alembic downgrade -1

Script de Validation Rapide

bash
# Vérifier imports circulaires
grep -r "from routeur" app/db/crud/ && echo "❌ Import circulaire détecté!"

# Vérifier hard delete
grep -rn "db.delete(" routeur/ app/db/crud/ && echo "⚠️ Hard delete détecté!"

# Vérifier dict() déprécié
grep -rn "\.dict()" app/ routeur/ && echo "⚠️ dict() déprécié détecté!"

# Vérifier migrations pending
alembic current

🔗 Fichiers Référence

FichierRôleChemin
Point d'entréeApplication FastAPImaintest.py
Base ModelSoft delete patternapp/models/base_model.py
DatabaseSession factoryapp/db/database.py
AuthOAuth2, JWTcore/auth/oauth2.py
Guide AgentDocumentation complèteAGENT.md

📚 Ressources Associées