Metadata-Version: 2.4
Name: cannasage
Version: 0.1.0b1
Summary: Official Python SDK for the CannaSage Developer API.
Project-URL: Homepage, https://cannasage.app/developers
Project-URL: Documentation, https://cannasage.app/developers/reference
Project-URL: Repository, https://github.com/AppSprout-dev/CannaSage
Project-URL: Issues, https://github.com/AppSprout-dev/CannaSage/issues
Author-email: CannaSage <support@cannasage.app>
License-Expression: MIT
License-File: LICENSE
Keywords: agriculture,api-client,cannabis,cannasage,cultivation,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# cannasage

Official Python SDK for the **CannaSage Developer API**. Async-first (httpx) with a sync compatibility wrapper.

> **Beta.** This SDK is pre-1.0 and may change. Pin a specific version in production.

## Install

```bash
pip install cannasage
```

## Quick start (async)

```python
import asyncio
from cannasage import AsyncCannaSageClient

async def main():
    async with AsyncCannaSageClient(api_key="csk_live_...") as cs:
        result = await cs.sops.list(category="ipm")
        print(result.data)

asyncio.run(main())
```

## Quick start (sync)

```python
from cannasage import CannaSageClient

cs = CannaSageClient(api_key="csk_live_...")
print(cs.sops.list(category="ipm").data)
```

The sync wrapper spawns a fresh asyncio event loop per call. If you're making many sequential calls, use `AsyncCannaSageClient` inside a single `asyncio.run()` block for better throughput.

## Authentication

| Mode | Usage |
|------|-------|
| API key | `AsyncCannaSageClient(api_key="csk_live_...")` |
| Bearer JWT | `AsyncCannaSageClient(bearer_token="<jwt>")` |
| OAuth client credentials | `AsyncCannaSageClient(client_id="...", client_secret="...", scopes=["read"])` |

The OAuth client exchanges credentials lazily on the first request and caches the access token until ~1 minute before expiry.

## Resources

| Resource | Methods |
|----------|---------|
| `sops` | `list`, `get` |
| `projects` | `list`, `get` |
| `connectors` | `list`, `get`, `create`, `update`, `delete`, `sync`, `devices`, `data`, `push_data` |
| `environmental` | `list_data` |
| `insights` | `list` |
| `grow_tracking` | `costs`, `harvests` |
| `webhooks` | `list_events`, `list`, `create`, `update`, `delete`, `list_deliveries`, `test`, `redeliver` |
| `oauth` | `list`, `create`, `delete` |
| `keys` | `list`, `create`, `revoke`, `rotate`, `force_revoke`, `set_rate_limit`, `usage` |

Every method returns an `ApiResponse` with `.data` and `.meta` attributes.

## Webhook signature verification

```python
from fastapi import FastAPI, Request, HTTPException
from cannasage import verify_webhook_signature

app = FastAPI()
SECRET = "whsec_..."

@app.post("/webhooks/cannasage")
async def handle_webhook(request: Request):
    raw_body = await request.body()
    signature = request.headers.get("X-CannaSage-Signature", "")
    if not verify_webhook_signature(raw_body, signature, SECRET):
        raise HTTPException(status_code=401, detail="invalid signature")
    event = await request.json()
    # ... handle event
    return {"ok": True}
```

`verify_webhook_signature` uses `hmac.compare_digest` (constant-time) and never raises on mismatch — it returns `bool`.

## Error handling

All HTTP errors raise `CannaSageAPIError`:

```python
from cannasage import CannaSageAPIError

try:
    await cs.sops.get("missing")
except CannaSageAPIError as err:
    print(err.code, err.status, err.request_id)
    if err.code == "NOT_FOUND":
        ...
```

## Retries, timeouts, idempotency

- **Retries**: up to 3 on `429` (rate limited) and `503` (overloaded). `Retry-After` is honoured with a 30s cap.
- **Timeout**: default 30s per request. Override via `AsyncCannaSageClient(timeout_seconds=60)`.
- **Idempotency**: `POST` requests automatically receive a random `Idempotency-Key` header (forward-compatible — server support coming in a future release).

## License

MIT — see [LICENSE](./LICENSE).

## Links

- [API reference](https://cannasage.app/developers/reference)
- [Changelog](https://cannasage.app/developers/changelog)
- [Issues](https://github.com/AppSprout-dev/CannaSage/issues)
