Python

Artigo 48 — Projeto 1: API REST Completa com FastAPI, PostgreSQL e Docker Já leu

17 min de leitura

Artigo 48 — Projeto 1: API REST Completa com FastAPI, PostgreSQL e Docker
Chegamos ao módulo final — Projetos Reais. Aqui consolidamos tudo que aprendemos nos módulos anteriores em sistemas completos e funcionais. O Projeto 1 é uma

Artigo 48 — Projeto 1: API REST Completa com FastAPI, PostgreSQL e Docker

Prof. Ricardo Matos Módulo 9 · Projetos Reais e Carreira · Artigo 48 de 52


Introdução

Chegamos ao módulo final — Projetos Reais. Aqui consolidamos tudo que aprendemos nos módulos anteriores em sistemas completos e funcionais. O Projeto 1 é uma API REST de gestão escolar construída com FastAPI, PostgreSQL e Docker — seguindo boas práticas de arquitetura, testes e deploy. É o tipo de projeto que você colocaria no portfólio e apresentaria em uma entrevista técnica.


Visão Geral do Projeto

escola-api/
├── app/
│   ├── __init__.py
│   ├── main.py                  ← entrada da aplicação
│   ├── config.py                ← configurações por ambiente
│   ├── database.py              ← conexão SQLAlchemy
│   ├── models/                  ← modelos ORM
│   │   ├── __init__.py
│   │   ├── aluno.py
│   │   ├── turma.py
│   │   └── disciplina.py
│   ├── schemas/                 ← modelos Pydantic
│   │   ├── __init__.py
│   │   ├── aluno.py
│   │   └── turma.py
│   ├── routers/                 ← endpoints por recurso
│   │   ├── __init__.py
│   │   ├── alunos.py
│   │   ├── turmas.py
│   │   └── auth.py
│   ├── services/                ← lógica de negócio
│   │   ├── __init__.py
│   │   ├── aluno_service.py
│   │   └── auth_service.py
│   ├── repositories/            ← acesso ao banco
│   │   ├── __init__.py
│   │   └── aluno_repo.py
│   └── dependencies.py          ← injeção de dependências
├── tests/
│   ├── conftest.py
│   ├── unit/
│   └── integration/
├── migrations/                  ← Alembic
│   ├── env.py
│   └── versions/
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
├── requirements.txt
└── .env.example

Configuração e Banco de Dados

# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache


class Configuracoes(BaseSettings):
    # Aplicação
    nome_app:    str   = "Escola API"
    versao:      str   = "1.0.0"
    debug:       bool  = False
    ambiente:    str   = "producao"

    # Banco de dados
    database_url:       str
    database_pool_size: int   = 5
    database_max_overflow: int = 10

    # Segurança
    secret_key:           str
    algoritmo_jwt:        str   = "HS256"
    expiracao_token_min:  int   = 30

    # CORS
    origens_permitidas: list[str] = ["http://localhost:3000"]

    class Config:
        env_file = ".env"


@lru_cache
def get_config() -> Configuracoes:
    return Configuracoes()
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker, Session
from typing import Generator
from app.config import get_config


config = get_config()

