Initial commit
This commit is contained in:
783
skills/fastapi-expert/SKILL.md
Normal file
783
skills/fastapi-expert/SKILL.md
Normal file
@@ -0,0 +1,783 @@
|
||||
---
|
||||
name: FastAPI Expert
|
||||
description: Expert in FastAPI framework for building high-performance async Python APIs. Use when working with FastAPI, async Python, Pydantic, type hints, automatic API documentation, WebSockets, or when the user mentions FastAPI, async APIs, or modern Python web development.
|
||||
---
|
||||
|
||||
# FastAPI Expert
|
||||
|
||||
A specialized skill for building high-performance, production-ready APIs with FastAPI. This skill covers async programming, Pydantic models, dependency injection, authentication, and FastAPI best practices.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Core Workflow
|
||||
|
||||
1. **Understand requirements**
|
||||
- Identify API type (REST, WebSockets, GraphQL)
|
||||
- Determine database needs (async SQLAlchemy, Tortoise-ORM, MongoDB)
|
||||
- Understand authentication requirements (OAuth2, JWT)
|
||||
- Identify performance requirements
|
||||
|
||||
2. **Project structure**
|
||||
- Organize with proper router structure
|
||||
- Implement dependency injection
|
||||
- Use Pydantic models for validation
|
||||
- Configure CORS and middleware
|
||||
|
||||
3. **Implement features**
|
||||
- Create path operations with proper type hints
|
||||
- Implement Pydantic schemas for validation
|
||||
- Add authentication and authorization
|
||||
- Use background tasks for async operations
|
||||
- Implement WebSockets if needed
|
||||
|
||||
4. **Testing and documentation**
|
||||
- Write tests using pytest and httpx
|
||||
- Leverage automatic Swagger/OpenAPI docs
|
||||
- Configure for production deployment
|
||||
|
||||
### FastAPI Project Structure
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── main.py # Application entry point
|
||||
├── core/
|
||||
│ ├── config.py # Settings with Pydantic
|
||||
│ ├── security.py # Auth utilities
|
||||
│ └── dependencies.py # Shared dependencies
|
||||
├── api/
|
||||
│ ├── v1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── api.py # Main API router
|
||||
│ │ └── endpoints/
|
||||
│ │ ├── users.py
|
||||
│ │ ├── auth.py
|
||||
│ │ └── items.py
|
||||
├── models/ # SQLAlchemy models
|
||||
│ ├── __init__.py
|
||||
│ ├── user.py
|
||||
│ └── item.py
|
||||
├── schemas/ # Pydantic schemas
|
||||
│ ├── __init__.py
|
||||
│ ├── user.py
|
||||
│ └── item.py
|
||||
├── crud/ # Database operations
|
||||
│ ├── __init__.py
|
||||
│ ├── user.py
|
||||
│ └── item.py
|
||||
├── db/
|
||||
│ ├── base.py # Database session
|
||||
│ └── init_db.py # Initial data
|
||||
└── tests/
|
||||
├── conftest.py
|
||||
├── test_users.py
|
||||
└── test_auth.py
|
||||
```
|
||||
|
||||
### Configuration with Pydantic Settings
|
||||
|
||||
```python
|
||||
# core/config.py
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# App
|
||||
PROJECT_NAME: str = "My FastAPI App"
|
||||
VERSION: str = "1.0.0"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str
|
||||
|
||||
# CORS
|
||||
BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:3000"]
|
||||
|
||||
# Redis
|
||||
REDIS_HOST: str = "localhost"
|
||||
REDIS_PORT: int = 6379
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
settings = Settings()
|
||||
```
|
||||
|
||||
### Pydantic Schemas
|
||||
|
||||
```python
|
||||
# schemas/user.py
|
||||
from pydantic import BaseModel, EmailStr, Field, validator
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: EmailStr
|
||||
username: str = Field(..., min_length=3, max_length=50)
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
is_active: bool = True
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(..., min_length=8)
|
||||
|
||||
@validator('password')
|
||||
def password_strength(cls, v):
|
||||
if not any(char.isdigit() for char in v):
|
||||
raise ValueError('Password must contain at least one digit')
|
||||
if not any(char.isupper() for char in v):
|
||||
raise ValueError('Password must contain at least one uppercase letter')
|
||||
return v
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
email: Optional[EmailStr] = None
|
||||
username: Optional[str] = Field(None, min_length=3, max_length=50)
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
password: Optional[str] = Field(None, min_length=8)
|
||||
|
||||
class UserInDBBase(UserBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True # For SQLAlchemy models
|
||||
|
||||
class User(UserInDBBase):
|
||||
"""User response model (excludes password)"""
|
||||
pass
|
||||
|
||||
class UserInDB(UserInDBBase):
|
||||
"""User model with hashed password"""
|
||||
hashed_password: str
|
||||
```
|
||||
|
||||
### Database Models (SQLAlchemy)
|
||||
|
||||
```python
|
||||
# models/user.py
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from db.base import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
full_name = Column(String)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superuser = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# db/base.py
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from core.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# Dependency
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
```
|
||||
|
||||
### CRUD Operations
|
||||
|
||||
```python
|
||||
# crud/user.py
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from models.user import User
|
||||
from schemas.user import UserCreate, UserUpdate
|
||||
from core.security import get_password_hash
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
async def get_user(db: AsyncSession, user_id: int) -> User | None:
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_user_by_email(db: AsyncSession, email: str) -> User | None:
|
||||
result = await db.execute(select(User).where(User.email == email))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_users(
|
||||
db: AsyncSession, skip: int = 0, limit: int = 100
|
||||
) -> list[User]:
|
||||
result = await db.execute(select(User).offset(skip).limit(limit))
|
||||
return result.scalars().all()
|
||||
|
||||
async def create_user(db: AsyncSession, user: UserCreate) -> User:
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = User(
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
full_name=user.full_name,
|
||||
hashed_password=hashed_password,
|
||||
is_active=user.is_active
|
||||
)
|
||||
|
||||
try:
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
except IntegrityError:
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User with this email or username already exists"
|
||||
)
|
||||
|
||||
async def update_user(
|
||||
db: AsyncSession, user_id: int, user_update: UserUpdate
|
||||
) -> User:
|
||||
db_user = await get_user(db, user_id)
|
||||
if not db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
update_data = user_update.model_dump(exclude_unset=True)
|
||||
|
||||
if "password" in update_data:
|
||||
update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(db_user, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
async def delete_user(db: AsyncSession, user_id: int) -> bool:
|
||||
db_user = await get_user(db, user_id)
|
||||
if not db_user:
|
||||
return False
|
||||
|
||||
await db.delete(db_user)
|
||||
await db.commit()
|
||||
return True
|
||||
```
|
||||
|
||||
### API Endpoints with Proper Type Hints
|
||||
|
||||
```python
|
||||
# api/v1/endpoints/users.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
|
||||
from db.base import get_db
|
||||
from schemas.user import User, UserCreate, UserUpdate
|
||||
from crud import user as crud_user
|
||||
from core.dependencies import get_current_active_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
*,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
user_in: UserCreate,
|
||||
) -> User:
|
||||
"""
|
||||
Create new user.
|
||||
|
||||
- **email**: valid email address (unique)
|
||||
- **username**: 3-50 characters (unique)
|
||||
- **password**: minimum 8 characters with uppercase, lowercase, and number
|
||||
- **full_name**: optional full name
|
||||
"""
|
||||
# Check if user exists
|
||||
existing_user = await crud_user.get_user_by_email(db, email=user_in.email)
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User with this email already exists",
|
||||
)
|
||||
|
||||
user = await crud_user.create_user(db=db, user=user_in)
|
||||
return user
|
||||
|
||||
@router.get("/", response_model=list[User])
|
||||
async def read_users(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=100),
|
||||
current_user: Annotated[User, Depends(get_current_active_user)] = None,
|
||||
) -> list[User]:
|
||||
"""
|
||||
Retrieve users with pagination.
|
||||
|
||||
- **skip**: number of records to skip (default: 0)
|
||||
- **limit**: maximum number of records to return (default: 100, max: 100)
|
||||
"""
|
||||
users = await crud_user.get_users(db, skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
@router.get("/{user_id}", response_model=User)
|
||||
async def read_user(
|
||||
user_id: int,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> User:
|
||||
"""
|
||||
Get user by ID.
|
||||
"""
|
||||
db_user = await crud_user.get_user(db, user_id=user_id)
|
||||
if db_user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
return db_user
|
||||
|
||||
@router.patch("/{user_id}", response_model=User)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_in: UserUpdate,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)] = None,
|
||||
) -> User:
|
||||
"""
|
||||
Update user.
|
||||
"""
|
||||
# Check authorization (users can only update themselves unless admin)
|
||||
if current_user.id != user_id and not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions"
|
||||
)
|
||||
|
||||
db_user = await crud_user.update_user(db, user_id=user_id, user_update=user_in)
|
||||
return db_user
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Delete user.
|
||||
"""
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions"
|
||||
)
|
||||
|
||||
success = await crud_user.delete_user(db, user_id=user_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
```
|
||||
|
||||
### Authentication with JWT
|
||||
|
||||
```python
|
||||
# core/security.py
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
# core/dependencies.py
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.config import settings
|
||||
from core.security import verify_password
|
||||
from db.base import get_db
|
||||
from crud import user as crud_user
|
||||
from schemas.user import User
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
token: str = Depends(oauth2_scheme)
|
||||
) -> User:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
user_id: int = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = await crud_user.get_user(db, user_id=user_id)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Inactive user"
|
||||
)
|
||||
return current_user
|
||||
|
||||
# api/v1/endpoints/auth.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import timedelta
|
||||
|
||||
from db.base import get_db
|
||||
from core.config import settings
|
||||
from core.security import create_access_token, verify_password
|
||||
from crud import user as crud_user
|
||||
from schemas.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
"""
|
||||
OAuth2 compatible token login.
|
||||
"""
|
||||
user = await crud_user.get_user_by_email(db, email=form_data.username)
|
||||
|
||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Inactive user"
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user.id)}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": User.model_validate(user)
|
||||
}
|
||||
|
||||
@router.get("/me", response_model=User)
|
||||
async def read_users_me(
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Get current user.
|
||||
"""
|
||||
return current_user
|
||||
```
|
||||
|
||||
### Background Tasks
|
||||
|
||||
```python
|
||||
from fastapi import BackgroundTasks
|
||||
from typing import Annotated
|
||||
|
||||
def send_email(email: str, message: str):
|
||||
"""Simulated email sending"""
|
||||
print(f"Sending email to {email}: {message}")
|
||||
# Actual email sending logic here
|
||||
|
||||
@router.post("/send-notification/")
|
||||
async def send_notification(
|
||||
email: str,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
background_tasks.add_task(send_email, email, "Welcome to our service!")
|
||||
return {"message": "Notification sent in the background"}
|
||||
```
|
||||
|
||||
### WebSocket Support
|
||||
|
||||
```python
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: list[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
@router.websocket("/ws/{client_id}")
|
||||
async def websocket_endpoint(websocket: WebSocket, client_id: int):
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
await manager.broadcast(f"Client #{client_id} says: {data}")
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
await manager.broadcast(f"Client #{client_id} left the chat")
|
||||
```
|
||||
|
||||
### Main Application Setup
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from core.config import settings
|
||||
from api.v1.api import api_router
|
||||
from db.base import engine, Base
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
# Shutdown
|
||||
await engine.dispose()
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
version=settings.VERSION,
|
||||
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.BACKEND_CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to FastAPI"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from main import app
|
||||
from db.base import Base, get_db
|
||||
from core.config import settings
|
||||
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_session():
|
||||
engine = create_async_engine(TEST_DATABASE_URL, echo=True)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(db_session):
|
||||
async def override_get_db():
|
||||
yield db_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
# tests/test_users.py
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user(client: AsyncClient):
|
||||
response = await client.post(
|
||||
"/api/v1/users/",
|
||||
json={
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "Test123456",
|
||||
"full_name": "Test User"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["email"] == "test@example.com"
|
||||
assert data["username"] == "testuser"
|
||||
assert "id" in data
|
||||
assert "hashed_password" not in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_users(client: AsyncClient):
|
||||
# Create a user first
|
||||
await client.post(
|
||||
"/api/v1/users/",
|
||||
json={
|
||||
"email": "test@example.com",
|
||||
"username": "testuser",
|
||||
"password": "Test123456"
|
||||
}
|
||||
)
|
||||
|
||||
# Login to get token
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={"username": "test@example.com", "password": "Test123456"}
|
||||
)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Get users
|
||||
response = await client.get(
|
||||
"/api/v1/users/",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) > 0
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### Always Do
|
||||
- Use type hints everywhere (FastAPI requires them)
|
||||
- Leverage Pydantic for validation and serialization
|
||||
- Use async/await for all I/O operations
|
||||
- Implement proper dependency injection
|
||||
- Use background tasks for long-running operations
|
||||
- Document all endpoints (FastAPI auto-generates OpenAPI)
|
||||
- Handle errors with proper HTTP status codes
|
||||
- Use environment variables for configuration
|
||||
- Implement proper authentication and authorization
|
||||
|
||||
### Never Do
|
||||
- Never use synchronous database operations
|
||||
- Never skip type hints (FastAPI won't work properly)
|
||||
- Never store passwords in plain text
|
||||
- Never expose sensitive data in responses
|
||||
- Never ignore Pydantic validation errors
|
||||
- Never mix sync and async code improperly
|
||||
- Never hardcode configuration values
|
||||
- Never skip error handling
|
||||
|
||||
## Knowledge Base
|
||||
|
||||
- **FastAPI Core**: Path operations, dependencies, middleware, background tasks
|
||||
- **Async Python**: asyncio, async/await, async database drivers
|
||||
- **Pydantic**: Models, validation, serialization, settings management
|
||||
- **SQLAlchemy**: Async ORM, relationships, migrations with Alembic
|
||||
- **Authentication**: OAuth2, JWT, password hashing
|
||||
- **Testing**: pytest, pytest-asyncio, httpx
|
||||
- **Performance**: Async operations, caching, connection pooling
|
||||
- **Documentation**: Automatic OpenAPI/Swagger generation
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- **Works with**: Fullstack Guardian, Test Master, DevOps Engineer
|
||||
- **Complements**: Django Expert (alternative Python framework), Code Documenter
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Type Hints**: Essential for FastAPI functionality
|
||||
2. **Async Everything**: Use async for I/O operations
|
||||
3. **Pydantic Models**: Validate all input/output
|
||||
4. **Dependency Injection**: Use FastAPI's DI system
|
||||
5. **Auto Documentation**: Leverage automatic OpenAPI docs
|
||||
6. **Error Handling**: Proper HTTP exceptions
|
||||
7. **Security**: JWT authentication, password hashing
|
||||
8. **Testing**: Comprehensive async tests
|
||||
9. **Performance**: Background tasks, caching, connection pooling
|
||||
10. **Configuration**: Pydantic Settings for environment management
|
||||
Reference in New Issue
Block a user