Metadata-Version: 2.4
Name: medsplain
Version: 1.1.1
Summary: Official Python client for the Medsplain TX medical text simplification API
Project-URL: Homepage, https://github.com/medsplain/medsplain-python
Project-URL: Documentation, https://github.com/medsplain/medsplain-python#readme
Project-URL: Issues, https://github.com/medsplain/medsplain-python/issues
Author: Medsplain TX
License: MIT
License-File: LICENSE
Keywords: api,client,medical,medsplain,sdk,text-simplification
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Requires-Dist: twine>=4.0; extra == 'dev'
Description-Content-Type: text/markdown

# Medsplain TX — Python SDK

Official Python client for the **Medsplain TX** medical text simplification API. Send
clinical / medical text and get back a plain-language explanation.

The SDK ships **three clients**, one per credential type:

| Client | Credential | Use it to… |
| --------------------- | --------------------------------------------------- | ------------------------------------------------- |
| `Medsplain`           | `mds_live_…` API key (`x-api-key`)                  | Simplify medical text (`translate`, health check).|
| `OrganizationClient`  | `mds_org_secret_…` secret key (org admin)           | Manage your organization and its API keys.        |
| `AdminClient`         | Cognito JWT access token (`Authorization: Bearer`)  | Platform admin: organizations, payments, admins, config. |

## Installation

```bash
pip install medsplain
```

