Client Configuration
The recommended Python entry point is Geopera — a fluent, namespaced client you construct once with a token and then call as client.<resource>.<action>(body). Every configuration knob is a keyword argument on the constructor, and any keyword you pass beyond token and base_url is forwarded verbatim to the underlying httpx client. So configuring the SDK is, in practice, configuring httpx with a few Geopera-specific knobs for auth.
from geopera import Geopera
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
result = client.catalog.search({"collections": ["sentinel-2-l2a"], "limit": 10})The token is your bearer token — either a gpra_-prefixed API key or a session token minted by a browser sign-in. Both are used the same way; there is no token-exchange or refresh step. See Authentication for where to get one. base_url defaults to https://api.geopera.com, so for the production API you only need a token.
Under the hood Geopera wraps one of two immutable attrs client classes that the package also exports directly:
AuthenticatedClient— adds a bearer token and the auth-header knobs. This is whatGeoperabuilds when you pass atoken, and it backs every authenticated operation.Client— the same surface without a token, for the rare unauthenticated request.Geoperabuilds this whentokenis omitted.
Both are the low-level escape hatch: client.client on a Geopera instance exposes the underlying AuthenticatedClient so you can reach its builders and the raw httpx client.
Constructor options
Pass all options as keyword arguments to Geopera(...). Only token is needed for authenticated use; base_url defaults to the production API. Everything else is forwarded to the httpx client.
| Option | Type | Default | Purpose |
|---|---|---|---|
token | str | None | Bearer token: a gpra_ API key or a session token. Omit it to build an unauthenticated Client. |
base_url | str | "https://api.geopera.com" | Root URL; every request is made relative to it. |
prefix | str | "Bearer" | Scheme placed before the token in the auth header. |
auth_header_name | str | "Authorization" | Header the token is written to. |
timeout | httpx.Timeout | None | Per-request timeout. None uses httpx’s default (5s). |
verify_ssl | str \| bool \| ssl.SSLContext | True | TLS verification. Keep True in production. |
follow_redirects | bool | False | Whether to follow 3xx redirects. |
headers | dict[str, str] | {} | Default headers sent with every request. |
cookies | dict[str, str] | {} | Default cookies sent with every request. |
httpx_args | dict[str, Any] | {} | Extra kwargs forwarded verbatim to the httpx.Client / httpx.AsyncClient constructor. The hook for transports, proxies, and pool limits. |
raise_on_unexpected_status | bool | False | Raise errors.UnexpectedStatus for status codes not in the OpenAPI document. |
A fully configured client:
import httpx
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
base_url="https://api.geopera.com",
timeout=httpx.Timeout(30.0, connect=5.0),
verify_ssl=True,
follow_redirects=False,
headers={"User-Agent": "acme-pipeline/1.4"},
raise_on_unexpected_status=True,
)Internally, Geopera(token=..., base_url=..., **client_kwargs) constructs AuthenticatedClient(base_url=base_url, token=token, **client_kwargs) (or Client(base_url=base_url, **client_kwargs) when token is None). Every knob below is therefore identical whether you set it on Geopera or on the raw client — the fluent client just threads the keyword through.
Base URL
base_url is the only host-level setting. Every operation is sent as a POST to a path relative to it. Point it at the production API (the default), a regional gateway, or a local mock during testing:
from geopera import Geopera
# Production (default — base_url can be omitted)
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
# A gateway in front of the API, or a local mock
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
base_url="http://localhost:8787",
)Authentication knobs
The token is written into the auth header lazily, the first time the underlying httpx client is built. The value is computed as:
# prefix + space + token, or just the bare token when prefix is empty
header_value = f"{prefix} {token}" if prefix else token
headers[auth_header_name] = header_valueWith the defaults (prefix="Bearer", auth_header_name="Authorization") a request carries:
POST /v1/op/catalog.search HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/jsonThis is what the Geopera API expects, so you rarely change these. They exist for the edge cases:
- Bare token (no scheme). Set
prefix=""to send the raw token with noBearerprefix. - Custom header. Set
auth_header_name="X-Api-Key"(or similar) to send the token somewhere other thanAuthorization— useful behind a proxy that reservesAuthorizationfor itself.
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
prefix="", # send the token with no scheme
auth_header_name="X-Api-Key", # send it in a custom header
)The auth header is materialised once, when the httpx client is first constructed. Changing the token,
prefix, orauth_header_nameafter the first request has no effect on an already-built client — create a newGeoperainstead.
The same token works whether it is a gpra_ API key or a session token from a browser sign-in: the API key is the bearer token, so there is no exchange, OIDC, or refresh round-trip. See Authentication for the difference between the two and where to mint each.
Timeouts
timeout accepts an httpx.Timeout. When a request exceeds it, the operation raises httpx.TimeoutException. Pass a single number for an overall budget, or break it out per phase:
import httpx
from geopera import Geopera
# 30s overall
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
timeout=httpx.Timeout(30.0),
)
# Fine-grained: 5s to connect, 60s to read (e.g. long-running op responses)
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
timeout=httpx.Timeout(60.0, connect=5.0),
)Leaving timeout=None (the default) defers to httpx’s built-in 5-second default — usually too tight for large operation payloads, so set it explicitly. To change the timeout on an already-built client without rebuilding it, use with_timeout (see Builder methods).
TLS verification
verify_ssl is forwarded to httpx’s verify. It accepts:
True(default) — verify against the system trust store. Use this in production.False— disable verification. Only for local testing against a self-signed dev gateway; never in production.- a path string to a CA bundle, or a pre-built
ssl.SSLContextfor custom roots or mTLS.
import ssl
from geopera import Geopera
ctx = ssl.create_default_context(cafile="/etc/ssl/corp-root.pem")
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
base_url="https://gateway.internal",
verify_ssl=ctx,
)Redirects
follow_redirects is False by default. The Geopera API does not rely on redirects for operation calls, so leave this off unless a proxy or load balancer in front of the API issues a redirect you explicitly want the client to chase.
Default headers and cookies
headers and cookies are merged into every request. Use headers for a custom User-Agent, a tracing header, or a tenant selector your gateway reads:
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
headers={"User-Agent": "acme-pipeline/1.4", "X-Trace-Source": "nightly-etl"},
cookies={"route": "eu-west"},
)Do not set Idempotency-Key here as a default — it must be unique per write. See Idempotency.
Builder methods
When you need a variant of an existing client, reach the underlying client through client.client and use its builders. Three methods return a new client derived from the current one, leaving the original untouched. If the underlying httpx client has already been built, the new values are also pushed onto it.
| Method | Returns | Effect |
|---|---|---|
with_headers(headers) | new client | Merge additional default headers. |
with_cookies(cookies) | new client | Merge additional default cookies. |
with_timeout(timeout) | new client | Replace the timeout configuration. |
import httpx
from geopera import Geopera
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
# A variant for a slow batch job, with extra tracing — the original is unchanged.
batch_client = (
client.client
.with_timeout(httpx.Timeout(120.0))
.with_headers({"X-Trace-Source": "batch-reprocess"})
)batch_client here is a raw AuthenticatedClient. You can call operations on it directly through the low-level module surface (see The low-level escape hatch). For most cases it is simpler to construct a second Geopera with the variant settings up front:
import httpx
from geopera import Geopera
base = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
batch = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
timeout=httpx.Timeout(120.0),
headers={"X-Trace-Source": "batch-reprocess"},
)The builders matter most when you are already working with the escape-hatch client and want short-lived variants without mutating shared state.
The underlying httpx client
You can reach (or replace) the httpx client directly via client.client. This is the extension point for anything not exposed as a constructor knob — connection pool limits, proxies, transports, and retries.
| Method | Purpose |
|---|---|
get_httpx_client() | Return the sync httpx.Client, building it from your config if not yet created. |
set_httpx_client(client) | Replace the sync client with one you built. |
get_async_httpx_client() | Return the async httpx.AsyncClient, building it on demand. |
set_async_httpx_client(client) | Replace the async client. |
from geopera import Geopera
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
# Reach the raw httpx.Client (constructs it if needed)
raw_httpx = client.client.get_httpx_client()
set_httpx_client/set_async_httpx_clientoverride all other settings on the client — includingheaders,cookies,timeout, and the auth header. If you swap in your own httpx client, you are responsible for setting theAuthorizationheader yourself.
import httpx
from geopera import Geopera
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
# You own the auth header now — set it yourself.
client.client.set_httpx_client(
httpx.Client(
base_url="https://api.geopera.com",
headers={"Authorization": "Bearer gpra_xxxxxxxxxxxxxxxxxxxxxxxx"},
timeout=httpx.Timeout(45.0),
)
)Context managers
The underlying clients are context managers. Entering opens the connection pool; exiting closes it. Use with for sync and async with for async so connections are cleaned up:
from geopera import Geopera
client = Geopera(token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx")
with client.client:
client.catalog.search({"collections": ["sentinel-2-l2a"], "limit": 10})
# pool closed on exitYou cannot enter the same client twice — that is an httpx constraint on the underlying client. For a long-lived service, build one Geopera at startup and reuse it; httpx pools and reuses connections across calls.
Proxies
There is no dedicated proxy knob — route through httpx via httpx_args. Pass a single proxy URL with proxy, or per-scheme mounts with mounts:
import httpx
from geopera import Geopera
# Single proxy for all traffic
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
httpx_args={"proxy": "http://proxy.internal:8080"},
)
# Per-scheme transports (e.g. only proxy HTTPS)
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
httpx_args={
"mounts": {
"https://": httpx.HTTPTransport(proxy="http://proxy.internal:8080"),
}
},
)Connection pool limits
Tune the connection pool through httpx_args with httpx.Limits — useful when running many concurrent operations and you want to cap or widen the pool:
import httpx
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
httpx_args={
"limits": httpx.Limits(max_connections=20, max_keepalive_connections=10),
},
)Retries
There are no built-in retries. A failed request raises immediately. You add retries yourself through httpx_args by supplying an httpx.HTTPTransport (or AsyncHTTPTransport) with a retries count — this retries connection-level failures (DNS, connect, broken pipe):
import httpx
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
httpx_args={"transport": httpx.HTTPTransport(retries=3)},
)For an async client, pass an AsyncHTTPTransport:
import httpx
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
httpx_args={"transport": httpx.AsyncHTTPTransport(retries=3)},
)httpx’s transport retries only covers connection establishment — it does not retry on HTTP status codes like 429 or 503. To retry on responses (with backoff and Retry-After), wrap the transport in a custom transport that inspects the response. The example below honours rate-limit responses and retries a small set of statuses:
import time
import httpx
from geopera import Geopera
class RetryTransport(httpx.HTTPTransport):
"""Retry idempotent-safe statuses with backoff, honouring Retry-After."""
RETRY_STATUSES = {429, 502, 503, 504}
def __init__(self, *args, max_retries: int = 3, **kwargs):
super().__init__(*args, **kwargs)
self.max_retries = max_retries
def handle_request(self, request: httpx.Request) -> httpx.Response:
attempt = 0
while True:
response = super().handle_request(request)
if response.status_code not in self.RETRY_STATUSES or attempt >= self.max_retries:
return response
response.read()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2.0 ** attempt
response.close()
time.sleep(delay)
attempt += 1
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
httpx_args={"transport": RetryTransport(retries=3, max_retries=4)},
)Because every operation is invoked as a POST (RPC over HTTP), only retry writes that are safe to repeat. Send an Idempotency-Key on retried writes so a duplicate delivery is collapsed server-side — see Idempotency.
Unexpected statuses
raise_on_unexpected_status is False by default: a response whose status code is not described in the API’s OpenAPI document is returned to you as-is for inspection. Set it to True to instead raise errors.UnexpectedStatus and fail fast in pipelines.
from geopera import Geopera
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
raise_on_unexpected_status=True,
)This is orthogonal to normal API errors — documented error responses are RFC 9457 application/problem+json bodies that the typed operation functions surface for you. See Errors.
The low-level escape hatch
Geopera is a thin wrapper. When you need the typed Response object, the async variants, or the per-operation sync / sync_detailed / asyncio / asyncio_detailed functions, drop to the raw AuthenticatedClient plus the generated operation modules. Construct the client directly, or reuse the one Geopera already built via client.client:
import httpx
from geopera import AuthenticatedClient
from geopera.api.operations import catalog_search
from geopera.models import CatalogSearchBody
client = AuthenticatedClient(
base_url="https://api.geopera.com",
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
timeout=httpx.Timeout(60.0, connect=5.0),
raise_on_unexpected_status=True,
)
body = CatalogSearchBody.from_dict({"collections": ["sentinel-2-l2a"], "limit": 10})
# Parsed model only
result = catalog_search.sync(client=client, body=body)
# Full typed Response (status code, headers, parsed body)
response = catalog_search.sync_detailed(client=client, body=body)
# Async equivalents
# result = await catalog_search.asyncio(client=client, body=body)
# response = await catalog_search.asyncio_detailed(client=client, body=body)Every configuration knob on AuthenticatedClient is exactly the one you would pass to Geopera — Geopera simply forwards them. The escape-hatch client and a fluent Geopera built with the same kwargs behave identically. See the Python SDK reference for the full operation surface.
Worked example
A production-grade client built through the fluent surface: explicit timeouts, connection retries plus status-aware retries, a tracing header, pool limits, and strict failure on unexpected statuses.
import time
import httpx
from geopera import Geopera
class RetryTransport(httpx.HTTPTransport):
RETRY_STATUSES = {429, 502, 503, 504}
def __init__(self, *args, max_retries: int = 4, **kwargs):
super().__init__(*args, **kwargs)
self.max_retries = max_retries
def handle_request(self, request: httpx.Request) -> httpx.Response:
attempt = 0
while True:
response = super().handle_request(request)
if response.status_code not in self.RETRY_STATUSES or attempt >= self.max_retries:
return response
response.read()
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2.0 ** attempt)
response.close()
attempt += 1
client = Geopera(
token="gpra_xxxxxxxxxxxxxxxxxxxxxxxx",
base_url="https://api.geopera.com",
timeout=httpx.Timeout(60.0, connect=5.0),
verify_ssl=True,
headers={"User-Agent": "acme-pipeline/2.0"},
raise_on_unexpected_status=True,
httpx_args={
"transport": RetryTransport(retries=3, max_retries=4),
"limits": httpx.Limits(max_connections=20, max_keepalive_connections=10),
},
)
with client.client:
result = client.catalog.search({"collections": ["sentinel-2-l2a"], "limit": 10})
# the pool is closed on exitNext steps
- Authentication — minting a
gpra_key versus using a session token. - Errors — the
application/problem+jsonshape returned on failures. - Rate limits —
Retry-Afterand the429you retry against. - Idempotency — making retried writes safe.
- Python SDK reference — calling operations once the client is configured.