Metadata-Version: 2.4
Name: ehcsapi
Version: 0.1.0
Summary: Async Python client for the Austrian eHealth Codierservice API (FHIR R5 ConceptMap).
Author-email: Christian González <office@nerdocs.at>
License-Expression: MIT
License-File: LICENSE
Keywords: austria,codierservice,ehealth,fhir,icd-10,snomed
Requires-Python: >=3.11
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.27
Description-Content-Type: text/markdown

# ehcsapi

Async Python client for the **eHealth Codierservice API** (Austrian eHealth, FHIR R5 ConceptMap).

The service maps free-text diagnoses to coded concepts (SNOMED CT, ICD-10, Orphanet). The typical flow is:

1. create a **search episode** once,
2. call `find_diagnoses(...)` repeatedly while the user types (type-ahead),
3. call `get_codes(...)` for the selected diagnosis to obtain the SNOMED/ICD-10 codes,
4. drop the episode.

The client is **async-only** (built on `httpx.AsyncClient`) so it fits naturally into a Django ASGI
type-ahead view, where in-flight requests can be cancelled as the user keeps typing.

## Installation

```bash
uv add ehcsapi          # or: pip install ehcsapi
```

Requires Python ≥ 3.11. Depends on `httpx` and `cryptography`.

## Authentication (mutual TLS)

The service requires a client certificate. The operator recommends loading the certificate **once at
startup** and keeping it in memory for the lifetime of the process. This client supports exactly that —
pass a pre-built `ssl.SSLContext`, or in-memory certificate bytes, or a file path:

```python
import ehcsapi

# Recommended: build the context once at startup (e.g. from your central cert service) and reuse it.
ctx = ehcsapi.build_ssl_context(cert_bytes, password="…")   # bytes (PEM or PKCS#12) or a path
client = ehcsapi.AsyncClient("at.nerdocs.aesculaping", ssl_context=ctx)

# Or let the client build it from a path / bytes:
client = ehcsapi.AsyncClient("at.nerdocs.aesculaping", cert="/path/to/client.p12", cert_password="…")
```

The base URL defaults to production (`ehcsapi.PRODUCTION_BASE_URL`). For the test system use
`ehcsapi.TEST_BASE_URL` (`https://csapi-t.ehealth.gv.at`):

```python
client = ehcsapi.AsyncClient("at.nerdocs.aesculaping", base_url=ehcsapi.TEST_BASE_URL, cert=cert)
```

## Usage

### Type-ahead (one episode, many keystrokes)

```python
async with ehcsapi.AsyncClient("at.nerdocs.aesculaping", ssl_context=ctx) as cs:
    async with cs.search_episode() as episode:        # POST on enter, DELETE on exit
        hits = await episode.find_diagnoses("influ")  # -> list[Diagnosis]
        codes = await episode.get_codes(hits[0])      # search terms reused from the Diagnosis
        print(codes.icd10)                            # [Coding(code='J11.1', display='Grippe …')]
```

In a stateless Django view you usually create the episode once, keep its id in the session, and
reconstruct a handle per request without a new POST:

```python
episode = cs.episode(request.session["episode_id"])   # existing episode, no round-trip
hits = await episode.find_diagnoses(query)
```

### One-shot lookup

```python
async with ehcsapi.AsyncClient("at.nerdocs.aesculaping", ssl_context=ctx) as cs:
    hits = await cs.find_diagnoses("influenza")        # opens + drops an ad-hoc episode
```

## API surface

| Method | Endpoint |
| --- | --- |
| `startup_probe()` | `HEAD /startupprobe` |
| `version()` / `dataset_version()` / `application_version()` | `GET /version[...]` |
| `create_episode()` / `create_episode_v2()` / `search_episode()` | `POST .../searchepisode` |
| `episode(id)` | resume an existing episode (no request) |
| `delete_episode(id)` / `delete_episode_v2(id)` | `DELETE .../searchepisode/{id}` |
| `SearchEpisode.find_diagnoses(...)` | `GET .../conceptmap?_query=findDiagnoses` |
| `SearchEpisode.get_codes(...)` | `GET .../conceptmap?_query=getCodeByElementId` |
| `emit_x(n)` / `emit_random(n)` | diagnostic `GET /emit/...` |

Errors raise a `CodierserviceError` subclass: `BadRequestError` (400), `ForbiddenError` (403),
`SearchEpisodeExpiredError` (410 — create a fresh episode), or `TransportError` for network failures.

## Test run against the test system

A runnable smoke script and an opt-in live test hit `csapi-t`:

```bash
# one-off manual run
CSAPI_CLIENT_TLS_CERT_PATH=/path/to/client.p12 CSAPI_CLIENT_TLS_CERT_PASSWORD=… \
    python examples/smoke.py influenza

# opt-in pytest
EHCSAPI_LIVE=1 CSAPI_CLIENT_TLS_CERT_PATH=/path/to/client.p12 CSAPI_CLIENT_TLS_CERT_PASSWORD=… \
    uv run pytest -m live
```

The reference OpenAPI document is in [`docs/openapi.json`](docs/openapi.json).

## Development

```bash
uv sync
uv run pytest          # unit tests, no network
uv run ruff check
uv run mypy src
```

## License

MIT — see [LICENSE](LICENSE).
