Python SDK

geopera is the official Python client for the Geopera platform — a single, fully type-annotated package whose fluent Geopera client gives you a typed call for every operation, over a generated lower-level client you can drop into whenever you need finer control.

Overview

Geopera is an RPC-over-HTTP platform: every capability — catalog search, archive and tasking orders, the data platform, billing — is an operation identified by a dotted id such as catalog.search or orders.tasking.estimate, and every operation is a POST whose meaning is carried in the operation name rather than in an HTTP verb or URL path. There are no GET, PUT, or DELETE routes to learn; you always send a JSON body and receive a typed result.

The SDK exposes this in two layers:

  • The fluent Geopera client (recommended). You construct one client and call operations as client.<resource>.<action>(body) — for example client.catalog.search({...}). The resource path mirrors the operation id, the body is a plain dict or the typed input model, and the return is the typed output model (or a typed error model). This is the surface most code should use.
  • The generated layer (escape hatch). Underneath sits an AuthenticatedClient plus one free-function module per operation, each offering four call variants (sync, sync_detailed, asyncio, asyncio_detailed). The fluent client wraps this layer and holds a configured instance of it at client.client. Drop down to it when you want async calls, explicit Response objects with status codes and headers, or an operation the fluent surface does not yet expose.

The package is published on PyPI as geopera and ships py.typed, so every input, output, and error is visible to your type checker. It is generated from the live OpenAPI document, so it never drifts from the API.

Install

bash
pip install geopera

Requires Python 3.11+. The only runtime dependencies are httpx and attrs.

Hello, world

Construct a Geopera client with your token and call a resource action. The body can be a plain dict or the matching typed input model; the return is the typed output model.

python
from geopera import Geopera

client = Geopera(token="gpra_...")   # a Geopera API key — or a session token (obtained by signing in to Geopera)

result = client.catalog.search({
    "host_name": "earthsearch-aws",
    "collections": ["sentinel-2-l2a"],
    "bbox": [151.0, -34.0, 152.0, -33.0],
    "limit": 10,
})
print(result)   # -> CatalogSearchOutput (typed)

That is the whole pattern. The action name mirrors the operation id: catalog.search is client.catalog.search(...), and deeply nested operations nest too — orders.archive.place is client.orders.archive.place({...}). Swap in any operation from the Operations reference and you have its typed call.

The fluent client

The fluent client mirrors operation ids directly. Attribute access descends a resource path; calling the node at the end invokes the operation. The id is split on dots: everything up to the last segment is the resource path you walk with attribute access, and the final segment is the action you call.

Operation idFluent call
catalog.searchclient.catalog.search({...})
orders.archive.placeclient.orders.archive.place({...})
orders.tasking.estimateclient.orders.tasking.estimate({...})
orders.tasking.templates.saveclient.orders.tasking.templates.save({...})

Each action accepts either a plain dict or the typed input model from geopera.models, and returns the typed output model. When you pass a dict, the fluent client converts it to the operation’s input model for you, so these two calls are equivalent:

python
from geopera import Geopera
from geopera.models import CatalogSearchInput

client = Geopera(token="gpra_...")

# A plain dict (converted to CatalogSearchInput under the hood)
client.catalog.search({"host_name": "earthsearch-aws", "limit": 10})

# The typed input model directly — editor autocomplete and type checking on every field
client.catalog.search(CatalogSearchInput(host_name="earthsearch-aws", limit=10))

An operation that takes no body can be called with no arguments — client.<resource>.<action>() — and the SDK sends an empty body. The full catalogue of operation ids, scopes, and request bodies lives in the Operations reference. The RPC-over-HTTP model (no GET/PUT/DELETE verbs, meaning carried in the operation name) is explained in Concepts.

Detailed responses from the fluent client

By default a fluent call returns the parsed model. Pass detailed=True to get the full typed Response instead, with the HTTP status code, headers, and the raw bytes alongside the parsed model:

python
from geopera import Geopera

client = Geopera(token="gpra_...")

resp = client.catalog.search({"host_name": "earthsearch-aws", "limit": 10}, detailed=True)
print(resp.status_code)   # 200
print(resp.headers)       # response headers
print(resp.parsed)        # CatalogSearchOutput (typed)

The fluent surface is synchronous. For async/await, use the generated operation modules described under Lower-level layer — their asyncio and asyncio_detailed variants give you the same typed results in an async context.

Authentication

The Geopera client sends Authorization: Bearer <token>. The token is either a minted Geopera API key (prefix gpra_, for headless server-to-server use) or a session token (obtained by signing in to Geopera). Both are bearer tokens used the same way — there is no token exchange, refresh, or grant step. Geopera accepts either credential and runs every call as that principal, with its scopes, audit, and provenance.

