How Mantle Works¶

Mantle builds on the dynamic power of Django’s ORM. It’s allows us move our domain logic in simple typed Python classes.

Mantle’s Query layer bridges from the ORM to your data shapes in an efficient and scalable way.

It’s a deliberate but small separation. We keep everything from Django, and we build on top of that.

At a high level, Mantle combines:

  • attrs for typed shape classes

  • django-readers for efficient ORM querying

  • cattrs for converting our data into attrs instances

Mantle’s Query is the glue: it connects these three pieces into one read pipeline: Mantle generates a readers spec from your attrs shape classes, processes that with django-readers, and uses cattrs to map the results back to instances of your shape class.

There are utilities for writing to the database, as well as validation, and tools for Customising query generation, but the core flow is Query.

The goal of this doc is to explain that.

attrs: your typed result shape¶

Mantle uses attrs classes as the source of truth for your data shapes.

from attrs import define

@define
class BookmarkData:
    url: str
    comment: str
    favourite: bool

The goal is to have explicit, typed Python objects, decoupled from the ORM.

Defining the classes is easy. The challenge is the mapping between those and the ORM.

django-readers: efficient ORM querying¶

django-readers is a great library that allows efficient querying from the ORM.

  • It specifies that exact fields you need for your data, avoiding unlimited field fetches

  • It automatically handles prefetching related objects, so there are no N+1 query problems when working with nested data

django-readers starts from a spec. A spec describes the fields (and relationships) you want.

In raw readers code, you process() the spec, and get back two functions:

  • prepare(queryset): returns an optimized queryset

  • project(instance): returns a dictionary with the requested data

You use these to fetch your data:

from django_readers import specs

spec = [
    "url",
    "comment",
    {"owner": ["username"]},
]

prepare, project = specs.process(spec)
queryset = prepare(Bookmark.objects.all())
rows = [project(instance) for instance in queryset]

A useful basic concept here is a reader pair: two functions grouped as a tuple that capture a dependency:

  • a queryset function that loads what is needed

  • a producer/projector function that reads from the prepared instance

So a pair is the contract: “prepare first, then produce/project”.

Mantle’s Query API gives you access to each of the steps here, but abstracts this flow by default.

It’s worth familiarising yourself with the django-readers docs, and at least reviewing the tutorial and cookbook sections there. You’ll need at least some understanding when you need to dig into Customising query generation.

Readers is wonderful, but it’s output is untyped.

cattrs: dicts -> attrs instances¶

django-readers projects model instances to dictionaries. Mantle then uses cattrs to map that raw data from the ORM into your attrs classes.

cattrs is a modern and performant library for structuring and unstructuring data (normally for serialisation).

Mantle ships with a default cattrs converter for common Django value types, including date, datetime, time, timedelta, UUID, and Decimal.

How Mantle connects everything¶

When you run:

rows = Query(Bookmark.objects.all(), BookmarkData).all()

Mantle roughly does this:

  1. Generate a readers spec from BookmarkData using to_spec().

  2. Process that spec with django-readers into (prepare, project).

  3. Call prepare(queryset) to build an optimized queryset.

  4. Execute the queryset and call project(instance) for each row (dict output).

  5. Use cattrs to structure each dict into BookmarkData.

So the full read flow is:

attrs class -> readers spec -> (prepare, project) -> dicts -> cattrs -> attrs instances

Where @overrides fits¶

Auto-generated specs cover common cases. When needed, @overrides lets you replace a field’s generated spec entry with an explicit django-readers pair.

from attrs import define
from django_readers import qs
from mantle import Query, overrides

prepare_is_public = qs.include_fields("status")

def produce_is_public(bookmark):
    return bookmark.status == "public"

@overrides({"is_public": (prepare_is_public, produce_is_public)})
@define
class BookmarkSummary:
    url: str
    is_public: bool

row = Query(Bookmark.objects.all(), BookmarkSummary).get()

See Customising query generation for more.