engine = create_engine(
    config.database_url,
    pool_size=config.database_pool_size,
    max_overflow=config.database_max_overflow,
    pool_pre_ping=True,   # detecta conexões mortas
    echo=config.debug
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


class Base(DeclarativeBase):
    pass


def get_db() -> Generator[Session, None, None]:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Modelos ORM

# app/models/aluno.py
from sqlalchemy import (
    Column, Integer, String, Float,
    Boolean, DateTime, ForeignKey, Text
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base


class Turma(Base):
    __tablename__ = "turmas"

    id         = Column(Integer, primary_key=True, index=True)
    nome       = Column(String(50),  nullable=False, unique=True)
    turno      = Column(String(20),  nullable=False)
    ano        = Column(Integer,     nullable=False)
    ativa      = Column(Boolean,     default=True)
    criado_em  = Column(DateTime(timezone=True), server_default=func.now())

    alunos = relationship("Aluno", back_populates="turma", lazy="dynamic")

    def __repr__(self):
        return f"<Turma {self.nome}>"


class Aluno(Base):
    __tablename__ = "alunos"

    id           = Column(Integer, primary_key=True, index=True)
    nome         = Column(String(100), nullable=False)
    email        = Column(String(100), nullable=False, unique=True, index=True)
    senha_hash   = Column(String(255), nullable=False)
    turma_id     = Column(Integer, ForeignKey("turmas.id"), nullable=True)
    ativo        = Column(Boolean,     default=True)
    admin        = Column(Boolean,     default=False)
    criado_em    = Column(DateTime(timezone=True), server_default=func.now())
    atualizado_em = Column(DateTime(timezone=True), onupdate=func.now())

    turma  = relationship("Turma", back_populates="alunos")
    notas  = relationship("Nota", back_populates="aluno", cascade="all, delete-orphan")

    def __repr__(self):
        return f"<Aluno {self.nome}>"


class Disciplina(Base):
    __tablename__ = "disciplinas"

    id    = Column(Integer, primary_key=True)
    nome  = Column(String(80), nullable=False)
    carga = Column(Integer, default=60)   # horas

    notas = relationship("Nota", back_populates="disciplina")


class Nota(Base):
    __tablename__ = "notas"

    id            = Column(Integer, primary_key=True)
    aluno_id      = Column(Integer, ForeignKey("alunos.id",     ondelete="CASCADE"))
    disciplina_id = Column(Integer, ForeignKey("disciplinas.id", ondelete="CASCADE"))
    valor         = Column(Float,  nullable=False)
    periodo       = Column(String(10), nullable=False)   # ex: "2024-1"
    criado_em     = Column(DateTime(timezone=True), server_default=func.now())

    aluno      = relationship("Aluno",      back_populates="notas")
    disciplina = relationship("Disciplina", back_populates="notas")

Schemas Pydantic

# app/schemas/aluno.py
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
from datetime import datetime


class TurmaBase(BaseModel):
    nome:  str = Field(..., min_length=2, max_length=50)
    turno: str = Field(..., pattern="^(manha|tarde|noite)$")
    ano:   int = Field(..., ge=2020, le=2030)


class TurmaCriar(TurmaBase):
    pass


class TurmaResposta(TurmaBase):
    id:       int
    ativa:    bool
    criado_em: datetime

    model_config = {"from_attributes": True}


class AlunoBase(BaseModel):
    nome:  str      = Field(..., min_length=2, max_length=100)
    email: EmailStr


class AlunoCriar(AlunoBase):
    senha:    str = Field(..., min_length=8)
    turma_id: Optional[int] = None

    @field_validator("senha")
    @classmethod
    def validar_senha(cls, v: str) -> str:
        import re
        if not re.search(r"[A-Z]", v):
            raise ValueError("Senha deve conter ao menos uma maiúscula.")
        if not re.search(r"\d", v):
            raise ValueError("Senha deve conter ao menos um número.")
        return v


class AlunoAtualizar(BaseModel):
    nome:     Optional[str]      = Field(None, min_length=2)
    email:    Optional[EmailStr] = None
    turma_id: Optional[int]      = None


class AlunoResposta(AlunoBase):
    id:          int
    turma_id:    Optional[int]
    ativo:       bool
    admin:       bool
    criado_em:   datetime
    turma:       Optional[TurmaResposta] = None

    model_config = {"from_attributes": True}


class AlunoRespostaLista(BaseModel):
    total:   int
    pagina:  int
    tamanho: int
    alunos:  list[AlunoResposta]


class NotaCriar(BaseModel):
    disciplina_id: int
    valor:         float = Field(..., ge=0.0, le=10.0)
    periodo:       str   = Field(..., pattern=r"^\d{4}-[12]$")


class NotaResposta(BaseModel):
    id:            int
    disciplina_id: int
    valor:         float
    periodo:       str

    model_config = {"from_attributes": True}


class TokenResposta(BaseModel):
    access_token:  str
    token_type:    str = "bearer"
    expira_em:     int


class LoginDados(BaseModel):
    email: EmailStr
    senha: str

Repositório

# app/repositories/aluno_repo.py
from sqlalchemy.orm import Session
from sqlalchemy import select, func, and_
from typing import Optional
from app.models.aluno import Aluno, Turma, Nota


class AlunoRepositorio:

    def __init__(self, db: Session):
        self.db = db

    def criar(self, dados: dict) -> Aluno:
        aluno = Aluno(**dados)
        self.db.add(aluno)
        self.db.commit()
        self.db.refresh(aluno)
        return aluno

    def buscar_id(self, aluno_id: int) -> Optional[Aluno]:
        return self.db.get(Aluno, aluno_id)

    def buscar_email(self, email: str) -> Optional[Aluno]:
        return self.db.scalar(
            select(Aluno).where(Aluno.email == email)
        )

    def listar(
        self,
        pagina:         int   = 1,
        tamanho:        int   = 10,
        turma_id:       Optional[int]  = None,
        apenas_ativos:  bool  = True,
        busca:          Optional[str]  = None
    ) -> tuple[list[Aluno], int]:
        stmt = select(Aluno)

        filtros = []
        if apenas_ativos:
            filtros.append(Aluno.ativo == True)
        if turma_id:
            filtros.append(Aluno.turma_id == turma_id)
        if busca:
            filtros.append(Aluno.nome.ilike(f"%{busca}%"))
        if filtros:
            stmt = stmt.where(and_(*filtros))

        total = self.db.scalar(
            select(func.count()).select_from(stmt.subquery())
        )

        alunos = self.db.scalars(
            stmt
            .order_by(Aluno.nome)
            .offset((pagina - 1) * tamanho)
            .limit(tamanho)
        ).all()

        return list(alunos), total or 0

    def atualizar(self, aluno: Aluno, dados: dict) -> Aluno:
        for campo, valor in dados.items():
            setattr(aluno, campo, valor)
        self.db.commit()
        self.db.refresh(aluno)
        return aluno

    def deletar(self, aluno: Aluno) -> None:
        aluno.ativo = False
        self.db.commit()

    def adicionar_nota(self, dados: dict) -> Nota:
        nota = Nota(**dados)
        self.db.add(nota)
        self.db.commit()
        self.db.refresh(nota)
        return nota

    def media_aluno(self, aluno_id: int, periodo: Optional[str] = None) -> float:
        stmt = select(func.avg(Nota.valor)).where(Nota.aluno_id == aluno_id)
        if periodo:
            stmt = stmt.where(Nota.periodo == periodo)
        return float(self.db.scalar(stmt) or 0.0)

Service

# app/services/aluno_service.py
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from passlib.context import CryptContext
from app.repositories.aluno_repo import AlunoRepositorio
from app.schemas.aluno import AlunoCriar, AlunoAtualizar
from app.models.aluno import Aluno

pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")


class AlunoService:

    def __init__(self, db: Session):
        self.repo = AlunoRepositorio(db)

    def cadastrar(self, dados: AlunoCriar) -> Aluno:
        if self.repo.buscar_email(dados.email):
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="E-mail já cadastrado."
            )
        aluno = self.repo.criar({
            "nome":        dados.nome,
            "email":       dados.email,
            "senha_hash":  pwd_ctx.hash(dados.senha),
            "turma_id":    dados.turma_id,
        })
        return aluno

    def buscar_ou_404(self, aluno_id: int) -> Aluno:
        aluno = self.repo.buscar_id(aluno_id)
        if not aluno or not aluno.ativo:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Aluno {aluno_id} não encontrado."
            )
        return aluno

    def atualizar(self, aluno_id: int, dados: AlunoAtualizar) -> Aluno:
        aluno = self.buscar_ou_404(aluno_id)

        if dados.email and dados.email != aluno.email:
            if self.repo.buscar_email(dados.email):
                raise HTTPException(
                    status_code=409, detail="E-mail já em uso."
                )

        campos = dados.model_dump(exclude_unset=True)
        return self.repo.atualizar(aluno, campos)

    def deletar(self, aluno_id: int) -> None:
        aluno = self.buscar_ou_404(aluno_id)
        self.repo.deletar(aluno)

    def lancar_nota(self, aluno_id: int, dados) -> None:
        self.buscar_ou_404(aluno_id)
        self.repo.adicionar_nota({
            "aluno_id":      aluno_id,
            "disciplina_id": dados.disciplina_id,
            "valor":         dados.valor,
            "periodo":       dados.periodo,
        })

