Skip to content

Commit 7d497f3

Browse files
jhnwu3claude
andcommitted
Keep hosted mapping tables reachable under backend="auto"
_resolve_backend raised ValueError for any pair outside PYHEALTH_NATIVE_PAIRS and ICD_MAPPINGS_PAIRS, but the native list omits six tables the resource server hosts (ATC<->ICD9CM, ATC<->UMLS, ICD9CM<->UMLS, ICD9PROC<->UMLS, RxNorm<->ATC, RxNorm<->UMLS). Those 12 directions load on master and failed here before any download. auto now sends every pair icd-mappings does not own to the resource server, as before. When neither direction of a table exists there, CrossMap raises a ValueError naming both files instead of a bare urllib HTTPError 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 33e8696 commit 7d497f3

3 files changed

Lines changed: 49 additions & 15 deletions

File tree

‎pyhealth/medcode/cross_map.py‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
PYHEALTH_NATIVE_PAIRS,
1313
_ICDMappingsBackend,
1414
)
15-
from pyhealth.medcode.utils import MODULE_CACHE_PATH, download_and_read_csv
15+
from pyhealth.medcode.utils import BASE_URL, MODULE_CACHE_PATH, download_and_read_csv
1616
from pyhealth.utils import load_pickle, save_pickle
1717

1818
logger = logging.getLogger(__name__)
@@ -114,7 +114,16 @@ def __init__(
114114
df = download_and_read_csv(local_filename, refresh_cache)
115115
except HTTPError:
116116
local_filename = f"{self.t_vocab}_to_{self.s_vocab}.csv"
117-
df = download_and_read_csv(local_filename, refresh_cache)
117+
try:
118+
df = download_and_read_csv(local_filename, refresh_cache)
119+
except HTTPError as e:
120+
raise ValueError(
121+
f"No mapping between {self.s_vocab} and {self.t_vocab} "
122+
f"is available: neither "
123+
f"{self.s_vocab}_to_{self.t_vocab}.csv nor "
124+
f"{local_filename} exists at {BASE_URL}, and "
125+
f"icd-mappings serves {sorted(ICD_MAPPINGS_PAIRS)}."
126+
) from e
118127
self.mapping = defaultdict(list)
119128
for _, row in df.iterrows():
120129
self.mapping[row[self.s_vocab]].append(row[self.t_vocab])
@@ -141,12 +150,9 @@ def _resolve_backend(
141150
return BACKEND_PYHEALTH
142151
if pair in ICD_MAPPINGS_PAIRS:
143152
return BACKEND_ICDMAPPINGS
144-
raise ValueError(
145-
f"No mapping available for {source_vocabulary}->"
146-
f"{target_vocabulary}. PyHealth serves "
147-
f"{sorted(PYHEALTH_NATIVE_PAIRS)} and icd-mappings serves "
148-
f"{sorted(ICD_MAPPINGS_PAIRS)}."
149-
)
153+
# Anything else is looked up on PyHealth's resource server, as it
154+
# was before icd-mappings existed; a missing table raises there.
155+
return BACKEND_PYHEALTH
150156
if backend == BACKEND_ICDMAPPINGS:
151157
if pair not in ALL_ICD_MAPPINGS_PAIRS:
152158
raise ValueError(

‎pyhealth/medcode/icd_mappings.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717

1818
logger = logging.getLogger(__name__)
1919

20-
#: Vocabulary pairs PyHealth serves from its own resource server. Listed in
21-
#: both orderings because ``CrossMap`` derives the reverse direction from the
22-
#: same CSV via its reverse-filename fallback. These always win over the
23-
#: ``icd-mappings`` backend under ``backend="auto"``.
20+
#: Vocabulary pairs PyHealth serves from its own resource server that must
21+
#: never be routed to ``icd-mappings`` under ``backend="auto"``. Listed in both
22+
#: orderings because ``CrossMap`` derives the reverse direction from the same
23+
#: CSV via its reverse-filename fallback. Not exhaustive: ``auto`` sends every
24+
#: pair outside ``ICD_MAPPINGS_PAIRS`` to the resource server too (e.g.
25+
#: RxNorm->ATC, ICD9CM->UMLS).
2426
PYHEALTH_NATIVE_PAIRS: frozenset = frozenset(
2527
{
2628
("ICD9CM", "CCSCM"),

‎tests/core/test_medcode_icd_mappings.py‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
"""
1010

1111
import logging
12+
import tempfile
1213
import unittest
14+
from unittest import mock
15+
from urllib.error import HTTPError
1316

1417
from pyhealth.medcode import ICD9CM, ICD10CM, CrossMap, FlatMap, InnerMap
1518
from pyhealth.medcode.cross_map import (
@@ -74,9 +77,32 @@ def test_gap_pairs_use_icd_mappings(self):
7477
msg=f"{pair} should fall back to icd-mappings",
7578
)
7679

77-
def test_unsupported_pair_raises_value_error(self):
78-
with self.assertRaises(ValueError):
79-
CrossMap._resolve_backend("ICD9CM", "RxNorm", "auto")
80+
def test_other_hosted_pairs_stay_on_pyhealth(self):
81+
# Pairs icd-mappings does not own must keep reaching PyHealth's
82+
# resource server, including hosted tables not listed in
83+
# PYHEALTH_NATIVE_PAIRS. These all load on master.
84+
for pair in [("RxNorm", "ATC"), ("ATC", "ICD9CM"), ("ICD9CM", "UMLS"),
85+
("ICD9PROC", "UMLS"), ("RxNorm", "UMLS"), ("ATC", "UMLS")]:
86+
self.assertEqual(
87+
CrossMap._resolve_backend(pair[0], pair[1], "auto"),
88+
BACKEND_PYHEALTH,
89+
msg=f"{pair} should resolve to PyHealth's own tables",
90+
)
91+
92+
def test_unhosted_pair_raises_value_error(self):
93+
# Neither direction exists on the resource server: the 404s must
94+
# surface as a ValueError that names the files, not a bare HTTPError.
95+
def not_found(filename, refresh_cache=False):
96+
raise HTTPError(filename, 404, "Not Found", None, None)
97+
98+
with tempfile.TemporaryDirectory() as cache, \
99+
mock.patch("pyhealth.medcode.cross_map.MODULE_CACHE_PATH", cache), \
100+
mock.patch("pyhealth.medcode.cross_map.download_and_read_csv",
101+
side_effect=not_found):
102+
with self.assertRaisesRegex(
103+
ValueError, "ICD9CM_to_RxNorm.csv nor RxNorm_to_ICD9CM.csv"
104+
):
105+
CrossMap.load("ICD9CM", "RxNorm")
80106

81107
def test_forcing_icd_mappings_on_unsupported_pair_raises(self):
82108
with self.assertRaises(ValueError):

0 commit comments

Comments
 (0)