python
from geopera import Geopera

# 1) A Geopera API key (headless / server-to-server)
client = Geopera(token="gpra_...")

# 2) A session token (obtained by signing in to Geopera)
client = Geopera(token=session_token)

Do not hard-code the token — read it from the environment or a secret manager:

python
import os
from geopera import Geopera

client = Geopera(
    base_url=os.environ.get("GEOPERA_API_URL", "https://api.geopera.com"),
    token=os.environ["GEOPERA_API_TOKEN"],
)

A Geopera constructed without a token uses the unauthenticated Client and can only reach operations that do not require a credential; secured operations return an authentication error. See Authentication for how to mint keys and what each credential authorises, Scopes for the permission model, and the dedicated SDK authentication page for the SDK specifics.

Error handling

Geopera returns errors as RFC 9457 application/problem+json. The SDK models that as a typed Problem, and 422 input-validation failures as HTTPValidationError. A fluent action returns a union of the success model and these error models, so the failure path is something you check rather than something that throws:

python
from geopera import Geopera
from geopera.models import Problem

client = Geopera(token="gpra_...")

result = client.catalog.search({"host_name": "earthsearch-aws"})

if isinstance(result, Problem):
    print("geopera error:", result.status, result.title, result.detail)
else:
    data = result                            # CatalogSearchOutput

Problem carries type_, title, status, and detail. When you need the HTTP status code or response headers on the failure path, pass detailed=True (or use the lower-level *_detailed variants) and read resp.status_code alongside resp.parsed:

python
from geopera.models import Problem

resp = client.catalog.search({"host_name": "earthsearch-aws"}, detailed=True)
if resp.status_code == 200:
    data = resp.parsed                       # CatalogSearchOutput
elif isinstance(resp.parsed, Problem):
    print(resp.status_code, resp.parsed.title, resp.parsed.detail)

Note — By default, an undocumented status code parses to None rather than raising. Pass raise_on_unexpected_status=True when constructing the client to turn those into an UnexpectedStatus exception instead.

See the SDK errors page and the platform Errors reference for the full taxonomy (402 payment, 409 conflict, 422 validation, 429 rate limits).

Async

The fluent client is synchronous. For concurrency, call the generated operation module’s asyncio (parsed model) or asyncio_detailed (typed Response) variant against an AuthenticatedClient used as an async context manager, so its underlying httpx.AsyncClient is closed cleanly:

python
import asyncio
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput

async def main():
    async with AuthenticatedClient(base_url="https://api.geopera.com", token="gpra_...") as client:
        result = await catalog_search.asyncio(
            client=client,
            body=CatalogSearchInput(host_name="earthsearch-aws", limit=10),
        )
        print(result)

asyncio.run(main())

You can reuse the fluent client’s already-configured client here too: await catalog_search.asyncio(client=Geopera(token="gpra_...").client, body=...). See Async for concurrency patterns and connection reuse.

Pagination

Operations that page accept a cursor in the body and return the next cursor in the output — there are no page/query params. For catalog search, pass the returned next value back as next_:

python
from geopera import Geopera

client = Geopera(token="gpra_...")

cursor = None
while True:
    page = client.catalog.search({
        "host_name": "earthsearch-aws",
        "collections": ["sentinel-2-l2a"],
        "limit": 100,
        "next_": cursor,
    })
    for feature in page.features:
        ...                       # process each result
    cursor = getattr(page, "next_", None)
    if not cursor:
        break

The general cursor model is described in Pagination; the per-operation details are on SDK pagination.

Lower-level layer: generated client and operation modules

The fluent Geopera client is built on a generated layer that you can use directly when you need finer control — async calls, explicit Response objects, all four call variants, or an operation not yet on the fluent surface. The fluent client already holds a configured instance of it:

python
from geopera import Geopera

client = Geopera(token="gpra_...")
raw = client.client          # the underlying AuthenticatedClient

You can also construct that AuthenticatedClient yourself and call the free-function module for an operation. Each module is named after the operation id with dots (and hyphens) replaced by underscores, so catalog.search lives at geopera.api.operations.catalog_search and orders.tasking.estimate at geopera.api.operations.orders_tasking_estimate:

python
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchInput

client = AuthenticatedClient(
    base_url="https://api.geopera.com",
    token="gpra_...",
)

resp = catalog_search.sync_detailed(
    client=client,
    body=CatalogSearchInput(host_name="earthsearch-aws", limit=10),
)
print(resp.status_code, resp.parsed)

Each operation module exposes four functions. They all take client= and body= as keyword arguments:

