gazebo
Everything needed to build OGC-style APIs, under one roof.
gazebo packages the recurring machinery of OGC-style services so it doesn't get re-implemented per project:
- Deferred links — a
Linkmodel whosehrefcan be a callable resolved at serialization time, so links are built without a request in hand; including RFC 6570 templated links (templated: true) that leave path/query variables for the client to expand. - Collection envelopes —
LinkedCollection[T]: items + links + counts, with a configurable items alias (features,records, ...), plus first-class GeoJSONFeature/FeatureCollectionfor OGC API Features. - Typed injection & state — a small, framework-agnostic DI container
(
gazebo.di) plus a FastAPI app (GazeboApp) that delivers app- and request-scoped resources as typed parameters, with teardown and parameter-based (not global-mutation) test overrides. - Proxy-aware URLs — pure-ASGI middleware that honors
X-Forwarded-Proto/Host/Prefix(with pluggable trust), so generated links are correct behind a load balancer. - The OGC request/response surface — RFC 7807 problems (with a reusable
ProblemType/ProblemRegistrycatalog of stable, linkabletypeURIs, which the framework's own validation/param400/422errors can draw from too), landing pages + conformance (aRootRouterthat emitsservice-desc/service-docand derives its conformance declaration from the running app), pagination, content negotiation (?f=thenAccept, with every negotiated media type auto-documented in OpenAPI from one representation list), typed OGC query params (bbox/datetime/crs/f) — fully self-documented in OpenAPI and available both asDependsadapters and as field types you fold into your own query model (the closed-setcrs/fasStrEnumbases you subclass) — CQL2 filtering +sortby, conditional requests (ETag / 304), RFC 8288Link:headers, and typedRel/MediaTypeconstants. - A pytest plugin — opt-in helpers that assert the OGC-ness of your service:
link/problem assertions and a pagination driver that walks
nextto exhaustion.
The core (gazebo) depends only on pydantic. Framework integration, GeoJSON, CQL2
filtering, a self-documenting serve CLI, and the test helpers are opt-in extras.
Install
pip install gazebo # core: pydantic only
pip install 'gazebo[fastapi]' # + the GazeboApp / FastAPI glue
pip install 'gazebo[cli]' # + the self-documenting serve CLI toolkit (server-agnostic)
pip install 'gazebo[uvicorn]' # + a batteries-included uvicorn serve command
pip install 'gazebo[geojson]' # + GeoJSON Feature / FeatureCollection
pip install 'gazebo[cql2]' # + CQL2 filtering (cql2-rs engine)
pip install 'gazebo[test]' # + the pytest plugin
Requires Python 3.12+. Full documentation lives at teotl.dev/gazebo.
Quickstart
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Annotated
from fastapi import Request
from gazebo.collection import LinkedCollection
from gazebo.link import Link
from gazebo.ext.fastapi import GazeboApp, GazeboRouter, Overrides, Providers
@dataclass
class Settings:
dsn: str = 'postgres://localhost/app'
@classmethod
def __provide__(cls) -> 'Settings':
return cls()
class Database:
def __init__(self, dsn: str) -> None:
self.dsn = dsn
@classmethod
@asynccontextmanager
async def __provide__(cls, settings: Settings) -> AsyncIterator['Database']:
db = cls(settings.dsn)
try:
yield db # built once (app scope); teardown on shutdown
finally:
... # await db.close()
@dataclass
class User: # request-scoped; derives from the request
name: str
@classmethod
async def __provide__(cls, request: Request) -> 'User':
return cls(request.headers.get('authorization', 'anon'))
class Things(LinkedCollection[dict], items_alias='things'):
pass
router = GazeboRouter()
@router.get('/things', response_model=Things)
async def list_things(db: Database, user: User, limit: int = 10):
items = [{'id': i, 'owner': user.name} for i in range(limit)]
return Things(items=items, links=[Link.self_link(), Link.root_link()])
def create_app(overrides: Overrides | None = None) -> GazeboApp:
providers = Providers()
providers.app(Settings).app(Database).request(User)
app = GazeboApp(providers, overrides=overrides)
app.include_router(router)
@app.get('/', name='landing')
async def landing():
return {'service': 'things'}
return app
app = create_app()
db and user are injected by type — db once per app, user per request.
Tests override by parameter, never by mutating a global:
from fastapi.testclient import TestClient
def test_things():
overrides = Overrides().set(Settings, Settings(dsn='sqlite://'))
with TestClient(create_app(overrides)) as client:
body = client.get('/things?limit=2', headers={'authorization': 'alice'}).json()
assert body['numberReturned'] == 2
GazeboApp and GazeboRouter are an intended pair, but you can mix in plain or
third-party routers, upgrade() an app you didn't construct, and mount a GazeboApp
under a root app. The documentation covers composition,
injecting external types, content negotiation, conditional requests, and the rest.
Example app
examples/garden/ is Gazebo Gardens — a complete,
standalone OGC-style API (a multi-tenant plant catalog) that exercises every
feature: injection with app/request scopes and teardown, qualified bindings,
deferred + paginated links, collection envelopes, RFC 7807 problems with a
ProblemType catalog, CQL2 filtering and sortby, a RootRouter service landing
with conformance, proxy-aware URLs, health, and request-id logging. It's
its own project with its own pyproject.toml, so:
cd examples/garden
uv run garden # serve on http://127.0.0.1:8000
uv run pytest # its test suite
See examples/garden/README.md for a feature map and
curl recipes.
Docs
Full documentation — guides, how-tos, and the generated API reference — lives at teotl.dev/gazebo.
Metadata
Release files for gazebo 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gazebo-0.10.0.tar.gz | 270.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gazebo-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 370.5 kB
Release files / gazebo-0.10.0.tar.gz
| Download URL | gazebo-0.10.0.tar.gz |
|---|---|
| Size | 270.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
570df8a4cb8c3f5b599d5032a542d7292c8a41a142543e5d0c27bbde9b13b085
|
|
BLAKE2b-256 checksum How to use checksums |
771183b57fa1ff7351a8bcae9bd52d238989fc5392efd9d900320c34642bb21f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency logRelease files / gazebo-0.10.0-py3-none-any.whl
| Download URL | gazebo-0.10.0-py3-none-any.whl |
|---|---|
| Size | 99.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
92cee7b20b8c0c3e27bd6703e9fce85bd1d192aec2e55ccfd204239023858a93
|
|
BLAKE2b-256 checksum How to use checksums |
f0c9099e24d1573c74f5e63588d42d6a68764a612d5af8896872ce5b3935ce5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency log