autoicd 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autoicd/__init__.py +50 -0
- autoicd/client.py +277 -0
- autoicd/errors.py +43 -0
- autoicd/types.py +210 -0
- autoicd-0.2.0.dist-info/METADATA +307 -0
- autoicd-0.2.0.dist-info/RECORD +8 -0
- autoicd-0.2.0.dist-info/WHEEL +4 -0
- autoicd-0.2.0.dist-info/licenses/LICENSE +21 -0
autoicd/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""AutoICD API — Python SDK.
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the AutoICD API: clinical text to ICD-10-CM diagnosis codes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import AutoICD
|
|
7
|
+
from .errors import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
AutoICDError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimit,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
)
|
|
14
|
+
from .types import (
|
|
15
|
+
AnonymizeResponse,
|
|
16
|
+
ChapterInfo,
|
|
17
|
+
CodeDetail,
|
|
18
|
+
CodeDetailFull,
|
|
19
|
+
CodeMatch,
|
|
20
|
+
CodeOptions,
|
|
21
|
+
CodeSearchResponse,
|
|
22
|
+
CodeTermInfo,
|
|
23
|
+
CodingEntity,
|
|
24
|
+
CodingResponse,
|
|
25
|
+
PIIEntity,
|
|
26
|
+
SearchOptions,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AutoICD",
|
|
31
|
+
# Errors
|
|
32
|
+
"AutoICDError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"RateLimitError",
|
|
35
|
+
"NotFoundError",
|
|
36
|
+
# Types
|
|
37
|
+
"CodeOptions",
|
|
38
|
+
"CodeMatch",
|
|
39
|
+
"CodingEntity",
|
|
40
|
+
"CodingResponse",
|
|
41
|
+
"SearchOptions",
|
|
42
|
+
"CodeDetail",
|
|
43
|
+
"CodeDetailFull",
|
|
44
|
+
"ChapterInfo",
|
|
45
|
+
"CodeSearchResponse",
|
|
46
|
+
"CodeTermInfo",
|
|
47
|
+
"PIIEntity",
|
|
48
|
+
"AnonymizeResponse",
|
|
49
|
+
"RateLimit",
|
|
50
|
+
]
|
autoicd/client.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import quote, urlencode
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
AutoICDError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimit,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
)
|
|
16
|
+
from .types import (
|
|
17
|
+
AnonymizeResponse,
|
|
18
|
+
ChapterInfo,
|
|
19
|
+
CodeDetail,
|
|
20
|
+
CodeDetailFull,
|
|
21
|
+
CodeMatch,
|
|
22
|
+
CodeOptions,
|
|
23
|
+
CodeSearchResponse,
|
|
24
|
+
CodeTermInfo,
|
|
25
|
+
CodingEntity,
|
|
26
|
+
CodingResponse,
|
|
27
|
+
PIIEntity,
|
|
28
|
+
SearchOptions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_DEFAULT_BASE_URL = "https://autoicdapi.com"
|
|
32
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Codes:
|
|
36
|
+
"""Sub-resource for ICD-10-CM code lookups."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, client: AutoICD) -> None:
|
|
39
|
+
self._client = client
|
|
40
|
+
|
|
41
|
+
def search(
|
|
42
|
+
self, query: str, options: SearchOptions | None = None
|
|
43
|
+
) -> CodeSearchResponse:
|
|
44
|
+
"""Search ICD-10-CM codes by description."""
|
|
45
|
+
params: dict[str, str] = {"q": query}
|
|
46
|
+
if options:
|
|
47
|
+
if options.limit is not None:
|
|
48
|
+
params["limit"] = str(options.limit)
|
|
49
|
+
if options.offset is not None:
|
|
50
|
+
params["offset"] = str(options.offset)
|
|
51
|
+
data = self._client._get(f"/api/v1/codes/search?{urlencode(params)}")
|
|
52
|
+
return CodeSearchResponse(
|
|
53
|
+
query=data["query"],
|
|
54
|
+
count=data["count"],
|
|
55
|
+
codes=[CodeDetail(**c) for c in data["codes"]],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def get(self, code: str) -> CodeDetailFull:
|
|
59
|
+
"""Get full details for an ICD-10-CM code.
|
|
60
|
+
|
|
61
|
+
Returns comprehensive info including synonyms (SNOMED CT, UMLS),
|
|
62
|
+
hierarchy (parent/children), and chapter/block classification.
|
|
63
|
+
"""
|
|
64
|
+
data = self._client._get(f"/api/v1/codes/{quote(code, safe='')}")
|
|
65
|
+
return _parse_code_detail_full(data)
|
|
66
|
+
|
|
67
|
+
def terms(self, code: str) -> list[CodeTermInfo]:
|
|
68
|
+
"""Get indexed terms and synonyms for an ICD-10-CM code."""
|
|
69
|
+
data = self._client._get(f"/api/v1/codes/{quote(code, safe='')}/terms")
|
|
70
|
+
return [CodeTermInfo(**t) for t in data]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AutoICD:
|
|
74
|
+
"""Client for the AutoICD API.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
api_key: Your API key (starts with ``sk_``).
|
|
78
|
+
base_url: API base URL (default ``https://autoicdapi.com``).
|
|
79
|
+
timeout: Request timeout in seconds (default 30).
|
|
80
|
+
http_client: Optional ``httpx.Client`` instance for custom configuration.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
api_key: str,
|
|
87
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
88
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
89
|
+
http_client: httpx.Client | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
if not api_key:
|
|
92
|
+
raise ValueError("api_key must be a non-empty string")
|
|
93
|
+
|
|
94
|
+
self._api_key = api_key
|
|
95
|
+
self._base_url = base_url.rstrip("/")
|
|
96
|
+
self._timeout = timeout
|
|
97
|
+
self._owns_client = http_client is None
|
|
98
|
+
self._http = http_client or httpx.Client(timeout=self._timeout)
|
|
99
|
+
self.codes = Codes(self)
|
|
100
|
+
self.last_rate_limit: RateLimit | None = None
|
|
101
|
+
|
|
102
|
+
def close(self) -> None:
|
|
103
|
+
"""Close the underlying HTTP client (only if we created it)."""
|
|
104
|
+
if self._owns_client:
|
|
105
|
+
self._http.close()
|
|
106
|
+
|
|
107
|
+
def __enter__(self) -> AutoICD:
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def __exit__(self, *_: Any) -> None:
|
|
111
|
+
self.close()
|
|
112
|
+
|
|
113
|
+
# ── Public methods ──────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def code(
|
|
116
|
+
self, text: str, options: CodeOptions | None = None
|
|
117
|
+
) -> CodingResponse:
|
|
118
|
+
"""Code clinical text to ICD-10-CM diagnoses.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
text: Clinical note or free-text input.
|
|
122
|
+
options: Optional coding parameters.
|
|
123
|
+
"""
|
|
124
|
+
body: dict[str, Any] = {"text": text}
|
|
125
|
+
if options:
|
|
126
|
+
if options.top_k is not None:
|
|
127
|
+
body["top_k"] = options.top_k
|
|
128
|
+
if options.include_negated is not None:
|
|
129
|
+
body["include_negated"] = options.include_negated
|
|
130
|
+
if options.strategy is not None:
|
|
131
|
+
body["strategy"] = options.strategy
|
|
132
|
+
data = self._post("/api/v1/code", body)
|
|
133
|
+
return _parse_coding_response(data)
|
|
134
|
+
|
|
135
|
+
def anonymize(self, text: str) -> AnonymizeResponse:
|
|
136
|
+
"""De-identify PHI/PII in clinical text.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
text: Clinical note containing PHI.
|
|
140
|
+
"""
|
|
141
|
+
data = self._post("/api/v1/anonymize", {"text": text})
|
|
142
|
+
return AnonymizeResponse(
|
|
143
|
+
original_text=data["original_text"],
|
|
144
|
+
anonymized_text=data["anonymized_text"],
|
|
145
|
+
pii_count=data["pii_count"],
|
|
146
|
+
pii_entities=[PIIEntity(**e) for e in data["pii_entities"]],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# ── HTTP internals ──────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
def _get(self, path: str) -> Any:
|
|
152
|
+
return self._request("GET", path)
|
|
153
|
+
|
|
154
|
+
def _post(self, path: str, body: dict[str, Any]) -> Any:
|
|
155
|
+
return self._request("POST", path, body=body)
|
|
156
|
+
|
|
157
|
+
def _request(
|
|
158
|
+
self,
|
|
159
|
+
method: str,
|
|
160
|
+
path: str,
|
|
161
|
+
body: dict[str, Any] | None = None,
|
|
162
|
+
) -> Any:
|
|
163
|
+
url = f"{self._base_url}{path}"
|
|
164
|
+
headers = {
|
|
165
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
"Accept": "application/json",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
response = self._http.request(
|
|
171
|
+
method,
|
|
172
|
+
url,
|
|
173
|
+
headers=headers,
|
|
174
|
+
json=body,
|
|
175
|
+
timeout=self._timeout,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Parse rate limit headers
|
|
179
|
+
self._parse_rate_limit(response.headers)
|
|
180
|
+
|
|
181
|
+
# Success
|
|
182
|
+
if 200 <= response.status_code < 300:
|
|
183
|
+
return response.json()
|
|
184
|
+
|
|
185
|
+
# Error handling
|
|
186
|
+
try:
|
|
187
|
+
error_body = response.json()
|
|
188
|
+
message = error_body.get("error", response.text)
|
|
189
|
+
except Exception:
|
|
190
|
+
message = response.text
|
|
191
|
+
|
|
192
|
+
if response.status_code == 401:
|
|
193
|
+
raise AuthenticationError(message)
|
|
194
|
+
if response.status_code == 404:
|
|
195
|
+
raise NotFoundError(message)
|
|
196
|
+
if response.status_code == 429:
|
|
197
|
+
rl = self.last_rate_limit or RateLimit(
|
|
198
|
+
limit=0, remaining=0, reset_at=datetime.now(timezone.utc)
|
|
199
|
+
)
|
|
200
|
+
raise RateLimitError(message, rate_limit=rl)
|
|
201
|
+
|
|
202
|
+
raise AutoICDError(response.status_code, message)
|
|
203
|
+
|
|
204
|
+
def _parse_rate_limit(self, headers: httpx.Headers) -> None:
|
|
205
|
+
limit = headers.get("X-RateLimit-Limit")
|
|
206
|
+
remaining = headers.get("X-RateLimit-Remaining")
|
|
207
|
+
reset_at = headers.get("X-RateLimit-Reset")
|
|
208
|
+
|
|
209
|
+
if limit is not None and remaining is not None and reset_at is not None:
|
|
210
|
+
self.last_rate_limit = RateLimit(
|
|
211
|
+
limit=int(limit),
|
|
212
|
+
remaining=int(remaining),
|
|
213
|
+
reset_at=datetime.fromisoformat(reset_at),
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
self.last_rate_limit = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── Response parsing helpers ────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _parse_code_match(data: dict[str, Any]) -> CodeMatch:
|
|
223
|
+
return CodeMatch(
|
|
224
|
+
code=data["code"],
|
|
225
|
+
description=data["description"],
|
|
226
|
+
similarity=data["similarity"],
|
|
227
|
+
confidence=data["confidence"],
|
|
228
|
+
matched_term=data["matched_term"],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_entity(data: dict[str, Any]) -> CodingEntity:
|
|
233
|
+
return CodingEntity(
|
|
234
|
+
entity_text=data["entity_text"],
|
|
235
|
+
entity_start=data["entity_start"],
|
|
236
|
+
entity_end=data["entity_end"],
|
|
237
|
+
negated=data["negated"],
|
|
238
|
+
historical=data["historical"],
|
|
239
|
+
family_history=data["family_history"],
|
|
240
|
+
uncertain=data["uncertain"],
|
|
241
|
+
severity=data.get("severity"),
|
|
242
|
+
codes=[_parse_code_match(c) for c in data.get("codes", [])],
|
|
243
|
+
merged_from=data.get("merged_from"),
|
|
244
|
+
corrected_from=data.get("corrected_from"),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _parse_coding_response(data: dict[str, Any]) -> CodingResponse:
|
|
249
|
+
return CodingResponse(
|
|
250
|
+
text=data["text"],
|
|
251
|
+
provider=data["provider"],
|
|
252
|
+
strategy=data["strategy"],
|
|
253
|
+
entity_count=data["entity_count"],
|
|
254
|
+
entities=[_parse_entity(e) for e in data.get("entities", [])],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _parse_code_detail_full(data: dict[str, Any]) -> CodeDetailFull:
|
|
259
|
+
parent_data = data.get("parent")
|
|
260
|
+
parent = CodeDetail(**parent_data) if parent_data else None
|
|
261
|
+
|
|
262
|
+
children = [CodeDetail(**c) for c in data.get("children", [])]
|
|
263
|
+
|
|
264
|
+
chapter_data = data.get("chapter")
|
|
265
|
+
chapter = ChapterInfo(**chapter_data) if chapter_data else None
|
|
266
|
+
|
|
267
|
+
return CodeDetailFull(
|
|
268
|
+
code=data["code"],
|
|
269
|
+
short_description=data["short_description"],
|
|
270
|
+
long_description=data["long_description"],
|
|
271
|
+
is_billable=data["is_billable"],
|
|
272
|
+
synonyms=data.get("synonyms", {}),
|
|
273
|
+
parent=parent,
|
|
274
|
+
children=children,
|
|
275
|
+
chapter=chapter,
|
|
276
|
+
block=data.get("block"),
|
|
277
|
+
)
|
autoicd/errors.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RateLimit:
|
|
9
|
+
"""Rate limit info from API response headers."""
|
|
10
|
+
|
|
11
|
+
limit: int
|
|
12
|
+
remaining: int
|
|
13
|
+
reset_at: datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AutoICDError(Exception):
|
|
17
|
+
"""Base exception for AutoICD API errors."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, status: int, message: str) -> None:
|
|
20
|
+
self.status = status
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(AutoICDError):
|
|
25
|
+
"""Raised when the API key is invalid or revoked (401)."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str = "Invalid API key") -> None:
|
|
28
|
+
super().__init__(401, message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RateLimitError(AutoICDError):
|
|
32
|
+
"""Raised when the request limit is exceeded (429)."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, rate_limit: RateLimit) -> None:
|
|
35
|
+
self.rate_limit = rate_limit
|
|
36
|
+
super().__init__(429, message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotFoundError(AutoICDError):
|
|
40
|
+
"""Raised when a resource is not found (404)."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str = "Resource not found") -> None:
|
|
43
|
+
super().__init__(404, message)
|
autoicd/types.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ── Coding ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CodeOptions:
|
|
12
|
+
"""Options for the ``code()`` method."""
|
|
13
|
+
|
|
14
|
+
top_k: int | None = None
|
|
15
|
+
"""Number of ICD-10 candidates per entity (1-25, default 5)."""
|
|
16
|
+
|
|
17
|
+
include_negated: bool | None = None
|
|
18
|
+
"""Include negated conditions in results (default True)."""
|
|
19
|
+
|
|
20
|
+
strategy: Literal["individual", "merged"] | None = None
|
|
21
|
+
"""Entity extraction strategy (default "individual")."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CodeMatch:
|
|
26
|
+
"""A single ranked ICD-10 candidate."""
|
|
27
|
+
|
|
28
|
+
code: str
|
|
29
|
+
"""ICD-10-CM code (e.g. ``"E11.21"``)."""
|
|
30
|
+
|
|
31
|
+
description: str
|
|
32
|
+
"""Official code description."""
|
|
33
|
+
|
|
34
|
+
similarity: float
|
|
35
|
+
"""0-1 cosine similarity score."""
|
|
36
|
+
|
|
37
|
+
confidence: Literal["high", "moderate"]
|
|
38
|
+
"""Confidence level."""
|
|
39
|
+
|
|
40
|
+
matched_term: str
|
|
41
|
+
"""The index term that produced this match."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CodingEntity:
|
|
46
|
+
"""An extracted diagnosis entity with ICD-10 candidates."""
|
|
47
|
+
|
|
48
|
+
entity_text: str
|
|
49
|
+
"""Extracted text span."""
|
|
50
|
+
|
|
51
|
+
entity_start: int
|
|
52
|
+
"""Character offset start."""
|
|
53
|
+
|
|
54
|
+
entity_end: int
|
|
55
|
+
"""Character offset end."""
|
|
56
|
+
|
|
57
|
+
negated: bool
|
|
58
|
+
"""Whether the condition was negated."""
|
|
59
|
+
|
|
60
|
+
historical: bool
|
|
61
|
+
"""Whether this is historical/resolved."""
|
|
62
|
+
|
|
63
|
+
family_history: bool
|
|
64
|
+
"""Whether this is a family member's condition."""
|
|
65
|
+
|
|
66
|
+
uncertain: bool
|
|
67
|
+
"""Whether the entity is hedged/uncertain."""
|
|
68
|
+
|
|
69
|
+
severity: str | None
|
|
70
|
+
"""Severity qualifier (e.g. ``"severe"``)."""
|
|
71
|
+
|
|
72
|
+
codes: list[CodeMatch] = field(default_factory=list)
|
|
73
|
+
"""Ranked ICD-10 candidates."""
|
|
74
|
+
|
|
75
|
+
merged_from: list[str] | None = None
|
|
76
|
+
"""Source texts if merged."""
|
|
77
|
+
|
|
78
|
+
corrected_from: str | None = None
|
|
79
|
+
"""Original text before spell correction."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class CodingResponse:
|
|
84
|
+
"""Complete coding result."""
|
|
85
|
+
|
|
86
|
+
text: str
|
|
87
|
+
"""Input text that was processed."""
|
|
88
|
+
|
|
89
|
+
provider: str
|
|
90
|
+
"""AI provider used for code matching."""
|
|
91
|
+
|
|
92
|
+
strategy: str
|
|
93
|
+
"""Strategy used."""
|
|
94
|
+
|
|
95
|
+
entity_count: int
|
|
96
|
+
"""Total number of entities."""
|
|
97
|
+
|
|
98
|
+
entities: list[CodingEntity] = field(default_factory=list)
|
|
99
|
+
"""Extracted entities sorted by position."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── Code Search ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class SearchOptions:
|
|
107
|
+
"""Options for ``codes.search()``."""
|
|
108
|
+
|
|
109
|
+
limit: int | None = None
|
|
110
|
+
"""1-100 results per page (default 20)."""
|
|
111
|
+
|
|
112
|
+
offset: int | None = None
|
|
113
|
+
"""Pagination offset (default 0)."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class CodeDetail:
|
|
118
|
+
"""Basic details for an ICD-10-CM code."""
|
|
119
|
+
|
|
120
|
+
code: str
|
|
121
|
+
short_description: str
|
|
122
|
+
long_description: str
|
|
123
|
+
is_billable: bool
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ChapterInfo:
|
|
128
|
+
"""ICD-10-CM chapter classification."""
|
|
129
|
+
|
|
130
|
+
number: int
|
|
131
|
+
"""Chapter number (1-22)."""
|
|
132
|
+
|
|
133
|
+
range: str
|
|
134
|
+
"""Code range (e.g. ``"E00-E89"``)."""
|
|
135
|
+
|
|
136
|
+
title: str
|
|
137
|
+
"""Chapter title."""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class CodeDetailFull(CodeDetail):
|
|
142
|
+
"""Comprehensive details for an ICD-10-CM code including hierarchy and synonyms."""
|
|
143
|
+
|
|
144
|
+
synonyms: dict[str, list[str]] = field(default_factory=dict)
|
|
145
|
+
"""Synonyms grouped by source: ``"snomed"``, ``"umls"``, ``"icd10_augmented"``."""
|
|
146
|
+
|
|
147
|
+
parent: CodeDetail | None = None
|
|
148
|
+
"""Parent code in the ICD-10 hierarchy, or ``None`` for top-level categories."""
|
|
149
|
+
|
|
150
|
+
children: list[CodeDetail] = field(default_factory=list)
|
|
151
|
+
"""Direct child codes in the ICD-10 hierarchy."""
|
|
152
|
+
|
|
153
|
+
chapter: ChapterInfo | None = None
|
|
154
|
+
"""ICD-10-CM chapter this code belongs to."""
|
|
155
|
+
|
|
156
|
+
block: str | None = None
|
|
157
|
+
"""Code block range (e.g. ``"E08-E13"``)."""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class CodeSearchResponse:
|
|
162
|
+
"""Search results for ICD-10-CM codes."""
|
|
163
|
+
|
|
164
|
+
query: str
|
|
165
|
+
count: int
|
|
166
|
+
codes: list[CodeDetail] = field(default_factory=list)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class CodeTermInfo:
|
|
171
|
+
"""An indexed term for an ICD-10-CM code."""
|
|
172
|
+
|
|
173
|
+
term: str
|
|
174
|
+
"""The term text."""
|
|
175
|
+
|
|
176
|
+
term_type: str
|
|
177
|
+
"""Term type (e.g. ``"long_desc"``, ``"synonym"``)."""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Anonymization ───────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class PIIEntity:
|
|
185
|
+
"""A detected PII entity."""
|
|
186
|
+
|
|
187
|
+
text: str
|
|
188
|
+
"""Original PII text."""
|
|
189
|
+
|
|
190
|
+
start: int
|
|
191
|
+
"""Character offset start."""
|
|
192
|
+
|
|
193
|
+
end: int
|
|
194
|
+
"""Character offset end."""
|
|
195
|
+
|
|
196
|
+
label: str
|
|
197
|
+
"""PII type: NAME, DATE, SSN, PHONE, EMAIL, ADDRESS, MRN, AGE."""
|
|
198
|
+
|
|
199
|
+
replacement: str
|
|
200
|
+
"""Replacement placeholder (e.g. ``"[NAME]"``)."""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class AnonymizeResponse:
|
|
205
|
+
"""Result of PHI de-identification."""
|
|
206
|
+
|
|
207
|
+
original_text: str
|
|
208
|
+
anonymized_text: str
|
|
209
|
+
pii_count: int
|
|
210
|
+
pii_entities: list[PIIEntity] = field(default_factory=list)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autoicd
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the AutoICD API — clinical text to ICD-10-CM diagnosis codes, powered by AI and medical NLP
|
|
5
|
+
Project-URL: Homepage, https://autoicdapi.com
|
|
6
|
+
Project-URL: Documentation, https://autoicdapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/fcggamou/autoicd-python
|
|
8
|
+
Project-URL: Issues, https://github.com/fcggamou/autoicd-python/issues
|
|
9
|
+
Author-email: AutoICD <info@autoicdapi.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: autoicd,clinical-decision-support,clinical-nlp,diagnosis-codes,ehr,emr,health-tech,hipaa,icd-10,icd-10-cm,medical-billing,medical-coding,medical-nlp,phi-deidentification,revenue-cycle-management
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# AutoICD API — Python SDK
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/autoicd/)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
|
|
37
|
+
Official Python SDK for the [AutoICD API](https://autoicdapi.com) — clinical text to ICD-10-CM diagnosis codes, powered by AI and medical NLP.
|
|
38
|
+
|
|
39
|
+
Single dependency (`httpx`). Works in **Python 3.10+**.
|
|
40
|
+
|
|
41
|
+
> Built for EHR integrations, health-tech platforms, medical billing, clinical decision support, and revenue cycle management.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Why AutoICD API
|
|
46
|
+
|
|
47
|
+
| | |
|
|
48
|
+
|---|---|
|
|
49
|
+
| **AI-Powered ICD-10 Coding** | Clinical NLP extracts diagnoses from free-text notes and maps them to ICD-10-CM codes — no manual lookup required |
|
|
50
|
+
| **74,000+ ICD-10-CM Codes** | Full 2025 code set enriched with SNOMED CT synonyms for comprehensive matching |
|
|
51
|
+
| **Negation & Context Detection** | Knows the difference between "patient has diabetes" and "patient denies diabetes" — flags negated, historical, uncertain, and family-history mentions |
|
|
52
|
+
| **PHI De-identification** | HIPAA-compliant anonymization of names, dates, SSNs, phone numbers, emails, addresses, MRNs, and ages |
|
|
53
|
+
| **Confidence Scoring** | Every code match includes a similarity score and confidence level so you can set your own acceptance thresholds |
|
|
54
|
+
| **Spell Correction** | Handles misspellings in clinical text — "diabeties" still maps to the right code |
|
|
55
|
+
| **Fully Typed** | Complete type annotations for all requests and responses |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install autoicd
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary>uv / poetry / pdm</summary>
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv add autoicd
|
|
70
|
+
poetry add autoicd
|
|
71
|
+
pdm add autoicd
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from autoicd import AutoICD
|
|
82
|
+
|
|
83
|
+
client = AutoICD(api_key="sk_...")
|
|
84
|
+
|
|
85
|
+
result = client.code(
|
|
86
|
+
"Patient has type 2 diabetes and essential hypertension"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for entity in result.entities:
|
|
90
|
+
print(entity.entity_text, "→", entity.codes[0].code)
|
|
91
|
+
# "type 2 diabetes" → "E11.9"
|
|
92
|
+
# "essential hypertension" → "I10"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Features
|
|
98
|
+
|
|
99
|
+
### Automated ICD-10 Medical Coding
|
|
100
|
+
|
|
101
|
+
Extract diagnosis entities from clinical notes and map them to ICD-10-CM codes. Each entity includes ranked candidates with confidence scores, negation status, and context flags.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
result = client.code(
|
|
105
|
+
"History of severe COPD with acute exacerbation. Patient denies chest pain."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
for entity in result.entities:
|
|
109
|
+
print(entity.entity_text)
|
|
110
|
+
print(f" Negated: {entity.negated}")
|
|
111
|
+
print(f" Historical: {entity.historical}")
|
|
112
|
+
for match in entity.codes:
|
|
113
|
+
print(
|
|
114
|
+
f" {match.code} — {match.description} "
|
|
115
|
+
f"({match.confidence}, {match.similarity * 100:.1f}%)"
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Fine-tune results with coding options:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from autoicd import CodeOptions
|
|
123
|
+
|
|
124
|
+
result = client.code(
|
|
125
|
+
"Patient presents with acute bronchitis and chest pain",
|
|
126
|
+
options=CodeOptions(
|
|
127
|
+
top_k=3, # Top 3 ICD-10 candidates per entity (default: 5)
|
|
128
|
+
strategy="merged", # "individual" or "merged" entity strategy
|
|
129
|
+
include_negated=False, # Exclude negated conditions from results
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### ICD-10 Code Search
|
|
135
|
+
|
|
136
|
+
Search the full ICD-10-CM 2025 code set by description. Perfect for building code lookup UIs, autocomplete fields, and validation workflows.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
results = client.codes.search("diabetes mellitus")
|
|
140
|
+
# results.codes → [CodeDetail(code="E11.9", short_description="...", ...), ...]
|
|
141
|
+
|
|
142
|
+
from autoicd import SearchOptions
|
|
143
|
+
results = client.codes.search("heart failure", options=SearchOptions(limit=5))
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### ICD-10 Code Details
|
|
147
|
+
|
|
148
|
+
Get full details for any ICD-10-CM code — descriptions, billable status, and indexed terms.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
detail = client.codes.get("E11.9")
|
|
152
|
+
print(detail.code) # "E11.9"
|
|
153
|
+
print(detail.long_description) # "Type 2 diabetes mellitus without complications"
|
|
154
|
+
print(detail.is_billable) # True
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### ICD-10 Code Terms & Synonyms
|
|
158
|
+
|
|
159
|
+
Retrieve all indexed terms and synonyms for a code — includes SNOMED CT mappings and clinical variants.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
terms = client.codes.terms("E11.9")
|
|
163
|
+
for t in terms:
|
|
164
|
+
print(t.term, f"({t.term_type})")
|
|
165
|
+
# "Type 2 diabetes mellitus without complications" (long_desc)
|
|
166
|
+
# "adult onset diabetes" (synonym)
|
|
167
|
+
# ...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### PHI De-identification
|
|
171
|
+
|
|
172
|
+
Strip protected health information from clinical notes before storage or analysis. HIPAA-compliant de-identification for names, dates, SSNs, phone numbers, emails, addresses, MRNs, and ages.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
result = client.anonymize(
|
|
176
|
+
"John Smith, DOB 01/15/1980, MRN 123456, has COPD"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
print(result.anonymized_text)
|
|
180
|
+
# "[NAME], DOB [DATE], MRN [MRN], has COPD"
|
|
181
|
+
|
|
182
|
+
print(result.pii_count) # 3
|
|
183
|
+
print(result.pii_entities) # [PIIEntity(text="John Smith", label="NAME", ...), ...]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Use Cases
|
|
189
|
+
|
|
190
|
+
- **EHR / EMR Integration** — Auto-code clinical notes as providers type, reducing manual coding burden
|
|
191
|
+
- **Medical Billing & RCM** — Accelerate claim submission with accurate ICD-10 codes
|
|
192
|
+
- **Clinical Decision Support** — Map patient conditions to standardized codes for analytics and alerts
|
|
193
|
+
- **Health-Tech SaaS** — Add ICD-10 coding to your platform without building ML infrastructure
|
|
194
|
+
- **Clinical Research** — Extract and standardize diagnoses from unstructured medical records
|
|
195
|
+
- **Insurance & Payer Systems** — Validate and suggest diagnosis codes during claims processing
|
|
196
|
+
- **Telehealth Platforms** — Generate diagnosis codes from visit notes and transcriptions
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Error Handling
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from autoicd import (
|
|
204
|
+
AutoICD,
|
|
205
|
+
AuthenticationError,
|
|
206
|
+
RateLimitError,
|
|
207
|
+
NotFoundError,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
result = client.code("...")
|
|
212
|
+
except AuthenticationError:
|
|
213
|
+
# Invalid or revoked API key (401)
|
|
214
|
+
...
|
|
215
|
+
except RateLimitError as e:
|
|
216
|
+
# Request limit exceeded (429)
|
|
217
|
+
print(e.rate_limit.remaining, e.rate_limit.reset_at)
|
|
218
|
+
except NotFoundError:
|
|
219
|
+
# ICD-10 code not found (404)
|
|
220
|
+
...
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Rate limit info is available after every request:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
client.code("...")
|
|
227
|
+
print(client.last_rate_limit)
|
|
228
|
+
# RateLimit(limit=1000, remaining=987, reset_at=datetime(...))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Configuration
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
client = AutoICD(
|
|
237
|
+
api_key="sk_...", # Required — get yours at https://autoicdapi.com
|
|
238
|
+
base_url="https://...", # Default: https://autoicdapi.com
|
|
239
|
+
timeout=60.0, # Default: 30.0 seconds
|
|
240
|
+
http_client=httpx.Client(...), # Custom httpx client (for proxies, mTLS, etc.)
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Use as a context manager for automatic cleanup:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
with AutoICD(api_key="sk_...") as client:
|
|
248
|
+
result = client.code("Patient has diabetes")
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## API Reference
|
|
254
|
+
|
|
255
|
+
Full REST API documentation at [autoicdapi.com/docs](https://autoicdapi.com/docs).
|
|
256
|
+
|
|
257
|
+
| Method | Description |
|
|
258
|
+
|--------|-------------|
|
|
259
|
+
| `client.code(text, options?)` | Code clinical text to ICD-10-CM diagnoses |
|
|
260
|
+
| `client.anonymize(text)` | De-identify PHI/PII in clinical text |
|
|
261
|
+
| `client.codes.search(query, options?)` | Search ICD-10-CM codes by description |
|
|
262
|
+
| `client.codes.get(code)` | Get details for an ICD-10-CM code |
|
|
263
|
+
| `client.codes.terms(code)` | Get indexed terms/synonyms for a code |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Types
|
|
268
|
+
|
|
269
|
+
All request and response types are exported:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from autoicd import (
|
|
273
|
+
CodingResponse,
|
|
274
|
+
CodingEntity,
|
|
275
|
+
CodeMatch,
|
|
276
|
+
CodeOptions,
|
|
277
|
+
CodeDetail,
|
|
278
|
+
CodeSearchResponse,
|
|
279
|
+
CodeTermInfo,
|
|
280
|
+
AnonymizeResponse,
|
|
281
|
+
PIIEntity,
|
|
282
|
+
RateLimit,
|
|
283
|
+
SearchOptions,
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Requirements
|
|
290
|
+
|
|
291
|
+
- **Python 3.10+**
|
|
292
|
+
- An API key from [autoicdapi.com](https://autoicdapi.com)
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Links
|
|
297
|
+
|
|
298
|
+
- [AutoICD API](https://autoicdapi.com) — Homepage and API key management
|
|
299
|
+
- [API Documentation](https://autoicdapi.com/docs) — Full REST API reference
|
|
300
|
+
- [TypeScript SDK](https://www.npmjs.com/package/autoicd) — `npm install autoicd`
|
|
301
|
+
- [ICD-10-CM 2025 Code Set](https://www.cms.gov/medicare/coding-billing/icd-10-codes) — Official CMS reference
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
autoicd/__init__.py,sha256=ip3RVonlUsVkD-DAHT8cp9uSahsMBpdgnhk6CmoRBkM,924
|
|
2
|
+
autoicd/client.py,sha256=aCV7cPOGQUjJTIEdu7jLLOBoRIYHh28gWFBHagXB4YE,9013
|
|
3
|
+
autoicd/errors.py,sha256=ivpEuy_SkTbagS4fnAd2nCdWN_sWuOAuFk4S0sRD__0,1100
|
|
4
|
+
autoicd/types.py,sha256=fhjVc2Oy7Rqmssbf59I5QgcxwbzgPTRmu-cjTDnevjM,5018
|
|
5
|
+
autoicd-0.2.0.dist-info/METADATA,sha256=JHnk21XJvAirId0ynFoSzPL_M2E_3sm3-f5w37ylv7Q,9490
|
|
6
|
+
autoicd-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
autoicd-0.2.0.dist-info/licenses/LICENSE,sha256=IYkACTgDzY__lf7SsIcjrjOtil429XVYegIOrZ_9Su8,1061
|
|
8
|
+
autoicd-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fede
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|