> Until this is published to PyPI, install from source — see [Local development](#local-development).

## Quick start

```python
from medsplain import Medsplain

client = Medsplain(api_key="mds_live_...")

result = client.translate("Patient presents with acute myocardial infarction.")
print(result.simplified_text)
print(f"{result.source_chars} chars in, {result.output_chars} chars out")
```

The client is a context manager, which closes the connection pool for you:

```python
with Medsplain(api_key="mds_live_...") as client:
    result = client.translate("CBC shows leukocytosis with left shift.")
    print(result.simplified_text)
```

## Configuration

| Argument      | Env var              | Default                  | Notes                                              |
| ------------- | -------------------- | ------------------------ | -------------------------------------------------- |
| `api_key`     | `MEDSPLAIN_API_KEY`  | —                        | Required. Your `mds_live_` key.                    |
| `base_url`    | `MEDSPLAIN_BASE_URL` | `http://localhost:8000`  | API root, **without** the `/v1` prefix.            |
| `timeout`     | —                    | `30.0`                   | Per-request timeout in seconds.                    |
| `max_retries` | —                    | `2`                      | Retries on connection errors, `429`, and `5xx`.    |

```python
import os
client = Medsplain(
    api_key=os.environ["MEDSPLAIN_API_KEY"],
    base_url="https://your-medsplain-host.example.com",
    timeout=60.0,
)
```

## API

### `translate(text, *, model=None) -> TranslationResult`
Simplify medical text into plain language. `text` must be non-empty and at most
**5000 characters**.

Automatically retries against a secondary backend if the primary is unreachable, erroring
server-side, or rate-limited — no configuration needed. Errors that would fail identically
against either backend (bad input, an invalid key, no active plan) are raised immediately
without a fallback attempt.

### `health_check() -> HealthStatus`
Verify the API is reachable (`GET /health-check`).

### `TranslationResult`
| Field             | Type   | Description                                       |
| ----------------- | ------ | ------------------------------------------------- |
| `original_text`   | `str`  | Cleaned input text the server processed.          |
| `simplified_text` | `str`  | The plain-language simplification.                |
| `source_chars`    | `int`  | Character count of the input.                     |
| `output_chars`    | `int`  | Character count of the output.                    |
| `is_medical_text` | `bool` | Whether the input was detected as medical.        |
| `is_conversation` | `bool` | Whether the input was detected as conversational. |
| `raw`             | `dict` | Full, unmodified response payload.                |

## Error handling

Every error inherits from `MedsplainError`. HTTP failures raise an `APIStatusError`
subclass chosen by status code:

```python
from medsplain import (
    Medsplain, AuthenticationError, RateLimitError,
    PermissionDeniedError, APIStatusError,
)

client = Medsplain(api_key="mds_live_...")
try:
    result = client.translate("...")
except AuthenticationError:
    ...                       # 401 — key missing/invalid/expired/revoked
except RateLimitError as e:
    print("retry after", e.retry_after)   # 429 — rate limit or token quota
except PermissionDeniedError:
    ...                       # 403 — no active plan / subscription
except APIStatusError as e:
    print(e.status_code, e.code, e.message)
```

| Exception               | Status | Meaning                                          |
| ----------------------- | ------ | ------------------------------------------------ |
| `BadRequestError`       | 400    | Malformed request / validation error.            |
| `AuthenticationError`   | 401    | API key missing, invalid, expired, or revoked.   |
| `PermissionDeniedError` | 403    | No active plan or subscription.                  |
| `NotFoundError`         | 404    | Resource not found.                              |
| `RateLimitError`        | 429    | Rate limit hit or token quota exhausted.         |
| `ServerError`           | 5xx    | Server-side failure.                             |
| `APIConnectionError`    | —      | The request never reached the server.            |

## Organization management

`OrganizationClient` manages your organization and the API keys your applications use.
It authenticates with your **organization secret key** (`mds_org_secret_…`) — a different
credential from the `mds_live_` key above — sent in the `X-Organization-Secret-Key` header.

```python
from medsplain import OrganizationClient

with OrganizationClient(
    org_id="org_000000000001",
    secret_key="mds_org_secret_...",
) as org:
    # Inspect the organization
    info = org.get_organization()
    print(info.plan_type, info.total_tokens_used)

    # Create a key — the full mds_live_ value is returned ONLY here
    created = org.create_api_key(name="production", description="prod server")
    print(created.api_key)          # store this securely; never shown again

    # List, fetch, disable, delete
    keys = org.list_api_keys(is_active=True)
    one = org.get_api_key(created.id)
    org.set_api_key_status(created.id, is_active=False)
    org.delete_api_key(created.id)
```

### Configuration

| Argument     | Env var                     | Default                 | Notes                                       |
| ------------ | --------------------------- | ----------------------- | ------------------------------------------- |
| `org_id`     | `MEDSPLAIN_ORG_ID`          | —                       | Required. e.g. `org_000000000001`.          |
| `secret_key` | `MEDSPLAIN_ORG_SECRET_KEY`  | —                       | Required. Your `mds_org_secret_` key.       |
| `base_url`   | `MEDSPLAIN_BASE_URL`        | `http://localhost:8000` | API root, **without** the `/v1` prefix.     |

`timeout` and `max_retries` work exactly as on `Medsplain`. With both env vars set you can
construct it with no arguments: `OrganizationClient()`. (The SDK does not load `.env` files —
export the variables yourself.)

### Methods

| Method | API call | Returns |
| ------ | -------- | ------- |
| `get_organization()` | `GET /v1/admin/organizations/{org_id}` | `Organization` |
| `create_api_key(name, *, description=None, expires_at=None)` | `POST …/api-keys` | `ApiKeyWithSecret` |
| `list_api_keys(*, is_active=None, limit=50, offset=0)` | `GET …/api-keys` | `ApiKeyList` |
| `get_api_key(key_id)` | `GET …/api-keys/{key_id}` | `ApiKey` |
| `set_api_key_status(key_id, *, is_active)` | `PATCH …/api-keys/{key_id}/status` | `ApiKey` |
| `delete_api_key(key_id)` | `DELETE …/api-keys/{key_id}` | `dict` |

`expires_at` accepts a `datetime` or an ISO-8601 string. Errors map to the same exception
hierarchy as `Medsplain` (e.g. an invalid secret key raises `AuthenticationError`, an
organization with no active plan raises `PermissionDeniedError`).

### Result models

`Organization` — `id`, `name`, `owner_email`, `plan_type`, `token_limit` (`int`,
`"Unlimited"`, or `None`), `total_tokens_used`, `max_api_keys`, `api_keys_created`,
`subscription_start_date`, `subscription_end_date`, `is_active`, `created_at`,
`updated_at`, `raw`.

`ApiKey` — `id`, `organization_id`, `key_prefix`, `name`, `description`,
`total_tokens_used`, `last_used_at`, `is_active`, `expires_at`, `created_at`, `raw`.

`ApiKeyWithSecret` — all `ApiKey` fields plus `api_key` (the full `mds_live_` key, returned
only by `create_api_key`).

`ApiKeyList` — `api_keys` (list of `ApiKey`), `total`, `raw`.

> Datetime fields are returned as raw ISO-8601 strings (or `None`), and every model keeps
> the untouched payload in `raw`.

## Platform administration

`AdminClient` manages the platform itself — organizations, payments, admin users, and
system config. It authenticates with a **Cognito JWT access token** for an admin user
(`Authorization: Bearer …`). A few operations require **super-admin** privileges; the
server enforces the role and returns `403` (`PermissionDeniedError`) otherwise.

```python
from medsplain import AdminClient

# Option 1 — log in (calls POST /v1/login)
admin = AdminClient.login("admin@example.com", "password", base_url="https://host")
print(admin.tokens["refresh_token"])     # full Cognito token set is on .tokens

# Option 2 — bring your own token
admin = AdminClient(access_token="eyJ...", base_url="https://host")

with admin:
    # Organizations
    org = admin.create_organization(name="Acme Health", owner_email="ops@acme.com")
    print(org.secret_key)                 # mds_org_secret_... — give this to the org owner
    admin.list_organizations(is_active=True)
    admin.update_organization(org.id, name="Acme Health Inc")
    admin.rotate_organization_secret(org.id)

    # Payments — status="paid" activates the org and applies the plan
    pay = admin.create_payment(org.id, amount=499, currency="usd",
                               status="paid", plan_type="pro")
    admin.list_payments(organization_id=org.id)
    admin.update_payment(org.id, pay.id, status="refunded")

    # Admin users (super-admin) and config
    admin.list_admins()
    admin.create_admin("newadmin@example.com")      # super-admin only
    admin.list_config()
    admin.update_config("rate_limit_api_key_max_requests", "120")  # super-admin only

    admin.delete_organization(org.id)
```

### Configuration

| Argument       | Env var                  | Default                 | Notes                                       |
| -------------- | ------------------------ | ----------------------- | ------------------------------------------- |
| `access_token` | `MEDSPLAIN_ADMIN_TOKEN`  | —                       | Required. Admin Cognito JWT access token.   |
| `base_url`     | `MEDSPLAIN_BASE_URL`     | `http://localhost:8000` | API root, **without** the `/v1` prefix.     |

`AdminClient.login(username, password, *, base_url=…)` returns a ready client and stores the
full token set (`access_token`, `refresh_token`, `id_token`, `is_verified`) on `.tokens`.
Cognito access tokens expire and there is no refresh endpoint — log in again (or pass a fresh
token) when you get a `401`.

### Methods

| Method | API call | Role | Returns |
| ------ | -------- | ---- | ------- |
| `login(username, password, *, base_url=…)` *(classmethod)* | `POST /v1/login` | — | `AdminClient` |
| `create_organization(*, name, owner_email)` | `POST /v1/admin/organizations` | admin | `OrganizationWithSecret` |
| `list_organizations(*, is_active=None, limit=50, offset=0)` | `GET /v1/admin/organizations` | admin | `OrganizationList` |
| `update_organization(org_id, *, name=None, owner_email=None)` | `PATCH /v1/admin/organizations/{org_id}` | admin | `Organization` |
| `delete_organization(org_id)` | `DELETE /v1/admin/organizations/{org_id}` | admin | `dict` |
| `rotate_organization_secret(org_id)` | `POST …/{org_id}/rotate-secret` | admin | `RotateSecretResult` |
| `create_payment(org_id, *, amount, currency, status, plan_type, …)` | `POST …/{org_id}/payments` | admin | `Payment` |
| `update_payment(org_id, payment_id, *, status=None, …)` | `PATCH …/{org_id}/payments/{payment_id}` | admin | `Payment` |
| `list_payments(*, organization_id=None, payment_status=None, limit=50, offset=0)` | `GET /v1/admin/payments` | admin | `PaymentList` |
| `list_admins(*, limit=50, offset=0)` | `GET /v1/admin/admins` | admin | `AdminUserList` |
| `create_admin(email)` | `POST /v1/admin/admins` | super-admin | `AdminUser` |
| `delete_admin(email)` | `DELETE /v1/admin/admins` | super-admin | `dict` |
| `list_config()` | `GET /v1/admin/config` | admin | `ConfigList` |
| `update_config(key, value)` | `PATCH /v1/admin/config` | super-admin | `ConfigEntry` |

`create_payment` accepts `status` ∈ `pending|paid|failed|refunded`, `plan_type` ∈
`basic|pro|enterprise`, and `paid_at` as a `datetime` or ISO-8601 string. The new models
(`OrganizationWithSecret`, `OrganizationList`, `RotateSecretResult`, `Payment`, `PaymentList`,
`AdminUser`, `AdminUserList`, `ConfigEntry`, `ConfigList`) follow the same dataclass style —
raw ISO-8601 datetimes and a `raw` payload field.

## Local development

```bash
cd sdk
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

pytest                              # run tests
ruff check .                        # lint
mypy src/                           # type-check
python -m build                     # build wheel + sdist into dist/
```

## License

MIT — see [LICENSE](LICENSE).