Routers

# app/routers/alunos.py
from fastapi import APIRouter, Depends, status, Query
from sqlalchemy.orm import Session
from typing import Annotated, Optional
from app.database import get_db
from app.schemas.aluno import (
    AlunoCriar, AlunoAtualizar, AlunoResposta,
    AlunoRespostaLista, NotaCriar
)
from app.services.aluno_service import AlunoService
from app.dependencies import usuario_atual, usuario_admin

router = APIRouter(prefix="/alunos", tags=["Alunos"])

DbDep    = Annotated[Session,     Depends(get_db)]
AdminDep = Annotated[dict,        Depends(usuario_admin)]


@router.get("/", response_model=AlunoRespostaLista)
async def listar_alunos(
    db:       DbDep,
    pagina:   int            = Query(1,    ge=1),
    tamanho:  int            = Query(10,   ge=1, le=100),
    turma_id: Optional[int]  = Query(None),
    busca:    Optional[str]  = Query(None, min_length=2)
):
    service       = AlunoService(db)
    alunos, total = service.repo.listar(
        pagina=pagina, tamanho=tamanho,
        turma_id=turma_id, busca=busca
    )
    return AlunoRespostaLista(
        total=total, pagina=pagina,
        tamanho=tamanho, alunos=alunos
    )


