Metadata-Version: 2.5
Name: zentracloud
Version: 0.1.4
Summary: Python SDK for the ZENTRA Cloud v5 API
Project-URL: Homepage, https://gitlab.com/meter-group-inc/pubpackages/zentracloud-py
Project-URL: Source, https://gitlab.com/meter-group-inc/pubpackages/zentracloud-py
Project-URL: Issues, https://gitlab.com/meter-group-inc/pubpackages/zentracloud-py/-/issues
Author-email: Travis Bates <travis@metergroup.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api client,environmental sensors,meter group,zentra,zentracloud
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Requires-Dist: pydantic>=2
Provides-Extra: dev
Requires-Dist: mypy<2,>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.3; extra == 'dev'
Description-Content-Type: text/markdown

# zentracloud

Python SDK for the [ZENTRA Cloud v5 API](https://docs.zentracloud.io). A typed
client for integrating ZENTRA Cloud device and sensor data into your
application — synchronous and asynchronous, with streaming pagination and
first-class type hints.

## Installation

```bash
pip install zentracloud
```

Requires Python 3.9+.

## Authentication

Get your API key from ZENTRA Cloud: **User Account → Integrations → Show Token**
(<https://app.zentracloud.io/profile/integrations>). Provide it directly or via
the `ZENTRACLOUD_API_KEY` environment variable.

```python
from zentracloud import ZentraClient

client = ZentraClient(api_key="your-api-key")   # or ZentraClient() to read the env var
devices = client.v5.devices.list()
```

## Quickstart (synchronous)

```python
from zentracloud import ZentraClient

with ZentraClient(api_key="your-api-key") as client:
    # Discover devices
    devices = client.v5.devices.list(expand=["max_min_timestamp"])
    for device in devices:
        print(device.device_id, device.name)

    # Stream readings (memory-safe for large pulls)
    for reading in client.v5.devices.iter_data(device_id="z6-00930", start="2026-01-01", end="2026-02-01"):
        print(reading.datetime, reading.measurement, reading.value, reading.error_label)

    # Or collect them all at once
    readings = client.v5.devices.data(device_id="z6-00930", start="2026-01-01")
```

## Quickstart (asynchronous)

```python
import asyncio

from zentracloud import AsyncZentraClient


async def main():
    async with AsyncZentraClient(api_key="your-api-key") as client:
        devices = await client.v5.devices.list()
        async for reading in client.v5.devices.iter_data(device_id="z6-00930", start="2026-01-01"):
            print(reading.datetime, reading.value)


asyncio.run(main())
```

## Time windows

There are three ways to bound a data pull, and you can use only one at a time:

- **`start` / `end`** accept a `datetime`, `date`, epoch seconds (`int`/`float`),
  or an ISO-8601 string. Naive datetimes are treated as UTC.
- **`start_timestamp` / `end_timestamp`** accept epoch seconds.
- **`window`** is a relative range measured back from the time of the request
  (server time, UTC), written as `<n><unit>` with a unit of `m` (minutes),
  `h` (hours), `d` (days), or `w` (weeks).

```python
# The last 24 hours
readings = client.v5.devices.data(device_id="z6-00930", window="24h")
```

A `window` must resolve to one year or less: the per-unit maximums are `525600m`,
`8760h`, `365d`, and `52w`. Anything larger, or any malformed value, raises
`ZentraInvalidRequestError` before the request is sent.

Surrounding whitespace is trimmed and the unit is case-insensitive, so
`window="24H"` is accepted and sent as `24h`. (The API itself is stricter, and
returns `422` for a raw `window=24H`.)

## Errors

All errors subclass `zentracloud.ZentraError`:
`ZentraAuthError`, `ZentraInvalidRequestError`, `ZentraRateLimitError`,
`ZentraConnectionError`, `ZentraAPIError`. Each carries `.status_code` and
`.detail`.

## Rate limits

Each endpoint allows a burst of ~5 requests, then refills at about 1 request per
minute (a GCRA rate limiter; roughly 300s of idle time restores the full burst).
The budget is scoped differently per endpoint:

- **`devices.list()`** (`GET /v5/devices`) is limited **per user** (your API key).
- **`devices.data()` / `devices.iter_data()`** (`GET /v5/devices/{device_id}/data`)
  is limited **per device**, so different devices have independent budgets and can
  be pulled concurrently without competing for one limit. Note this budget is per
  device rather than per caller: multiple clients or app instances reading the same
  device share it and can throttle one another.

On HTTP 429 the API returns the next-allowed time as an absolute Unix timestamp in
the response body's `detail` (there is no `Retry-After` header). The client parses
that, waits until then, and retries automatically (up to `max_retries`).

See the [Rate Limiting docs](https://docs.zentracloud.io/l/en/article/f6wdg770d3-rate-limiting)
for details.

## License

MIT © METER Group, Inc.
