DocsPython cadres
De Django à Nexy
Dans Nexy, une vue Django et sa ligne urls.py deviennent un seul fichier sous src/routes/, les modèles restent similaires à Jinja2, et l'ORM, les formulaires et l'administrateur sont votre choix au lieu d'être intégrés.
Chaque section ci-dessous montre une habitude Django, puis la même chose dans Nexy.
Une vue et son URL
from django.http import JsonResponse def hello(request): return JsonResponse({"message": "Hello"})from django.urls import path from . import views urlpatterns = [path("api/hello/", views.hello)]Il n'y a pas de urls.py : le chemin du fichier est l'URL, le nom de la fonction est la méthode et un dict renvoyé est JSON.
async def GET(): return {"message": "Hello"}Convertisseurs de chemin
urlpatterns = [path("api/posts/<int:id>/", views.post_detail)]Le convertisseur devient une annotation de type sur un segment [id] :
async def GET(id: int): return {"id": id}<path:rest> devient [...rest].py.
request.GET et request.POST
def search(request): q = request.GET.get("q", "") page = int(request.GET.get("page", 1)) return JsonResponse({"q": q, "page": page})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}Vues basées sur les classes
from django.views import View class NoteView(View): def get(self, request, id): return JsonResponse({"id": id}) def delete(self, request, id): return JsonResponse({"deleted": id})Une fonction par méthode, dans le fichier de l'URL :
async def GET(id: int): return {"id": id} async def DELETE(id: int): return {"deleted": id}Modèles et render
from django.shortcuts import render def post_list(request): posts = [{"title": "Hello"}, {"title": "Second"}] return render(request, "blog/post_list.html", {"posts": posts}){% extends "base.html" %}{% block content %}<ul> {% for post in posts %} <li>{{ post.title }}</li> {% endfor %}</ul>{% endblock %}La vue et le modèle forment un seul fichier .nexy. Les balises sont des Jinja2, proches de celles de Django :
posts = [{"title": "Hello"}, {"title": "Second"}]<ul> {% for post in posts %} <li>{{ post.title }}</li> {% endfor %}</ul>base.html et {% block %}
<!DOCTYPE html><html> <body>{% block content %}{% endblock %}</body></html>Un layout.nexy enveloppe chaque page de son dossier et au-dessous, sans {% extends %} dans les pages :
children: prop[NexyElement]<!DOCTYPE html><html> <body>{{ children | safe }}</body></html>{% include %} et balises d'inclusion
<article><h2>{{ title }}</h2>{{ body }}</article>{% include "card.html" with title="Latest" body="This week" %}
Un composant est un fichier .nexy avec des accessoires typés et un emplacement pour ses enfants :
title: prop[str]<article> <h2>{{ title }}</h2> <Slot /></article>from "@/components/Card.nexy" import Card<Card title="Latest"> <p>This week</p></Card>@ est src/ : nx new écrit useAliases = {"@": "src"} dans nexyconfig.py.
Filtres de modèles
| Django | Jinja2 dans .nexy |
|---|---|
{{ name|default:"guest" }} |
{{ name or "guest" }} ou {{ name | default("guest") }} |
{{ title|upper }} |
{{ title | upper }} |
{{ body|safe }} |
{{ body | safe }} |
{{ items|length }} |
{{ items | length }} |
{% url "post" post.id %} |
/posts/{{ post.id }} : l'URL est le chemin du fichier |
Les modèles et l'ORM
from django.db import models class Post(models.Model): title = models.CharField(max_length=200) published = models.BooleanField(default=False)def published(request): posts = Post.objects.filter(published=True) return JsonResponse({"titles": [p.title for p in posts]})Nexy n'a pas d'ORM propre. nx new peut échafauder SQLModel, SQLAlchemy ou Tortoise-ORM ; avec SQLModel le modèle et la requête se lisent comme ceci :
from sqlmodel import Field, SQLModel class Post(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) title: str published: bool = Falsefrom sqlmodel import Session, select from src.configs.db import enginefrom src.models.post_model import Post async def GET(): with Session(engine) as session: posts = session.exec(select(Post).where(Post.published)).all() return {"titles": [post.title for post in posts]}Voir Bases de données.
Migrations
| Django | Nexy |
|---|---|
python manage.py makemigrations |
nx migrate -m "add posts" |
python manage.py migrate |
nx migrate --upgrade |
nx migrate entraîne Alembic pour SQLModel et SQLAlchemy, et Aerich pour Tortoise-ORM.
Formulaires et CSRF
from django.shortcuts import redirect def add_note(request): if request.method == "POST": NOTES.append(request.POST["title"]) return redirect("notes") return render(request, "notes.html", {"notes": NOTES})<form method="post"> {% csrf_token %} <input name="title"> <button type="submit">Add</button></form>Une action est la fonction sur laquelle le formulaire est publié. Ses paramètres sont les champs du formulaire, et le visiteur est renvoyé 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>Il n'y a pas de {% csrf_token %} : voir Actions pour savoir ce qui protège le chemin d'action et ce que vous vérifiez toujours vous-même.
Django formulaires et sérialiseurs DRF
from rest_framework import serializers class PostSerializer(serializers.Serializer): title = serializers.CharField(max_length=200) published = serializers.BooleanField(default=False)Un modèle Pydantic valide le corps, et un modèle invalide répond 422 avec le champ qui a échoué :
from pydantic import BaseModel, Field class NewPost(BaseModel): title: str = Field(max_length=200) published: bool = False async def POST(post: NewPost): return post@login_required
from django.contrib.auth.decorators import login_required @login_requireddef dashboard(request): return render(request, "dashboard.html")Un dependencies.py garde un dossier entier :
from nexy import unauthorizedfrom nexy.http import Request def require_login(request: Request) -> None: if not request.session.get("user"): unauthorized() dependencies = [require_login]from nexy import useSession user = useSession()["user"]<h1>Welcome back, {{ user }}</h1>Les sessions sont activées dans nexyconfig.py :
from nexy.core.models import NexyConfigModel class NexyConfig(NexyConfigModel): useAliases = {"@": "src"} useSession = {"secret_key": "change-me"}Intergiciel
class AppHeaderMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) response["X-App"] = "demo" return responseMIDDLEWARE = [..., "blog.middleware.AppHeaderMiddleware"]Un middleware.py enveloppe les routes de son dossier ; celui à la racine les enveloppe tous :
from nexy.http import Request async def middleware(request: Request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responseHttp404 et 404.html
from django.shortcuts import get_object_or_404 def post_detail(request, slug): post = get_object_or_404(Post, slug=slug) return render(request, "post.html", {"post": post})notFound() arrête la page, et notfound.nexy à la racine des routes joue le rôle de 404.html :
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>cache_page
from django.views.decorators.cache import cache_page @cache_page(60)def post_list(request): ...Une ligne dans le frontmatter met en cache la page par URL et la restitue à nouveau en arrière-plan :
revalidate = 60<h1>Archive</h1>settings.py
| réglage Django | nexyconfig.py |
|---|---|
SECRET_KEY + SessionMiddleware |
useSession = {"secret_key": ...} |
ALLOWED_HOSTS |
useTrustedHost = {"allowed_hosts": [...]} |
SECURE_SSL_REDIRECT |
useHTTPSRedirect = True |
CORS_ALLOWED_ORIGINS (django-cors-en-têtes) |
useCORS = {"allow_origins": [...]} |
MIDDLEWARE |
Fichiers useMiddlewares = [...] ou middleware.py |
DATABASES |
DATABASE_URL dans .env, lu par src/configs/db.py |
STATIC_URL, STATICFILES_DIRS |
public/, servi à / |
Applications
Un projet Django est divisé en applications. Dans Nexy, un dossier de src/routes/ est l'unité des URL, et src/models/ et src/services/ contiennent le code qu'ils partagent. Pour les contrôleurs et les modules avec injection de dépendances, le routeur modulaire est la correspondance la plus proche.
L'administrateur
Nexy n'a pas de site d'administration. Montez l'un des FastAPI packages d'administration, tels que SQLAdmin, ou créez les pages dont vous avez besoin à partir de .nexy fichiers et actions.
Essais
from django.test import TestCase class HelloTests(TestCase): def test_hello(self): self.assertEqual(self.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
| Django | Nexy |
|---|---|
django-admin startproject |
nx new |
python manage.py runserver |
nx dev |
gunicorn project.wsgi |
nx build, puis nx start |
python manage.py test |
nx test |
python manage.py collectstatic |
nx build |