REST SchemaSets
Purpose: Provide a Django QuerySet-like interface for querying, filtering, and slicing data from external REST APIs with lazy evaluation and result caching.
Why REST SchemaSets?
Fetching data from REST APIs typically involves imperative HTTP calls and manual result processing. The REST SchemaSet system provides:
- Declarative, Django QuerySet-like method chaining (
.filter(),.order_by(),.limit()) - Lazy evaluation - HTTP requests only happen when data is accessed
- Result caching to avoid redundant API calls
- Support for both lambda predicates and Django-style field lookups
- Nested field filtering using double-underscore syntax
- Slicing and indexing support
Quick Start
1. Create a Connector
from django_spire.contrib.rest import BaseRestHttpConnector
class DummyJsonAPIRestConnector(BaseRestHttpConnector):
base_url = 'https://dummyjson.com'
2. Define the SchemaSet
from __future__ import annotations
from typing import TYPE_CHECKING
from django_spire.contrib.rest import RestSchemaSet
from your_app.rest.connector import DummyJsonAPIRestConnector
if TYPE_CHECKING:
from your_app.rest.schema import PirateRestSchema
class PirateRestSchemaSet(RestSchemaSet['PirateRestSchema']):
connector = DummyJsonAPIRestConnector()
def _read_many(self, **request_params) -> list[PirateRestSchema]:
from your_app.rest.schema import PirateRestSchema
response = self.connector.get('users', params=request_params)
data = response.json()
return [PirateRestSchema(**user) for user in data.get('users', [])]
3. Use QuerySet-Like Methods
from your_app.rest.schema import PirateRestSchema
# All records
pirates = PirateRestSchema.objects.all()
# Filter by field
pirates = PirateRestSchema.objects.filter(firstName='Jack')
# Filter with lambda
pirates = PirateRestSchema.objects.filter(lambda p: p.id > 10)
# Order and limit
pirates = PirateRestSchema.objects.order_by('-lastName').limit(5)
# Get first result
pirate = PirateRestSchema.objects.first()
Core Concepts
RestSchemaSet
Abstract generic class providing a QuerySet-like interface for REST data.
| Class Attribute | Type | Description |
|---|---|---|
connector |
BaseRestHttpConnector |
The HTTP connector instance for making requests |
| Abstract Method | Description |
|---|---|
_read_many(**request_params) |
Fetch and return a list of schema instances |
| Optional Method | Description |
|---|---|
_read_one(**request_params) |
Fetch a single schema instance (optimization for .get()) |
Lazy Evaluation
SchemaSets are lazy - they do not make HTTP requests until you iterate over them or call a terminal method.
Terminal methods (trigger evaluation):
list(),forloop,len(),bool().first(),.last(),.get(),.count(),.exists().values_list()
Non-terminal methods (return new SchemaSet):
.all(),.filter(),.exclude(),.order_by(),.limit(),.offset()- Slicing with
[:]
Result Caching
After evaluation, results are cached within the SchemaSet instance. Subsequent iterations reuse the cached data.
pirates = PirateRestSchema.objects.filter(firstName='Jack')
# First iteration: makes HTTP request
for p in pirates:
print(p.username)
# Second iteration: uses cached results
for p in pirates:
print(p.email)
Creating a new SchemaSet via chaining clears the cache:
pirates = PirateRestSchema.objects.all() # HTTP request on iteration
filtered = pirates.filter(id=1) # New SchemaSet, cache not shared
Main Operations
Filtering with Field Lookups
Filter using keyword arguments that match schema field names:
# Exact match
pirates = PirateRestSchema.objects.filter(firstName='Jack')
# Multiple conditions (AND)
pirates = PirateRestSchema.objects.filter(firstName='Jack', lastName='Sparrow')
Filtering with Lambda Predicates
Filter using custom functions for complex conditions:
# Single condition
pirates = PirateRestSchema.objects.filter(lambda p: p.id > 10)
# Complex condition
pirates = PirateRestSchema.objects.filter(
lambda p: p.firstName.startswith('J') and p.id < 100
)
Filtering Nested Fields
Use double-underscore syntax for nested field access:
# Schema with nested object
class ShipRestSchema(RestSchema):
captain: CaptainInfo # has 'name' field
# Filter by nested field
ships = ShipRestSchema.objects.filter(captain__name='Jack Sparrow')
Excluding Records
Exclude records matching conditions (inverse of filter):
# Exclude by field
pirates = PirateRestSchema.objects.exclude(firstName='Will')
# Exclude with lambda
pirates = PirateRestSchema.objects.exclude(lambda p: p.id < 5)
Ordering Results
Order by one or more fields. Prefix with - for descending:
# Ascending
pirates = PirateRestSchema.objects.order_by('firstName')
# Descending
pirates = PirateRestSchema.objects.order_by('-firstName')
# Multiple fields
pirates = PirateRestSchema.objects.order_by('-lastName', 'firstName')
Limiting and Offsetting
Control result count and starting position:
# First 10 results
pirates = PirateRestSchema.objects.limit(10)
# Skip first 5, take next 10
pirates = PirateRestSchema.objects.offset(5).limit(10)
# Using slice syntax
pirates = PirateRestSchema.objects.all()[5:15]
Getting Single Records
Retrieve individual records:
# First result (or None)
pirate = PirateRestSchema.objects.first()
# Last result (or None)
pirate = PirateRestSchema.objects.last()
# Exactly one result (raises LookupError if 0 or >1)
pirate = PirateRestSchema.objects.get(username='jack_sparrow')
# By index
pirate = PirateRestSchema.objects.all()[0]
Counting and Existence Checks
# Count results
count = PirateRestSchema.objects.filter(firstName='Jack').count()
# Check if any exist
has_jacks = PirateRestSchema.objects.filter(firstName='Jack').exists()
Extracting Field Values
Get specific field values as lists:
# Single field (flat list)
usernames = PirateRestSchema.objects.values_list('username', flat=True)
# ['jack_sparrow', 'will_turner', ...]
# Multiple fields (list of tuples)
names = PirateRestSchema.objects.values_list('firstName', 'lastName')
# [('Jack', 'Sparrow'), ('Will', 'Turner'), ...]
Passing Parameters to the API
Use with_request_params() to pass query parameters to the underlying HTTP request:
# Pass pagination params to the API
pirates = (
PirateRestSchema.objects
.with_request_params(limit=50, skip=100)
.all()
)
These parameters are passed to _read_many() as **request_params.
Chaining Operations
All non-terminal methods return new SchemaSet instances and can be chained:
result = (
PirateRestSchema.objects
.with_request_params(limit=100)
.filter(lambda p: p.id > 10)
.exclude(firstName='Will')
.order_by('-lastName', 'firstName')
.limit(20)
.offset(5)
)
# Evaluate
pirates = list(result)
Implementing SchemaSets
The _read_many Method
The _read_many method is abstract and must be implemented. It receives any parameters from with_request_params():
class PirateRestSchemaSet(RestSchemaSet['PirateRestSchema']):
connector = DummyJsonAPIRestConnector()
def _read_many(self, **request_params) -> list[PirateRestSchema]:
from your_app.rest.schema import PirateRestSchema
# Make HTTP request with optional params
response = self.connector.get('users', params=request_params)
data = response.json()
# Parse response and return schema instances
return [PirateRestSchema(**user) for user in data.get('users', [])]
The _read_one Method (Optional)
Override _read_one for efficient single-record fetches:
class PirateRestSchemaSet(RestSchemaSet['PirateRestSchema']):
connector = DummyJsonAPIRestConnector()
def _read_many(self, **request_params) -> list[PirateRestSchema]:
from your_app.rest.schema import PirateRestSchema
response = self.connector.get('users', params=request_params)
data = response.json()
return [PirateRestSchema(**user) for user in data.get('users', [])]
def _read_one(self, **request_params) -> PirateRestSchema:
from your_app.rest.schema import PirateRestSchema
user_id = request_params.get('id')
response = self.connector.get(f'users/{user_id}')
return PirateRestSchema(**response.json())
API Reference
Query Methods
| Method | Returns | Description |
|---|---|---|
all() |
Self |
Returns a clone of the SchemaSet |
filter(predicate=None, **kwargs) |
Self |
Filter by predicate and/or field lookups |
exclude(predicate=None, **kwargs) |
Self |
Exclude matching records |
order_by(*fields) |
Self |
Order by fields (prefix - for descending) |
limit(n) |
Self |
Limit to n results |
offset(n) |
Self |
Skip first n results |
with_request_params(**kwargs) |
Self |
Pass parameters to _read_many() |
Terminal Methods
| Method | Returns | Description |
|---|---|---|
first() |
TSchema \| None |
First result or None |
last() |
TSchema \| None |
Last result or None |
get(**kwargs) |
TSchema |
Exactly one result (raises LookupError if 0 or >1) |
count() |
int |
Number of results |
exists() |
bool |
True if any results exist |
values_list(*fields, flat=False) |
list |
Extract field values |
Dunder Methods
| Method | Description |
|---|---|
__iter__ |
Iterate over results |
__len__ |
Get result count |
__bool__ |
True if results exist |
__getitem__ |
Index or slice access |
__repr__ |
String representation |
Common Patterns
Handling Paginated APIs
When the external API returns paginated data, handle it in _read_many:
class PirateRestSchemaSet(RestSchemaSet['PirateRestSchema']):
connector = DummyJsonAPIRestConnector()
def _read_many(self, **request_params) -> list[PirateRestSchema]:
from your_app.rest.schema import PirateRestSchema
all_users = []
skip = request_params.get('skip', 0)
limit = request_params.get('limit', 100)
while True:
response = self.connector.get('users', params={'skip': skip, 'limit': limit})
data = response.json()
users = data.get('users', [])
if not users:
break
all_users.extend([PirateRestSchema(**user) for user in users])
skip += limit
if len(users) < limit:
break
return all_users
Handling Nested API Responses
When the API response is nested:
class PirateRestSchemaSet(RestSchemaSet['PirateRestSchema']):
connector = DummyJsonAPIRestConnector()
def _read_many(self, **request_params) -> list[PirateRestSchema]:
from your_app.rest.schema import PirateRestSchema
response = self.connector.get('users', params=request_params)
data = response.json()
# Handle nested structure: {"data": {"users": [...]}}
users = data.get('data', {}).get('users', [])
return [PirateRestSchema(**user) for user in users]
Using with Django Views
from django.template.response import TemplateResponse
from your_app.rest.schema import PirateRestSchema
def pirate_list_view(request):
pirates = PirateRestSchema.objects.order_by('lastName').limit(25)
return TemplateResponse(
request=request,
context={'pirates': pirates},
template='pirates/list.html',
)