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