Metadata-Version: 2.5
Name: hubble-sdk-python
Version: 0.3.0
Summary: Python client for the Hubble Patient Access API.
Project-URL: Homepage, https://github.com/hubble-dot-ai/hubble-sdks
Project-URL: Repository, https://github.com/hubble-dot-ai/hubble-sdks
Project-URL: Issues, https://github.com/hubble-dot-ai/hubble-sdks/issues
Author: Hubble
License-Expression: MIT
License-File: LICENSE
Keywords: health records,hubble,medical records,patient
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<0.29.0,>=0.23.1
Requires-Dist: python-dateutil>=2.8.0
Provides-Extra: docs
Requires-Dist: pydoc-markdown<5,>=4.8; extra == 'docs'
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == 'test'
Requires-Dist: respx>=0.21; extra == 'test'
Description-Content-Type: text/markdown

# hubble-sdk-python

Python client for the Hubble Patient Access API: create consents and patient
links, and read FHIR R4 data with your application's access token.

```bash
pip install hubble-sdk-python
```

```python
from hubble_sdk import Hubble

hub = Hubble(client_id="hbl_client_prod_...", client_secret="hbl_prod_...")

# 1. Create a consent and send the patient its link. Optionally prefill with
#    patient={"first_name": ...} demographics, providers=[{"name": ...}], and
#    the optional organization your verified Hubble organization represents.
consent = hub.create_consent(patient_ref="abc123", on_behalf_of_organization_name="Acme Health")
print(consent.link_url)   # returned only here; store it if you may resend it

# 2. Read the patient's FHIR data whenever you like (see below for every read).
summary = hub.summary(consent.id)
labs = hub.search(consent.id, "Observation", {"category": "laboratory"})

# Consent management
for c in hub.list_consents(status="active"):
    print(c.id, c.patient_ref)
hub.revoke_consent(consent.id)
```

## Reading with an application token

Read any consent in your environment by its id, without the device flow: while
the patient is still adding sources, after they finish, or after the link
expires. The client gets the application access token from `POST /api/v1/token`,
caches it and renews it for you, and names the consent on every request. One
`Hubble` client is safe to share across threads: they share the one token, and
only one thread fetches or renews it at a time.

```python
obs = hub.read(consent_id, "Observation", "abc-123")                  # one resource
page = hub.search(consent_id, "Observation", {"category": "laboratory"})  # first page (a Bundle)
for resource in hub.iter_search(consent_id, "Observation", {"category": "laboratory"}):
    ...                                                                # every page, lazily
summary = hub.summary(consent_id)
record = hub.everything(consent_id, {"_count": 100})
export = hub.export_pdf(consent_id, connection=connection_id)         # one source's PDF
```

Reuse one `Hubble` client so its token is reused too.

### The device flow

Existing integrations can keep using the device flow, which waits for the
patient to finish and reads with a consent-scoped token:

```python
link = hub.create_link(patient_ref="abc123")
print(link.verification_uri_complete)
session = hub.wait_for_authorization(link)   # polls until the patient finishes
bundle = session.everything()                # refreshes the consent token for you
```

## Receiving webhooks

Verify each delivery against your endpoint's signing secret (from the portal's
Webhooks page) and get the event back. Pass the **raw** body: a body that was
parsed and re-serialized will not verify.

```python
from hubble_sdk import WebhookSignatureError, construct_webhook_event

# e.g. Flask
@app.post("/hubble/webhooks")
def hubble_webhook():
    try:
        event = construct_webhook_event(
            request.get_data(), request.headers.get("Hubble-Signature"), WEBHOOK_SECRET
        )
    except WebhookSignatureError:
        return "bad signature", 400
    if event.type == "consent.source.available":
        print(event.consent_id, event.source.id)
    # newer event types may arrive; ignore what you don't handle
    return "", 204
```

In FastAPI, read the body with `await request.body()`. Deliveries are
at-least-once, so dedupe on `event.id`. Pass `secret` as a list to accept the old
and new secret during a rotation, and `tolerance` to change the default 5-minute
replay window.

## What the client handles for you

- **Auth per plane** — HTTP Basic (your app credentials) for consent management
  and the token endpoint; Bearer (the consent token) for FHIR reads. You never
  wire the schemes yourself.
- **Default base URL** — `https://portal.hubble.ai`. Override only with a trusted
  value; your access token is sent to it.
- **Device-flow polling** — `wait_for_authorization` honors the server's
  `interval` and `slow_down`.
- **Correct FHIR paths** — operation and sub-resource paths like
  `Patient/$everything` are sent unencoded.
- **Transparent refresh** — FHIR reads refresh and retry once on a 401.
- **FHIR responses** — reads return the raw FHIR R4 resource as a dict. For typed
  models, parse with the optional [`fhir.resources`](https://pypi.org/project/fhir.resources/)
  package: `from fhir.resources.bundle import Bundle; Bundle.model_validate(session.everything())`.

## Layout

`Hubble` / `PatientSession` and the webhook helpers are hand-written. The low-level client generated
from the OpenAPI contract with
[openapi-python-client](https://github.com/openapi-generators/openapi-python-client)
lives under `hubble_sdk._generated` (regenerated on every spec change;
never edited by hand). Reach into it only if you need raw operations. See the
repo root README for how to regenerate.
