Metadata-Version: 2.1
Name: ampverve
Version: 1.1.0
Summary: AmpVerve Partner API client and webhook signature verifier
Project-URL: Documentation, https://www.ampverve.com/api-docs/
License: Proprietary
Requires-Python: >=3.8
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Description-Content-Type: text/markdown

# AmpVerve Python SDK

A thin, typed client for the AmpVerve Partner API and a webhook signature
verifier. Standard library only, Python 3.8 or later.

Version `1.1.0`. It is not on PyPI: AmpVerve serves the built wheel at
<https://app.ampverve.com/api-docs/sdks/>. Each operation's status label
(live, beta and so on) is in the reference.

## What it covers

Only what an `avk_` API key can call, which is the Partner API under
`/api/devtools/partner/v1`:

| Method | Operation | Scope |
|---|---|---|
| `me()` | `GET /api/devtools/partner/v1/me` | none |
| `list_alerts(status=, since=, limit=, cursor=)`, `alerts_page(...)`, `iter_alerts(status=, since=, page_size=)` | `GET /api/devtools/partner/v1/alerts` | `alerts:read` |
| `list_devices(limit=, cursor=)`, `devices_page(...)`, `iter_devices(page_size=)` | `GET /api/devtools/partner/v1/devices` | `devices:read` |
| `latest_telemetry(asset_id=, hours=, limit=, cursor=)`, `telemetry_page(...)`, `iter_telemetry(asset_id=, hours=, page_size=)` | `GET /api/devtools/partner/v1/telemetry/latest` | `telemetry:read` |
| `sandbox_scenario()` | `GET /api/devtools/partner/v1/sandbox/scenario` | none, sandbox keys only |
| `send_sandbox_test_event(event_type, endpoint_id=, idempotency_key=)` | `POST /api/devtools/partner/v1/sandbox/webhooks/test` | none, sandbox keys only |

plus `verify_signature` and `construct_event` for webhook deliveries.

Every other AmpVerve operation takes a user access token, not an API key, so
it is not in this client. Issuing keys and registering webhook endpoints are
done by an organisation administrator in the console (**Settings, API
access**) or with `POST /api/devtools/apikeys` and `POST /api/devtools/webhooks`.

The full reference is at <https://www.ampverve.com/api-docs/>.

## Install

```bash
pip install https://app.ampverve.com/api-docs/sdks/ampverve-1.1.0-py3-none-any.whl
```

The file's size and sha256 are in
<https://app.ampverve.com/api-docs/sdks/manifest.json>. From a checkout of
the AmpVerve repository, `pip install ./sdks/python` installs the same code.

## Use

```python
import os
from ampverve import AmpVerveClient, PermissionDeniedError

client = AmpVerveClient(os.environ["AMPVERVE_API_KEY"])  # base_url defaults to https://app.ampverve.com

me = client.me()
print(me["organisation_id"], me["environment"], me["key"]["scopes"])

try:
    for device in client.list_devices(limit=10):
        print(device["id"], device["device_type"])
except PermissionDeniedError as exc:
    print(exc.code)  # insufficient_scope when the key lacks devices:read

print(client.last_rate_limit.remaining)  # RateLimit-Remaining of the last response
```

## Pagination

The three lists page with `limit` and an opaque `cursor`. `list_*` returns
one page as a list, as before. `*_page` returns a `Page` with `items`,
`next_cursor` (`None` on the last page) and `has_more`. `iter_*` follows
`next_cursor` lazily, one request per page, and yields every item:

```python
for device in client.iter_devices(page_size=500):
    print(device["id"])

page = client.alerts_page(status="all", limit=50)
while page.has_more:
    page = client.alerts_page(status="all", limit=50, cursor=page.next_cursor)
```

Send the same filters with every page; a cursor sent with other filters is a
`400` `invalid_cursor` (`APIError.code`). The telemetry window is fixed by the
first page. `iter_*` stops with `APIError` (`invalid_response`) if the server
ever repeats a cursor, rather than loop.

Responses are plain dictionaries typed with `TypedDict` (`WhoAmI`, `Alert`,
`Device`, `TelemetryReading`, `SandboxScenario`), so a field the server adds
later is passed through. Telemetry values that were not measured are `None`,
never zero.

### Errors

Any status of 400 or above raises a subclass of `APIError` with `status`,
`code`, `message`, `request_id` and `detail`, read from the Partner API's error
envelope:

| Status | Class | Typical `code` |
|---|---|---|
| 401 | `AuthenticationError` | `api_key_missing`, `api_key_invalid`, `api_key_revoked`, `api_key_expired`, `api_key_required` |
| 403 | `PermissionDeniedError` | `insufficient_scope`, `sandbox_only` |
| 404 | `NotFoundError` | `not_found` |
| 409 | `ConflictError` | `conflict` |
| 422 | `ValidationError` | `validation_error` |
| 429 | `RateLimitError` (has `retry_after`) | `rate_limited` |
| 5xx | `ServerError` | `internal_error`, `service_unavailable` |

A request that gets no HTTP response raises `TransportError`. Branch on `code`,
and quote `request_id` when you contact api@ampverve.com.

### Webhooks

Verify every delivery over the **raw body bytes** before parsing it:

```python
from ampverve import SIGNATURE_HEADER, WebhookVerificationError, construct_event

def handle(raw_body: bytes, headers) -> int:
    try:
        event = construct_event(raw_body, headers[SIGNATURE_HEADER], os.environ["AMPVERVE_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return 400
    # Delivery is at least once: deduplicate on event["id"] (also AmpVerve-Webhook-Id).
    print(event["type"], event["data"])
    return 204
```

The scheme is `AmpVerve-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]`, each
`v1` being HMAC-SHA256 of `"<t>." + body` keyed with the whole `whsec_...`
secret. A delivery is accepted when any `v1` matches and the timestamp is
within 300 seconds, so a secret rotation's grace period (two `v1` values) needs
no code change. The tests check this verifier against the server's own signer
(`backend/core/partner_access/signing.py`).

## Sandbox example

A sandbox key (issued with `environment=sandbox`) uses the same base URL and
gets simulated sites, devices, alerts and telemetry:

```bash
export AMPVERVE_API_KEY=avk_...   # sandbox key
python examples/sandbox.py  # from sdks/python in the repository
```

With `--listen 8787` and `AMPVERVE_WEBHOOK_SECRET` set it also runs a small
receiver that verifies the signed test event.

## Tests

```bash
cd sdks/python
python -m pip install pytest
python -m pytest -q
```

No network. The webhook tests use `sdks/testdata/webhook_vectors.json`, which
`sdks/testdata/generate_webhook_vectors.py` writes from the server's signer;
inside the repository the tests also re-derive it and sign live with the
server's code.