FunctionReturnsUse when
sync(client, body=...)the parsed model (or None)the common synchronous call
sync_detailed(client, body=...)a Response wrapping the parsed modelyou need the HTTP status code or headers
asyncio(client, body=...)the parsed model (awaitable)the common asynchronous call
asyncio_detailed(client, body=...)a Response (awaitable)async + status/headers

A Response (from geopera.types) carries status_code (an http.HTTPStatus), content (raw bytes), headers, and parsed (the typed model, or None for an undocumented status). Adding an operation to the platform adds a typed module here on the next release. See Operations for how the operation modules and the four call variants work.

How the package is laid out

geopera
├── client.py        -> Geopera, Client, AuthenticatedClient
├── api/operations/  -> one module per operation (e.g. catalog_search)
├── models/          -> request/response/error models (CatalogSearchInput, ...)
├── errors.py        -> UnexpectedStatus
├── types.py         -> UNSET, Unset, Response, File
└── py.typed

Three import sites cover the lower-level layer:

You want…Import fromExample
The clientgeoperafrom geopera import Geopera, AuthenticatedClient
An operationgeopera.api.operationsfrom geopera.api.operations import catalog_search
A modelgeopera.modelsfrom geopera.models import CatalogSearchInput

geopera.types adds the building blocks the generated code uses: UNSET/Unset for “field not provided” (distinct from an explicit None), Response for the detailed variants, and File for streamed file bodies.

Configuration

Geopera accepts token and base_url positionally or by keyword, and forwards any remaining keyword arguments to the underlying AuthenticatedClient (or Client when no token is given). The underlying client is attrs-defined and accepts the httpx knobs:

python
import httpx
from geopera import Geopera

client = Geopera(
    base_url="https://api.geopera.com",
    token="gpra_...",
    timeout=httpx.Timeout(30.0),
    verify_ssl=True,
    follow_redirects=False,
    headers={"X-Request-Source": "my-app"},
    httpx_args={"http2": True},
    raise_on_unexpected_status=True,
)
KeywordDefaultPurpose
base_urlhttps://api.geopera.comAPI root; every request is relative to this
tokenBearer credential (gpra_... key or session token)
timeouthttpx defaulthttpx.Timeout; exceeding it raises httpx.TimeoutException
verify_sslTrueTLS verification (bool, CA path, or ssl.SSLContext)
follow_redirectsFalseWhether to follow HTTP redirects
headers{}Extra headers sent with every request
cookies{}Cookies sent with every request
httpx_args{}Passed straight to the httpx.Client/AsyncClient constructor (e.g. http2, proxies)
raise_on_unexpected_statusFalseRaise UnexpectedStatus on undocumented status codes

The underlying client is immutable; with_headers(...), with_cookies(...), and with_timeout(...) return a new client with the override applied:

python
raw = client.client
scoped = raw.with_headers({"X-Trace-Id": "abc123"})

For full control you can supply your own transport via set_httpx_client(...) / get_httpx_client(...) (and the *_async_httpx_client equivalents) on client.client — note these override the other settings on the client. See Configuration for timeouts, retries, and proxies.

Worked example: search, then estimate

A typical two-step flow — search a host’s catalog, then price a tasking estimate — using the fluent client, a context manager so the connection is reused and closed, and typed error handling:

python
import os
from geopera import Geopera
from geopera.models import Problem

client = Geopera(
    base_url=os.environ.get("GEOPERA_API_URL", "https://api.geopera.com"),
    token=os.environ["GEOPERA_API_TOKEN"],   # gpra_... key or a session token
)

with client:
    result = client.catalog.search({
        "host_name": "earthsearch-aws",
        "collections": ["sentinel-2-l2a"],
        "bbox": [151.0, -34.0, 152.0, -33.0],
        "datetime_": "2024-01-01T00:00:00Z/2024-12-31T23:59:59Z",
        "limit": 25,
    })

    if isinstance(result, Problem):
        raise SystemExit(f"{result.status} {result.title}: {result.detail}")

    for feature in result.features:
        print(feature.id, feature.properties.get("datetime"))

    estimate = client.orders.tasking.estimate({
        "host_name": "earthsearch-aws",
        "geometry": {"type": "Point", "coordinates": [151.2, -33.8]},
        "datetime_": "2025-01-01T00:00:00Z/2025-01-31T23:59:59Z",
    })

    if isinstance(estimate, Problem):
        raise SystemExit(f"{estimate.status} {estimate.title}: {estimate.detail}")

    print("estimated price:", estimate)

Versioning

The SDK follows Semantic Versioning, and the version tracks the Geopera API contract — breaking API changes bump the major version. See the changelog for what shipped.

Explore

Links