Skip to main content

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 covers exactly the two credentials issued to your organization — each client has an async twin:

Client Async twin Credential Use it to…
Medsplain AsyncMedsplain mds_live_… API key (x-api-key) Simplify medical text (translate, health check).
OrganizationClient AsyncOrganizationClient mds_org_secret_… secret key Read your organization and manage its API keys.

Platform administration — creating organizations, recording payments, managing admin users and system config — is not part of this SDK. Those endpoints require an internal admin credential and are not intended for organization use.

Installation

pip install medsplain

Quick start

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:

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

Async

Every client has an async twin with the same methods, same models, and the same retry policy — the only difference is await, async with, and aclose() instead of close():

import asyncio
from medsplain import AsyncMedsplain

async def main():
    async with AsyncMedsplain(api_key="mds_live_...") as client:
        result = await client.translate("CBC shows leukocytosis with left shift.")
        print(result.simplified_text)

asyncio.run(main())

Async requests can of course be issued concurrently:

async with AsyncMedsplain(api_key="mds_live_...") as client:
    results = await asyncio.gather(*(client.translate(t) for t in texts))

Mind your plan's rate limit when you do — a burst of concurrent calls is exactly what trips RateLimitError.

Configuration

Argument Env var Default Notes
api_key MEDSPLAIN_API_KEY Required. Your mds_live_ key.
base_url MEDSPLAIN_BASE_URL PRODUCTION_BASE_URL API root, without the /v1 prefix. See Environments.
timeout 30.0 Per-request timeout in seconds.
max_retries 2 See Retries.

Environments

There is one package for every environment. Which deployment you talk to is a runtime choice, not a build or an install channel:

Constant Host
PRODUCTION_BASE_URL (default) https://medsplain.ionixxtech.com
DEVELOPMENT_BASE_URL https://medsplaindev.ionixxtech.com
from medsplain import Medsplain, DEVELOPMENT_BASE_URL

prod = Medsplain(api_key="mds_live_...")                                # production
dev  = Medsplain(api_key="mds_live_...", base_url=DEVELOPMENT_BASE_URL) # development

Or set MEDSPLAIN_BASE_URL once and pass nothing — handy for CI, and the only way to point at a local backend:

export MEDSPLAIN_BASE_URL=http://localhost:8000   # or :8001 if you ran `python app.py`

Retries

A request is only replayed when replaying it cannot duplicate work. Losing a response is annoying; silently creating a second API key is worse.

Failure GET / DELETE / PUT POST / PATCH
429 — server declined to process it retried retried
Connect failure, never delivered (ConnectError, ConnectTimeout, PoolTimeout) retried retried
5xx — may have committed work before failing retried not retried
Mid-flight failure (ReadTimeout, dropped connection) retried not retried

Backoff is exponential (0.5s, 1s, 2s… capped at 8s), or the server's retry_after when it supplies one. Set max_retries=0 to disable retries entirely.

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 via POST /v1/translate. text must be non-empty and at most 5000 characters (the SDK checks this before sending; the server answers 413 for anything longer).

model is deprecated and raises a DeprecationWarning. The server selects the model itself and ignores what you pass; the argument will be removed in a future release.

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:

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 or token missing, invalid, expired, or revoked.
PermissionDeniedError 403 No active plan or subscription; insufficient role.
NotFoundError 404 Resource not found.
ConflictError 409 Duplicate resource or state conflict — check .code.
PayloadTooLargeError 413 Text exceeds the server's 5000-character limit.
UnprocessableEntityError 422 Readable input with no usable content (EMPTY_TEXT).
RateLimitError 429 Rate limit hit or token quota exhausted.
ServerError 5xx Server-side failure (including 503 — retried automatically).
APIConnectionError The request never reached the server.

Every APIStatusError carries .status_code, .code, .message, .retry_after, and .body — the full parsed payload. Some errors put actionable detail in there: a 400 API_KEY_LIMIT_EXCEEDED from create_api_key, for instance, lists the expired keys you could delete to free a slot.

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.

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.) AsyncOrganizationClient takes the same arguments.

Methods

Method API call Returns
get_organization() GET /v1/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

Organizationid, name, owner_email, access_request_id (set only when the org was created from an approved API-access request, else None), 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.

ApiKeyid, 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).

ApiKeyListapi_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.

Local development

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/

Releasing

Bump the version in three hardcoded places — keep them in step: pyproject.toml, __version__ in src/medsplain/__init__.py, and USER_AGENT in src/medsplain/_base.py. Record the change in CHANGELOG.md.

TestPyPI is a rehearsal of the upload and install mechanics — the same artifact that will go to PyPI, uploaded somewhere harmless first to confirm it builds, renders, and installs. It is not a channel for a dev-pointing build:

rm -rf dist/                                     # ALWAYS — see below
python -m build                                  # one artifact, for both indexes
twine check dist/*                               # gate: metadata + README render

twine upload --repository testpypi dist/*        # 1) rehearse
twine upload dist/*                              # 2) publish for real

Verify the TestPyPI upload in a throwaway venv, never this one — an editable install here would shadow whatever you downloaded. Dependencies resolve from real PyPI:

python -m venv /tmp/verify
/tmp/verify/bin/pip install --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ medsplain
/tmp/verify/bin/python -c "import medsplain as m; print(m.__version__, m.DEFAULT_BASE_URL)"

Clean dist/ before every build. python -m build adds to the directory, it does not replace it, so a stale dist/ accumulates old versions — and twine upload dist/* uploads everything it finds. That means failing on versions already published, and permanently releasing any half-finished version still sitting there. Neither index lets you delete or overwrite a version afterwards. Starting from an empty dist/ makes dist/* mean exactly what you just built.

Never ship a build whose DEFAULT_BASE_URL differs from the one on PyPI. Two artifacts with the same version and different backends is how "works on my machine" gets manufactured — and neither index lets you overwrite a version to undo it. Environment is the caller's runtime decision; see Environments.

Request bodies and query params are built by the pure functions in src/medsplain/_payloads.py, shared by the sync and async clients. Put validation and field-omission rules there, not in a client method, so both surfaces stay identical.

License

MIT — see LICENSE.

Release files for medsplain 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for medsplain 1.5.0
File Size Uploaded
medsplain-1.5.0.tar.gz 20.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for medsplain 1.5.0
File Interpreter ABI Platform
medsplain-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 45.0 kB

Release files / medsplain-1.5.0.tar.gz

Download URL medsplain-1.5.0.tar.gz
Size 20.4 kB
Tags Source
SHA-256 checksum
How to use checksums
23f94c8549c12260ecb0ea783bc4ed9bee8fe79f2667c7ad77cae5b65b7e33de
BLAKE2b-256 checksum
How to use checksums
cb49d931eabb961926af79808740f2c9c3ae0cecf518d1e0dcf3735f14868d24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Release files / medsplain-1.5.0-py3-none-any.whl

Download URL medsplain-1.5.0-py3-none-any.whl
Size 24.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b62d622c69012b76356b719fa869dbdffaf9538bf38c63f515ff104b1df1d250
BLAKE2b-256 checksum
How to use checksums
fefa6572a9ebb7deece5d87da680bae492d567cfce8318d0d1afc0537cdf965e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Release history Release notifications | RSS feed

This release

1.5.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page