Metadata-Version: 2.4
Name: neolife
Version: 0.1.0
Summary: Official Python SDK for the neolife fulfillment-rail API — a typed client plus Standard-Webhooks signature verification.
Project-URL: Homepage, https://neolife.health
Project-URL: Documentation, https://docs.neolife.health
Project-URL: Repository, https://github.com/mitchellmclennan/neolife
Project-URL: Changelog, https://github.com/mitchellmclennan/neolife/blob/main/packages/sdk-python/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/mitchellmclennan/neolife/issues
Author-email: neolife <developers@neolife.health>
License: Proprietary
Keywords: fulfillment,healthcare,neolife,pharmacy,sdk,webhooks
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.24
Provides-Extra: dev
Requires-Dist: pytest<9,>=7; extra == 'dev'
Description-Content-Type: text/markdown

# neolife (Python)

Official Python SDK for the [neolife](https://neolife.health) fulfillment-rail API — a typed client
plus Standard-Webhooks signature verification. One runtime dependency (`httpx`), Python 3.9+.

## Install

```bash
pip install neolife
```

## Quickstart

```python
import os
from neolife import Neolife, verify_webhook, NeolifeError

# 1. Init with a machine API key. A nk_sandbox_/rk_sandbox_ key => client.mode == "sandbox".
client = Neolife(api_key=os.environ["NEOLIFE_API_KEY"])
print(client.mode)  # "live" or "sandbox"

# 2. List + submit an order. submit() auto-generates an Idempotency-Key (a UUID) so a retried
#    call can never double-ship — pass your own to make a whole workflow idempotent.
orders = client.orders.list(status="approved", limit=25)
order = client.orders.retrieve("ord_123")
client.orders.approve(order["id"])                      # provider-gated; auto-submits
client.orders.submit(order["id"], idempotency_key="my-workflow-key-1")

# 3. Verify an incoming signed webhook (pass the RAW body, before json.loads).
#    e.g. in Flask:
#
#    @app.post("/webhooks")
#    def webhook():
#        try:
#            event = verify_webhook(request.get_data(), request.headers,
#                                   os.environ["NEOLIFE_WEBHOOK_SECRET"])
#        except WebhookVerificationError:
#            return "", 400
#        print(event.type, event.data)  # PHI-free: ids + status only
#        return "", 204
```

## Resources

| Namespace | Methods |
| --- | --- |
| `client.orders` | `list`, `retrieve`/`get`, `reconciliation`, `plan_routing`, `resolve`, `approve`, `submit`, `reroute`, `cancel` |
| `client.catalog` | `list_products`, `list_protocols` |
| `client.intake` | `list_questionnaires`, `get_questionnaire`, `list_submissions`, `get_submission` |
| `client.developer_keys` | `list`, `create`, `revoke` |
| `client.events` | `list`, `retrieve`/`get`, `replay` |
| `client.webhook_endpoints` | `event_catalog`, `list`, `create`, `update`, `remove`, `send_test`, `deliveries` |

All methods return parsed JSON (dicts / lists). Every mutation accepts an optional
`idempotency_key=`.

## Errors

Every non-2xx response is raised as a `NeolifeError` carrying the API's structured, PHI-free fields:

```python
try:
    client.orders.submit(order_id)
except NeolifeError as err:
    err.type        # "authentication_error" | "conflict" | "rate_limit_error" | ...
    err.code        # stable machine code
    err.status      # HTTP status
    err.request_id  # req_… — quote this in support (from the X-Request-Id header)
    if err.is_retryable:
        ...          # back off + retry — idempotency keeps it safe
```

Error types: `authentication_error`, `permission_error`, `not_found`, `conflict`,
`rate_limit_error`, `validation_error`, `api_error`, `service_unavailable`, `pharmacy_error`.

## Idempotency

`orders.submit`, `orders.approve`, `developer_keys.create`, and every other mutation accept an
optional `idempotency_key=`. If omitted, the SDK generates a `uuid.uuid4()` and sends it as the
`Idempotency-Key` header. The API replays the first response for a repeated key and returns a
`409 conflict` if the same key is reused with a different body.

## Webhooks

`verify_webhook(payload, headers, secret, *, tolerance_secs=300, now_secs=None)` performs
constant-time (`hmac.compare_digest`) Standard-Webhooks HMAC-SHA256 verification over the
`webhook-id` / `webhook-timestamp` / `webhook-signature` headers, rejects timestamps outside ±5
minutes (replay protection), supports rotated-key signature lists (space-separated `v1,<sig>`
candidates), and returns the parsed `WebhookEvent(id, type, timestamp, data)`.

The signed content is `f"{id}.{timestamp}.{raw_body}"` and the secret is `whsec_<base64>` (the
base64 part is the raw HMAC key) — a byte-for-byte match to the neolife server signer. Always pass
the **raw** request bytes: re-serializing the JSON first changes whitespace/key order and breaks the
signature.

Event types are exported as `WEBHOOK_EVENTS` (`intake.*` / `order.*` / `refill.*`). Payloads are
PHI-free by contract — ids and status only; hydrate PHI over an authenticated, BAA-covered GET.

## Development

```bash
cd packages/sdk-python
pip install -e ".[dev]"    # or: pip install httpx pytest
python -m pytest -q
```
