Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 231 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,245 @@
# Gundi Client
## Introduction
[Gundi](https://www.earthranger.com/), a.k.a "The Portal" is a platform to manage integrations.
The gundi-client is an async python client to interact with Gundi's REST API.
# Gundi Client v2

An async Python client for the [Gundi](https://www.earthranger.com/) 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 traces
- **`GundiDataSenderClient`** — post observations, events, and messages

## Installation
```

```bash
pip install gundi-client-v2
```

## Usage
## Quick Start

```python
from gundi_client_v2 import GundiClient

async with GundiClient() as client:
connection = await client.get_connection_details(
integration_id="your-integration-uuid"
)
print(connection.provider.name)
```

Set your credentials via environment variables (see [Configuration](#configuration)) or pass them directly:

```python
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

```python
from gundi_client_v2 import GundiClient, GundiDataSenderClient

async with GundiClient() as client:
# 1. List integrations and find yours by name
integrations = await client.get_integrations()
my_integration = next(i for i in integrations if i.name == "My Integration")

# 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)
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"},
}
])
```

## Configuration

All settings can be provided as environment variables or keyword arguments to the client constructor.

### Developer Authentication

| Variable | Description | Required |
|---|---|---|
| `GUNDI_USERNAME` | Your Gundi username (email) | Yes (for password auth) |
| `GUNDI_PASSWORD` | Your Gundi password | Yes (for password auth) |

### Service Authentication

| Variable | Description | Required |
|---|---|---|
| `OAUTH_CLIENT_ID` | OAuth client ID | Yes |
| `OAUTH_CLIENT_SECRET` | OAuth client secret | Yes (for client credentials auth) |

### Common

| Variable | Description | Default |
|---|---|---|
| `OAUTH_ISSUER` | OAuth issuer URL (token URL is derived as `{issuer}/protocol/openid-connect/token`) | — |
| `OAUTH_AUDIENCE` | OAuth audience | — |
| `GUNDI_API_BASE_URL` | Gundi API base URL | — |
| `SENSORS_API_BASE_URL` | Sensors/routing API base URL | — |
| `GUNDI_API_SSL_VERIFY` | Verify SSL certificates | `true` |
| `LOG_LEVEL` | Logging level | `INFO` |

## 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.

> **Note:** The legacy `KEYCLOAK_*` env var names (`KEYCLOAK_ISSUER`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET`, `KEYCLOAK_AUDIENCE`) and `keycloak_*` constructor kwargs are still accepted for backward compatibility.

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.

```python
from gundi_client_v2 import GundiClient
import httpx

# You can use it as an async context-managed client
async with GundiClient() as client:
try:
# List integrations you have access to
integrations = await client.get_integrations()
for integration in integrations:
print(integration.name, integration.id)

# Find an integration by name and get its API key
target = next(i for i in integrations if i.name == "My Integration")
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-integration-uuid"
)
except httpx.RequestError as e:
logger.exception("Request Error")
...
except httpx.TimeoutException as e:
logger.exception("Request timed out")
...
except httpx.HTTPStatusError as e:
logger.exception("Response returned error")
else:
for integration in connection.destinations:
...
...

# Or create an instance and close the client explicitly later
integration_id="some-uuid"
)

# Get integration details
integration = await client.get_integration_details(
integration_id="some-uuid"
)

# Get route details
route = await client.get_route_details(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"}
)
```

You can also create a client without a context manager and close it manually:

```python
client = GundiClient()
try:
response = await client.get_connection_details(
integration_id="some-integration-uuid"
)
except httpx.RequestError as e:
logger.exception("Request Error")
...
except httpx.TimeoutException as e:
logger.exception("Request timed out")
...
except httpx.HTTPStatusError as e:
logger.exception("Response returned error")
else:
for integration in connection.destinations:
...
...
await client.close() # Close the session used to send requests to Gundi
connection = await client.get_connection_details(
integration_id="some-uuid"
)
finally:
await client.close()
```

## 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.

```python
from gundi_client_v2 import GundiDataSenderClient

sender = GundiDataSenderClient(integration_api_key="your-api-key")

# 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())],
)
```

## Error Handling

The client raises specific exceptions for different failure modes:

```python
from gundi_client_v2 import GundiClient, AuthenticationError, GundiAPIError, GundiClientError

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}")
```

## License

Apache 2.0
20 changes: 20 additions & 0 deletions examples/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copy this file to .env and fill in your values.
# Required for examples (e.g. send_observations.py)
GUNDI_USERNAME=your-username
GUNDI_PASSWORD=your-password
GUNDI_INTEGRATION_NAME="Your Integration Name"

# OAuth (password grant). Use OAUTH_ISSUER or the full OAUTH_TOKEN_URL.
OAUTH_ISSUER=https://cdip-auth.pamdas.org/auth/realms/cdip-dev
OAUTH_CLIENT_ID=cdip-oauth2
OAUTH_AUDIENCE=cdip-admin-portal
# Optional: full token URL instead of OAUTH_ISSUER
#OAUTH_TOKEN_URL=https://cdip-auth.pamdas.org/auth/realms/cdip-dev/protocol/openid-connect/token

# API base URLs (optional; defaults depend on deployment)
GUNDI_API_BASE_URL=https://api.stage.gundiservice.org
SENSORS_API_BASE_URL=https://sensors.api.stage.gundiservice.org

# Optional
# GUNDI_API_SSL_VERIFY=true
# LOG_LEVEL=INFO
47 changes: 47 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Gundi Client examples

Simple examples for using the Gundi client with **username and password** credentials to access Gundi's API.

## Setup

From the repository root, install the package in editable mode:

```bash
pip install -e .
```

Then set the required environment variables (or pass credentials in code where the example allows it). You can copy `examples/.env.example` to `examples/.env` and fill in your values; the client will load `.env` from the current directory when run.

## Examples

### send_observations.py

Demonstrates:

1. Authenticating with a username and password
2. Querying for integrations
3. Selecting one integration by name and fetching its API key
4. Using `GundiDataSenderClient` with that API key to send observations to Gundi

**Required environment variables:**

- `GUNDI_USERNAME` – your Gundi username
- `GUNDI_PASSWORD` – your Gundi password
- `GUNDI_INTEGRATION_NAME` – exact name of the integration to use for sending (e.g. the display name in the portal)

**Optional (OAuth / API endpoints):**

- `OAUTH_ISSUER` – OAuth issuer base URL (e.g. `https://auth.example.com/auth/realms/my-realm`); token URL is derived as `{OAUTH_ISSUER}/protocol/openid-connect/token`
- `OAUTH_TOKEN_URL` – full OAuth token URL (overrides `OAUTH_ISSUER` if set)
- `OAUTH_CLIENT_ID` – OAuth client id for the password grant
- `OAUTH_AUDIENCE` – OAuth audience
- `GUNDI_API_BASE_URL` – Gundi API base URL (portal/configuration API)
- `SENSORS_API_BASE_URL` – Sensors/ingestion API base URL (used by `GundiDataSenderClient`)

**Run:**

```bash
python examples/send_observations.py
```

Ensure the integration named in `GUNDI_INTEGRATION_NAME` exists in your Gundi deployment and has an API key configured.
Loading