CLI¶
Instalar o tempest-fastapi-sdk expõe um console script tempest. Ele faz dois trabalhos: criar um novo serviço em camadas a partir do esqueleto preferido do SDK e rodar os quatro gates de qualidade (ruff check, ruff format, mypy, pytest) sem copiar e colar os mesmos comandos em cada projeto.
Erro de uso? O help completo aparece junto
Quando você digita um comando inexistente, uma opção inválida ou
esquece um argumento obrigatório, o tempest imprime o --help
completo daquele comando (todos os parâmetros, defaults e
descrições) antes da linha de erro — em vez do Try '... --help'
enxuto do Click. Você corrige na hora, sem reexecutar com --help.
Gerar um novo serviço¶
tempest new my_service # gera em ./my_service
tempest new my_service --path ~/projects # diretório-pai customizado
tempest new my_service \
--bind-host 0.0.0.0 \ # HOST padrão no .env.example
--bind-port 9090 \ # PORT padrão no .env.example
--extras auth,upload # extras do SDK fixados
tempest new my_service --force # sobrescreve diretório existente
O esqueleto casa com a arquitetura em camadas documentada em Arquitetura »:
my_service/
├── main.py # one-liner → src.server.run()
├── pyproject.toml # fixa tempest-fastapi-sdk + ruff/mypy/pytest
├── .env.example # TITLE/VERSION/SERVER_HOST/SERVER_PORT/DATABASE_URL/JWT_SECRET/CORS_ORIGINS
├── docker-compose.yaml # serviços baseados nos extras escolhidos
├── .gitignore
├── README.md
├── CLAUDE.md # regras do projeto para agentes de IA e humanos
├── src/
│ ├── server.py # uvicorn.run() + app FastAPI no nível do módulo
│ ├── api/
│ │ ├── app.py # create_app() conecta middleware + handlers do SDK
│ │ ├── routers/ # router de negócio placeholder
│ │ └── dependencies/ # auth.py (require_token) + factories
│ ├── controllers/ # orquestração entre services
│ ├── services/ # lógica de negócio
│ ├── schemas/ # DTOs Pydantic
│ ├── core/ # settings.py + exceptions.py
│ ├── db/
│ │ ├── models/
│ │ └── repositories/
│ ├── ui/ # só com o extra [ssr] — pages/layout/components/styles
│ └── utils/
└── tests/
└── test_smoke.py # garante que /api/ e /health/liveness sobem
O CLAUDE.md gerado é o contrato do projeto
Todo projeto novo nasce com um CLAUDE.md que fixa as regras que
mantêm os serviços parecidos entre si: a direção das dependências
entre camadas, a ordem exata dos sete passos de uma feature nova
(schema → model → repository → service → controller → provider →
router), a tabela do que não reimplementar porque o SDK já
entrega, e o "definition of done" que termina em tempest check.
Ele existe para um agente de IA ler antes de escrever a primeira
linha — e por isso os exemplos dele são verificados na CI do SDK:
tests/cli/test_scaffold_runtime.py escreve o domínio de exemplo
num projeto scaffoldado e o executa (POST 201, duplicado 409 com o
code certo, listagem paginada no envelope do SDK). Um símbolo
renomeado no SDK quebra o teste, não o projeto de quem usa.
O pyproject.toml gerado fixa a versão atual do SDK (tempest-fastapi-sdk[auth,admin]>=<versão> por padrão — mude com --extras). O .env.example criado usa a nomenclatura de settings da v0.8.0 (SERVER_HOST/SERVER_PORT/SERVER_DEBUG/SERVER_RELOAD/LOG_LEVEL/…), e src/server.py delega a tempest_fastapi_sdk.run_server para que o uvicorn seja importado de forma preguiçosa e os testes possam importar o app sem ele. Regras de validação: o nome do projeto deve casar com ^[a-z][a-z0-9_]*$ e não pode colidir com uma palavra-chave do Python, então tempest new Bad-Name e tempest new class saem com código 2 antes de qualquer arquivo ser escrito.
Título / versão da API vêm do .env
A partir da v0.48.0 o Settings scaffoldado carrega TITLE,
VERSION e DESCRIPTION, e src/api/app.py os consome
(FastAPI(title=settings.TITLE, version=settings.VERSION,
description=settings.DESCRIPTION), make_health_router(version=
settings.VERSION) e AdminSite(title=f"{settings.TITLE} admin")).
Ajuste o título mostrado no Swagger/ReDoc e no header do /admin
direto no .env, sem editar código:
docker-compose.yaml baseado nos extras¶
Desde a v0.25.0 o scaffold gera um docker-compose.yaml com apenas os serviços que os extras escolhidos precisam — sem ZooKeeper, Kafka ou qualquer outra coisa que você não vai usar.
| Extra | Container subido | Porta(s) exposta(s) |
|---|---|---|
| (sempre) | postgres:18-alpine |
5432 |
[cache] |
redis:8-alpine |
6379 |
[queue] / [tasks] |
rabbitmq:4-management-alpine |
5672 (AMQP) + 15672 (UI) |
[minio] |
minio/minio + bootstrap mc |
9000 (API) + 9001 (Console) |
[email] |
mailhog/mailhog |
1025 (SMTP) + 8025 (UI) |
Exemplo — projeto que usa cache + uploads em S3 + emails:
Gera:
postgres,redis,minio(+minio-bootstrapque cria o bucketuploads),mailhog.env.examplecomREDIS_URL,MINIO_*,SMTP_HOST=localhost,SMTP_PORT=1025,SMTP_USE_TLS=false(MailHog é plain — sem STARTTLS)
Credenciais vêm do .env, não estão hardcoded no compose
A partir da v0.37.0, nenhuma credencial é gravada direto no
docker-compose.yaml. Cada bloco environment: usa a forma
${VAR:-default}, e o Docker Compose resolve VAR a partir do
.env ao lado do compose. O :-default mantém o stack subindo
antes de você copiar .env.example para .env — mas defina
segredos reais no .env para qualquer deploy não-descartável.
Variáveis lidas pelo compose: POSTGRES_USER / POSTGRES_PASSWORD
/ POSTGRES_DB, RABBITMQ_DEFAULT_USER / RABBITMQ_DEFAULT_PASS
/ RABBITMQ_DEFAULT_VHOST, MINIO_ROOT_USER /
MINIO_ROOT_PASSWORD — todas com seus padrões já no .env.example.
Subir tudo:
Derrubar mantendo volumes:
Derrubar limpando volumes:
As tags das imagens são fixadas — atualize por pyproject.toml da SDK, não no projeto gerado. Versões atuais (v0.26.0+): postgres:18-alpine, redis:8-alpine, rabbitmq:4-management-alpine.
Regerar o docker-compose.yaml num projeto existente¶
Quando você muda os extras instalados (uv add "tempest-fastapi-sdk[minio]") ou o SDK bumpa as imagens, regenere com:
tempest generate --docker # lê extras do pyproject.toml local
tempest generate --docker --extras cache,minio # força extras explicitos
tempest generate --docker --name my-svc # sobrescreve prefixo do container
tempest generate --docker --force # sobrescreve compose existente
O comando lê o [project] name + extras do pyproject.toml do diretório atual (use --path pra outro). Recusa overwrite sem --force pra não pisar em edits manuais. O .env.example é atualizado de forma idempotente — re-rodar não duplica blocos.
Dockerfile para containerizar a app¶
Fullstack: a SPA é detectada e compilada num stage Node
Se o projeto tem um frontend — um package.json em web/, frontend/,
client/ ou ui/ — o Dockerfile gerado ganha um stage Node que instala e
compila a SPA, e só o dist/ resultante é copiado para a imagem final.
Nem node_modules nem o toolchain do Node chegam ao runtime.
Regenerated Dockerfile
Regenerated .dockerignore
SPA stage: builds web/ and copies web/dist into the image.
| Opção | Efeito |
|---|---|
| (nenhuma) | Detecta pelo package.json. Diretório vazio não conta |
--spa-dir apps-web |
Layout fora da convenção |
--no-spa |
Imagem backend-only mesmo com frontend presente |
Sirva o resultado com make_spa_router("web/dist"),
incluído depois de todos os routers de API. Projeto sem frontend gera
um Dockerfile byte a byte idêntico ao de antes.
Desde a v0.71.0, tempest new também gera um Dockerfile + .dockerignore prontos pra empacotar o serviço como imagem. O Dockerfile é multi-stage e usa uv:
- stage
builder— instala as dependências num/app/.venv(camada cacheada que só re-roda quandopyproject.toml/uv.lockmudam), depois instala o projeto. - stage final — copia só o venv + o código, roda como usuário não-root (
app, uid 1000) e expõe a porta configurada.
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
WORKDIR /app
COPY pyproject.toml uv.lock* ./
RUN uv sync --no-dev --no-install-project
COPY . .
RUN uv sync --no-dev
FROM python:3.13-slim
RUN useradd --create-home --uid 1000 app
WORKDIR /app
COPY --from=builder --chown=app:app /app /app
ENV PATH="/app/.venv/bin:$PATH" SERVER_HOST=0.0.0.0 SERVER_PORT=8000
USER app
EXPOSE 8000
CMD ["python", "main.py"]
Construir e rodar:
A imagem faz bind em 0.0.0.0 por padrão
O stage final define ENV SERVER_HOST=0.0.0.0 para que a app
seja acessível de fora do container mesmo sem .env. Localmente
o scaffold mantém SERVER_HOST=127.0.0.1 (serviço interno) — o
container sobrescreve pra 0.0.0.0 porque ali o bind precisa
aceitar conexões externas. Passe --env-file .env pra apontar
DATABASE_URL na infra do docker-compose.yaml.
docker-compose.yaml continua só com infra
O compose gerado sobe apenas Postgres + os serviços dos extras
(Redis, RabbitMQ, MinIO, MailHog) — não embute um serviço app.
O Dockerfile é standalone: use docker build / docker run, ou
adicione um serviço app: com build: . ao compose à mão se quiser
subir tudo num comando só.
Regerar o Dockerfile — tempest generate --dockerfile¶
tempest generate --dockerfile # Dockerfile + .dockerignore
tempest generate --dockerfile --name my-svc # sobrescreve nome nos comentários
tempest generate --dockerfile --force # sobrescreve arquivos existentes
tempest generate --docker --dockerfile --src # tudo numa tacada
A porta do EXPOSE / SERVER_PORT é lida do SERVER_PORT no .env (ou .env.example), caindo em 8000 se não achar. Como os outros generators, recusa overwrite sem --force.
Gerar as camadas src dos extras — tempest generate --src¶
As camadas sempre presentes (api, controllers, services, schemas, db, core, utils) já vêm no scaffold. As camadas que só fazem sentido com um extra específico — [queue] (FastStream) e [tasks] (TaskIQ) — não entram no esqueleto base: deixar pacotes placeholder vazios em todo serviço contraria as regras de layout. Quando você adiciona um desses extras a um projeto existente (uv add "tempest-fastapi-sdk[queue]"), gere a camada correspondente com:
tempest generate --src # lê extras do pyproject.toml local
tempest generate --src --extras tasks # força extras explícitos
tempest generate --src --force # sobrescreve arquivos existentes
tempest generate --docker --src # compose + camadas numa tacada
Mapeamento extra → camada gerada:
| Extra | Arquivos criados (sob src/ ou app/) |
|---|---|
[queue] |
queue/__init__.py (broker + AsyncBrokerManager + get_broker), queue/handlers.py (subscriber de exemplo) |
[tasks] |
tasks/__init__.py (broker + AsyncTaskBrokerManager + get_task_manager), tasks/jobs.py (task de exemplo) |
A raiz (src ou app) é detectada automaticamente, e os imports gerados (from src.queue import broker) já apontam pra ela. A operação é idempotente: arquivos existentes são mantidos a menos que você passe --force, então um handler editado à mão nunca é sobrescrito silenciosamente — o arquivo irmão que ainda não existe é criado normalmente. Extras sem camada associada (ex.: só [cache]) não geram nada e o comando avisa.
tempest new já gera as camadas dos extras escolhidos
Um tempest new my_service --extras auth,queue já entrega
src/queue/ pronto — o generate --src é pra quando você adiciona
o extra depois de criar o projeto.
Depois de gerar:
cd my_service
uv sync # instala SDK + ferramentas de dev
cp .env.example .env
uv run python main.py # serve no HOST:PORT configurado
uv run pytest # o smoke test embutido
Banco de dados — tempest db¶
Wrapper Alembic. Usa o AlembicHelper por trás, então a configuração (alembic.ini + env.py) continua sendo a fonte da verdade.
Resolução do DATABASE_URL na seguinte ordem:
- Flag
--database-url. - Env var
DATABASE_URL. src.core.settings.settings.DATABASE_URL(quando rodando no diretório do projeto scaffoldado).sqlalchemy.urldoalembic.ini.
tempest db init # cria alembic.ini + alembic/env.py
tempest db revision -m "init users table" # autogenerate por padrão
tempest db revision -m "manual change" --manual # cria arquivo vazio pra editar
tempest db upgrade # alembic upgrade head
tempest db upgrade <rev> # upgrade até rev específico
tempest db downgrade # rollback de 1 step
tempest db downgrade <rev> # rollback até rev específico
tempest db current # imprime revision aplicado
tempest db history # histórico de revisions
tempest db history -v # com message body completo
tempest db stamp head # marca o DB sem rodar migrations
tempest db squash -m "init" --yes # colapsa o histórico em 1 migration
tempest db backup # dump em backups/<db>_<ts>.<ext>
tempest db backup -o dump.sql # plain SQL (Postgres) por extensão
tempest db restore dump.dump --yes # restaura (clean + recreate)
tempest db seed # roda src.db.seeds:seed
tempest db seed --seed src.db.fixtures:demo # callable customizado
Colapsar o histórico — tempest db squash¶
Com o tempo o diretório alembic/versions/ cresce sem limite — cada ajuste de schema vira mais um arquivo que o Alembic precisa percorrer em todo upgrade. O squash zera esse histórico para uma única migration raiz que descreve o schema atual, sem perder os bancos já existentes.
Operação destrutiva — rode contra um banco de desenvolvimento
O squash faz downgrade base no banco configurado (DROPa todas as tabelas) para conseguir autogerar o schema completo num arquivo só. Por isso exige --yes. Confirme que o DATABASE_URL aponta para um banco de dev antes de rodar.
O fluxo é:
- Captura o
headatual (vira o nome do diretório de backup). downgrade base— limpa o banco para o autogenerate enxergar o schema vazio.- Move as revisions antigas para
alembic/versions/_squashed_<oldhead>/(subdiretório que o Alembic ignora). Use--no-backuppara apagar de vez. - Autogera uma migration raiz a partir de
BaseModel.metadata. upgrade headrecria o schema e marca a nova revision.
tempest db squash -m "init" --yes # backup recuperável (padrão)
tempest db squash -m "init" --yes --no-backup # apaga os arquivos antigos
Bancos de produção não são tocados
O squash só mexe no banco configurado. Depois de fazer deploy da árvore colapsada, marque os bancos de produção como migrados sem recriar tabelas:
Squash manual (sem perder dados) → stamp --purge
Se você unificar as migrations à mão (apaga versions/, escreve 1 migration baseline, mantém o banco com os dados), o alembic_version ainda aponta para a revision antiga — que não existe mais no tree. Um stamp normal falha com Can't locate revision. Use --purge para limpar o ponteiro órfão e gravar o novo baseline:
Recap: squash troca um histórico que cresce sem fim por uma migration inicial limpa; stamp reconcilia os bancos que já estão no schema final (use --purge quando a revision gravada não existir mais). Backup recuperável por padrão — o Git é a sua segunda rede.
Backup e restore — tempest db backup / tempest db restore¶
Snapshot do banco para um arquivo e volta. A estratégia muda por dialeto, mas a CLI é a mesma:
- PostgreSQL —
pg_dump/pg_restore. O formato vem da extensão do arquivo:.dump→ custom (pg_dump -Fc, comprimido, restaura compg_restore),.sql→ plain (psql). Force com--plain/--custom. Exige os client tools do Postgres noPATH. - SQLite — cópia do arquivo do banco.
Pré-requisito: client tools do Postgres
backup / restore num banco PostgreSQL dependem dos binários pg_dump, pg_restore e psql disponíveis no PATH. Eles não vêm com o pacote Python — são instalados pelo sistema operacional. Sem eles, a CLI falha com uma mensagem clara ('pg_dump' not found on PATH).
SQLite não precisa de nada — o backup é uma cópia de arquivo feita pela stdlib.
tempest db backup # backups/<db>_<YYYYMMDD-HHMMSS>.dump
tempest db backup -o snapshot.sql # plain SQL (Postgres) pela extensão
tempest db backup -o snap.dump --custom # força formato custom
tempest db restore snapshot.sql --yes # restaura (psql)
tempest db restore snap.dump --yes # restaura (pg_restore --clean --if-exists)
Restore sobrescreve o banco de destino
Por padrão o restore é clean + recreate: dropa os objetos existentes antes de recriar, então o resultado é uma cópia fiel do backup (pg_restore --clean --if-exists; no plain dropa/recria o schema public; no SQLite sobrescreve o arquivo). Por isso exige --yes. Use --no-clean para aplicar o dump por cima do schema atual.
A senha do Postgres não vaza no ps
A URL é parseada em -h/-p/-U/-d e a senha vai via PGPASSWORD no environment do subprocesso — nunca na linha de comando.
Recap: backup tira um snapshot (formato pela extensão no Postgres, cópia no SQLite); restore --yes traz de volta, limpando o destino por padrão.
Popular o banco — tempest db seed¶
Roda um callable de seed do projeto dentro de uma sessão gerenciada (commit no sucesso, rollback no erro). O callable recebe uma AsyncSession posicional e pode ser sync ou async; o que ele insere é decisão sua — o SDK só cuida do ciclo de vida da sessão. Por padrão importa src.db.seeds:seed.
from sqlalchemy.ext.asyncio import AsyncSession
from src.db.models import CategoryModel
async def seed(session: AsyncSession) -> int:
"""Popula categorias iniciais. Retorna a contagem (opcional)."""
session.add_all([CategoryModel(name="Livros"), CategoryModel(name="Games")])
await session.flush()
return 2
Quando o callable devolve um int, a CLI mostra a contagem: Seeded via src.db.seeds:seed (2 rows).
Usuários — tempest user¶
Insere/lista usuários direto no banco usando o UserModel concreto do projeto (default src.db.models:UserModel). Útil pra bootstrapear o primeiro admin sem rodar SQL manual.
# Cria usuário comum
tempest user create --email ana@example.com --password senha-forte-12 --no-admin
# Cria admin (pode logar no /admin)
tempest user create --email admin@local --password admin-pass-12 --admin
# Pede senha interativamente (não fica no shell history)
tempest user create --email admin@local --admin
# Modelo customizado fora do layout scaffoldado — DEVE ser subclasse de BaseUserModel
tempest user create --email x@y --password pass-12-chars --model myapp.models.user:UserModel
# Colunas que o SEU UserModel adiciona — repetível, uma por coluna
tempest user create --email ana@example.com --password senha-forte-12 \
--set display_name=Ana --set locale=pt-BR
# Promove/rebaixa um usuário existente (liga/desliga is_admin)
tempest user promote --email ana@example.com # vira admin
tempest user revoke --email ana@example.com # volta a ser comum
# Lista
tempest user list # todos
tempest user list --admin # só admins
UserModel com coluna própria obrigatória: use --set
O create preenche email, hashed_password, is_admin e
is_active. Toda outra coluna do seu UserModel entra por
--set <coluna>=<valor>, validada contra as colunas mapeadas e
convertida pro tipo da coluna (bool, int, float, Decimal, UUID,
ISO-8601 de data/hora, enum pelo valor e JSON).
tempest user create -e novato@example.com -p 'senha-forte-12' --no-admin
# error: UserModel requires a value for: display_name.
# Pass each one as --set <column>=<value>.
Coluna NOT NULL sem default que você não passou é perguntada num
terminal interativo (uma pergunta por coluna) e vira erro com código 2
numa execução sem TTY — em vez do IntegrityError cru que o banco
levantava. Chave desconhecida também sai com 2, listando as colunas
aceitas; --set email= / --set hashed_password= / --set is_admin=
são recusadas apontando a flag dona (--email, --password,
--admin/--no-admin).
Insert recusado pelo banco (email duplicado, quase sempre) sai com
código 1 e a mensagem do banco — error: could not insert user: … —
em vez de traceback.
Sem --admin/--no-admin, o create pergunta
Quando você não passa nem --admin nem --no-admin num
terminal interativo, o tempest user create pergunta
Should this user be an administrator? [y/N]. Em execuções
não-interativas (CI, pipes, scripts) o prompt é pulado e o usuário
nasce comum (is_admin=False) — passe --admin explicitamente pra
criar admin sem TTY.
tempest user promote / tempest user revoke localizam o usuário por email (case-insensitive) e só alternam is_admin. Quando nenhum usuário casa com o email, saem com código 1 e a mensagem no user found.
Resolução do DATABASE_URL igual ao tempest db (env var > settings > alembic.ini).
Segredos — tempest secrets¶
Gera e rotaciona os segredos da aplicação (JWT_SECRET / TOKEN_SECRET por padrão), reescrevendo as linhas correspondentes no .env no lugar — fazendo backup do arquivo antigo antes — e deixando as outras linhas intactas.
# Rotaciona JWT_SECRET e TOKEN_SECRET no .env (gera .env.bak)
tempest secrets rotate
# Só imprime os novos valores (não escreve nada) — pra pipar num secret manager
tempest secrets rotate --print
# Chaves e arquivo customizados
tempest secrets rotate --keys JWT_SECRET,SESSION_SECRET --env .env.prod
# Mais entropia, sem backup
tempest secrets rotate --length 64 --no-backup
Warning
Rotacionar JWT_SECRET invalida todo token assinado com o valor antigo: usuários são deslogados e links de reset/ativação pendentes param de funcionar. Rotacione numa janela de manutenção e reinicie o serviço pra carregar os novos valores.
Modelos — tempest model¶
Análise, benchmark, conversão e quantização de modelos ONNX. Precisa do
extra [modelops-onnx], exceto hardware, que só reporta o que esta
máquina consegue medir:
tempest model analyze models/classify.onnx
tempest model bench models/classify.onnx --dim height=224 --dim width=224
tempest model quantize models/classify.onnx models/classify.int8.onnx
tempest model export-ort models/classify.int8.onnx -o dist/mobile -t arm
tempest model hardware
| Comando | O que faz |
|---|---|
analyze |
Parâmetros, tamanho, opset e shapes, sem executar o modelo. |
bench |
Latência (mediana/IQR/p95/p99), RAM, GPU e energia sobre N repetições. |
optimize |
Persiste as otimizações de grafo do ONNX Runtime num novo .onnx. |
quantize |
Quantização dinâmica int8. |
export-ort |
Converte para .ort + .required_operators.config. |
hardware |
O que a máquina roda e qual sampler de energia está disponível. |
analyze, bench e hardware aceitam --json, o que os torna
utilizáveis como passo de CI. Extra ausente sai com código 2 e a linha de
instalação, nunca com traceback. Detalhes em
Modelops.
Erros documentados no OpenAPI — tempest openapi-errors¶
Compara, por rota, as AppException que o fluxo pode levantar com o que a rota
declarou em error_responses(...) / @raises(...):
tempest openapi-errors # relatório informativo (exit 0)
tempest openapi-errors --check # exit 1 quando há drift (gate de CI)
tempest openapi-errors --path src --path libs # diretórios explícitos, repetível
tempest openapi-errors --check --allow-unreachable # só falha em undocumented
tempest openapi-errors --fix --dry-run # diff das declarações faltantes
tempest openapi-errors --fix # grava (exige árvore git limpa)
Sem --path, varre ./src ou ./app — o que existir. A análise é estática
(ast, sem importar a aplicação) e percorre router -> controller -> service ->
repository, lendo os raise e as seções Raises: das docstrings.
src/api/routers/jobs.py:15 POST /{service_id}/candidates
undocumented: CandidateAlreadyExistsException, ServiceFullException
1 route(s) with drift, 2 undocumented exception(s).
--fix fecha o buraco escrevendo responses=error_responses(...) na rota (mais
os imports que faltarem), estendendo a declaração que já existir em vez de
substituí-la. Ele só acrescenta: findings unreachable nunca são removidos, já
que alcançabilidade não enxerga raise dinâmico.
Detalhes e limitações na receita Erros no OpenAPI ».
Guards de permissão — tempest permissions¶
Lê o contrato dos guards do @requires direto do código-fonte (ast, sem
importar a aplicação) e reporta o que o decorator não consegue ver em tempo de
import:
tempest permissions # relatório informativo (exit 0)
tempest permissions --check # exit 1 se houver erro (gate de CI)
tempest permissions --check --strict # falha também nos warnings
tempest permissions --path src --path libs
src/api/routers/orders.py:41 delete_order
error: guard-foreign-exception: guard 'order_owner' raises ValueError, which is
not an AppException subclass; the API layer answers it as HTTP 500 without an
error code
2 finding(s), 1 error(s).
Erros: guard que levanta fora da hierarquia AppException, guard-predicado
(-> bool), aridade errada, guard async em função sync, rota sem parâmetro de
usuário. Warnings: guard que nunca nega, anotação faltando, guard que o checker
não conseguiu resolver (lambda, nome duplicado, definição fora do escopo).
Detalhes na receita Guards de permissão (@requires) ».
Cliente de integração — tempest openapi-client¶
Gera schemas Pydantic + um cliente HTTP tipado a partir da especificação OpenAPI de um sistema de terceiros:
tempest openapi-client https://api.terceiro.com/openapi.json --name terceiro
tempest openapi-client ./vendor/spec.yaml --name terceiro --force
tempest openapi-client <spec> -H "Authorization: Bearer $TOKEN" --schemas-only
Escreve <src|app>/integrations/<name>/ com schemas.py (uma classe por
componente, com title/description/examples da spec preenchidos) e
client.py (um método async por operação, sobre um HTTPClient injetado).
+ src/integrations/terceiro/__init__.py
+ src/integrations/terceiro/client.py
+ src/integrations/terceiro/schemas.py
4 schema(s), 12 operation(s).
Detalhes, cobertura de OpenAPI e limitações na receita Cliente de integração (OpenAPI) ».
Descrição de PR com IA — tempest pr-prompt¶
A branch ficou verde e falta a parte que todo mundo pula: escrever a descrição do Pull Request. Qualquer assistente escreve uma boa — o que falta pra ele são as duas coisas que moram no repositório: o template que o time combinou e o diff que a branch realmente produziu.
tempest pr-prompt monta os dois num prompt só e joga em stdout, então ele
pipa direto pra IA que você usa:
tempest pr-prompt # compara com main, prompt no stdout
tempest pr-prompt develop # outra base
tempest pr-prompt | claude -p # pipa direto pro assistente
tempest pr-prompt --out pr_prompt.txt # grava num arquivo pra colar
O prompt carrega três blocos:
- O template do PR. O do próprio repositório vence — é o contrato que os
revisores daquele projeto leem. São procurados, nessa ordem:
.github/pull_request_template.md,.github/PULL_REQUEST_TEMPLATE.md,.github/PULL_REQUEST_TEMPLATE/pull_request_template.md,.gitlab/merge_request_templates/default.md,docs/pull_request_template.md,.pull_request_template.mdepull_request_template.md. Sem nenhum deles, entra o template embutido do SDK. - As regras que impedem a IA de devolver o template com os placeholders
dentro: nada de
Sim/Nãosem escolher, nada de_texto em itálico_de instrução, nenhuma seção apagada, e nada de inventar migration, env var ou dependência que não aparece no diff. - O contexto da branch: assuntos dos commits, a lista
--name-statusdos arquivos alterados e um trecho do patch de cada arquivo.
O diff é o mesmo que a forge mostra
Os patches são lidos como base...head (três pontos) — o diff em relação
ao merge-base, que é o que o GitHub/GitLab exibe no PR. Com dois pontos,
todo commit que entrou na base depois que sua branch nasceu seria
atribuído a você.
O que é completo e o que é amostrado
A lista de commits e a lista de arquivos alterados vão inteiras, sempre — a IA sempre sabe o que mudou. O que é limitado são os trechos de diff: por padrão os 10 arquivos com mais linhas alteradas, cada um cortado em 1500 caracteres. Ou seja, o que é amostrado é como mudou.
Os trechos são limitados porque o diff inteiro de uma branch grande não cabe
no contexto e é quase todo ruído (lock file, changelog, migration gerada). Os
arquivos entram ordenados por linhas alteradas, não em ordem alfabética —
senão o orçamento vai embora em .github/ e CHANGELOG.md antes de chegar no
arquivo sobre o qual o PR é:
tempest pr-prompt --full # todos os arquivos, patch inteiro
tempest pr-prompt --max-files 20 # 20 arquivos com patch (default: 10)
tempest pr-prompt --max-files 0 # só a lista de arquivos, sem patch
tempest pr-prompt --max-chars 4000 # mais patch por arquivo (default: 1500)
--full levanta os dois limites de uma vez — use numa branch pequena o
bastante pra mandar inteira. Ele recusa vir junto com --max-files /
--max-chars (saída 2): sobrescrever em silêncio um número que você digitou
seria pior que reclamar.
Nada é cortado em silêncio
Um patch truncado leva a marca trecho cortado e os arquivos que ficaram
sem trecho viram uma linha explícita no prompt (Mais N arquivo(s)
alterado(s)…). A IA lê um contexto parcial como parcial, em vez de tratar
o pedaço como a mudança inteira. O corte também respeita a quebra de linha,
pra não sobrar meia linha de diff — que a IA leria como código que não
existe.
O resumo (template escolhido, contagem de commits/arquivos) sai em stderr,
então o pipe continua limpo:
template: .github/pull_request_template.md
7 commit(s), 12 changed file(s), 10 excerpt(s), 2 file(s) without a patch.
Outras opções:
tempest pr-prompt --head feat/outra # descreve outra branch, sem checkout
tempest pr-prompt --lang en # regras e template embutido em inglês
tempest pr-prompt -t docs/meu_template.md # template explícito, vence todos
tempest pr-prompt -p ../outro-repo # roda contra outro repositório
Base inexistente vira origin/<base>
Num clone recém-feito costuma não existir uma main local, só
origin/main. Quando a base não resolve, o comando tenta
origin/<base> antes de falhar. Se nem isso existir, ele sai com código
2 e diz qual ref não encontrou. Quando a comparação não tem nem commit
nem arquivo, a saída é código 1 — quase sempre a base está errada.
Gates de qualidade¶
Esses comandos vêm do tempest-cli (v0.226.0)
O gate de qualidade não tem nada de FastAPI — é ruff, mypy e
pytest. Desde a v0.226.0 ele vive num pacote próprio,
tempest-cli, que o SDK
declara como dependência e monta na CLI dele.
Nada muda para você: tempest check é o mesmo comando, com as
mesmas flags e o mesmo [tool.tempest] typing_strictness. O que
muda é que quem não usa FastAPI agora consegue instalar só o
gate:
Por que separar: para chegar nesses quatro comandos era preciso instalar FastAPI, SQLAlchemy, Alembic e Pydantic — 38,7 MB de dependências e cerca de 0,5 s de import por invocação, medidos, para comandos que não tocam em nada disso.
Os dois nunca divergem porque há uma implementação só: o SDK chama
register_commands(app) do tempest_cli. Importar
tempest_fastapi_sdk.cli.lint ou .pr_prompt continua funcionando
e devolve as mesmas funções.
Os comandos de lint chamam a ferramenta do projeto. Eles procuram o executável no PATH primeiro e, caso contrário, caem para uv run <tool> para que um virtualenv local do projeto funcione sem ativação manual.
tempest lint # ruff check .
tempest fix # ruff check --fix . + ruff format . (escreve)
tempest fix --unsafe # também aplica os --unsafe-fixes do ruff
tempest format # ruff format . (escreve)
tempest fmt-check # ruff format --check . (somente leitura)
tempest type # mypy .
tempest test # pytest
tempest test tests/api/ # pytest com filtro de caminho
tempest check # lint + fmt-check + type + test, para no primeiro erro
tempest fix é a passada única de "organize o projeto" — ordena e remove imports duplicados, descarta imports não usados, normaliza aspas de strings, remove espaços em branco no fim das linhas e então roda ruff format para alinhar indentação, comprimento de linha, linhas em branco e a quebra de linha final. Rode-o antes do push quando o CI fica pegando detalhes de estilo.
O ruff format roda sempre — mesmo com erros restantes
O ruff check --fix sai com código ≠ 0 quando sobra qualquer
violação que ele não consegue corrigir sozinho (uma string ou
comentário longo demais, um nome indefinido, …). O tempest fix
roda o ruff format mesmo assim, então uma única linha não-corrigível
não impede a formatação do arquivo inteiro — as linhas de código
longas continuam sendo quebradas e as linhas em branco extras
removidas. O exit code do lint ainda é propagado depois, então o CI
continua falhando nas pendências reais.
Strings e comentários longos não quebram
Nem o ruff format nem o tempest fix quebram strings literais ou
comentários longos — comportamento idêntico ao Black. Essas linhas
E501 permanecem e precisam ser encurtadas à mão ou silenciadas com
# noqa: E501.
Todo comando retorna o exit code da ferramenta subjacente, então tempest check é seguro para conectar ao CI (tempest check || exit 1) ou a hooks de pre-commit. Quando nem o executável nem o uv estão no PATH, o wrapper imprime error: '<tool>' is not on PATH and 'uv' is unavailable e sai com 127 em vez de falhar silenciosamente.
Recap¶
O tempest cobre o ciclo inteiro — do scaffold (new),
passando pela infra (generate --docker / --dockerfile / --src),
migrações (db), usuários (user) e segredos (secrets), até os gates
de qualidade (lint / fix / type / test / check).
Próximos passos¶
Depois de gerar o serviço, siga para as receitas relacionadas:
- Banco de dados » —
BaseRepository, sessões async e o fluxo de migrações por trás dotempest db. - Filas e tarefas » — as camadas que
tempest generate --srcgera para os extras[queue](FastStream) e[tasks](TaskIQ). - Deploy seguro » — migrações destrutivas e shutdown
gracioso ao containerizar com o
Dockerfilegerado.