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
Geoperaclient (recommended). You construct one client and call operations asclient.<resource>.<action>(body)— for exampleclient.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
AuthenticatedClientplus 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 atclient.client. Drop down to it when you want async calls, explicitResponseobjects 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
pip install geoperaRequires 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.
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 id | Fluent call |
|---|---|
catalog.search | client.catalog.search({...}) |
orders.archive.place | client.orders.archive.place({...}) |
orders.tasking.estimate | client.orders.tasking.estimate({...}) |
orders.tasking.templates.save | client.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:
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:
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.
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:
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:
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 # CatalogSearchOutputProblem 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:
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
Nonerather than raising. Passraise_on_unexpected_status=Truewhen constructing the client to turn those into anUnexpectedStatusexception 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:
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_:
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:
breakThe 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:
from geopera import Geopera
client = Geopera(token="gpra_...")
raw = client.client # the underlying AuthenticatedClientYou 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:
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:
| Function | Returns | Use when |
|---|---|---|
sync(client, body=...) | the parsed model (or None) | the common synchronous call |
sync_detailed(client, body=...) | a Response wrapping the parsed model | you 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.typedThree import sites cover the lower-level layer:
| You want… | Import from | Example |
|---|---|---|
| The client | geopera | from geopera import Geopera, AuthenticatedClient |
| An operation | geopera.api.operations | from geopera.api.operations import catalog_search |
| A model | geopera.models | from 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:
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,
)| Keyword | Default | Purpose |
|---|---|---|
base_url | https://api.geopera.com | API root; every request is relative to this |
token | — | Bearer credential (gpra_... key or session token) |
timeout | httpx default | httpx.Timeout; exceeding it raises httpx.TimeoutException |
verify_ssl | True | TLS verification (bool, CA path, or ssl.SSLContext) |
follow_redirects | False | Whether 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_status | False | Raise 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:
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:
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
- PyPI: pypi.org/project/geopera
- Source: github.com/geo-pera/geopera-python
- Changelog: CHANGELOG.md