An async client for Gundi's API
Project description
Gundi Client v2
An async Python client for the Gundi API. Gundi (a.k.a. "The Portal") is a platform for managing wildlife conservation integrations — connecting sensors, cameras, and other data sources to analytical platforms like EarthRanger.
This library provides two clients:
GundiClient— query connections, integrations, routes, and tracesGundiDataSenderClient— post observations, events, and messages
Installation
pip install gundi-client-v2
Quick Start
import asyncio
from gundi_client_v2 import GundiClient
async def main():
async with GundiClient() as client:
connection = await client.get_connection_details(
integration_id="your-integration-uuid"
)
print(connection.provider.name)
if __name__ == "__main__":
asyncio.run(main())
Configure the client via environment variables (see Configuration) or pass values directly. The Quick Start snippet needs more than credentials at runtime — at minimum a base_url / GUNDI_API_BASE_URL, an OAUTH_CLIENT_ID, and either an OAUTH_ISSUER or oauth_token_url. Example with kwargs:
client = GundiClient(
username="you@example.com",
password="your-password",
oauth_client_id="your-client-id",
oauth_audience="your-audience",
oauth_token_url="https://auth.example.com/.../token",
base_url="https://api.example.com",
)
End-to-End: List, Get Key, Send Data
import asyncio
import os
from gundi_client_v2 import GundiClient, GundiDataSenderClient
async def main():
async with GundiClient() as client:
# 1. List integrations and find yours by name
my_integration = None
async for i in client.get_integrations():
if i.name == "My Integration":
my_integration = i
break
if my_integration is None:
raise RuntimeError("Integration 'My Integration' not found")
# 2. Get the API key for that integration
api_key = await client.get_integration_api_key(
integration_id=str(my_integration.id)
)
# 3. Use the API key to send data
sender = GundiDataSenderClient(
integration_api_key=api_key,
sensors_api_base_url=os.environ["SENSORS_API_BASE_URL"], # or pass the URL directly
)
await sender.post_events(data=[
{
"title": "Animal Detected",
"event_type": "wildlife_sighting_rep",
"recorded_at": "2024-01-15T10:30:00Z",
"location": {"lat": -1.286, "lon": 36.817},
"event_details": {"species": "lion"},
}
])
if __name__ == "__main__":
asyncio.run(main())
Configuration
Settings can be provided as environment variables or constructor keyword arguments. The two namespaces differ in several places — see the tables below.
Environment variables
| Env var | Description | Default |
|---|---|---|
GUNDI_USERNAME |
Your Gundi username (email) — required for password grant | — |
GUNDI_PASSWORD |
Your Gundi password — required for password grant | — |
OAUTH_CLIENT_ID |
OAuth client ID | — |
OAUTH_CLIENT_SECRET |
OAuth client secret — required for client-credentials grant | — |
OAUTH_ISSUER |
OIDC issuer URL. When set without OAUTH_TOKEN_URL, the token endpoint is discovered at runtime from {OAUTH_ISSUER}/.well-known/openid-configuration. A trailing slash is normalized. |
— |
OAUTH_TOKEN_URL |
Full OAuth token endpoint URL. When set, overrides OIDC discovery from OAUTH_ISSUER. |
— |
OAUTH_AUDIENCE |
OAuth audience. Sent to the token endpoint when set. Required by some IdPs (e.g. Auth0 won't issue a usable API access token without it); ignored by others (Keycloak password grant). | — |
OAUTH_SCOPE |
OAuth scope | openid |
GUNDI_API_BASE_URL |
Gundi API base URL | — |
SENSORS_API_BASE_URL |
Sensors/routing API base URL (used by GundiDataSenderClient) |
— |
GUNDI_API_SSL_VERIFY |
Verify SSL certificates | true |
LOG_LEVEL |
Logging level (env-only) | INFO |
GUNDI_CLIENT_ENVFILE |
Path to a .env file to load (env-only; defaults to .env in cwd) |
— |
GundiClient constructor kwargs
| Kwarg | Corresponding env var | Description |
|---|---|---|
base_url |
GUNDI_API_BASE_URL |
Gundi API base URL |
use_ssl |
GUNDI_API_SSL_VERIFY |
Verify SSL certificates |
username |
GUNDI_USERNAME |
Gundi username |
password |
GUNDI_PASSWORD |
Gundi password |
oauth_client_id |
OAUTH_CLIENT_ID |
OAuth client ID |
oauth_client_secret |
OAUTH_CLIENT_SECRET |
OAuth client secret |
oauth_token_url |
OAUTH_TOKEN_URL |
Full OAuth token endpoint URL. When set, used as-is. |
oauth_issuer |
OAUTH_ISSUER |
OIDC issuer URL. When set without oauth_token_url, the token endpoint is discovered via OIDC discovery. |
oauth_audience |
OAUTH_AUDIENCE |
OAuth audience. IdP-dependent — required for Auth0, optional for Keycloak password grant. |
oauth_scope |
OAUTH_SCOPE |
OAuth scope |
max_http_retries |
— | Max HTTP retries (default 5) |
connect_timeout |
— | Connect timeout in seconds (default 3.1) |
data_timeout |
— | Data timeout in seconds (default 20) |
GundiDataSenderClient constructor kwargs
| Kwarg | Corresponding env var | Description |
|---|---|---|
integration_api_key |
— | API key for the integration (required) |
sensors_api_base_url |
SENSORS_API_BASE_URL |
Sensors/routing API base URL |
Authentication
The client supports two authentication modes:
Password grant (for developers) — Provide GUNDI_USERNAME and GUNDI_PASSWORD along with OAUTH_CLIENT_ID. This is the recommended approach for external developers.
Client credentials grant (for services) — Provide OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET. This is used by internal backend services.
Token URL resolution
The client resolves the OAuth token endpoint in this order:
oauth_token_url(kwarg) /OAUTH_TOKEN_URL(env) — used as-is when set.oauth_issuer(kwarg) /OAUTH_ISSUER(env) — the client fetches{issuer}/.well-known/openid-configuration(OIDC discovery) and uses itstoken_endpoint. Result is cached for the process lifetime; callgundi_client_v2.auth.clear_discovery_cache()to invalidate.- Neither set —
AuthenticationError("No token URL configured. Set oauth_token_url or oauth_issuer.")is raised on the first auth attempt.
OIDC discovery works for any compliant IdP (Keycloak, Auth0, Okta, …). Configure OAUTH_ISSUER and the token endpoint is found automatically.
Audience
OAUTH_AUDIENCE is sent to the token endpoint when configured. Whether it is required depends on the IdP:
- Auth0 — required. Without
audience, Auth0 issues an opaque token that cannot authorize API requests. - Keycloak — optional. Both grant types this library supports (password and client_credentials) ignore it.
The parameter name audience reflects the Auth0/Keycloak convention. The OAuth 2.0 / OIDC standard equivalent is resource (RFC 8707, Resource Indicators). If we add support for IdPs that strictly require resource instead, it will be introduced as a resource kwarg alongside audience, not as a rename. Existing OAUTH_AUDIENCE / oauth_audience configurations will keep working.
Backward compatibility: The legacy
KEYCLOAK_*env var names (KEYCLOAK_ISSUER,KEYCLOAK_CLIENT_ID,KEYCLOAK_CLIENT_SECRET,KEYCLOAK_AUDIENCE) are still accepted as fallbacks for the correspondingOAUTH_*env vars. Three legacy constructor kwargs are also still accepted:keycloak_client_id,keycloak_client_secret, andkeycloak_audience. There is nokeycloak_issuerconstructor kwarg — useoauth_token_urloroauth_issuerinstead.
If both are configured, password grant takes precedence. Contact the Gundi team for credentials.
Usage: GundiClient
Use GundiClient as an async context manager to query the Gundi API.
import asyncio
from gundi_client_v2 import GundiClient
async def main():
async with GundiClient() as client:
# List integrations you have access to (async generator — paginated)
async for integration in client.get_integrations():
print(integration.name, integration.id)
# Find an integration by name and get its API key
target = None
async for i in client.get_integrations():
if i.name == "My Integration":
target = i
break
if target is None:
raise RuntimeError("Integration 'My Integration' not found")
api_key = await client.get_integration_api_key(
integration_id=str(target.id)
)
# List connections
connections = await client.get_connections()
# Get connection details
connection = await client.get_connection_details(
integration_id="some-uuid"
)
# Get integration details
integration = await client.get_integration_details(
integration_id="some-uuid"
)
# List routes (optionally pass params= for server-side filtering)
routes = await client.get_routes()
# List routes where a connection is the data provider
routes = await client.get_routes_for_connection(
connection_id="some-provider-uuid"
)
# Get route details
route = await client.get_route_details(route_id="some-uuid")
# Create a route
new_route = await client.create_route(data={
"name": "TrapTagger → ER (events)",
"owner": "your-org-uuid",
"data_providers": ["provider-integration-uuid"],
"destinations": ["destination-integration-uuid"],
})
# Update a route (partial update via PATCH)
updated_route = await client.update_route(
route_id="some-uuid",
data={"name": "Renamed Route"},
)
# Delete a route
await client.delete_route(route_id="some-uuid")
# Query traces
traces = await client.get_traces(params={"object_id": "some-uuid"})
# Register an integration type
integration_type = await client.register_integration_type(
data={"name": "MyIntegration", "value": "my_integration"}
)
if __name__ == "__main__":
asyncio.run(main())
You can also create a client without a context manager and close it manually:
import asyncio
from gundi_client_v2 import GundiClient
async def main():
client = GundiClient()
try:
connection = await client.get_connection_details(
integration_id="some-uuid"
)
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())
Usage: GundiDataSenderClient
Use GundiDataSenderClient to post data through the Gundi sensors/routing API. This client authenticates with an integration API key rather than user credentials. sensors_api_base_url is required — you can set it via the SENSORS_API_BASE_URL environment variable (as the example script does) or pass it directly.
import asyncio
import os
from gundi_client_v2 import GundiDataSenderClient
async def main():
sender = GundiDataSenderClient(
integration_api_key="your-api-key",
sensors_api_base_url=os.environ["SENSORS_API_BASE_URL"], # or pass the URL directly
)
# Post observations
await sender.post_observations(data=[
{
"source": "device-123",
"source_name": "GPS Tracker",
"type": "tracking-device",
"recorded_at": "2024-01-15T10:30:00Z",
"location": {"lat": -1.286389, "lon": 36.817223},
}
])
# Post events
await sender.post_events(data=[
{
"title": "Animal Detected",
"event_type": "wildlife_sighting_rep",
"recorded_at": "2024-01-15T10:30:00Z",
"location": {"lat": -1.286389, "lon": 36.817223},
"event_details": {"species": "lion"},
}
])
# Post messages
await sender.post_messages(data=[
{
"sender": "ranger-radio-1",
"recipients": ["hq@example.org"],
"text": "All clear at checkpoint.",
"recorded_at": "2024-01-15T10:30:00Z",
}
])
# Update an event
await sender.update_event(
event_id="event-uuid",
data={"title": "Updated Title"},
)
# Post event attachments
with open("photo.jpg", "rb") as f:
await sender.post_event_attachments(
event_id="event-uuid",
attachments=[("photo.jpg", f.read())],
)
if __name__ == "__main__":
asyncio.run(main())
Command-Line Interface
An optional gundi command wraps GundiClient for common integration tasks. It
ships in the cli extra (keeps Typer out of the core install):
pip install "gundi-client-v2[cli]"
Authentication reuses the same environment variables as the library (see
Configuration). The CLI needs GUNDI_API_BASE_URL,
OAUTH_CLIENT_ID, plus:
- Credentials — either
GUNDI_USERNAME+GUNDI_PASSWORD(password grant, for developers) orOAUTH_CLIENT_SECRET(client-credentials, for services). - Token endpoint —
OAUTH_ISSUER(preferred; resolved via OIDC discovery) or an explicitOAUTH_TOKEN_URL.
OAUTH_AUDIENCE is forwarded when set. A .env file in the working directory
is loaded automatically.
# List all integrations (table: ID, NAME, TYPE, ENABLED, STATUS)
gundi integrations list
# Filter by integration type, by enabled state, and emit JSON for piping to jq
gundi integrations list --type earth_ranger
gundi integrations list --enabled # only enabled (--disabled for only disabled)
gundi integrations list --status healthy # by health status (healthy/unhealthy/disabled)
gundi integrations list --type earth_ranger --enabled --status unhealthy
gundi integrations list --json | jq '.[].name'
# List the available integration types (table: NAME, SLUG, ID)
gundi integrations types
# Read the latest activity logs for an integration or a whole type
gundi integrations logs 338225f3-91f9-4fe1-b013-353a229ce504
gundi integrations logs --type earth_ranger --limit 100
# Filter logs by level (matches that level and above), origin, and a date range
gundi integrations logs 338225f3-91f9-4fe1-b013-353a229ce504 --level error
gundi integrations logs --type earth_ranger --level warning --origin dispatcher \
--since 2026-07-01 --until 2026-07-06
# Enable / disable an integration by id
gundi integrations enable 338225f3-91f9-4fe1-b013-353a229ce504
gundi integrations disable 338225f3-91f9-4fe1-b013-353a229ce504
Run gundi --help or gundi integrations --help for the full command list.
Exit codes: 0 success · 1 API or authentication error (a clean
Error: … message, no traceback) · 2 missing or invalid configuration.
Named environments
Instead of exporting auth env vars each time, save named environments and switch
between them. Config lives in ~/.config/gundi/config.json (mode 0600);
secrets are never written — only the OAuth tokens derived from them, cached
per environment under ~/.config/gundi/tokens/.
# Add environments (connection config only — no secrets)
gundi env add prod --base-url https://api.gundi.example.com \
--client-id my-client --issuer https://auth.example.com/realms/prod
gundi env add dev --base-url https://api.dev.example.com \
--client-id my-client --issuer https://auth.example.com/realms/dev --username me@example.com
gundi env use prod # set the active environment
gundi env list # '*' marks the active one
gundi env show prod
# Authenticate once; the token is cached and reused by later commands.
# The secret/password is read from env (OAUTH_CLIENT_SECRET / GUNDI_PASSWORD)
# or prompted (hidden) — and never stored.
gundi auth login # grant chosen by the env's config
gundi auth login --username me@example.com # force the password grant
gundi auth status
gundi integrations list # reuses the cached token; no re-auth
# Override the active environment per command:
gundi integrations list --profile dev
gundi auth logout
Environment selection precedence: --profile flag → GUNDI_PROFILE env var
→ stored active environment → raw OAUTH_* / GUNDI_* env vars (the original
behavior, used when no environments are configured).
Token refresh: password-grant environments refresh transparently using the
cached refresh token. Client-credentials environments (no refresh token) require
gundi auth login again once the access token expires.
Error Handling
The client raises specific exceptions for different failure modes:
import asyncio
from gundi_client_v2 import GundiClient, AuthenticationError, GundiAPIError, GundiClientError
async def main():
async with GundiClient() as client:
try:
connection = await client.get_connection_details(
integration_id="some-uuid"
)
except AuthenticationError as e:
# OAuth token request failed (bad credentials, expired, etc.)
print(f"Auth failed: {e}")
except GundiAPIError as e:
# Non-2xx response from the API
print(f"API error {e.status_code}: {e.detail}")
except GundiClientError as e:
# Base exception for all client errors
print(f"Client error: {e}")
if __name__ == "__main__":
asyncio.run(main())
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gundi_client_v2-3.5.0.tar.gz.
File metadata
- Download URL: gundi_client_v2-3.5.0.tar.gz
- Upload date:
- Size: 206.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b773d4d95f878dddbc8982060d31491a2835c8ef9ad52df4647b32f92a9115c
|
|
| MD5 |
c7e14cd0d8ad6a3f61243ce19b77eaf9
|
|
| BLAKE2b-256 |
eccbcc9a7f39bbb613f58c74def865d0650ec1516b2eaf4dd42ed5027c0e11bb
|
Provenance
The following attestation bundles were made for gundi_client_v2-3.5.0.tar.gz:
Publisher:
release.yml on PADAS/gundi-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gundi_client_v2-3.5.0.tar.gz -
Subject digest:
0b773d4d95f878dddbc8982060d31491a2835c8ef9ad52df4647b32f92a9115c - Sigstore transparency entry: 2085459306
- Sigstore integration time:
-
Permalink:
PADAS/gundi-client@bfba226d9c1f3778d8da6206a234fc507e687901 -
Branch / Tag:
refs/tags/v3.5.0 - Owner: https://github.com/PADAS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bfba226d9c1f3778d8da6206a234fc507e687901 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gundi_client_v2-3.5.0-py3-none-any.whl.
File metadata
- Download URL: gundi_client_v2-3.5.0-py3-none-any.whl
- Upload date:
- Size: 38.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbbe68d77ca1bc71914e9f08ff892319026fab4db4ff93f11ff5a5928998dccd
|
|
| MD5 |
25e281d34c7b20f1a9fe5fb3a2a1fed9
|
|
| BLAKE2b-256 |
0ced9974aabea541fa53fc8a378a50b45e2b9f54db44e12c5607e17d02b7b670
|
Provenance
The following attestation bundles were made for gundi_client_v2-3.5.0-py3-none-any.whl:
Publisher:
release.yml on PADAS/gundi-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gundi_client_v2-3.5.0-py3-none-any.whl -
Subject digest:
cbbe68d77ca1bc71914e9f08ff892319026fab4db4ff93f11ff5a5928998dccd - Sigstore transparency entry: 2085459363
- Sigstore integration time:
-
Permalink:
PADAS/gundi-client@bfba226d9c1f3778d8da6206a234fc507e687901 -
Branch / Tag:
refs/tags/v3.5.0 - Owner: https://github.com/PADAS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bfba226d9c1f3778d8da6206a234fc507e687901 -
Trigger Event:
push
-
Statement type: