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 deleted column on BaseModel (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 a debug flag into every response and Book.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.