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, ClassVar
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
21def _utc_naive_now() -> datetime.datetime:
22 return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
23
24
25class BaseModel(DeclarativeBase):
26 """Base model with timestamp and soft delete columns."""
27
28 # ``created`` and ``updated`` are automatically managed timestamps.
29 created: Mapped[datetime.datetime] = mapped_column(DateTime, default=_utc_naive_now)
30 updated: Mapped[datetime.datetime] = mapped_column(DateTime, default=_utc_naive_now, onupdate=_utc_naive_now)
31 # ``deleted`` enables soft deletes when ``API_SOFT_DELETE`` is set.
32 deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
33
34 def get_session(*args: Any, **kwargs: Any):
35 """Return the current database session."""
36
37 return db.session
38
39
40# Global SQLAlchemy instance using the custom base model above.
41db = SQLAlchemy(model_class=BaseModel)
42
43
44class Author(db.Model):
45 """Author of one or more books."""
46
47 __tablename__ = "author"
48
49 class Meta:
50 # ``tag`` and ``tag_group`` drive grouping in the generated documentation.
51 tag = "Author"
52 tag_group = "People"
53 # Restrict HTTP methods to ``GET`` and ``POST`` only.
54 allowed_methods: ClassVar[list[str]] = ["GET", "POST"]
55 # Apply a rate limit specifically to author creation.
56 post_rate_limit = "5 per minute"
57 # Provide custom descriptions for documentation per HTTP method.
58 description: ClassVar[dict[str, str]] = {
59 "GET": "Retrieve authors, optionally including soft-deleted records.",
60 "POST": "Create a new author record.",
61 }
62
63 id: Mapped[int] = mapped_column(Integer, primary_key=True)
64 # Author's name - simple string field.
65 name: Mapped[str] = mapped_column(String(80))
66 # Optional contact email with validation and helpful docs metadata.
67 email: Mapped[str | None] = mapped_column(
68 String(120),
69 info={
70 "description": "Author's contact email.",
71 "format": "email",
72 "validator": "email",
73 "validator_message": "Invalid email address.",
74 },
75 )
76 # Optional website; ``format`` automatically enables URL validation.
77 website: Mapped[str | None] = mapped_column(
78 String(255),
79 info={"description": "Author website", "format": "uri"},
80 )
81 # Back-reference of books written by this author.
82 books: Mapped[list[Book]] = relationship(back_populates="author")
83
84
85class Book(db.Model):
86 """Book written by an author."""
87
88 __tablename__ = "book"
89
90 class Meta:
91 tag = "Book"
92 tag_group = "Content"
93 # Allow nested writes so books can be created alongside their author.
94 allow_nested_writes = True
95 # Capitalise titles before saving using ``_add_callback`` below.
96 add_callback = staticmethod(lambda obj, model: _add_callback(obj))
97 # Provide custom descriptions for generated documentation.
98 description: ClassVar[dict[str, str]] = {
99 "GET": "Retrieve books with their associated authors.",
100 "POST": "Create a book and, optionally, its author in one request.",
101 "PATCH": "Update a book's details.",
102 }
103 # Demonstrate HTTP method specific configuration.
104 patch_rate_limit = "10 per minute"
105
106 id: Mapped[int] = mapped_column(Integer, primary_key=True)
107 # The book's title.
108 title: Mapped[str] = mapped_column(String(120))
109 # Foreign key relationship to ``Author``.
110 # ``author_id`` is optional to support nested author creation.
111 author_id: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
112 author: Mapped[Author] = relationship(back_populates="books")
113
114
115def _add_callback(obj: Book) -> Book:
116 """Ensure book titles are capitalised before saving."""
117
118 obj.title = obj.title.title()
119 return obj
120
121
122def dump_callback(data: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
123 """Attach a debug flag to every serialised response."""
124
125 data["debug"] = True
126 return data
127
128
129def create_app() -> Flask:
130 """Build the Flask application and initialise flarchitect.
131
132 Returns:
133 Configured Flask application.
134 """
135
136 app = Flask(__name__)
137 app.config.update(
138 SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",
139 API_TITLE="Advanced API",
140 API_VERSION="1.0",
141 API_BASE_MODEL=db.Model,
142 API_ALLOW_NESTED_WRITES=True,
143 API_SOFT_DELETE=True,
144 API_SOFT_DELETE_ATTRIBUTE="deleted",
145 API_SOFT_DELETE_VALUES=(False, True),
146 API_DUMP_CALLBACK=dump_callback,
147 )
148
149 db.init_app(app)
150 with app.app_context():
151 db.create_all()
152 Architect(app)
153
154 return app
155
156
157if __name__ == "__main__":
158 create_app().run(debug=True)
Key points¶
Soft deletes are enabled via API_SOFT_DELETE and the
deletedcolumn onBaseModel(see Soft delete).Nested writes allow creating related objects in one request.
Book.Meta.allow_nested_writesturns it on for books.Custom callbacks modify behaviour:
return_callbackinjects adebugflag into every response andBook.Meta.add_callbacktitle-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.