Metadata-Version: 2.4
Name: uphealth-signal
Version: 0.1.0
Summary: Official Python SDK for the Uphealth Signal API — cued health-message streams with a receptivity score and an audience-safety verdict on every cue.
Project-URL: Homepage, https://uphealth.us/docs
Project-URL: Documentation, https://uphealth.us/docs
Project-URL: Source, https://github.com/ericinsf/uphealth
Author: Uphealth (Anschutz Media, Inc.)
License-Expression: MIT
License-File: LICENSE
Keywords: api,behavior-change,digital-health,health,healthcare,messaging,patient-engagement,sdk,signal,uphealth
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# uphealth-signal

The official Python SDK for the **Uphealth Signal API** — cued health-message streams. Create a stream for a patient, then advance it one cue at a time; each cue returns the next federally-sourced message the engine selects, plus an advisory **receptivity** score and an **audience-safety** verdict.

- ✅ Fully typed, **zero runtime dependencies** (standard library only)
- ✅ `run_stream` helper drives the create → cue → feedback loop for you
- ✅ Typed exceptions (`RateLimitError`, `ConflictError`, …) with structured fields
- ✅ Automatic, idempotency-safe retries with backoff
- ✅ Idempotency keys generated for you
- ✅ Forward-compatible: every model keeps a `.raw` dict of the original payload

> **Display mode never touches PHI and needs no BAA.** Keep your API key server-side.

## Install

```bash
pip install uphealth-signal
```

Get a free Discovery sandbox key (no credit card) at **https://uphealth.us/signup**, then:

```bash
export UPHEALTH_API_KEY=up_sandbox_…
```

## Quickstart

```python
from uphealth_signal import Signal, is_stream_exhausted

signal = Signal()  # reads UPHEALTH_API_KEY

stream = signal.streams.create(template_id="general_wellness_daily")
print(stream.first_cue.body)         # the opening message
print(stream.first_cue.receptivity)  # 0.5 for a brand-new patient

# The one rule: every cue after the first requires feedback on the prior one.
outcome = signal.streams.cue(stream.stream_id, "did_it")
if not is_stream_exhausted(outcome):
    print(outcome.next_cue.body)
```

## Run a whole sequence

`run_stream` creates the stream and loops the cue/feedback contract until it ends, so you can't trip `409 feedback_required`:

```python
def decide(cue, index, stream_id):
    return "will_try" if cue.kind == "tip" else "acknowledge"

run = signal.streams.run_stream(decide, template_id="general_wellness_daily", max_cues=50)
print(len(run.transcript), "cues; ended:", run.ended_reason)
```

The decider returns a response-action string or a dict of `cue()` keyword arguments.

## Configuration

```python
signal = Signal(
    api_key="up_sandbox_…",   # or the UPHEALTH_API_KEY env var
    base_url="https://api.uphealth.us/v1/signal",  # default
    timeout=30.0,             # seconds
    max_retries=2,            # 5xx + network retries for idempotent calls
    default_headers={},
)
```

## The four endpoints

| Call | Endpoint |
|---|---|
| `signal.streams.create(template_id=…)` | `POST /streams` |
| `signal.streams.get(stream_id)` | `GET /streams/:id` |
| `signal.streams.cue(stream_id, response_action, …)` | `POST /streams/:id/cue` |
| `signal.streams.sandbox_topics()` | `GET /sandbox-topics` |

Responses are typed dataclasses with attribute access; each carries a `.raw` dict and a `.meta` legal/disclosure block you should not strip from buyer-facing surfaces.

### Response actions

`did_it` · `already_do` · `will_try` · `check_it` · `new_to_me` · `needed_this` · `know_it` · `acknowledge` · `no_response`

Importable as `RESPONSE_ACTIONS`. The vocabulary grows over time; treat unknown values you receive as `acknowledge`-class.

## Errors

```python
from uphealth_signal import RateLimitError, ConflictError

try:
    signal.streams.cue(stream_id, "did_it")
except RateLimitError as err:
    show_upgrade(err.upgrade_url, err.current_count, err.cap)
except ConflictError as err:
    if err.code == "feedback_required":
        ...  # submit feedback on the prior cue first
```

| Class | Status | Notable attributes |
|---|---|---|
| `AuthenticationError` | 401 | `code` |
| `PermissionDeniedError` | 403 | `required_scope` |
| `NotFoundError` | 404 | — |
| `ConflictError` | 409 | `code`, `last_cue_event_id`, `existing_event_id` |
| `ValidationError` | 422 | `details`, `required`, `incompatible` |
| `RateLimitError` | 429 | `retry_after`, `cap`, `current_count`, `window`, `upgrade_url` |
| `ServiceUnavailableError` | 503 | `code` |
| `APIConnectionError` | — | network/timeout |

All extend `SignalError` and carry `status`, `code`, `request_id`, and `body`.

## Retries & idempotency

- `cue()` is retry-safe: an idempotency key is generated if you don't pass one.
- `5xx` and network errors are retried with exponential backoff + jitter, up to `max_retries`.
- `create()` is **not** auto-retried on ambiguous network errors (it isn't idempotent server-side yet).
- `429` is never auto-retried (the Discovery window resets monthly); handle it via `RateLimitError`.

## Links

- Docs: https://uphealth.us/docs
- OpenAPI spec: https://uphealth.us/openapi/signal-v1.yaml
- Get a sandbox key: https://uphealth.us/signup

## License

MIT © Anschutz Media, Inc. (Uphealth)
