DocsPython estruturas
De FastAPI para Nexy
Nexy é FastAPI com a rota escrita como um arquivo: uma função que responde em @app.get("/x") responde em src/routes/x.py como GET, e o resto do seu código permanece o mesmo.
O que fica, o que muda
Nexy atende seus manipuladores com o próprio FastAPI, então tudo dentro de um manipulador funciona inalterado:
| Permanece o mesmo | Mudanças |
|---|---|
| Pydantic modelos, órgãos de solicitação e respostas | @app.get("/x") torna-se src/routes/x.py com async def GET() |
Depends, Query, Header, Cookie, Form, File |
{id} no caminho se torna um arquivo [id].py ou pasta [id]/ |
HTTPException, status, JSONResponse, RedirectResponse |
APIRouter(prefix="/users") se torna a pasta users/ |
Request, BackgroundTasks, UploadFile, WebSockets |
dependencies=[...] em um roteador se torna um arquivo dependencies.py |
Classes de middleware, /docs, TestClient |
app.add_middleware(...) torna-se um campo de nexyconfig.py: useCORS, useGZip, useMiddlewares |
Importe-os de fastapi como antes, ou de nexy.http, que reexporta os mesmos objetos.
Cada seção abaixo mostra um hábito FastAPI, então a mesma coisa no Nexy.
Um ponto final
FastAPI registra uma função em um aplicativo com um decorador:
from fastapi import FastAPI app = FastAPI() @app.get("/api/hello")async def hello(): return {"message": "Hello"}Nexy lê o URL do caminho do arquivo e o método do nome da função:
async def GET(): return {"message": "Hello"}Parâmetros de caminho e consulta
@app.get("/api/users/{id}")async def get_user(id: int, fields: str | None = None): return {"id": id, "fields": fields}O parâmetro path se torna um segmento [id] do nome do arquivo. O parâmetro de consulta não se move:
async def GET(id: int, fields: str | None = None): return {"id": id, "fields": fields}/api/users/abc responde 404, porque abc não é um int.
Um corpo de solicitação
from pydantic import BaseModel class NewItem(BaseModel): name: str price: float @app.post("/api/items", status_code=201)async def create_item(item: NewItem): return itemO modelo Pydantic é o mesmo. O código de status passa para a resposta:
from pydantic import BaseModel from nexy.http import JSONResponse class NewItem(BaseModel): name: str price: float async def POST(item: NewItem): return JSONResponse(item.model_dump(), status_code=201)Um corpo que não corresponde ao modelo responde 422, como em FastAPI.
Vários métodos em um URL
@app.get("/api/notes/{id}")async def read_note(id: int): ... @app.delete("/api/notes/{id}")async def delete_note(id: int): ...Um arquivo contém todos os métodos de uma URL:
async def GET(id: int): return {"id": id} async def DELETE(id: int): return {"deleted": id}Roteadores e prefixos
from fastapi import APIRouter admin = APIRouter(prefix="/admin") @admin.get("/stats")async def stats(): return {"users": 3} app.include_router(admin)A pasta é o prefixo e não há nada a incluir:
async def GET(): return {"users": 3}Dependências de um grupo de rotas
from fastapi import Depends, Header, HTTPException def require_admin(x_role: str = Header(...)) -> None: if x_role != "admin": raise HTTPException(status_code=403, detail="Admins only") admin = APIRouter(prefix="/admin", dependencies=[Depends(require_admin)])Um dependencies.py na pasta é executado antes de cada rota abaixo dela:
from nexy.http import Header, HTTPException def require_admin(x_role: str = Header(...)) -> None: if x_role != "admin": raise HTTPException(status_code=403, detail="Admins only") dependencies = [require_admin]Uma dependência para um manipulador ainda é Depends, em sua assinatura:
from nexy.http import Depends, Header def current_user(authorization: str = Header(...)) -> str: return authorization.removeprefix("Bearer ") async def GET(user: str = Depends(current_user)): return {"user": user}Middleware
from fastapi import Request @app.middleware("http")async def add_header(request: Request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responseUm middleware.py agrupa as rotas de sua pasta, o root agrupa todas elas:
from nexy.http import Request async def middleware(request: Request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responseCORS, GZip e outros middlewares para todo o aplicativo
from fastapi.middleware.cors import CORSMiddlewarefrom fastapi.middleware.gzip import GZipMiddleware app.add_middleware(CORSMiddleware, allow_origins=["https://example.com"])app.add_middleware(GZipMiddleware, minimum_size=1000)Cada um é um campo de nexyconfig.py, com os mesmos argumentos:
from nexy.core.models import NexyConfigModel class NexyConfig(NexyConfigModel): useCORS = {"allow_origins": ["https://example.com"]} useGZip = {"minimum_size": 1000}Páginas HTML
from fastapi import Requestfrom fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") @app.get("/users")async def users_page(request: Request): users = [{"name": "Ada"}, {"name": "Alan"}] return templates.TemplateResponse(request, "users.html", {"users": users})Uma página .nexy contém o Python acima da linha --- e o Jinja2 abaixo dela, em um arquivo:
users = [{"name": "Ada"}, {"name": "Alan"}]<ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %}</ul>Herança de modelo
<html> <body>{% block content %}{% endblock %}</body></html>Um layout.nexy envolve todas as páginas de sua pasta, sem {% extends %} nas páginas:
children: prop[NexyElement]<!DOCTYPE html><html> <body>{{ children | safe }}</body></html>Arquivos estáticos
from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory="static"), name="static")Os arquivos em public/ são servidos a partir da raiz: public/robots.txt é /robots.txt.
Páginas não encontradas
from fastapi import Requestfrom fastapi.responses import HTMLResponsefrom starlette.exceptions import HTTPException as StarletteHTTPException @app.exception_handler(StarletteHTTPException)async def not_found(request: Request, exc: StarletteHTTPException): return HTMLResponse("<h1>Not found</h1>", status_code=exc.status_code)Um notfound.nexy responde aos navegadores, e os clientes da API ainda recebem {"detail": ...}:
<h1>Not found</h1><p><a href="/">Back home</a></p>WebSockets
from fastapi import WebSocket @app.websocket("/ws/echo")async def echo(websocket: WebSocket): await websocket.accept() await websocket.send_text(await websocket.receive_text())from fastapi import WebSocket async def SOCKET(websocket: WebSocket): await websocket.accept() await websocket.send_text(await websocket.receive_text())Testes
from fastapi.testclient import TestClient from main import app def test_hello(): assert TestClient(app).get("/api/hello").json() == {"message": "Hello"}O cliente é o mesmo; o aplicativo vem da Nexy:
from fastapi.testclient import TestClient from nexy.app import app def test_hello(): assert TestClient(app).get("/api/hello").json() == {"message": "Hello"}Correndo
| FastAPI | Nexy |
|---|---|
uvicorn main:app --reload |
nx dev |
uvicorn main:app --workers 4 |
nx build, então nx start |
/docs de FastAPI() |
/docs, listando todas as .py rotas |
Manter um aplicativo existente
Você não precisa mover todas as rotas ao mesmo tempo. Um APIRouter você já tem montagens como estão:
from nexy.core.models import NexyConfigModel from legacy.api import router class NexyConfig(NexyConfigModel): useRouter = routerSuas rotas antigas e os arquivos de src/routes/ respondem lado a lado. Enquanto useRouter estiver definido, .nexy páginas não serão agrupadas por layout.nexy: mova primeiro a API, depois as páginas e, em seguida, remova o campo. Consulte Integração de roteador personalizado.