Metadata-Version: 2.4
Name: isa-sdk
Version: 1.0.5
Summary: Unified Python SDK for ISA APIs (zyins, rapidsign, proxy).
Project-URL: Homepage, https://github.com/Software-Automation-Holdings-LLC/isa-platform
Project-URL: Source, https://github.com/Software-Automation-Holdings-LLC/isa-platform
Author: Software Automation Holdings, LLC
License: Apache-2.0
Keywords: insurance,isa,rapidsign,sdk,zyins
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.27
Requires-Dist: libcst>=1.4
Requires-Dist: pydantic>=2.6
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: libcst>=1.4; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# isa-sdk

Python SDK for the [Best Plan Pro API](https://docs.isaapi.com) — powered by the ZyINS engine. Mirrors the
canonical TypeScript SDK with Python-idiomatic naming (`snake_case`) and
pydantic v2 models.

## Install

```bash
pip install isa-sdk
```

## Quick start

```python
from isa_sdk.zyins import (
    Isa, Applicant, Coverage, Sex,
    NicotineUsageInput, NicotineDuration,
)
from isa_sdk.zyins.prequalify_v3 import PrequalifyV3Request
from isa_sdk.zyins.product import Product, ProductSelection, ProductType

# Reads ISA_TOKEN from the environment — no explicit token needed. The bare
# `isa.zyins.prequalify` facade routes to v3 by default; `prequalify_v3` is
# the explicit, typed entry point. Both resolve to an envelope.
isa = Isa.with_bearer()

result = isa.zyins.prequalify_v3(PrequalifyV3Request(
    applicant=Applicant(
        dob="1962-04-18",
        sex=Sex.MALE,
        height_inches=70,
        weight_pounds=195,
        state="NC",
        nicotine_use=NicotineUsageInput(last_used=NicotineDuration.NEVER),
    ),
    coverage=Coverage.face_value(100_000),
    products=ProductSelection.of(Product(
        brand="aetna-accendo",
        type=ProductType.FINAL_EXPENSE,
        wire_token="fex",
        display_name="Final Expense",
    )),
))

for offer in result.data.plans:
    headline = next((row for row in offer.pricing if row.primary), None)
    premium = headline.premium.amount.display if headline and headline.premium else None
    print(offer.carrier.name, offer.product.name, premium)
```

`Isa.with_bearer()` reads `ISA_TOKEN` from the environment. `Authorization: Bearer <token>`,
`Idempotency-Key`, and the date-pinned `Version` header are set automatically.

## First call in <15 lines

```python
from isa_sdk.zyins import Isa, Applicant, Coverage, Sex
from isa_sdk.zyins.prequalify_v3 import PrequalifyV3Request, offer_premium
from isa_sdk.zyins.product import Product, ProductSelection, ProductType

isa = Isa.with_keycode(
    keycode="ABC-123-XYZ",
    email="john.doe@acme-agency.com",
)
result = isa.zyins.prequalify_v3(PrequalifyV3Request(
    applicant=Applicant(
        dob="1962-04-18", sex=Sex.MALE,
        height_inches=70, weight_pounds=195, state="NC",
    ),
    coverage=Coverage.face_value(25_000),
    products=ProductSelection.of(Product(
        brand="aetna-accendo", type=ProductType.FINAL_EXPENSE,
        wire_token="fex", display_name="Final Expense",
    )),
))
first = result.data.plans[0]
headline = offer_premium(first)
print(first.carrier.name, headline.amount.display if headline else None)
```

## Per-surface API versions

The ISA API is a federation of independently versioned surfaces. Every SDK
release exports a frozen `BUNDLED_API_VERSIONS` mapping recording which
`/vN` each surface targets:

```python
from isa_sdk import BUNDLED_API_VERSIONS

print(BUNDLED_API_VERSIONS)
# {
#   "prequalify": "v3",
#   "quote":      "v3",
#   "datasets":   "v3",
#   "reference":  "v3",
#   "sessions":   "v1",
#   "branding":   "v1",
#   "cases":      "v1",
# }
```

Pin individual surfaces with a per-surface `api_version` map. There is **no**
`default` key and **no** string shorthand — resolution is
`api_version.get(surface, BUNDLED_API_VERSIONS[surface])`:

```python
isa = Isa.with_keycode(
    keycode="ABC-123-XYZ",
    email="john.doe@acme-agency.com",
    api_version={"quote": "v2"},   # pin only quote; everything else bundled
)
```

`prequalify` / `quote` / `datasets` / `reference` default to `v3` — the flat
`plans` shape, the Money primitive, and the reference adapters all work out of
the box. Pin `api_version={"prequalify": "v2"}` only when a consumer still
needs the legacy v2 shape. See [SDK syntax proposal §2.7][syntax-27].

[syntax-27]: ../../docs/sdk-syntax-proposal.md#27-versioning--per-surface-not-global

## Reference data — `.match()`

The unversioned `isa.zyins.reference` namespace canonicalizes free-text
medication and condition input. Unknown text never rejects — it returns a
structured envelope so the final canonicalization fires server-side at
`/vN/prequalify`:

The bundleless `match` lazily builds and caches the reference index on
its first call — no explicit dataset fetch is required.

```python
from isa_sdk.zyins.reference import Sort

insulin = isa.zyins.medications.match("insulin")
print(insulin.id, insulin.name, insulin.is_known)
# med_01KSR2WVAGC05ZGR6FA4QYEB12  INSULIN  True

# Symmetric traversal — what conditions is insulin used for?
used_for = insulin.conditions(Sort.MOST_COMMON_FIRST)
# frequency-ordered list; cond_01KSR2WVAGC05ZGR6FA4QYEA8X first

novel = isa.zyins.medications.match("NewExperimental XR 2026")
# → {"is_known": False, "input_text": "NewExperimental XR 2026", ...}
```

`Sort.MOST_COMMON_FIRST` and `Sort.ALPHABETICAL` are the two supported
orderings.

## Case storage — bring your own

`isa.zyins.cases.*` routes through a `CaseStorage` adapter. The default is
the zero-knowledge store — ISA's servers only hold ciphertext and an opaque
ID. To plug a carrier-controlled store, pass your adapter at construction:

```python
isa = Isa.with_keycode(
    keycode=..., email=...,
    case_storage=CarrierCaseStorage(),   # optional; default = ZeroKnowledgeCaseStorage
)
```

See [cases guide](https://docs.isaapi.com/docs/cases) for the full
bring-your-own pattern.

## Auth deviation from the TS SDK

The TypeScript SDK in this monorepo still carries the pre-#286 HMAC device
signing surface (`AuthContext` with `licenseKey + orderId + email + deviceId`).
The Python SDK is built against the post-#286 wire contract: a single
bearer token (`isa_live_*` / `isa_test_*`) is the entire auth surface.
This is the intentional simplification called out in the platform's
`platform_v1_architecture` notes.

## Surface

| TypeScript                                | Python                              |
| ----------------------------------------- | ----------------------------------- |
| `client.prequalify(req)`                  | `client.prequalify.run(input)`      |
| `client.license.activate/deactivate/check`| `client.license.*` (mirrored)       |
| `client.case.email(req)`                  | `client.case.email(input)`          |
| (new)                                     | `client.quote.run(input)`           |
| (new)                                     | `client.datasets.list/get`          |
| (new)                                     | `client.reference_data.get(kind)`   |
| (new)                                     | `client.usage.summary(period)`      |

Errors mirror the TS hierarchy: `ISAError` (alias `ZyInsError`, also
exported as `IsaApiError`) → `LicenseError`, `PrequalifyError`,
`ValidationError`, `RateLimitError`, `AuthError`,
`IsaIdempotencyConflictError`.

## `Isa` factory client

Per [SDK_DESIGN.md §3](https://github.com/Software-Automation-Holdings-LLC/isa-platform/blob/main/docs/SDK_DESIGN.md),
the recommended entry point is the `Isa` class with three named factories:

```python
from isa_sdk.zyins import Isa

# Reads ISA_TOKEN from the environment.
isa = Isa.with_bearer()
env = isa.zyins.prequalify(req)
print(env.data, env.request_id, env.idempotency_key, env.retry_attempts)

# Or pass the token explicitly.
isa = Isa.with_bearer("isa_live_…")

# License factory — reads ISA_LICENSE_KEYCODE / ISA_LICENSE_EMAIL.
isa = Isa.with_license()

# Session factory — reads ISA_SESSION_ID / ISA_SESSION_SECRET.
isa = Isa.with_session()
```

Each factory raises `IsaConfigError` with a clear, actionable message if
the required env vars are unset and no explicit arguments are supplied.

### Raw HTTP access

Every method has a `.with_raw_response()` variant returning both the
parsed envelope and the underlying HTTP metadata:

```python
env, raw = isa.zyins.prequalify.with_raw_response(req)
raw.status     # int
raw.url        # str
raw.headers    # read-only mapping
```

### Debug logging

Set `ISA_LOG=debug` to dump every request and response to **stderr** —
never stdout, so parent processes piping the consumer's JSON output stay
clean. Credential headers (`Authorization`, `X-Device-Signature`,
`X-Session-Signature`) and PII body fields (`email`, `dob`, `ssn`,
`phone`) are redacted automatically.

### Idempotency conflicts

When the same `Idempotency-Key` is replayed with a different body the
server returns 409 `idempotency_conflict`. The SDK raises
`IsaIdempotencyConflictError` with `.key` and `.first_seen_at` so the
caller can audit the queued-write bug class:

```python
from isa_sdk.zyins import IsaIdempotencyConflictError

try:
    isa.zyins.prequalify(req, idempotency_key="case-42")
except IsaIdempotencyConflictError as e:
    log.error("key %s first seen at %s", e.key, e.first_seen_at)
```

## Concurrency

The `Isa` client is safe for use with `asyncio.gather` and
`concurrent.futures` — every request mints a fresh request-id and
idempotency key, and shared client state (auth, base URL, debug logger)
is read-only after construction. Reuse a single `Isa` instance across
all concurrent requests; the underlying HTTP transport pools connections
for you.

```python
import asyncio
from isa_sdk.zyins import Isa

isa = Isa.with_bearer()

async def one(req):
    return isa.zyins.prequalify(req)

results = await asyncio.gather(*(one(r) for r in batch))
# Each result.request_id is distinct.
```

## Development

```bash
hatch run test         # pytest
hatch run lint         # ruff + mypy --strict
hatch build            # wheel + sdist
```

Live-integration tests run only when `ZYINS_TEST_TOKEN` is set:

```bash
ZYINS_TEST_TOKEN=isa_test_... hatch run test -- -m integration
```

## Licenses and Ready

The Python SDK exposes the public BPP license-lifecycle surface and the
platform readiness probe on every `ZyInsClient`:

```python
from isa_sdk.zyins import LicenseCheckInput, ZyInsClient

client = ZyInsClient("isa_live_...")

result = client.license.check(
    LicenseCheckInput(
        email="john.doe@acme-agency.com",
        keycode="ABC-123-XYZ",
        device_id="dev_01HZK2N5GQR9T8X4B6FJW3Y1AS",
    )
)
# result.status: "valid" | "invalid" | "inactive"

ready = client.health.get_readiness()
# ready.ready: True on every required probe = "serving"
```

The pre-existing `client.license` (singular) sub-client targets the
authenticated `/v1/license/*` self-status endpoints and is untouched.
