Skip to main content

Everything needed to build OGC-style APIs, under one roof

Project description

gazebo

Documentation

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 Link model whose href can be a callable resolved at serialization time, so links are built without a request in hand.
  • Collection envelopesLinkedCollection[T]: items + links + counts, with a configurable items alias (features, records, ...).
  • 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.
  • OGC bits — RFC 7807 problem responses, landing pages + conformance, pagination links, and typed Rel/MediaType constants.

The core (gazebo) depends only on pydantic. Framework integration is opt-in.

![NOTE] This is an experiment using AI to refine a number of patterns I've established building out APIs over the years. The current implementation mainly targets use with FastAPI, but I've tried to keep the core abstractions agnostic to the framework, and recognize FastAPI is not the only framework that could value from these things.

I acknowledge the documentation is AI slop and does not clearly express the value of these abstractions, but I think the code, while an early version and subject to change, is solid and solves some key problems in convenient and clever ways. The primary goals are to reduce boilerplate and make implementing more robust patterns easier, and I think those goals are realized here.

Install

pip install gazebo            # core: pydantic only
pip install 'gazebo[fastapi]' # + the GazeboApp / FastAPI glue

Requires Python 3.12+.

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, Inject, 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

External types you can't add __provide__ to are bound with a standalone provider and injected with Annotated[T, Inject]:

@asynccontextmanager
async def provide_session(database: Database) -> AsyncIterator[Session]:
    async with database.session() as s:
        yield s

providers.request(Session, provide_session)

@router.get('/x')
async def handler(session: Annotated[Session, Inject]): ...

Composition

gazebo's request machinery (typed injection + proxy-correct link context) lives in GazeboApp; routes that use bare-type injection live on a GazeboRouter. They are a pair — use both. Beyond that, you can mix and match:

Combination Works?
GazeboApp + GazeboRouter (injection) ✅ the intended pairing
GazeboApp + plain/external APIRouter (no injection)
plain FastAPI + GazeboRouter with injection ❌ needs GazeboApp's middleware
root FastAPI mounting a GazeboApp ✅ forward the sub-app's lifespan

External / third-party routers that don't use gazebo injection can be included into a GazeboApp unchanged. If you accidentally put an injectable-typed route on a plain APIRouter, the app fails loudly at startup naming the route (rather than silently treating the parameter as a request body).

Upgrade an existing app you didn't construct (created by a framework, or with custom config) instead of subclassing:

from fastapi import FastAPI
from gazebo.ext.fastapi import upgrade, GazeboRouter, Providers

app = FastAPI(...)              # someone else's app
app.include_router(my_gazebo_router)
upgrade(app, providers)         # adds the middleware, lifespan, handlers, health

Mount a GazeboApp under a root app. A mounted sub-app's lifespan isn't run automatically, so forward it (this is general framework behavior, not gazebo-specific):

from gazebo.ext.fastapi import forward_lifespans

root = FastAPI(lifespan=forward_lifespans(sub_app))
root.mount('/api', sub_app)     # sub_app is a GazeboApp

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, hierarchical landing pages, 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.

Design docs

  • docs/design.md — the OGC/web shapes (links, collections, pagination, problems, landing pages, proxy headers).
  • docs/design-di.md — the injection & state system (providers, recipes, scopes, GazeboApp).
  • docs/examples/wiring.py (stock FastAPI baseline) and wiring_gazeboapp.py (the gazebo version).

Status

Early / pre-1.0. The gazebo.di container is intentionally minimal and extraction-ready (stdlib only); it sits behind a Providers interface so a mature container could be adopted later without changing user code.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gazebo-0.1.0.tar.gz (119.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gazebo-0.1.0-py3-none-any.whl (28.2 kB view details)

Uploaded Python 3

File details

Details for the file gazebo-0.1.0.tar.gz.

File metadata

  • Download URL: gazebo-0.1.0.tar.gz
  • Upload date:
  • Size: 119.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for gazebo-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8c887ba8d5f32a14d96c5e237339d23e8d1eb0cfd625c6f0872ca8ec7110dae2
MD5 311e2d27cd040a9b1117548d2e5e735f
BLAKE2b-256 2c40ba430220c216f84a24d1dc8c60bcd0c7fd4f9d98dc4530366380e8a9046e

See more details on using hashes here.

Provenance

The following attestation bundles were made for gazebo-0.1.0.tar.gz:

Publisher: release.yml on jkeifer/gazebo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gazebo-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: gazebo-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 28.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for gazebo-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b7e95950ff3d8d1f9f45266934f07bb72bd0d459c2ab054794ee0942bef4f2d2
MD5 6752b871b2ab64c04e844df1fc051a42
BLAKE2b-256 e083fc064cc0bea901efe3ebe963ab2c9b3f9a2b217917e156b9a1ce5a5c5169

See more details on using hashes here.

Provenance

The following attestation bundles were made for gazebo-0.1.0-py3-none-any.whl:

Publisher: release.yml on jkeifer/gazebo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page