@router.post("/", response_model=AlunoResposta,
             status_code=status.HTTP_201_CREATED)
async def criar_aluno(dados: AlunoCriar, db: DbDep):
    return AlunoService(db).cadastrar(dados)


@router.get("/{aluno_id}", response_model=AlunoResposta)
async def buscar_aluno(aluno_id: int, db: DbDep):
    return AlunoService(db).buscar_ou_404(aluno_id)


@router.patch("/{aluno_id}", response_model=AlunoResposta)
async def atualizar_aluno(
    aluno_id: int,
    dados:    AlunoAtualizar,
    db:       DbDep,
    _:        AdminDep
):
    return AlunoService(db).atualizar(aluno_id, dados)


@router.delete("/{aluno_id}", status_code=status.HTTP_204_NO_CONTENT)
async def deletar_aluno(aluno_id: int, db: DbDep, _: AdminDep):
    AlunoService(db).deletar(aluno_id)


@router.post("/{aluno_id}/notas",
             status_code=status.HTTP_201_CREATED)
async def lancar_nota(aluno_id: int, dados: NotaCriar, db: DbDep, _: AdminDep):
    AlunoService(db).lancar_nota(aluno_id, dados)
    return {"mensagem": "Nota lançada com sucesso."}


@router.get("/{aluno_id}/media")
async def media_aluno(
    aluno_id: int,
    db:       DbDep,
    periodo:  Optional[str] = Query(None)
):
    service = AlunoService(db)
    service.buscar_ou_404(aluno_id)
    media = service.repo.media_aluno(aluno_id, periodo)
    return {
        "aluno_id": aluno_id,
        "media":    round(media, 2),
        "periodo":  periodo or "geral",
        "aprovado": media >= 6.0
    }

Dependências e Autenticação

# app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from jose import JWTError, jwt
from typing import Annotated
from app.database import get_db
from app.models.aluno import Aluno
from app.config import get_config

config  = get_config()
oauth2  = OAuth2PasswordBearer(tokenUrl="/auth/login")


async def usuario_atual(
    token: Annotated[str, Depends(oauth2)],
    db:    Annotated[Session, Depends(get_db)]
) -> Aluno:
    exc = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Token inválido ou expirado.",
        headers={"WWW-Authenticate": "Bearer"}
    )
    try:
        payload  = jwt.decode(
            token, config.secret_key,
            algorithms=[config.algoritmo_jwt]
        )
        aluno_id = int(payload.get("sub", 0))
    except (JWTError, ValueError):
        raise exc

    aluno = db.get(Aluno, aluno_id)
    if not aluno or not aluno.ativo:
        raise exc
    return aluno


async def usuario_admin(
    aluno: Annotated[Aluno, Depends(usuario_atual)]
) -> Aluno:
    if not aluno.admin:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Requer privilégios de administrador."
        )
    return aluno

Router de Autenticação

# app/routers/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta
from typing import Annotated
from app.database import get_db
from app.repositories.aluno_repo import AlunoRepositorio
from app.schemas.aluno import TokenResposta
from app.config import get_config

router  = APIRouter(prefix="/auth", tags=["Autenticação"])
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
config  = get_config()


def criar_token(aluno_id: int, email: str, admin: bool) -> str:
    exp = datetime.utcnow() + timedelta(minutes=config.expiracao_token_min)
    return jwt.encode(
        {"sub": str(aluno_id), "email": email, "admin": admin, "exp": exp},
        config.secret_key, algorithm=config.algoritmo_jwt
    )


@router.post("/login", response_model=TokenResposta)
async def login(
    form: Annotated[OAuth2PasswordRequestForm, Depends()],
    db:   Annotated[Session, Depends(get_db)]
):
    repo  = AlunoRepositorio(db)
    aluno = repo.buscar_email(form.username)

    if not aluno or not pwd_ctx.verify(form.password, aluno.senha_hash):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="E-mail ou senha incorretos.",
            headers={"WWW-Authenticate": "Bearer"}
        )
    if not aluno.ativo:
        raise HTTPException(status_code=400, detail="Conta desativada.")

    return TokenResposta(
        access_token=criar_token(aluno.id, aluno.email, aluno.admin),
        expira_em=config.expiracao_token_min * 60
    )

Main: Montando a Aplicação

# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from app.config import get_config
from app.database import engine, Base
from app.routers import alunos, turmas, auth
import logging

logger = logging.getLogger("escola.main")
config = get_config()


@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info(f"Iniciando {config.nome_app} v{config.versao}")
    Base.metadata.create_all(bind=engine)   # cria tabelas em dev
    yield
    logger.info("Encerrando aplicação.")


def criar_app() -> FastAPI:
    app = FastAPI(
        title=config.nome_app,
        version=config.versao,
        lifespan=lifespan,
        docs_url="/docs"    if config.debug else None,
        redoc_url="/redoc"  if config.debug else None,
    )

    app.add_middleware(
        CORSMiddleware,
        allow_origins=config.origens_permitidas,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    app.add_middleware(GZipMiddleware, minimum_size=1000)

    app.include_router(auth.router)
    app.include_router(alunos.router)

    @app.get("/health", tags=["Saúde"])
    async def health():
        return {"status": "healthy", "versao": config.versao}

    return app


app = criar_app()

Docker e Docker Compose

# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY app/ ./app/
COPY migrations/ ./migrations/
COPY alembic.ini .

RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
# docker-compose.yml
version: "3.9"

services:
  api:
    build: .
    container_name: escola-api
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://escola:senha@db:5432/escola
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=false
      - AMBIENTE=producao
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    container_name: escola-db
    environment:
      - POSTGRES_USER=escola
      - POSTGRES_PASSWORD=senha
      - POSTGRES_DB=escola
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U escola"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api

volumes:
  postgres_data:

Testes do Projeto

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import criar_app
from app.database import Base, get_db
from app.models.aluno import Aluno, Turma
from passlib.context import CryptContext

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine_test = create_engine(
    SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False}
)
SessionTeste = sessionmaker(autocommit=False, autoflush=False, bind=engine_test)
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")


@pytest.fixture(scope="session")
def app():
    Base.metadata.create_all(bind=engine_test)
    _app = criar_app()
    _app.dependency_overrides[get_db] = lambda: SessionTeste()
    yield _app
    Base.metadata.drop_all(bind=engine_test)


