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 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.