Collections¶
LinkedCollection[T]— the OGC collection envelope: a list of items plus hypermedia links and counts.
The envelope¶
LinkedCollection[T] is the standard OGC collection wrapper: a Sequence[T] of
items plus a links list and counts. numberReturned is computed from the
items; numberMatched (the total across all pages) is optional and dropped when
unset. Because the links are deferred, the whole envelope — items, links, and all
— is built in business logic with no request in hand.
from gazebo.collection import LinkedCollection
from gazebo.link import Link
class FeatureCollection(LinkedCollection[dict], items_alias='features'):
pass
collection = FeatureCollection(
items=[{'id': 1}, {'id': 2}],
links=[Link.self_link()],
number_matched=42,
)
Naming the items field¶
OGC specs name the items array differently per resource type — features in
Features, records in Records. Subclass and set the serialization alias once with
the items_alias class keyword; the field stays items in Python but serializes
under your name:
Both class keywords survive generic parametrization (FeatureCollection[P]).
Omitting numberReturned¶
numberReturned is emitted by default, but some OGC envelopes don't define it — the
/collections listing, for one. Turn it off per subclass with the number_returned
class keyword (this is exactly how the built-in Collections
envelope is defined):
class Collections(LinkedCollection[Collection], items_alias='collections',
number_returned=False):
pass
Omitting null members¶
OGC omits absent members rather than emitting null, so an unset numberMatched
or Link.title simply doesn't appear. That behavior comes from OmitNullModel,
the base both Link and LinkedCollection build on. Subclass it directly when you
define your own resource models and want the same: optional fields left unset are
dropped on JSON serialization, and — unlike a hand-rolled null-dropping serializer —
the OpenAPI response schema still reflects the real fields rather than collapsing to
an opaque object.
from gazebo import OmitNullModel
class Style(OmitNullModel):
name: str
description: str | None = None
# an unset optional member is omitted on the wire, not emitted as null
basic = Style(name='basic').model_dump_json()
It only drops top-level None members; nulls inside values (an open-ended
temporal interval [start, null]) are preserved.
Pagination links¶
paginate() returns deferred next/prev links. At serialization each takes the
current request URL and rewrites only the pagination query params — token and
limit by default, both configurable — preserving every other param. You own the
token semantics (opaque cursor, offset, keyset); gazebo just builds the links. The
underlying with_query helper is public if you need to rewrite a URL yourself.
from gazebo.pagination import paginate
# next/prev links that, at serialization, rewrite only the pagination query params
# of the current request URL and preserve everything else.
collection.links.extend(paginate(next_token='abc', limit=10))
Opaque cursors¶
When you'd rather not invent a token format, encode_cursor/decode_cursor pack an
arbitrary payload into one opaque, URL-safe string. The cursor is encoded, not
signed — so it's opaque to clients, but always validate the decoded contents. A
malformed cursor raises a ParamError (a 400 problem via the glue).
paginate() also emits first/last/self links on request:
from gazebo.pagination import decode_cursor, encode_cursor, paginate
# Wrap an arbitrary token payload in one opaque, URL-safe cursor instead of
# hand-rolling a token format. encode/decode round-trip; a malformed cursor raises a
# ParamError (which the FastAPI glue renders as a 400 problem).
cursor = encode_cursor({'offset': 20})
assert decode_cursor(cursor) == {'offset': 20}
# paginate() also emits first/last/self when asked (token_param renames the param).
cursor_links = paginate(next_token=cursor, first=True, self_=True, token_param='cursor', limit=10)
Offset/limit¶
For classic offset paging, paginate_offset() derives the whole
self/first/prev/next/last set from the current page position (and the
total, when known) — so prev/first appear only past the first page, and next/
last only when another page follows:
from gazebo.pagination import paginate_offset
# Offset/limit pagination: say where you are (and the total, if known) and the
# self/first/prev/next/last links are derived from the page position.
offset_links = paginate_offset(offset=20, limit=10, total=55)
POST-body pagination (stateless servers)¶
The builders are a thin convenience over Link, not a lossy wrapper: every
generated link can carry the full Link surface — a type, headers, a title (or
any extra member, via **link_fields), and a method/body. That last pair is what
makes pagination work for a POST search on a stateless server: with
method='POST' the page token rides in the request body (merged into the body you
pass) instead of the query string, so each next link re-states the whole search the
server doesn't remember. The companion drive_pagination
test driver follows these POST next links by reposting that body.
# POST pagination for a *stateless* server: the token rides in the request body (merged
# with the original criteria) rather than the query, so each `next` re-states the whole
# search. Any Link member (here `type`) can be set on every emitted link.
post_links = paginate(
next_token='page-2',
method='POST',
body={'filter': {'collection': 'plants'}},
type='application/json',
)
next_link = post_links[0]
Reference¶
See gazebo.collection,
gazebo.pagination, and
OmitNullModel.