@pytest.fixture
def client(app):
    return TestClient(app)


@pytest.fixture
def db():
    session = SessionTeste()
    yield session
    session.rollback()
    session.close()


@pytest.fixture
def admin_token(client):
    # Cria admin e retorna token
    db = SessionTeste()
    admin = Aluno(
        nome="Admin", email="admin@test.com",
        senha_hash=pwd_ctx.hash("Senha123"),
        admin=True
    )
    db.add(admin)
    db.commit()

    resp = client.post("/auth/login", data={
        "username": "admin@test.com",
        "password": "Senha123"
    })
    return resp.json()["access_token"]


# tests/integration/test_alunos.py
class TestAPIAlunos:

    def test_criar_aluno(self, client, admin_token):
        resp = client.post("/alunos", json={
            "nome":  "Ana Silva",
            "email": "ana@test.com",
            "senha": "Senha123"
        })
        assert resp.status_code == 201
        assert resp.json()["nome"] == "Ana Silva"

    def test_criar_duplicado(self, client):
        dados = {"nome": "Bruno", "email": "bruno@test.com", "senha": "Senha123"}
        client.post("/alunos", json=dados)
        resp = client.post("/alunos", json=dados)
        assert resp.status_code == 409

    def test_listar_paginado(self, client):
        resp = client.get("/alunos?pagina=1&tamanho=5")
        assert resp.status_code == 200
        body = resp.json()
        assert "total" in body
        assert "alunos" in body

    def test_buscar_nao_existe(self, client):
        resp = client.get("/alunos/9999")
        assert resp.status_code == 404

    def test_deletar_requer_admin(self, client):
        resp = client.delete("/alunos/1")
        assert resp.status_code == 401

    def test_deletar_como_admin(self, client, admin_token):
        # Cria aluno
        c = client.post("/alunos", json={
            "nome": "Temp", "email": "temp@test.com", "senha": "Senha123"
        })
        aluno_id = c.json()["id"]

        resp = client.delete(
            f"/alunos/{aluno_id}",
            headers={"Authorization": f"Bearer {admin_token}"}
        )
        assert resp.status_code == 204

Resumo do Projeto

Este projeto integra todos os conceitos vistos na série:

Conceito Onde foi aplicado
FastAPI + Pydantic Routers, schemas, validação
SQLAlchemy ORM Models, repositório, migrations
JWT + bcrypt Auth router, dependencies
Docker + Compose Dockerfile multi-stage, orquestração
Repository Pattern AlunoRepositorio
Service Layer AlunoService
Dependency Injection get_db, usuario_atual
pytest + TestClient Testes de integração
Type hints Em todo o código
Configuração por ambiente pydantic-settings

Referências e Leituras Complementares

  • FastAPI — documentação oficial — https://fastapi.tiangolo.com/
  • SQLAlchemy 2.0 — https://docs.sqlalchemy.org/en/20/
  • Alembic — migrações — https://alembic.sqlalchemy.org/
  • pydantic-settings — https://docs.pydantic.dev/latest/concepts/pydantic_settings/
  • Docker Compose — https://docs.docker.com/compose/
  • PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. — Repository, Service Layer e Dependency Injection em Python.

Prof. Ricardo Matos — Dominando o Python em 1 Ano · Artigo 48 de 52 Próximo: Artigo 49 — Projeto 2: Pipeline de Data Science do Zero ao Deploy


you asked

Sim


claude response

Dominando o Python em 1 Ano

Comentários

Mais em Python

Variáveis e Tipos de Dados
Variáveis e Tipos de Dados

Todo programa manipula informa&ccedil;&atilde;o. Para isso, a linguagem preci...

Autenticação e Segurança em APIs Python
Autenticação e Segurança em APIs Python

Uma API sem autenticação é uma porta aberta. Segurança não é um recurso opcio...

Introdução ao Data Science com Python
Introdução ao Data Science com Python

Data Science é a disciplina que extrai conhecimento e insights de dados — com...