Advanced Demo¶
This annotated example combines soft deletes, nested writes and custom callbacks.
The code lives in demo/advanced_features/app.py
.
1"""Advanced demo showcasing multiple flarchitect features.
2
3This example combines soft deletes, nested writes, validation, custom
4callbacks and per HTTP-method configuration. Run this file directly and use
5the ``curl`` commands in the repository ``README`` to exercise the API.
6"""
7
8from __future__ import annotations
9
10import datetime
11from typing import Any
12
13from flask import Flask
14from flask_sqlalchemy import SQLAlchemy
15from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
16from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
17
18from flarchitect import Architect
19
20
21class BaseModel(DeclarativeBase):
22 """Base model with timestamp and soft delete columns."""
23
24 # ``created`` and ``updated`` are automatically managed timestamps.
25 created: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
26 updated: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
27 # ``deleted`` enables soft deletes when ``API_SOFT_DELETE`` is set.
28 deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
29
30 def get_session(*args: Any, **kwargs: Any): # noqa: D401 - simple passthrough
31 """Return the current database session."""
32
33 return db.session
34
35
36# Global SQLAlchemy instance using the custom base model above.
37db = SQLAlchemy(model_class=BaseModel)
38
39
40class Author(db.Model):
41 """Author of one or more books."""
42
43 __tablename__ = "author"
44
45 class Meta:
46 # ``tag`` and ``tag_group`` drive grouping in the generated documentation.
47 tag = "Author"
48 tag_group = "People"
49 # Restrict HTTP methods to ``GET`` and ``POST`` only.
50 allowed_methods = ["GET", "POST"]
51 # Apply a rate limit specifically to author creation.
52 post_rate_limit = "5 per minute"
53 # Provide custom descriptions for documentation per HTTP method.
54 description = {
55 "GET": "Retrieve authors, optionally including soft-deleted records.",
56 "POST": "Create a new author record.",
57 }
58
59 id: Mapped[int] = mapped_column(Integer, primary_key=True)
60 # Author's name – simple string field.
61 name: Mapped[str] = mapped_column(String(80))
62 # Optional contact email with validation and helpful docs metadata.
63 email: Mapped[str | None] = mapped_column(
64 String(120),
65 info={
66 "description": "Author's contact email.",
67 "format": "email",
68 "validator": "email",
69 "validator_message": "Invalid email address.",
70 },
71 )
72 # Optional website; ``format`` automatically enables URL validation.
73 website: Mapped[str | None] = mapped_column(
74 String(255),
75 info={"description": "Author website", "format": "uri"},
76 )
77 # Back-reference of books written by this author.
78 books: Mapped[list[Book]] = relationship(back_populates="author")
79
80
81class Book(db.Model):
82 """Book written by an author."""
83
84 __tablename__ = "book"
85
86 class Meta:
87 tag = "Book"
88 tag_group = "Content"
89 # Allow nested writes so books can be created alongside their author.
90 allow_nested_writes = True
91 # Capitalise titles before saving using ``_add_callback`` below.
92 add_callback = staticmethod(lambda obj, model: _add_callback(obj))
93 # Provide custom descriptions for generated documentation.
94 description = {
95 "GET": "Retrieve books with their associated authors.",
96 "POST": "Create a book and, optionally, its author in one request.",
97 "PATCH": "Update a book's details.",
98 }
99 # Demonstrate HTTP method specific configuration.
100 patch_rate_limit = "10 per minute"
101
102 id: Mapped[int] = mapped_column(Integer, primary_key=True)
103 # The book's title.
104 title: Mapped[str] = mapped_column(String(120))
105 # Foreign key relationship to ``Author``.
106 # ``author_id`` is optional to support nested author creation.
107 author_id: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
108 author: Mapped[Author] = relationship(back_populates="books")
109
110
111def _add_callback(obj: Book) -> Book:
112 """Ensure book titles are capitalised before saving."""
113
114 obj.title = obj.title.title()
115 return obj
116
117
118def dump_callback(data: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
119 """Attach a debug flag to every serialised response."""
120
121 data["debug"] = True
122 return data
123
124
125def create_app() -> Flask:
126 """Build the Flask application and initialise flarchitect.
127
128 Returns:
129 Configured Flask application.
130 """
131
132 app = Flask(__name__)
133 app.config.update(
134 SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",
135 API_TITLE="Advanced API",
136 API_VERSION="1.0",
137 API_BASE_MODEL=db.Model,
138 API_ALLOW_NESTED_WRITES=True,
139 API_SOFT_DELETE=True,
140 API_SOFT_DELETE_ATTRIBUTE="deleted",
141 API_SOFT_DELETE_VALUES=(False, True),
142 API_DUMP_CALLBACK=dump_callback,
143 )
144
145 db.init_app(app)
146 with app.app_context():
147 db.create_all()
148 Architect(app)
149
150 return app
151
152
153if __name__ == "__main__":
154 create_app().run(debug=True)
Key points¶
Soft deletes are enabled via API_SOFT_DELETE and the
deleted
column onBaseModel
(see Soft delete).Nested writes allow creating related objects in one request.
Book.Meta.allow_nested_writes
turns it on for books.Custom callbacks modify behaviour:
return_callback
injects adebug
flag into every response andBook.Meta.add_callback
title-cases book names before saving.
Run the demo¶
python demo/advanced_features/app.py
curl -X POST http://localhost:5000/api/book \
-H "Content-Type: application/json" \
-d '{"title": "my book", "author": {"name": "Alice"}}'
curl http://localhost:5000/api/book?include_deleted=true
For authentication strategies and role management, see Authentication and the Defining roles section.