API reference¶
Auto-generated from the source docstrings and type hints.
Core¶
gazebo.link ¶
The Link model with deferred-href resolution.
A link's href may be a plain URL or a callable taking the active
:class:~gazebo.context.RequestContext and returning a URL. Callable hrefs are
resolved during JSON serialization, so links can be constructed in business logic
with no request in hand. Resolution pulls the context from
:data:~gazebo.context.link_context (set by the framework glue), falling back to
a pydantic serialization context.
UrlResolver ¶
A callable that, given the request context, returns a URL (str or AnyUrl).
Url ¶
Url = Annotated[
Annotated[
UrlResolver,
PlainSerializer(
_resolve_href, return_type=str, when_used='json'
),
]
| Annotated[Any, AfterValidator(_to_url)],
Field(union_mode='left_to_right'),
WithJsonSchema({'type': 'string', 'format': 'uri'}),
]
A URL field that accepts either a resolver callable or a concrete URL value.
Link ¶
Bases: OmitNullModel
An OGC-style link. Null fields are omitted on JSON serialization.
Source code in src/gazebo/link.py
self_link
classmethod
¶
self_link(
href: Url | None = None,
*,
rel: str = Rel.SELF,
type: str | None = MediaType.JSON,
**kwargs: Any,
) -> Self
Link to the current request URL (absolute, proxy-correct).
Source code in src/gazebo/link.py
root_link
classmethod
¶
root_link(
*,
landing: str = 'landing',
rel: str = Rel.ROOT,
type: str | None = MediaType.JSON,
**kwargs: Any,
) -> Self
Link to the API root/landing page, resolved by route name.
Source code in src/gazebo/link.py
to_route
classmethod
¶
to_route(
name: str,
*,
rel: str,
type: str | None = MediaType.JSON,
path: dict[str, Any] | None = None,
**kwargs: Any,
) -> Self
Link to a named route (resolved via ctx.url_for(name, **path)).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The route name to resolve. |
required |
rel
|
str
|
The link relation. |
required |
type
|
str | None
|
The target media type. |
JSON
|
path
|
dict[str, Any] | None
|
Path parameters for the route. Bound into the deferred resolver; not stored on the link itself. |
None
|
**kwargs
|
Any
|
Extra link fields (e.g. |
{}
|
Source code in src/gazebo/link.py
gazebo.collection ¶
The collection-envelope shape: items + links + counts.
Generic over the item type only. Subclasses set the serialization alias for the
items field (OGC calls it features, records ...) via the items_alias
class keyword. Because links are deferred, a LinkedCollection is fully
constructible in business logic with no request in hand.
LinkedCollection ¶
Bases: BaseModel
A list of T with hypermedia links and counts.
class FeatureCollection(LinkedCollection[Feature], items_alias='features'): ...
Two class keywords tune serialization, both held as class variables (so they
survive generic parametrization like FeatureCollection[P] — mutating
model_fields would not, as pydantic rebuilds them per specialization):
items_alias— the JSON key the items serialize under (OGCfeatures/records/collections...).number_returned— setFalseto omit the computednumberReturnedmember (e.g. an OGC/collectionslisting, where it isn't defined).
class Collections(LinkedCollection[C], items_alias='collections', ... number_returned=False): ...
Source code in src/gazebo/collection.py
gazebo.pagination ¶
Pagination link helpers.
Builds next/prev (and optional first/last/self) links as deferred
resolvers: at serialization they take the current request URL from the context and
rewrite only the pagination query params, preserving everything else. The caller owns
token semantics; gazebo only builds the links.
The builders are a thin convenience over :class:~gazebo.link.Link, not a lossy
abstraction over it: 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 POST pagination work on a
stateless server: with method='POST' the page token rides in the request body
(merged into the body you pass) rather than the query string, so each next link
re-states the full search criteria the server doesn't remember.
Two flavours, both additive and framework-agnostic:
- :func:
paginate— token-based: you supply opaquenext/prevtokens (and optionally alasttoken). Pair it with :func:encode_cursor/:func:decode_cursorif you want a ready-made opaque cursor format instead of hand-rolling one. - :func:
paginate_offset— offset/limit-based: you supply the currentoffset, the pagelimit, and (optionally) thetotal; thefirst/prev/next/last/selflinks are derived for you.
last_page_offset ¶
The zero-based offset of the last page for total items at page size limit.
Zero when there are no items. limit must be positive. Shared so callers
deriving their own last cursor don't re-spell the rounding math.
Source code in src/gazebo/pagination.py
paginate ¶
paginate(
*,
next_token: str | None = None,
prev_token: str | None = None,
limit: int | None = None,
first: bool = False,
last_token: str | None = None,
self_: bool = False,
method: str = 'GET',
body: Mapping[str, Any] | None = None,
type: str | None = MediaType.JSON,
headers: Mapping[str, str | list[str]] | None = None,
token_param: str = 'token',
limit_param: str = 'limit',
**link_fields: Any,
) -> list[Link]
Return token-based pagination links for the values provided (deferred).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
next_token
|
str | None
|
Token for the next page; emits a |
None
|
prev_token
|
str | None
|
Token for the previous page; emits a |
None
|
limit
|
int | None
|
Page size to carry on every emitted link (omitted when |
None
|
first
|
bool
|
Emit a |
False
|
last_token
|
str | None
|
Token for the last page; emits a |
None
|
self_
|
bool
|
Emit a |
False
|
method
|
str
|
|
'GET'
|
body
|
Mapping[str, Any] | None
|
Base request body for |
None
|
type
|
str | None
|
The |
JSON
|
headers
|
Mapping[str, str | list[str]] | None
|
Optional |
None
|
token_param
|
str
|
Query/body parameter name carrying the token. |
'token'
|
limit_param
|
str
|
Query/body parameter name carrying the limit. |
'limit'
|
**link_fields
|
Any
|
Any further |
{}
|
The next/prev links lead the list (unchanged from earlier releases); any
first/last/self links follow.
Source code in src/gazebo/pagination.py
paginate_offset ¶
paginate_offset(
*,
offset: int,
limit: int,
total: int | None = None,
self_: bool = True,
method: str = 'GET',
body: Mapping[str, Any] | None = None,
type: str | None = MediaType.JSON,
headers: Mapping[str, str | list[str]] | None = None,
offset_param: str = 'offset',
limit_param: str = 'limit',
**link_fields: Any,
) -> list[Link]
Return offset/limit pagination links, derived from the current page (deferred).
Emits self/first/prev/next/last as the position warrants:
first/prev only when offset > 0; next whenever total is unknown
or another page follows; last only when total is known and differs from the
current page. Each link is canonical — it carries the explicit offset/limit.
Accepts the same method/body/type/headers/**link_fields
pass-through as :func:paginate (so offset paging can also ride a POST body).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offset
|
int
|
The current page's zero-based item offset. |
required |
limit
|
int
|
The page size (must be positive). |
required |
total
|
int | None
|
Total matching items, if known; enables the |
None
|
self_
|
bool
|
Emit a |
True
|
offset_param
|
str
|
Query/body parameter name carrying the offset. |
'offset'
|
limit_param
|
str
|
Query/body parameter name carrying the limit. |
'limit'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/gazebo/pagination.py
encode_cursor ¶
Encode an arbitrary token payload as one opaque, URL-safe cursor string.
The payload (any JSON-serializable mapping — e.g. {'after_id': 42}) is
serialized to compact JSON and base64url-encoded without padding, so the result
is safe to drop straight into a next/prev token. Round-trips through
:func:decode_cursor. The cursor is opaque, not secret — it is encoded, not
signed or encrypted, so never trust it without validating the decoded contents.
Source code in src/gazebo/pagination.py
decode_cursor ¶
Decode a cursor produced by :func:encode_cursor back into its payload.
A malformed or non-object cursor raises :class:~gazebo.params.ParamError (which
the FastAPI glue renders as a 400 problem) carrying parameter — treat a
bad client-supplied cursor as a client error, not a 500.
Source code in src/gazebo/pagination.py
gazebo.linkheader ¶
RFC 8288 Link: header serialization for already-resolved links.
Core layer: stdlib only, no web framework. Turns a list of resolved links (the
serialized link dicts gazebo already emits in a JSON body) into a single Link:
header value, so non-JSON-parsing clients and crawlers can follow self/next/
prev/alternate without reading the body. The framework glue (see
:mod:gazebo.ext.fastapi) installs the header from a response's top-level links;
this module owns only the formatting and the deliberate narrowing that keeps the
header small.
Two guards keep the header from bloating — the classic failure mode of Link:
when a collection carries hundreds of per-item links:
- A rel allow-list. Only navigational relations (:data:
NAV_RELS) are emitted by default — never arbitrary or per-item rels. - A hard cap (:data:
DEFAULT_MAX_LINKS) on how many links are serialized.
Callers that want everything can pass rels=None (no filter) and a large
max_links, but the defaults are intentionally conservative.
NAV_RELS
module-attribute
¶
NAV_RELS: tuple[str, ...] = (
'self',
'first',
'prev',
'next',
'last',
'alternate',
'root',
'up',
'collection',
'describedby',
'conformance',
'service-desc',
)
The link relations safe to surface in a Link: header by default.
Navigational/hypermedia rels a client or crawler follows — deliberately not every
rel a body might carry, and never per-item links. Override via the rels argument
to :func:format_link_header (or the FastAPI set_link_header helper).
DEFAULT_MAX_LINKS
module-attribute
¶
Default ceiling on links emitted into one header, so a pathological body can't produce an oversized header that trips server/proxy header-size limits.
format_link_header ¶
format_link_header(
links: Iterable[Mapping[str, Any]],
*,
rels: Sequence[str] | None = NAV_RELS,
max_links: int = DEFAULT_MAX_LINKS,
) -> str
Serialize resolved links into an RFC 8288 Link: header value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
links
|
Iterable[Mapping[str, Any]]
|
Resolved link mappings — each a dict with at least |
required |
rels
|
Sequence[str] | None
|
The relations to include, in any order; a link whose |
NAV_RELS
|
max_links
|
int
|
Stop after this many links (after filtering), guarding header size. |
DEFAULT_MAX_LINKS
|
Returns:
| Type | Description |
|---|---|
str
|
The header value (links joined by |
str
|
callers should then omit the header entirely rather than send an empty one. |
Source code in src/gazebo/linkheader.py
gazebo.caching ¶
Conditional-request / caching primitives (RFC 7232 / RFC 9111).
Core layer: pydantic + stdlib only, no web framework. Provides the pure pieces a
service needs to support conditional GETs — derive an ETag from a value, format/
parse HTTP dates, and evaluate If-None-Match / If-Modified-Since preconditions
— leaving the request/response plumbing to the FastAPI glue (not_modified /
set_cache_headers in :mod:gazebo.ext.fastapi).
ETags are weak by default (W/"…"): they are derived from a serialization of
the value, which signals semantic equivalence rather than byte-for-byte identity — the
honest validator strength for a hash of a JSON dump.
etag_for ¶
Derive an ETag from value (a model, mapping, str, or bytes).
The value is reduced to canonical bytes — a pydantic model via
model_dump_json(by_alias=True), anything else via sorted-key JSON — and hashed
(SHA-256). The result is a quoted entity-tag, prefixed W/ when weak (the
default).
Note
A model carrying deferred (callable-href) links only serializes inside an active request context; outside one, ETag such a model from its underlying data rather than the link-bearing envelope.
Source code in src/gazebo/caching.py
http_date ¶
Format value as an IMF-fixdate HTTP date (e.g. for Last-Modified).
parse_http_date ¶
Parse an HTTP date header into an aware datetime (None if malformed).
Source code in src/gazebo/caching.py
if_none_match_satisfied ¶
Whether etag matches an If-None-Match header value (weak comparison).
* matches any current entity. Per RFC 7232, If-None-Match uses the weak
comparison function, so the W/ prefix is ignored on both sides.
Source code in src/gazebo/caching.py
is_not_modified ¶
is_not_modified(
*,
method: str = 'GET',
etag: str | None = None,
last_modified: datetime | None = None,
if_none_match: str | None = None,
if_modified_since: str | None = None,
) -> bool
Evaluate the conditional-GET preconditions; True means respond 304.
Only GET/HEAD are eligible. If-None-Match takes precedence over
If-Modified-Since (which is ignored entirely when the former is present, per
RFC 7232 §3.3). HTTP dates carry one-second resolution, so last_modified is
truncated to whole seconds before comparison.
Source code in src/gazebo/caching.py
gazebo.context ¶
Request-context seam for deferred URL generation.
The core never imports a web framework. Link hrefs may be callables that need
"the current request" to produce a URL; that request is abstracted behind the
RequestContext protocol and delivered ambiently through a ContextVar (set
by the framework glue) with a pydantic-serialization-context fallback for manual
dumps and tests.
RequestContext ¶
Bases: Protocol
The minimal surface link factories need to build URLs.
Any object structurally satisfying this (e.g. a framework request adapter) can be
placed in :data:link_context. The core only ever calls these members.
Source code in src/gazebo/context.py
RequestIdFilter ¶
Bases: Filter
Logging filter that stamps each record with the active request id.
Add to a handler/logger and reference %(request_id)s in the format. The
field is always present (- when no request is active), so the format
string never breaks outside a request.
Source code in src/gazebo/context.py
use_context ¶
Bind ctx as the active request context for the duration of the block.
Uses an explicit reset in finally so it is correct on every supported
Python version (Token only became a context manager in 3.14).
Source code in src/gazebo/context.py
resolve_context ¶
Find the active request context.
Resolution order: the :data:link_context ContextVar first, then a
request/context entry in a pydantic serialization info.context
mapping (the manual-dump / test escape hatch).
Source code in src/gazebo/context.py
with_query ¶
Return the current URL with overrides merged into the query string.
A None value removes that parameter. Other values are stringified. The shared
"derive a URL from the active context" helper behind deferred pagination and
content-negotiation hrefs.
Source code in src/gazebo/context.py
gazebo.problems ¶
RFC 7807 / 9457 problem details.
The model is core (pydantic only); rendering it into an HTTP response lives in the
framework glue. Raise :class:ProblemException from anywhere; the glue's handler
turns it into an application/problem+json response.
ProblemDetail ¶
Bases: BaseModel
An RFC 7807/9457 problem object. Extensions allowed.
Source code in src/gazebo/problems.py
ProblemException ¶
Bases: Exception
Raise to produce a problem response. Carries a :class:ProblemDetail.
Named like the familiar HTTPException (rather than ...Error): it's
a control-flow signal to emit an HTTP response, not a programming error.
Source code in src/gazebo/problems.py
from_detail
classmethod
¶
Wrap an already-built :class:ProblemDetail (no field re-assembly).
Source code in src/gazebo/problems.py
ProblemType ¶
Bases: BaseModel
A documented, reusable kind of problem: a stable type URI plus defaults.
Define these once and raise them by reference, so a service's error catalog lives
in one place and its type URIs stop defaulting to about:blank and stay
stable/linkable. The per-occurrence detail/instance (and any extension
members) are supplied at the raise site::
NOT_FOUND = ProblemType(
type='https://errors.example/not-found', title='Resource not found', status=404,
)
raise NOT_FOUND.exception(detail='plant 5 not found', instance='/plants/5')
Source code in src/gazebo/problems.py
problem ¶
problem(
*,
detail: str | None = None,
instance: str | None = None,
**extensions: Any,
) -> ProblemDetail
Build a :class:ProblemDetail for one occurrence of this problem type.
Source code in src/gazebo/problems.py
exception ¶
exception(
*,
detail: str | None = None,
instance: str | None = None,
**extensions: Any,
) -> ProblemException
Build a :class:ProblemException to raise for this problem type.
Source code in src/gazebo/problems.py
ProblemRegistry ¶
A catalog of :class:ProblemType instances, keyed by a short name.
Register a service's problem kinds once, reference them by key, and serve the
whole set from a catalog endpoint so the type URIs resolve to documentation.
problems = ProblemRegistry() not_found = problems.define( ... 'not-found', type='https://errors.example/not-found', ... title='Resource not found', status=404, ... ) raise problems['not-found'].exception(detail='plant 5 not found')
Source code in src/gazebo/problems.py
register ¶
Add an already-built :class:ProblemType under key (returns it).
Source code in src/gazebo/problems.py
define ¶
define(
key: str,
*,
type: str,
title: str,
status: int,
detail: str | None = None,
) -> ProblemType
Build and register a :class:ProblemType in one call (returns it).
Source code in src/gazebo/problems.py
catalog ¶
gazebo.params ¶
Typed parsers for the standard OGC query parameters.
Core layer: pydantic + stdlib only, no web framework. These models turn the raw
string values every OGC API accepts — bbox, datetime, crs — into typed,
validated objects. A malformed value raises :class:ParamError, which the FastAPI
glue renders as a 400 application/problem+json response (see
:mod:gazebo.ext.fastapi). The framework-agnostic parse classmethods can also
be called directly from any code that already has the raw string in hand.
CRS84
module-attribute
¶
The OGC default CRS: WGS 84 longitude/latitude (lon, lat axis order).
Same datum as EPSG:4326 but with GeoJSON's lon/lat ordering, which is why OGC
API Features uses it as the default and most common allow-list entry.
ParamError ¶
Bases: Exception
A query parameter failed to parse or validate.
Carries the offending parameter name and a human detail; the FastAPI
glue maps it to a 400 problem, with the parameter name as an extension
member. OGC treats a malformed query parameter as a client error (400), so
this is deliberately distinct from request-body validation (422).
Source code in src/gazebo/params.py
BBox ¶
Bases: BaseModel
A bounding box: minx,miny,maxx,maxy (2D) or with minz/maxz (3D).
Parsed from the OGC bbox query value. The x axis is allowed to wrap (minx
may exceed maxx to denote a box crossing the antimeridian); the y and z axes
must be ordered min <= max.
Source code in src/gazebo/params.py
contains ¶
Whether the point (lon, lat) falls within this (2D) box.
Handles the antimeridian case the box itself allows: when minx > maxx the
x extent wraps across +/-180, so a longitude matches if it is east of minx
or west of maxx. The y axis is a plain inclusive range. The z extent (if
any) is not considered — this is a horizontal point-in-box test.
Source code in src/gazebo/params.py
parse
classmethod
¶
Parse a bbox query value (4 or 6 comma-separated numbers).
Source code in src/gazebo/params.py
DatetimeInterval ¶
Bases: BaseModel
An RFC 3339 instant or interval, as accepted by the OGC datetime param.
start/end of None denote an open (unbounded) end. An instant
(a single timestamp with no /) is represented as start == end.
Source code in src/gazebo/params.py
contains ¶
Whether when falls within the (possibly half-open) interval.
A naive when (or naive bound) is treated as UTC, so this never raises a
naive-vs-aware TypeError regardless of how the interval was built.
Source code in src/gazebo/params.py
parse
classmethod
¶
Parse a datetime query value (an instant or a start/end interval).
Source code in src/gazebo/params.py
validate_crs ¶
validate_crs(
value: str | None,
allowed: tuple[str, ...],
*,
parameter: str = 'crs',
default: str | None = CRS84,
) -> str
Resolve and validate a crs/bbox-crs URI against an allow-list.
A present value must be in allowed (the OGC conformance requirement);
otherwise raises :class:ParamError (-> 400). When value is unset it
resolves to default — which must itself be in allowed. Pass
default=None to require the parameter when there is no safe default: an
absent value then raises :class:ParamError rather than assuming one.
Raises :class:ValueError if a non-None default is not in allowed
(a server misconfiguration, not bad client input).
Source code in src/gazebo/params.py
gazebo.negotiation ¶
Content negotiation: resolve a representation from ?f= then Accept.
Core layer: pydantic + stdlib only, no web framework. OGC APIs let a client pick a
representation with a ?f=json|html query parameter (which takes precedence) and
fall back to the HTTP Accept header. This module owns that resolution — given the
representations a resource offers, pick one — plus the alternate links that point at
the others. It ships no HTML/templating opinion: rendering a chosen representation is
the caller's job (a callable or template hook); gazebo only tells you which one and
links the rest.
Resolution order (per OGC API Common):
?f=— an explicit format key wins. An unknown key is a client error (:class:~gazebo.params.ParamError→400).Accept— standard HTTP negotiation over the offered media types. When anAcceptis present but nothing it lists is on offer, that's a406(:class:~gazebo.problems.ProblemException).- Otherwise the
default(or the first offered representation).
Both error types already have handlers in the FastAPI glue, so a failed negotiation
renders as application/problem+json with no extra wiring.
Representation
dataclass
¶
negotiate ¶
negotiate(
available: Sequence[Representation],
*,
f: str | None = None,
accept: str | None = None,
default: Representation | None = None,
f_param: str = 'f',
) -> Representation
Resolve which of available to serve from f (wins) then accept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
available
|
Sequence[Representation]
|
The representations the resource offers, in server-preferred order. |
required |
f
|
str | None
|
The |
None
|
accept
|
str | None
|
The |
None
|
default
|
Representation | None
|
The representation to serve when neither |
None
|
f_param
|
str
|
The query parameter name to cite in an unknown-format error. |
'f'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ParamError
|
If |
ProblemException
|
If |
Source code in src/gazebo/negotiation.py
alternate_links ¶
alternate_links(
current: Representation,
available: Sequence[Representation],
*,
f_param: str = 'f',
rel: str = Rel.ALTERNATE,
) -> list[Link]
Build deferred alternate links to every offered representation but current.
Each link points at the current request URL with ?f= set to that
representation's key, so a client on the JSON view can discover (and switch to) the
HTML one, and vice versa. Pair with a normal self link for current.
Source code in src/gazebo/negotiation.py
gazebo.filtering ¶
Request-side CQL2 filtering: queryables, sortables, and the engine seam.
Core layer: pydantic + stdlib only. gazebo owns the OGC plumbing around filtering — the
filter/filter-lang/sortby parsing, the queryables/sortables resources derived
from a pydantic model, and validating that a filter only references queryable fields —
while delegating CQL2 parsing/evaluation to a pluggable :class:FilterEngine. The bundled
engine (:class:~gazebo.filtering.cql2.Cql2Engine, adapting cql2-rs) lives in
:mod:gazebo.filtering.cql2 behind the gazebo[cql2] extra and is not imported
here, so this package never pulls in the CQL2 dependency. The FastAPI FilterParam /
SortByParam adapters live in :mod:gazebo.ext.fastapi.
Compiled ¶
Bases: Protocol
An engine's parsed and validated filter expression.
matches contract: returns True/False; a referenced property that is absent
or null evaluates to unknown, so the item does not match — SQL WHERE / CQL2
three-valued logic. Property names may be dotted paths (site.coord.lat) that
traverse nested mappings. properties returns every referenced property name (dotted
where nested), which gazebo checks against the collection's queryables.
Source code in src/gazebo/filtering/engine.py
Filter ¶
A compiled, validated filter ready to evaluate — what a route parameter receives.
Holds the engine-native :class:Compiled expression (reachable as compiled for
engine-specific features such as SQL translation) plus the resolved lang and
crs. :meth:matches is the in-memory convenience; it inherits the engine's
null-handling contract, so it is safe to use directly in a list comprehension over
sparse data.
Source code in src/gazebo/filtering/engine.py
FilterEngine ¶
Bases: Protocol
Compiles a raw filter value into a :class:Compiled expression.
Implementations must parse and validate (some CQL2 parsers accept malformed text
leniently), raising :class:FilterError on any failure.
Source code in src/gazebo/filtering/engine.py
FilterError ¶
Bases: Exception
A filter failed to compile, validate, or referenced a non-queryable property.
Framework-agnostic: the FastAPI glue maps it to a 400 application/problem+json by
re-raising it as a :class:~gazebo.params.ParamError for the filter parameter.
Raising or catching it directly is appropriate anywhere a filter is compiled outside a
request (business logic, tests).
Source code in src/gazebo/filtering/engine.py
FilterLang ¶
Direction ¶
Queryables ¶
Bases: _SchemaResource
The OGC queryables resource (GET /collections/{id}/queryables).
A JSON Schema whose properties are the fields a CQL2 filter may reference;
:attr:names is the allow-list :func:~gazebo.filtering.queryables.validate_properties
checks against. Build one from a model with
:func:~gazebo.filtering.queryables.queryables_from_model.
Source code in src/gazebo/filtering/models.py
Sort ¶
Sortables ¶
Bases: _SchemaResource
The sortables resource (GET /collections/{id}/sortables).
A JSON Schema whose properties are the fields sortby may name. Build one with
:func:~gazebo.filtering.queryables.sortables_from_model.
Source code in src/gazebo/filtering/models.py
SortBy ¶
Bases: BaseModel
A parsed OGC/STAC sortby value: an ordered list of :class:Sort terms.
Source code in src/gazebo/filtering/models.py
parse
classmethod
¶
Parse a sortby query value (+name/-name/name, comma-separated).
A leading - selects descending, + or no sign ascending. Fields may be
dotted (matching flattened sortables). Raises :class:~gazebo.params.ParamError
(-> 400) on an empty term, a missing field name, a duplicate field, or — when
sortables is given — a field outside that allow-list.
Source code in src/gazebo/filtering/models.py
apply ¶
Return items stably sorted by these terms.
Multi-key order is achieved by sorting on the least-significant term first (Python sort is stable). Missing/null values sort last under ascending order (hence first under descending). Field access is dotted, consistent with the queryables.
Source code in src/gazebo/filtering/models.py
filter_conformance_classes ¶
The conformance-class URIs a CQL2-filterable collection should declare.
Source code in src/gazebo/filtering/models.py
queryables_from_model ¶
queryables_from_model(
model: type[BaseModel],
*,
id: str | None = None,
title: str | None = None,
additional: bool = False,
max_depth: int = 4,
) -> Queryables
Build a :class:~gazebo.filtering.models.Queryables from a pydantic model.
Scalars (and their constraints/enums/formats) are advertised as-is; nested models are
flattened to dotted accessors; geometry fields become spatial queryables; arrays
advertise their item type. additional sets additionalProperties — keep it
False for an honest closed allow-list. max_depth guards recursive models.
Source code in src/gazebo/filtering/queryables.py
sortables_from_model ¶
sortables_from_model(
model: type[BaseModel],
*,
id: str | None = None,
title: str | None = None,
max_depth: int = 4,
) -> Sortables
Build a :class:~gazebo.filtering.models.Sortables from a pydantic model.
Like :func:queryables_from_model but scalar-only: geometry and array fields (which
have no total order) are excluded, while nested scalar leaves are still flattened.
Source code in src/gazebo/filtering/queryables.py
validate_properties ¶
Raise :class:FilterError if filter references a non-queryable property.
The check is the filter's referenced-property set minus the queryables' declared names; dotted nested references are compared against the flattened allow-list.
Source code in src/gazebo/filtering/queryables.py
gazebo.filtering.cql2 ¶
The bundled CQL2 engine, adapting cql2-rs (the gazebo[cql2] extra).
This is the only module that imports cql2; importing it requires the extra. It is
not imported by :mod:gazebo.filtering at package import, so the core stays free of the
dependency. gazebo ships exactly one engine but keeps :class:~gazebo.filtering.engine's
FilterEngine Protocol open, so a user who prefers another CQL2 implementation can
supply their own without gazebo bundling or testing a second one.
Two cql2-rs behaviors shape this adapter:
- Its text parser is lenient — malformed text can parse to a stray property reference
rather than raising — so :meth:
Cql2Engine.compilealways callsvalidate(). matchesraises (rather than returningFalse) when a referenced property is absent or null. To get SQLWHEREsemantics without depending on that error message, :meth:Cql2Compiled.matchesevaluates viareduceand treats only a literalTrueas a match (an "unknown" comparison stays a partial expression — see the method).
Cql2Compiled ¶
A validated :class:cql2.Expr, adapting it to the :class:Compiled Protocol.
Source code in src/gazebo/filtering/cql2.py
native
instance-attribute
¶
The underlying :class:cql2.Expr — for engine-specific features (to_sql).
Cql2Engine ¶
A :class:~gazebo.filtering.engine.FilterEngine backed by cql2-rs.
Source code in src/gazebo/filtering/cql2.py
referenced_properties ¶
Collect every {'property': <name>} reference anywhere in a cql2-json node.
cql2-json is a serialized AST; property references — including the dotted paths used
for nested fields — appear uniformly as {'property': <name>}, at any depth and
inside every operator (comparison, logical, spatial, temporal, array, function).
Source code in src/gazebo/filtering/cql2.py
gazebo.geojson ¶
GeoJSON models (RFC 7946) with gazebo hypermedia, for OGC API Features.
Optional extra: importing this module requires geojson-pydantic (the
gazebo[geojson] extra). It reuses geojson-pydantic for the coordinate-validated
geometry and feature shapes — the tedious, easy-to-get-wrong part — and layers
gazebo's deferred links on top:
- :class:
Featuresubclasses geojson-pydantic'sFeatureto add alinksarray. - :class:
FeatureCollectionis a :class:~gazebo.collection.LinkedCollection(so it carrieslinks+numberReturned/numberMatched) rather than geojson-pydantic's plain collection, which has none of that. Items serialize underfeaturesand an optional top-levelbboxis supported (RFC 7946 §5).
The geometry types are re-exported for convenience.
Feature ¶
Bases: Feature[Geometry, P]
A GeoJSON Feature with OGC-style hypermedia links.
Generic over the properties model P. Inherits geojson-pydantic's
coordinate validation for geometry and adds gazebo's deferred links,
resolved at serialization like every other gazebo link.
Source code in src/gazebo/geojson.py
FeatureCollection ¶
Bases: LinkedCollection[Feature[P]]
A GeoJSON FeatureCollection that is also a gazebo LinkedCollection.
Items serialize under features; the envelope additionally carries
links, numberReturned/numberMatched (from LinkedCollection),
and an optional top-level bbox.
Source code in src/gazebo/geojson.py
gazebo.ogc ¶
OGC API Common models: landing page, conformance.
Pure pydantic models plus a small conformance-class registry. The framework glue generates the actual landing-page links from the router tree.
DEFAULT_TRS
module-attribute
¶
The OGC default temporal reference system (the Gregorian calendar / UTC).
LandingPage ¶
Bases: BaseModel
OGC API Common landing page (GET /).
Source code in src/gazebo/ogc.py
ConformanceDeclaration ¶
Bases: BaseModel
OGC API Common conformance declaration (GET /conformance).
Source code in src/gazebo/ogc.py
SpatialExtent ¶
Bases: BaseModel
The spatial extent of a collection: one or more bounding boxes in crs.
Per OGC, the first bbox is the overall extent; further entries may partition it.
Each bbox is [minx, miny, maxx, maxy] (or the 6-number 3D form).
Source code in src/gazebo/ogc.py
TemporalExtent ¶
Bases: BaseModel
The temporal extent: one or more intervals in trs.
Each interval is a [start, end] pair; null on either side means open.
Source code in src/gazebo/ogc.py
Extent ¶
Bases: OmitNullModel
A collection's spatial and/or temporal extent.
An unset spatial/temporal is omitted on the wire rather than emitted as
null (OGC treats them as optional members).
Source code in src/gazebo/ogc.py
Collection ¶
Bases: OmitNullModel
OGC API Common collection metadata (GET /collections/{id}).
An unset extent is omitted on the wire rather than emitted as null.
Source code in src/gazebo/ogc.py
Collections ¶
Bases: LinkedCollection[Collection]
The /collections envelope: a list of :class:Collection under collections.
Omits numberReturned — the OGC /collections object does not define it.
Source code in src/gazebo/ogc.py
Conformance ¶
A small registry of conformance-class URIs.
conformance = Conformance(Conformance.CORE) conformance.add(Conformance.JSON) conformance.declaration()
Source code in src/gazebo/ogc.py
gazebo.rels ¶
Typed constants for link relations and media types.
Kills stringly-typed rel/type bugs. StrEnum members are str
subclasses, so they drop into Link(rel=Rel.SELF, type=MediaType.JSON) and
serialize as their plain string value.
Rel ¶
Bases: StrEnum
Common IANA / OGC link relation types.
Source code in src/gazebo/rels.py
MediaType ¶
Bases: StrEnum
Common media types for OGC-style APIs.
Source code in src/gazebo/rels.py
gazebo.serialization ¶
Shared serialization helpers (pure pydantic + pydantic-core; no gazebo imports).
OGC omits absent members rather than emitting null. gazebo gets that with a JSON
@model_serializer that drops null fields — but a model with a @model_serializer
makes pydantic treat the serialized shape as opaque: its serialization JSON schema —
and therefore FastAPI's OpenAPI response schema — collapses to
{"additionalProperties": true}. The two halves must travel together, so
:class:OmitNullModel bundles them: subclass it and absent optional members are
omitted on the wire while the documented response schema stays honest. Models with
richer serializers (e.g. :class:~gazebo.collection.LinkedCollection) reuse the
pieces directly — :func:drop_none for the wire shape and
:func:faithful_serialization_schema from their own __get_pydantic_json_schema__.
Why not pydantic's native exclude_none? It is a dump-time flag, not a model
property — there is no model-level "always omit none" in pydantic. Relying on it would
push the responsibility onto each caller (model_dump(exclude_none=True)) or, under
the framework glue, onto per-route response_model_exclude_none=True — easy to
forget and inert for non-FastAPI users. Baking omission into the model keeps the
behavior self-contained and correct regardless of how the model is serialized.
OmitNullModel ¶
Bases: BaseModel
A pydantic model that omits null fields on JSON serialization, OGC-style.
Subclass it for any model whose absent optional members should be omitted on
the wire rather than emitted as null. It bundles the two halves that have to
travel together: a JSON-mode @model_serializer that drops None values,
and a __get_pydantic_json_schema__ that reconstructs the real field shape so
the serializer does not opacify the OpenAPI response schema.
Source code in src/gazebo/serialization.py
drop_none ¶
Drop keys whose value is None — OGC omits absent members on the wire.
strip_model_serializers ¶
Deep-copy a core schema with model-level function serializers removed.
Only the serialization entry attached to a model node — pydantic's
representation of a @model_serializer — is dropped, since that is what
opacifies the output shape. Field-level serializers (PlainSerializer and
friends, which sit on the field's own node) and computed-field schemas are
preserved, so the reconstructed schema still reflects each field's real
serialized type rather than its pre-serialization one.
Source code in src/gazebo/serialization.py
faithful_serialization_schema ¶
faithful_serialization_schema(
core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue
A serialization JSON schema reflecting the real fields, not an opaque object.
Call from __get_pydantic_json_schema__ on any model whose @model_serializer
would otherwise opacify its serialization schema. Validation schemas (request
bodies) are unaffected and returned as-is.
Source code in src/gazebo/serialization.py
gazebo.tags ¶
gazebo.asgi ¶
Pure-ASGI middleware — no web-framework import required.
Works with any ASGI app (Starlette, FastAPI, Litestar, Quart). Provides proxy-header normalization (with pluggable trust) and a context-setting middleware parametrized by a scope->RequestContext factory.
TrustedClient ¶
Trust requests whose immediate client host is in an allowlist.
Source code in src/gazebo/asgi.py
SharedSecret ¶
Trust requests carrying a matching shared-secret header (proxy-chain auth).
Source code in src/gazebo/asgi.py
ProxyHeadersMiddleware ¶
Apply X-Forwarded-{Proto,Host,Prefix} to the ASGI scope when trusted.
Supersedes uvicorn's partial --proxy-headers: it also sets the scheme from
X-Forwarded-Proto (so URLs come out https behind a TLS-terminating
proxy) and mutates the header list in place rather than round-tripping a dict.
Source code in src/gazebo/asgi.py
ContextMiddleware ¶
Set :data:~gazebo.context.link_context for each request.
factory turns the ASGI scope into a :class:RequestContext; the
framework glue supplies it (e.g. wrapping the framework's request object). Use this
when you are not using GazeboApp (which manages context via its request
scope).
Source code in src/gazebo/asgi.py
gazebo.testing ¶
Pytest helpers for testing the OGC-ness of a gazebo service, declaratively.
A pytest plugin you opt into — add pytest_plugins = ['gazebo.testing'] to your
top-level conftest.py (it does not auto-register, so it never imposes its fixtures
on an unrelated downstream suite). Opting in also enables pytest's assertion rewriting
for these helpers, so a failed assert_has_link / assert_problem gets full
introspection, not just the message. Importing it requires pytest (the
gazebo[test] extra).
What you get:
- :func:
assert_has_link/ :func:assert_problem— envelope and problem+json assertions with descriptive failures. - :func:
drive_pagination— follownextlinks to exhaustion, checking the envelope invariants on every page, with a loop guard. Works with gazebo's resolved (deferred) links directly, and with GET or POSTnextlinks. - opt-in fixtures:
gazebo_link_context(isolate the link-context contextvar for a test) andgazebo_overrides(a freshOverrides).
Note: the assertion helpers use bare assert, so a test run under python -O
(PYTHONOPTIMIZE) strips them and they become no-ops — don't optimize your tests.
find_link ¶
Return the first link in body with relation rel, or None.
body may be a serialized model (a mapping with a links array) or the
links list itself.
Source code in src/gazebo/testing.py
assert_has_link ¶
assert_has_link(
body: Mapping[str, Any] | Sequence[Any],
rel: str,
*,
type: str | None = None,
href_suffix: str | None = None,
) -> dict[str, Any]
Assert body carries a link with rel (and optionally type/href).
Returns the matched link so callers can make further assertions.
Source code in src/gazebo/testing.py
assert_problem ¶
assert_problem(
response: Any,
*,
status: int | None = None,
type: str | None = None,
) -> dict[str, Any]
Assert response is an RFC 7807/9457 problem (content-type and shape).
response is any object with status_code, headers, and json()
(an httpx / Starlette TestClient response). Returns the parsed body.
Source code in src/gazebo/testing.py
drive_pagination ¶
drive_pagination(
client: Any,
url: str,
*,
items_key: str,
method: str = 'GET',
body: Any = None,
rel: str = 'next',
limit: int | None = None,
max_pages: int = 1000,
request_kwargs: Mapping[str, Any] | None = None,
) -> list[Any]
Follow rel (next by default) links to exhaustion; return all items.
Asserts the envelope invariants on every page — numberReturned matches the
item count, and (if limit is given) no page exceeds it — and guards against a
runaway/looping next link. For POST-driven pagination, a body member on
the next link is carried into the next request (per STAPI).
request_kwargs is forwarded to every client.request call, so an
authenticated service can pass request_kwargs={'headers': {...}} without
wrapping the client.
Source code in src/gazebo/testing.py
gazebo_link_context ¶
Isolate the link-context contextvar for a test, so a leak can't bleed.
Opt in by requesting this fixture. It is deliberately not autouse: this
plugin auto-registers wherever gazebo and pytest are installed, and forcing an
autouse fixture onto every unrelated test in a downstream suite would be too
intrusive. Make it autouse in your own conftest.py if you want that::
@pytest.fixture(autouse=True)
def _isolate(gazebo_link_context): ...
Source code in src/gazebo/testing.py
gazebo_overrides ¶
A fresh :class:~gazebo.di.Overrides to populate and pass to an app factory.
Dependency injection¶
gazebo.di ¶
gazebo.di — a small, framework-agnostic, type-driven injection container.
Extraction-ready: depends only on the standard library, never on gazebo's OGC code or any web framework.
Recipe ¶
Recipe = Callable[
...,
T
| Awaitable[T]
| Iterator[T]
| AsyncIterator[T]
| AbstractContextManager[T]
| AbstractAsyncContextManager[T],
]
A callable building T: sync/async function, (async) generator, or (async) CM.
Container ¶
A configured, validated injection container.
Source code in src/gazebo/di/container.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
check ¶
Validate the graph: missing deps, scope mismatch, cycles. Raises on error.
Source code in src/gazebo/di/container.py
graph ¶
Adjacency of the dependency DAG (for visualization/debugging).
Source code in src/gazebo/di/container.py
reachable_app_keys ¶
App-scoped keys reachable from entry_types (dead-provider elimination).
Source code in src/gazebo/di/container.py
open_app_scope
async
¶
Enter the app scope, optionally eagerly building reachable app providers.
Source code in src/gazebo/di/container.py
open_request_scope
async
¶
open_request_scope(
app_state: ScopeState,
*,
root: Any,
name: str = 'request',
) -> AsyncIterator[ScopeState]
Enter a request (operation) scope as a child of the app scope.
Source code in src/gazebo/di/container.py
DIError ¶
ScopeState ¶
An entered scope: a resolution cache + teardown stack, plus parent/root.
Source code in src/gazebo/di/container.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | |
health_probes ¶
Yield (label, probe) for every resolved value carrying a __health__.
A value's __health__ is its health-check callable (sync or async, returning
a truthy "ok"). Only values already resolved in this scope are probed — for
an app scope that is everything eagerly built at startup (see
:meth:Container.open_app_scope). Exposed here so callers (e.g. the FastAPI
health endpoint) ask the scope for its probes rather than reaching into the
resolution cache.
Source code in src/gazebo/di/container.py
get
async
¶
Resolve a value for key_type within this scope's lineage.
Source code in src/gazebo/di/container.py
Binding
dataclass
¶
HasProvide ¶
Bases: Protocol
A type that colocates its own recipe as a __provide__ classmethod.
Source code in src/gazebo/di/providers.py
Key
dataclass
¶
A registry key: a type plus an optional qualifier.
Source code in src/gazebo/di/providers.py
Overrides
dataclass
¶
A typed layer of replacements for bound recipes/values.
Mechanically a partial Providers layer: it replaces a binding's recipe (or
supplies a constant instance), inheriting the binding's scope.
Source code in src/gazebo/di/providers.py
Providers ¶
The central registry binding each type to a scope (and its recipe).
Source code in src/gazebo/di/providers.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
Qualify
dataclass
¶
Annotation marker to disambiguate duplicate types.
def h(db: Annotated[Database, Qualify('replica')]): ...
Source code in src/gazebo/di/providers.py
parse_annotation ¶
Split a type annotation into (base type, Qualify qualifier, metadata).
For Annotated[T, ...] returns T (when it is a class), the qualifier from
any :class:Qualify marker, and the remaining Annotated metadata. For a plain
annotation the metadata tuple is empty. A non-class base resolves to None so
callers can treat it as unresolvable.
Source code in src/gazebo/di/providers.py
FastAPI integration¶
gazebo.ext.fastapi ¶
FastAPI glue.
Turns a central :class:~gazebo.di.Providers registry into a working app:
GazeboApp enters the app scope in its lifespan, opens a request scope per
request (publishing the link RequestContext), and resolves bound types injected
into routes. Routes opt into bare-type injection by being declared on a
GazeboRouter (or directly on the app): any parameter whose type carries a
__provide__ recipe, or is marked Annotated[T, Inject], is resolved from the
per-request DI scope.
This is the only part of gazebo that imports fastapi (the gazebo[fastapi]
extra). It is organized as a package — one module per concern (injection, OGC param
adapters, CORS, response helpers, routers, app wiring) — but the public surface is
flat: import everything straight from gazebo.ext.fastapi.
BBoxParam
module-attribute
¶
Parses the OGC bbox query value into a :class:~gazebo.params.BBox.
DatetimeParam
module-attribute
¶
Parses the OGC datetime query value into a :class:~gazebo.params.DatetimeInterval.
Cors ¶
How to configure CORS: None/False off, True permissive, a list of
allowed origins, or a full :class:CorsConfig.
Overrides
dataclass
¶
A typed layer of replacements for bound recipes/values.
Mechanically a partial Providers layer: it replaces a binding's recipe (or
supplies a constant instance), inheriting the binding's scope.
Source code in src/gazebo/di/providers.py
Providers ¶
The central registry binding each type to a scope (and its recipe).
Source code in src/gazebo/di/providers.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
GazeboApp ¶
Bases: FastAPI
A FastAPI app wired from a :class:Providers registry (thin over :func:upgrade).
Source code in src/gazebo/ext/fastapi/app.py
RequestContextAdapter ¶
Adapts a FastAPI Request to the :class:RequestContext protocol.
Source code in src/gazebo/ext/fastapi/context.py
CorsConfig
dataclass
¶
A CORS policy for a gazebo app, mirroring Starlette's CORSMiddleware.
The permissive defaults (allow_origins=['*'] with credentials off) are what
cors=True selects — fine for local development, but tighten allow_origins
for anything browser-facing in production. allow_origins=['*'] with
allow_credentials=True is rejected by browsers, so credentials default off.
Source code in src/gazebo/ext/fastapi/cors.py
resolve
classmethod
¶
Normalize a loose cors= argument into a config (None means off).
None/False → off, True → permissive defaults, a string or list →
an allow-list of origins, a :class:CorsConfig → itself.
Source code in src/gazebo/ext/fastapi/cors.py
apply ¶
Install this policy on app as a CORSMiddleware layer.
The field names mirror CORSMiddleware's parameters one-for-one, so the
config is the keyword set — asdict keeps the two in sync with no
hand-maintained mapping. Call it last in upgrade so CORS ends up the
outermost middleware (headers ride on every response, including problems).
Source code in src/gazebo/ext/fastapi/cors.py
GazeboRouter ¶
Bases: APIRouter
An APIRouter that rewrites routes for bare-type injection at decoration.
Source code in src/gazebo/ext/fastapi/routers.py
LinkedRouter ¶
Bases: GazeboRouter
A :class:GazeboRouter that auto-generates a hierarchical landing page.
Mounts a landing endpoint at its root; include_router of another
LinkedRouter (that declares a rel) adds a link to that child's landing
page, so the hierarchy falls out of router nesting.
Source code in src/gazebo/ext/fastapi/routers.py
RootRouter ¶
Bases: LinkedRouter
The service's root landing page: hierarchy plus service-level wiring.
A :class:LinkedRouter for the top of the tree. Beyond the hierarchical landing
page, its landing page additionally:
- emits
service-desc/service-doclinks to the app's OpenAPI document and its docs UI (each omitted when the app has that URL disabled), - falls back its
title/descriptionto the app's when not set explicitly (so the service name lives in one place — on the app), and - links to a
/conformancedeclaration it auto-mounts. That declaration's baseline (core/landing-page/json, plusoas30when the app exposes OpenAPI) is derived from the running app, then merged with any conformance classes you contribute — so the declaration stays honest instead of drifting from what's actually wired.
Contribute feature-level classes via conformance= (a :class:Conformance or a
list of class URIs), e.g. conformance=[*filter_conformance_classes()].
Source code in src/gazebo/ext/fastapi/routers.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
etag_for ¶
Derive an ETag from value (a model, mapping, str, or bytes).
The value is reduced to canonical bytes — a pydantic model via
model_dump_json(by_alias=True), anything else via sorted-key JSON — and hashed
(SHA-256). The result is a quoted entity-tag, prefixed W/ when weak (the
default).
Note
A model carrying deferred (callable-href) links only serializes inside an active request context; outside one, ETag such a model from its underlying data rather than the link-bearing envelope.
Source code in src/gazebo/caching.py
forward_lifespans ¶
A lifespan that runs each mounted sub-app's lifespan.
Use when mounting a GazeboApp under a root app, since a mounted sub-app's
lifespan is not run automatically::
root = FastAPI(lifespan=forward_lifespans(sub))
root.mount('/api', sub)
Source code in src/gazebo/ext/fastapi/app.py
upgrade ¶
upgrade(
app: FastAPI,
providers: Providers | None = None,
*,
overrides: Overrides | None = None,
trust: TrustPolicy = trust_none,
cors: Cors = None,
health_path: str | None = '/health',
) -> FastAPI
Add gazebo's injection/context machinery to an existing FastAPI app.
Equivalent to constructing a :class:GazeboApp, but applied to an app you did
not create (e.g. one built by a framework or with custom config). Wraps the
app's lifespan (opening the app scope), installs the proxy-headers and
request-scope middleware, registers the problem handlers, and rewrites
@app.get routes for injection. Injectable routes still belong on a
GazeboRouter (or @app.get on this app). Idempotent.
Source code in src/gazebo/ext/fastapi/app.py
FilterParam ¶
FilterParam(
queryables: Queryables,
*,
engine: FilterEngine | None = None,
name: str = 'filter',
lang_name: str = 'filter-lang',
crs_name: str = 'filter-crs',
crs_allowed: Sequence[str] = (CRS84,),
) -> Any
Build a Depends resolving the OGC filter parameter into a :class:Filter.
Drop it into a route as filter: Annotated[Filter | None, FilterParam(QUERYABLES)].
An absent filter resolves to None. filter-lang selects the encoding (else
it is inferred); filter-crs is validated against crs_allowed. A parse/validation
failure, an unknown language, an unsupported CRS, or a reference to a non-queryable
property each become a 400 problem.
Source code in src/gazebo/ext/fastapi/filtering.py
SortByParam ¶
Build a Depends resolving the OGC/STAC sortby parameter into a :class:SortBy.
sortables is the allow-list (a :class:Sortables resource or a set of field names);
a term naming a field outside it — or malformed sortby syntax — is a 400
problem. An absent sortby resolves to None.
Source code in src/gazebo/ext/fastapi/filtering.py
inject_signature ¶
Rewrite endpoint so injectable params resolve from the DI scope.
Injectable parameters are discovered via :func:_candidate_params (which resolves
each annotation independently and leniently), so an injectable parameter still wires
even when a sibling annotation cannot be resolved. Idempotent: include_router
re-invokes this on the same endpoint, so the first application is recorded and later
calls return early.
Source code in src/gazebo/ext/fastapi/injection.py
CrsParam ¶
CrsParam(
allowed: Sequence[str] = (CRS84,),
*,
name: str = 'crs',
default: str | None = None,
) -> Any
Build a Depends validating a crs/bbox-crs URI against an allow-list.
Pass name='bbox-crs' for the companion parameter. A value outside allowed
raises ParamError (-> 400). When the parameter is absent, it resolves to:
- the explicit
default(which must be inallowed), if given; else - :data:
~gazebo.params.CRS84— the OGC default output CRS — if it is allowed; else - nothing: with a non-default allow-list and no marked default there is no safe assumption, so the parameter is required and an absent value is a 400.
Source code in src/gazebo/ext/fastapi/params.py
Negotiate ¶
Negotiate(
available: Sequence[Representation],
*,
default: Representation | None = None,
name: str = 'f',
) -> Any
Build a Depends resolving the negotiated representation from ?f=/Accept.
Drop the result into a route as rep: Annotated[Representation, Negotiate([JSON,
HTML])]: the endpoint then branches on rep (e.g. render HTML vs return the
model) and can attach :func:~gazebo.negotiation.alternate_links. An unknown ?f=
becomes a 400 and an unsatisfiable Accept a 406, both as problem+json.
Source code in src/gazebo/ext/fastapi/params.py
not_modified ¶
not_modified(
request: Request,
*,
etag: str | None = None,
last_modified: datetime | None = None,
cache_control: str | None = None,
) -> Response | None
Return a 304 Not Modified response if the request's preconditions match.
Reads If-None-Match / If-Modified-Since from request and compares them
against the supplied etag / last_modified (see
:func:gazebo.caching.is_not_modified for the precedence rules). Returns a ready
304 carrying the validators when they match, else None — so the caller
proceeds to build the full response.
Pass the same cache_control you set on the 200 path: per RFC 9111 §4.3.4 a
304 should refresh the cache's freshness directives, so omitting it would make a
revalidating cache fall back to stale or more-conservative behavior::
@router.get('/thing', response_model=Thing)
async def thing(request: Request, response: Response):
obj = load_thing()
tag = etag_for(obj)
if (resp := not_modified(request, etag=tag, cache_control='max-age=60')) is not None:
return resp
set_cache_headers(response, etag=tag, cache_control='max-age=60')
return obj
Source code in src/gazebo/ext/fastapi/responses.py
set_cache_headers ¶
set_cache_headers(
response: Response,
*,
etag: str | None = None,
last_modified: datetime | None = None,
cache_control: str | None = None,
) -> None
Stamp ETag / Last-Modified / Cache-Control onto response.
The companion to :func:not_modified: set the validators on the success response
so the next request can be made conditional.
Source code in src/gazebo/ext/fastapi/responses.py
set_link_header ¶
set_link_header(
response: Response,
links: Sequence[Link],
*,
rels: Sequence[str] | None = NAV_RELS,
max_links: int = DEFAULT_MAX_LINKS,
) -> None
Set an RFC 8288 Link header on response from links.
A peer of :func:set_cache_headers: call it inside an endpoint to mirror a
response's navigational links into a Link header, so non-JSON clients and
crawlers can follow them without parsing the body. links is any sequence of
:class:~gazebo.link.Link (a collection envelope's .links, or a hand-built
list) — it is not tied to any response type.
Deferred (callable) hrefs are resolved against the active request context, so this
must be called within a request. Only navigational rels (:data:NAV_RELS) are
emitted by default and the count is capped at max_links; pass rels=None to
include every rel. Sets nothing when nothing qualifies.
Source code in src/gazebo/ext/fastapi/responses.py
CLI / serving¶
gazebo.ext.cli ¶
Server-agnostic CLI toolkit: self-documenting settings options for a serve command.
This is a topmost, optional layer — like ext/fastapi it sits above the core and
must not be imported by it. It imports click and pydantic-settings only
(never a web server); :mod:gazebo.ext.uvicorn builds the batteries-included uvicorn
serve command on top of these pieces, but you can compose the same pieces atop any
server (granian, hypercorn, ...) with no uvicorn dependency. Requires the gazebo[cli]
extra.
The building blocks:
- :func:
settings_options— one documented, self-propagatingclick.Optionper settings field, so--helpshows every setting, its env var, default, and description (the self-documentation), and a passed option writes its env var so the value reaches the app (and any server workers) through the environment they already read. No serialization, no transport across the worker boundary. - :func:
secrets_epilog— the--helpepilog documenting secret fields (their env var, requiredness) without accepting them as flags, so a composed command can document secrets without ever putting them on the command line. - :func:
default_log_config— a complete dictConfig that survives worker spawn.
A settings option is self-propagating — it carries a callback that writes its env var
when passed — so you can drop it onto your own click command (renamed, reordered,
alongside your own options) and it still reaches the app with no export step of your own.
Secrets are never accepted on the command line: model them as SecretStr and they get
no value flag, only a documented entry in --help (their env var) via
:func:secrets_epilog, so they stay out of shell history / ps. Supply them via the
settings class's secrets_dir (pydantic-settings reads /run/secrets-style files)
or env.
JsonFormatter ¶
Bases: Formatter
Minimal structured formatter: one JSON object per line. The access logger's
rendered line lands in message; fully-structured access fields (status, path
as separate keys) are a later enhancement if needed.
Source code in src/gazebo/ext/cli.py
default_log_config ¶
default_log_config(
level: str = 'INFO',
*,
json_logs: bool = False,
request_id: bool = False,
) -> dict[str, Any]
A complete dictConfig so a server's loggers coexist with app loggers
(disable_existing_loggers=False), error + access are formatted consistently,
and it re-applies cleanly in every spawned worker.
The console (non-json_logs) format names uvicorn.logging.DefaultFormatter /
AccessFormatter as () dictConfig strings; these are lazy string references
resolved by logging.config at config time, not imports here — but they do assume
uvicorn is installed when the config is applied. The json_logs mode uses only
:class:JsonFormatter and has no uvicorn dependency at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
str
|
Log level for the server loggers and the root logger. |
'INFO'
|
json_logs
|
bool
|
Emit one JSON object per line (for log aggregation) instead of
the console format. Pin per environment, e.g.
|
False
|
request_id
|
bool
|
Wire :class: |
False
|
Source code in src/gazebo/ext/cli.py
settings_options ¶
settings_options(
settings_cls: type[BaseSettings],
*,
exclude: Collection[str] = (),
rename: Mapping[str, str] | None = None,
) -> list[click.Parameter]
One self-documenting, self-propagating click.Option per non-secret field.
Each option is prefixed by the class's (required) env_prefix (e.g.
--app-host) so it namespaces cleanly against the server's own options and other
settings groups, carries its env var for --help, and — via a callback — writes
that env var when passed, so the value reaches the app with no export step of your
own. Options are expose_value=False: they act purely by side effect, so they
don't appear in the command callback's signature.
Compose the lists from several classes to expose more than one group on one command
([*settings_options(A), *settings_options(B)]); each class needs a distinct
env_prefix. This is the presentation half of serve_command, exposed so a
custom CLI can attach these options to its own command.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings_cls
|
type[BaseSettings]
|
The |
required |
exclude
|
Collection[str]
|
Field names to omit. Use this to drop a field you pin to a constant — set its env var yourself (or leave the app's default to stand) instead. |
()
|
rename
|
Mapping[str, str] | None
|
|
None
|
No type gating: an option just writes a string to the env var, and pydantic
deserializes it exactly as it does for env loading — so an option can carry whatever
an env var can (scalars directly; complex types as a JSON string). Secret fields
(SecretStr/SecretBytes) get no option, so a secret never lands on the
command line.
Source code in src/gazebo/ext/cli.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
secrets_epilog ¶
Render a --help epilog documenting secret fields as a configuration surface —
their env vars, but no value-accepting flag, so secrets never land in shell history
or ps. Supply them via the environment or the class's secrets_dir.
Returns None when no class declares a secret field, so a composed command can use
it directly as its epilog regardless.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
type[BaseSettings] | Sequence[type[BaseSettings]]
|
A single |
required |
Source code in src/gazebo/ext/cli.py
gazebo.ext.uvicorn ¶
Uvicorn assembly for the serve command: an argv-boundary over uvicorn's CLI.
This sits above :mod:gazebo.ext.cli (the server-agnostic toolkit) and is the only
module here that imports uvicorn. It treats uvicorn as a CLI, not a library: the
only execution-path coupling is uvicorn's documented command-line interface. Instead of
copying uvicorn's option params and delegating to its callback (three couplings to
uvicorn internals), :func:serve forwards documented argv to
uvicorn.main.main(args=..., standalone_mode=False) and lets uvicorn do its own
parsing, defaults, UVICORN_* env vars, and value transforms.
Discoverability splits cleanly: serve --help documents app configuration (our
contribution via :func:gazebo.ext.cli.settings_options), while serve --help-server
prints uvicorn's own help. The --help-server path is a display-only coupling
(uvicorn.main.get_help) that fails soft — a broken help screen never stops a server.
Requires the gazebo[uvicorn] extra (which pulls in gazebo[cli] plus uvicorn
itself). The import uvicorn here resolves to the real package, mirroring
ext/fastapi's import fastapi.
serve ¶
Launch app via uvicorn by forwarding documented CLI argv to its console entry
point (uvicorn.main.main(args=..., standalone_mode=False)).
uvicorn_args are exactly uvicorn's documented command-line arguments, so
serve('pkg.mod:app', '--workers', '4') mirrors uvicorn pkg.mod:app --workers
4. Uvicorn does its own parsing, defaults, UVICORN_* env vars, and value
transforms (--header x:y splitting, --app-dir on sys.path, ...). An
unknown flag gets uvicorn's own error (with its "did you mean" suggestion): raised as
a :class:click.UsageError when called outside a click context, and surfaced as a
normal usage error when called inside one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app
|
str | Any
|
A |
required |
*uvicorn_args
|
str
|
Uvicorn CLI arguments, forwarded verbatim after the injected
|
()
|
factory
|
bool
|
Force factory mode for a string |
False
|
log_config
|
Any
|
dictConfig loading for uvicorn. |
None
|
Source code in src/gazebo/ext/uvicorn.py
serve_command ¶
serve_command(
app: str | Any,
*,
settings: type[BaseSettings]
| Sequence[type[BaseSettings]]
| None = None,
name: str = 'serve',
factory: bool = False,
log_config: Any = None,
uvicorn_args: Sequence[str] = (),
) -> click.Command
Build a click serve command for app.
serve --help documents your app's configuration (one option per settings
field); every uvicorn option is still accepted and forwarded verbatim to uvicorn (run
serve --help-server to list them). Operator arguments on the command line follow
the author's uvicorn_args defaults, so — being later — the operator can override
them (uvicorn_args=('--workers', '4') is a default an operator overrides with
--workers 8).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app
|
str | Any
|
A |
required |
settings
|
type[BaseSettings] | Sequence[type[BaseSettings]] | None
|
A pydantic-settings class or a sequence of them. Each becomes a
self-documenting option group, namespaced by its (required, distinct)
|
None
|
name
|
str
|
The command name (default |
'serve'
|
factory
|
bool
|
Force factory mode for a string |
False
|
log_config
|
Any
|
dictConfig for uvicorn; defaults to
:func: |
None
|
uvicorn_args
|
Sequence[str]
|
Author-supplied uvicorn CLI defaults, forwarded before operator arguments so an operator can override them at the command line. |
()
|
Source code in src/gazebo/ext/uvicorn.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |