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).
modelis deprecated and raises aDeprecationWarning. 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
Organization — id, 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.
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 inraw.
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 buildadds to the directory, it does not replace it, so a staledist/accumulates old versions — andtwine 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 emptydist/makesdist/*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)
| File | Size | Uploaded | |
|---|---|---|---|
| medsplain-1.5.0.tar.gz | 20.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|