DocsPython cadres
De Flask à Nexy
Dans Nexy, une vue Flask devient un fichier sous src/routes/ : un fichier .py pour JSON, un fichier .nexy pour HTML, avec Jinja2 conservé et les décorateurs, les plans et request globaux disparus.
Chaque section ci-dessous montre une habitude Flask, puis la même chose dans Nexy.
Un itinéraire
from flask import Flask app = Flask(__name__) @app.get("/api/hello")def hello(): return {"message": "Hello"}Le chemin du fichier est l'URL et le nom de la fonction est la méthode :
async def GET(): return {"message": "Hello"}Variables d'URL
@app.get("/api/users/<int:id>")def get_user(id): return {"id": id}Le convertisseur devient une annotation de type, et la variable un segment [id] :
async def GET(id: int): return {"id": id}<path:rest> est un segment fourre-tout, [...rest].py.
Chaînes de requête
from flask import request @app.get("/api/search")def search(): q = request.args.get("q", "") page = request.args.get("page", 1, type=int) return {"q": q, "page": page}Il n'y a pas de request global : un paramètre de requête est un paramètre de fonction, converti et validé :
async def GET(q: str = "", page: int = 1): return {"q": q, "page": page}Un corps JSON
@app.post("/api/items")def create_item(): data = request.get_json() if "name" not in data: return {"error": "name is required"}, 400 return {"created": data["name"]}, 201Un modèle Pydantic valide le corps, et un mauvais modèle répond 422 tout seul :
from pydantic import BaseModel from nexy.http import JSONResponse class NewItem(BaseModel): name: str async def POST(item: NewItem): return JSONResponse({"created": item.name}, status_code=201)Plusieurs méthodes sur une seule URL
@app.route("/api/notes/<int:id>", methods=["GET", "DELETE"])def note(id): if request.method == "DELETE": return {"deleted": id} return {"id": id}Une fonction par méthode, dans le même fichier :
async def GET(id: int): return {"id": id} async def DELETE(id: int): return {"deleted": id}Modèles
from flask import render_template @app.get("/users")def users(): return render_template("users.html", users=[{"name": "Ada"}, {"name": "Alan"}])La vue et son modèle sont un seul fichier .nexy : Python au-dessus de la ligne ---, le même Jinja2 en dessous :
users = [{"name": "Ada"}, {"name": "Alan"}]<ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %}</ul>Un modèle de base
<!DOCTYPE html><html> <body>{% block content %}{% endblock %}</body></html>Un layout.nexy enveloppe chaque page de son dossier et en dessous ; les pages ne le font pas {% extends %} il :
children: prop[NexyElement]<!DOCTYPE html><html> <body>{{ children | safe }}</body></html>Macros et inclusions
{% macro card(title) %}<article><h2>{{ title }}</h2>{{ caller() }}</article>{% endmacro %}Un composant est un fichier .nexy avec des accessoires, importé dans le frontmatter :
title: prop[str]<article> <h2>{{ title }}</h2> <Slot /></article>from "@/components/Card.nexy" import Card<Card title="Latest"> <p>Everything published this week.</p></Card>@ est src/ : nx new écrit useAliases = {"@": "src"} dans nexyconfig.py.
Plans
from flask import Blueprint admin = Blueprint("admin", __name__, url_prefix="/admin") @admin.get("/stats")def stats(): return {"users": 3} app.register_blueprint(admin)Un dossier est le préfixe, et rien n'est enregistré :
async def GET(): return {"users": 3}before_request
from flask import abort @admin.before_requestdef require_admin(): if request.headers.get("X-Role") != "admin": abort(403)Un dependencies.py s'exécute avant chaque route de son dossier et ci-dessous :
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]after_request
@app.after_requestdef add_header(response): response.headers["X-App"] = "demo" return responseUn middleware.py voit la requête avant la route et la réponse après :
from nexy.http import Request async def middleware(request: Request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responseFormulaires et redirections
from flask import redirect, url_for @app.post("/notes")def add_note(): NOTES.append(request.form["title"]) return redirect(url_for("notes"))Une action est une fonction Python dans laquelle le formulaire est publié. Nexy lui donne un chemin, lit les champs du formulaire comme paramètres et renvoie le visiteur vers la page avec un 303 :
from nexy.decorators import Action NOTES: list[str] = [] @Actionasync def add_note(title: str) -> None: NOTES.append(title)from src.actions.notes import NOTES<form method="post" action="{{ __actions_path['add_note'] }}"> <input name="title"> <button type="submit">Add</button></form><ul> {% for note in NOTES %} <li>{{ note }}</li> {% endfor %}</ul>Séances
from flask import session app.secret_key = "change-me" @app.post("/api/login")def login(): session["user"] = request.json["name"] return {"user": session["user"]}Le secret est déplacé vers nexyconfig.py et la session suspend la requête :
from nexy.core.models import NexyConfigModel class NexyConfig(NexyConfigModel): useSession = {"secret_key": "change-me"}from pydantic import BaseModel from nexy.http import Request class Login(BaseModel): name: str async def POST(request: Request, body: Login): request.session["user"] = body.name return {"user": body.name}Une page le lit avec useSession() :
from nexy import useSession user = useSession().get("user", "guest")<h1>Hello {{ user }}</h1>abort(404) et gestionnaires d'erreurs
@app.errorhandler(404)def not_found(error): return render_template("404.html"), 404 @app.get("/posts/<slug>")def post(slug): if slug not in POSTS: abort(404) return render_template("post.html", post=POSTS[slug])notFound() arrête la page, et notfound.nexy dessine la réponse :
from nexy import notFound POSTS = {"hello": "Hello, world"} slug: prop[str]if slug not in POSTS: notFound()<h1>{{ POSTS[slug] }}</h1><h1>Not found</h1><p><a href="/">Back home</a></p>Fichiers statiques
static/logo.png Flask: /static/logo.pngpublic/logo.png Nexy: /logo.png
Flacon-CORS
from flask_cors import CORS CORS(app, origins=["https://example.com"])from nexy.core.models import NexyConfigModel class NexyConfig(NexyConfigModel): useCORS = {"allow_origins": ["https://example.com"]}Essais
from app import app def test_hello(): assert app.test_client().get("/api/hello").json == {"message": "Hello"}from fastapi.testclient import TestClient from nexy.app import app def test_hello(): assert TestClient(app).get("/api/hello").json() == {"message": "Hello"}Courir
| Flask | Nexy |
|---|---|
flask run --debug |
nx dev |
gunicorn app:app |
nx build, puis nx start |
flask db migrate (Flask-Migrer) |
nx migrate -m "message" |
| pas de documentation API | /docs, répertoriant chaque .py itinéraire |
Prochaines étapes
- De FastAPI à Nexy : les gestionnaires Nexy sont des gestionnaires FastAPI
- Actions
- Fichiers .nexy
- Recettes : connexion, CRUD et téléchargements