Flaxon 0.3.0 and Teloce-Py 0.3.0 are available on PyPI. Release instructions are maintained on this site; source links identify the framework snapshot. Check version and upgrade guidance.
05 | Owned project APIs
Goal
Create, list, read, update and delete projects belonging to the signed-in customer.
What you are building
owned_project combines the requested ID with the authenticated owner's ID in the ORM query. Every detail, edit and delete uses that guard. Creation takes owner_id from the session rather than accepting it from JSON. list_projects filters before returning rows. A nonexistent project and another person's project both return 404. The small HTTP demo establishes cookies and CSRF automatically, registers an account and exercises a working project endpoint.
Build this chapter
Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.
Step 1: CREATE - make parent folders, then create this file - modules/projects/init.py
This file owns the feature named by its module or component. Read the route/form flow before continuing.
Create the parent directories, then save this complete file.
"""Module-owned APIs and interface files."""
Say: "This file owns the feature named by its module or component. Read the route/form flow before continuing."
Type: Follow the action above in modules/projects/__init__.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 2: CREATE - make parent folders, then create this file - modules/projects/module.py
Create the projects module, then implement owned_project before the handlers that use it. The mount adds /api/projects once. Every ORM query for an existing project includes the signed-in customer's owner ID.
Create the parent directories, then save this complete file.
"""Project APIs always constrain queries by the signed-in user's id."""
from pathlib import Path
from models import Project
from flaxon.modules import FlaxonModule
from flaxon.http import JSONResponse
from flaxon.exceptions import NotFound
from security import require_user, check_csrf
from validation import json_object, text
projects = FlaxonModule(
"projects",
ui_dir=Path(__file__).parent / "ui",
ui_routes={
"ProjectList.js": "/projects",
"ProjectDetails/[id].js": "/projects/:id",
},
)
async def owned_project(request, project_id):
user = await require_user(request)
project = await Project.filter(id=project_id, owner_id=user["id"]).first().values()
if project is None:
raise NotFound("Project not found.")
return project
@projects.get("/")
async def list_projects(request):
user = await require_user(request)
items = await Project.filter(owner_id=user["id"]).order_by("-id").values()
return {"data": items}
@projects.post("/")
async def create_project(request):
await check_csrf(request)
user = await require_user(request)
data = await json_object(request)
name = text(data, "name")
description = text(data, "description", 1000, required=False)
project = await Project.create(
owner_id=user["id"], name=name, description=description
)
project_id = project.id
return JSONResponse(
{"data": await owned_project(request, project_id)}, status_code=201
)
@projects.get("/<int:project_id>")
async def get_project(request, project_id):
return {"data": await owned_project(request, project_id)}
@projects.put("/<int:project_id>")
async def update_project(request, project_id):
await check_csrf(request)
project = await owned_project(request, project_id)
data = await json_object(request)
name = text(data, "name")
description = text(data, "description", 1000, required=False)
await Project.filter(id=project_id, owner_id=project["owner_id"]).update(
name=name, description=description
)
return {"data": await owned_project(request, project_id)}
@projects.delete("/<int:project_id>")
async def delete_project(request, project_id):
await check_csrf(request)
project = await owned_project(request, project_id)
await Project.filter(id=project_id, owner_id=project["owner_id"]).delete()
return {"data": {"deleted": True}}
Say: "Create the projects module, then implement owned_project before the handlers that use it. The mount adds /api/projects once. Every ORM query for an existing project includes the signed-in customer's owner ID."
Type: Follow the action above in modules/projects/module.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 3: CREATE - make parent folders, then create this file - scripts/course_api_demo.py
This file owns the feature named by its module or component. Read the route/form flow before continuing.
Create the parent directories, then save this complete file.
"""Show the project API over HTTP; start the development server first."""
import secrets
import httpx
def main():
# Each run creates its own learner and project in your development database.
with httpx.Client(base_url="http://127.0.0.1:8000", trust_env=False) as client:
response = client.get("/api/auth/session")
response.raise_for_status()
csrf = response.json()["data"]["csrf"]
response = client.post(
"/api/auth/register",
headers={"X-CSRF-Token": csrf},
json={"name": "Sample Learner", "email": f"sample-{secrets.token_hex(6)}@example.test", "password": "LearningPython123!"},
)
response.raise_for_status()
csrf = response.json()["data"]["csrf"]
response = client.post(
"/api/projects/", headers={"X-CSRF-Token": csrf},
json={"name": "My first project", "description": "Record the sample lesson"},
)
response.raise_for_status()
assert response.status_code == 201
project = response.json()["data"]
print("Created:", project)
response = client.get(f"/api/projects/{project['id']}")
response.raise_for_status()
print("Read:", response.json()["data"])
response = client.post("/api/projects/", headers={"X-CSRF-Token": csrf}, json={"name": " "})
assert response.status_code == 400
print("Invalid input rejected:", response.status_code)
print("Use your own browser account to create a separate project on /projects.")
if __name__ == "__main__":
main()
Say: "This file owns the feature named by its module or component. Read the route/form flow before continuing."
Type: Follow the action above in scripts/course_api_demo.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 4: EDIT - replace the marked blocks - app.py
The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact.
Edit 1: find this exact block
Open app.py. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.
from flaxon import Flaxon, Request
from flaxon.http import JSONResponse
from modules.auth.module import auth
from modules.welcome.module import welcome
from settings import (
ROOT,
Replace that block with
from flaxon import Flaxon, Request
from flaxon.http import JSONResponse
from modules.auth.module import auth
from modules.projects.module import projects
from modules.welcome.module import welcome
from settings import (
ROOT,
Leave the rest of the file unchanged.
Edit 2: find this exact block
Open app.py. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.
app.dummy_password_hash = app.hasher.hash("dummy-password-for-timing-only")
for module, prefix in [
(auth, "/api/auth"),
(welcome, "/api/welcome"),
]:
app.mount_module(module, prefix=prefix)
Replace that block with
app.dummy_password_hash = app.hasher.hash("dummy-password-for-timing-only")
for module, prefix in [
(auth, "/api/auth"),
(projects, "/api/projects"),
(welcome, "/api/welcome"),
]:
app.mount_module(module, prefix=prefix)
Leave the rest of the file unchanged.
Say: "The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact."
Type: Follow the action above in app.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Run and check
python management.py runserver
# In a second terminal:
python scripts/course_api_demo.py
Expected result
The HTTP demo creates a project and reads it back. Projects return a data envelope with the owner, name and description. A second customer's request cannot retrieve it.
Common errors
Do not repeat /api/projects inside decorators: app.py supplies that mount prefix. Use a trailing slash for the list/create endpoint. A 401 means no valid customer session; 404 after sign-in can be an ownership denial.
Short exercise
Extend the demo to update a project's description and assert the read response changed.
What to say and show
Say: "Create, list, read, update and delete projects belonging to the signed-in customer. The server remains responsible for persistence and authorization; the browser presents the result."
Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.
Save your checkpoint
git add .
git commit -m "Complete chapter 05: Owned project APIs"