cumulus-library-rxnorm 1.0__tar.gz
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.
- cumulus_library_rxnorm-1.0/PKG-INFO +68 -0
- cumulus_library_rxnorm-1.0/README.md +46 -0
- cumulus_library_rxnorm-1.0/cumulus_library_rxnorm/__init__.py +0 -0
- cumulus_library_rxnorm-1.0/cumulus_library_rxnorm/manifest.toml +9 -0
- cumulus_library_rxnorm-1.0/cumulus_library_rxnorm/rxnorm_builder.py +209 -0
- cumulus_library_rxnorm-1.0/pyproject.toml +65 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cumulus-library-rxnorm
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: A RxNorm study for the Cumulus project
|
|
5
|
+
Keywords: FHIR,SQL,Health Informatics
|
|
6
|
+
Requires-Python: >= 3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Requires-Dist: cumulus-library >= 6, <7.0
|
|
13
|
+
Requires-Dist: ruff == 0.15.20 ; extra == "dev"
|
|
14
|
+
Requires-Dist: pre-commit ; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest ; extra == "test"
|
|
16
|
+
Requires-Dist: responses ; extra == "test"
|
|
17
|
+
Project-URL: Documentation, https://docs.smarthealthit.org/cumulus/
|
|
18
|
+
Project-URL: Home, https://smarthealthit.org/cumulus
|
|
19
|
+
Project-URL: Source, https://github.com/smart-on-fhir/cumulus-library-rxnorm
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
|
|
23
|
+
# Cumulus Library UMLS
|
|
24
|
+
|
|
25
|
+
An installation of the Unified Medical Language System® RxNorm dataset. Part of the [SMART on FHIR Cumulus Project](https://smarthealthit.org/cumulus)
|
|
26
|
+
|
|
27
|
+
For more information, [browse the documentation](https://docs.smarthealthit.org/cumulus/library).
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
In order to use RxNorm, you'll need to get an API key for access from the National Library of Medicine, which you can sign up for [here](https://uts.nlm.nih.gov/uts/signup-login).
|
|
31
|
+
|
|
32
|
+
You can then install this module by running `pip install cumulus-library-rxnorm`.
|
|
33
|
+
|
|
34
|
+
This will add an `rxnorm` target to `cumulus-library`. You'll need to pass your
|
|
35
|
+
API key via the `--umls-key` CLI flag, or set the `UMLS_API_KEY` environment variable
|
|
36
|
+
to the key you received from NIH.
|
|
37
|
+
|
|
38
|
+
We download a file from NIH, extract it, create parquet files for Athena, and then upload it.
|
|
39
|
+
We try to preserve some of those artifacts along the way to make rebuilds faster. If you need
|
|
40
|
+
to force recreation from scratch, the `--force-upload` CLI flag will handle this.
|
|
41
|
+
|
|
42
|
+
Note: This study is explicitly namespaced in its own schema, `rxnorm`. Make sure your
|
|
43
|
+
database is not using this schema for another use. Do not create tables inside this
|
|
44
|
+
schema by another means.
|
|
45
|
+
|
|
46
|
+
## Licensing details
|
|
47
|
+
|
|
48
|
+
The `cumulus-library-rxnorm` study is provided as a convenience to install the
|
|
49
|
+
UMLS RxNorm dataset, but is not shipped with said dataset. It will
|
|
50
|
+
require an API key to download the data from NIH directly.
|
|
51
|
+
|
|
52
|
+
As a reminder, the
|
|
53
|
+
[RxNorm terms of service](https://www.nlm.nih.gov/research/umls/rxnorm/docs/termsofservice.html)
|
|
54
|
+
provides several restrictions on this usage of this data (including distributing
|
|
55
|
+
the dataset). When you sign up for a UMLS key, you are assuming responsibility
|
|
56
|
+
for complying with these terms, or an alternate licensing agreement with the
|
|
57
|
+
owners of the RxNorm data if you are provided with one.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## NIH Requried Disclaimer
|
|
61
|
+
|
|
62
|
+
This product uses publicly available data courtesy of the U.S. National Library of Medicine (NLM), National Institutes of Health, Department of Health and Human Services; NLM is not responsible for the product and does not endorse or recommend this or any other product.
|
|
63
|
+
|
|
64
|
+
## Citations
|
|
65
|
+
|
|
66
|
+
Bodenreider O. The Unified Medical Language System (UMLS): integrating biomedical terminology. Nucleic Acids Res. 2004 Jan 1;32(Database issue):D267-70. doi: 10.1093/nar/gkh061. PubMed PMID: 14681409; PubMed Central PMCID: PMC308795.
|
|
67
|
+
|
|
68
|
+
Nelson SJ, Zeng K, Kilbourne J, Powell T, Moore R. Normalized names for clinical drugs: RxNorm at 6 years. J Am Med Inform Assoc. 2011 Jul-Aug;18(4)441-8. doi: 10.1136/amiajnl-2011-000116. Epub 2011 Apr 21. PubMed PMID: 21515544; PubMed Central PMCID: PMC3128404.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Cumulus Library UMLS
|
|
2
|
+
|
|
3
|
+
An installation of the Unified Medical Language System® RxNorm dataset. Part of the [SMART on FHIR Cumulus Project](https://smarthealthit.org/cumulus)
|
|
4
|
+
|
|
5
|
+
For more information, [browse the documentation](https://docs.smarthealthit.org/cumulus/library).
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
In order to use RxNorm, you'll need to get an API key for access from the National Library of Medicine, which you can sign up for [here](https://uts.nlm.nih.gov/uts/signup-login).
|
|
9
|
+
|
|
10
|
+
You can then install this module by running `pip install cumulus-library-rxnorm`.
|
|
11
|
+
|
|
12
|
+
This will add an `rxnorm` target to `cumulus-library`. You'll need to pass your
|
|
13
|
+
API key via the `--umls-key` CLI flag, or set the `UMLS_API_KEY` environment variable
|
|
14
|
+
to the key you received from NIH.
|
|
15
|
+
|
|
16
|
+
We download a file from NIH, extract it, create parquet files for Athena, and then upload it.
|
|
17
|
+
We try to preserve some of those artifacts along the way to make rebuilds faster. If you need
|
|
18
|
+
to force recreation from scratch, the `--force-upload` CLI flag will handle this.
|
|
19
|
+
|
|
20
|
+
Note: This study is explicitly namespaced in its own schema, `rxnorm`. Make sure your
|
|
21
|
+
database is not using this schema for another use. Do not create tables inside this
|
|
22
|
+
schema by another means.
|
|
23
|
+
|
|
24
|
+
## Licensing details
|
|
25
|
+
|
|
26
|
+
The `cumulus-library-rxnorm` study is provided as a convenience to install the
|
|
27
|
+
UMLS RxNorm dataset, but is not shipped with said dataset. It will
|
|
28
|
+
require an API key to download the data from NIH directly.
|
|
29
|
+
|
|
30
|
+
As a reminder, the
|
|
31
|
+
[RxNorm terms of service](https://www.nlm.nih.gov/research/umls/rxnorm/docs/termsofservice.html)
|
|
32
|
+
provides several restrictions on this usage of this data (including distributing
|
|
33
|
+
the dataset). When you sign up for a UMLS key, you are assuming responsibility
|
|
34
|
+
for complying with these terms, or an alternate licensing agreement with the
|
|
35
|
+
owners of the RxNorm data if you are provided with one.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## NIH Requried Disclaimer
|
|
39
|
+
|
|
40
|
+
This product uses publicly available data courtesy of the U.S. National Library of Medicine (NLM), National Institutes of Health, Department of Health and Human Services; NLM is not responsible for the product and does not endorse or recommend this or any other product.
|
|
41
|
+
|
|
42
|
+
## Citations
|
|
43
|
+
|
|
44
|
+
Bodenreider O. The Unified Medical Language System (UMLS): integrating biomedical terminology. Nucleic Acids Res. 2004 Jan 1;32(Database issue):D267-70. doi: 10.1093/nar/gkh061. PubMed PMID: 14681409; PubMed Central PMCID: PMC308795.
|
|
45
|
+
|
|
46
|
+
Nelson SJ, Zeng K, Kilbourne J, Powell T, Moore R. Normalized names for clinical drugs: RxNorm at 6 years. J Am Med Inform Assoc. 2011 Jul-Aug;18(4)441-8. doi: 10.1136/amiajnl-2011-000116. Epub 2011 Apr 21. PubMed PMID: 21515544; PubMed Central PMCID: PMC3128404.
|
|
File without changes
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import pandas
|
|
4
|
+
from cumulus_library import BaseTableBuilder, base_utils, study_manifest
|
|
5
|
+
from cumulus_library.apis import umls
|
|
6
|
+
from cumulus_library.template_sql import base_templates
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RxNormBuilder(BaseTableBuilder):
|
|
10
|
+
def rmtree(self, root: pathlib.Path):
|
|
11
|
+
"""Deletes a dir and all files underneath
|
|
12
|
+
|
|
13
|
+
:param root: the location at the base of the path you want to remove
|
|
14
|
+
|
|
15
|
+
TODO: replace with native pathlib.walk when upgrading to python 3.12
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# just in case, if we get passed a file (like if there's an error and a
|
|
19
|
+
# zip file exists in the download dir)
|
|
20
|
+
if not root.is_dir():
|
|
21
|
+
root.unlink()
|
|
22
|
+
else:
|
|
23
|
+
for p in root.iterdir():
|
|
24
|
+
if p.is_dir():
|
|
25
|
+
self.rmtree(p)
|
|
26
|
+
else:
|
|
27
|
+
p.unlink()
|
|
28
|
+
root.rmdir()
|
|
29
|
+
|
|
30
|
+
def get_rxnorm_data(
|
|
31
|
+
self,
|
|
32
|
+
download_path: pathlib.Path,
|
|
33
|
+
parquet_path: pathlib.Path,
|
|
34
|
+
force_upload: bool,
|
|
35
|
+
umls_key: str,
|
|
36
|
+
) -> (list, bool, str):
|
|
37
|
+
"""Fetches and extracts data from the UMLS API
|
|
38
|
+
|
|
39
|
+
:param download_path: the location to read from
|
|
40
|
+
:param parquet_path: the location output is written; only used for deletion
|
|
41
|
+
if a new dataset is downloaded
|
|
42
|
+
:param force_upload: if True, will download from UMLS regardless of data on disk
|
|
43
|
+
:param umls_key: the UMLS API key to use to auth requests
|
|
44
|
+
:returns:
|
|
45
|
+
- filtered_files - a list of files to process (excluding language tables)
|
|
46
|
+
- download_required - if True, a new UMLS release needed to be retrieved
|
|
47
|
+
- release_version - the name of the folder data was extracted to
|
|
48
|
+
"""
|
|
49
|
+
api = umls.UmlsApi(api_key=umls_key)
|
|
50
|
+
metadata = api.get_latest_umls_file_release(
|
|
51
|
+
target="rxnorm-full-monthly-release"
|
|
52
|
+
)
|
|
53
|
+
download_required = False
|
|
54
|
+
|
|
55
|
+
if not (download_path / metadata["releaseVersion"]).exists():
|
|
56
|
+
print("New RxNorm release available, downloading & updating...")
|
|
57
|
+
download_required = True
|
|
58
|
+
for version in download_path.iterdir():
|
|
59
|
+
self.rmtree(version)
|
|
60
|
+
for version in (parquet_path).iterdir():
|
|
61
|
+
self.rmtree(version)
|
|
62
|
+
if download_required or force_upload:
|
|
63
|
+
api.download_umls_files(
|
|
64
|
+
target="rxnorm-full-monthly-release", path=download_path, unzip=False
|
|
65
|
+
)
|
|
66
|
+
base_utils.unzip_file(
|
|
67
|
+
download_path / metadata["fileName"],
|
|
68
|
+
download_path / metadata["releaseVersion"],
|
|
69
|
+
)
|
|
70
|
+
(download_path / metadata["fileName"]).unlink()
|
|
71
|
+
files = list(
|
|
72
|
+
download_path.glob(f"./{metadata['releaseVersion']}/scripts/oracle/*.ctl")
|
|
73
|
+
)
|
|
74
|
+
return files, download_required, metadata["releaseVersion"]
|
|
75
|
+
|
|
76
|
+
def sql_type_to_df_parquet_type(self, text: str) -> str:
|
|
77
|
+
"""Converts types extract from the MySQL .ctl definition to parquet types
|
|
78
|
+
|
|
79
|
+
:param text: the type to convert
|
|
80
|
+
:returns: the parquet type
|
|
81
|
+
"""
|
|
82
|
+
text = text.split("(")[0].strip(",").replace(" external", "")
|
|
83
|
+
match text:
|
|
84
|
+
case "char":
|
|
85
|
+
return "string", "String"
|
|
86
|
+
case "integer":
|
|
87
|
+
return "Int64", "Integer"
|
|
88
|
+
case "float":
|
|
89
|
+
return "float", "Float"
|
|
90
|
+
case _:
|
|
91
|
+
raise Exception(f"'{text}' missing a type converter")
|
|
92
|
+
|
|
93
|
+
def parse_ctl_file(self, contents: list[str]) -> (str, dict):
|
|
94
|
+
"""Extracts table and type definitions from a *.ctl file
|
|
95
|
+
|
|
96
|
+
:param contents: an array of strings, expected from a file.readlines call()
|
|
97
|
+
:returns:
|
|
98
|
+
- datasource - the name of the datasource for population
|
|
99
|
+
- table -a dict describing the table
|
|
100
|
+
"""
|
|
101
|
+
datasource = None
|
|
102
|
+
table = {"headers": [], "dtype": {}, "parquet_types": []}
|
|
103
|
+
is_col_def_section = False
|
|
104
|
+
for line in contents:
|
|
105
|
+
if line is None:
|
|
106
|
+
continue
|
|
107
|
+
if line.startswith("infile"):
|
|
108
|
+
datasource = line.split(" ")[1].rstrip().replace("'", "")
|
|
109
|
+
elif line == "trailing nullcols(\n":
|
|
110
|
+
is_col_def_section = True
|
|
111
|
+
continue
|
|
112
|
+
elif line.startswith("("):
|
|
113
|
+
is_col_def_section = True
|
|
114
|
+
line = line[1:]
|
|
115
|
+
elif line.strip(" ").startswith(")"):
|
|
116
|
+
is_col_def_section = False
|
|
117
|
+
if is_col_def_section:
|
|
118
|
+
if line is not None:
|
|
119
|
+
line = line.strip().split("\t")
|
|
120
|
+
# Some files in RxNorm ctls are formatted differently
|
|
121
|
+
# (i.e. space deliniated, extra whitespace), so we
|
|
122
|
+
# do some sanity checking here.
|
|
123
|
+
if len(line) == 1:
|
|
124
|
+
if len(line[0]) == 0:
|
|
125
|
+
continue
|
|
126
|
+
else:
|
|
127
|
+
line = line[0].split()
|
|
128
|
+
df_type, parquet_type = self.sql_type_to_df_parquet_type(line[1])
|
|
129
|
+
table["headers"].append(line[0])
|
|
130
|
+
table["dtype"][line[0]] = df_type
|
|
131
|
+
table["parquet_types"].append(parquet_type)
|
|
132
|
+
return datasource, table
|
|
133
|
+
|
|
134
|
+
def create_parquet(
|
|
135
|
+
self,
|
|
136
|
+
rrf_path: pathlib.Path,
|
|
137
|
+
parquet_path: pathlib.Path,
|
|
138
|
+
table: dict[list],
|
|
139
|
+
config: base_utils.StudyConfig,
|
|
140
|
+
):
|
|
141
|
+
"""Creates a parquet file from a .rrf metathesaurus file
|
|
142
|
+
|
|
143
|
+
:param rrf_path: the location of the .rrf files
|
|
144
|
+
:param parquet_path: the location to write output parquet to
|
|
145
|
+
:param table: a table definition created by parse_ctl_files
|
|
146
|
+
:param force_upload: if true, upload to a remote source regardless of what
|
|
147
|
+
already exists there
|
|
148
|
+
"""
|
|
149
|
+
if not config.force_upload:
|
|
150
|
+
if (parquet_path / f"{rrf_path.stem}/{rrf_path.stem}.parquet").exists():
|
|
151
|
+
return
|
|
152
|
+
else:
|
|
153
|
+
(parquet_path / rrf_path.stem).mkdir(parents=True, exist_ok=True)
|
|
154
|
+
df = pandas.read_csv(
|
|
155
|
+
rrf_path,
|
|
156
|
+
delimiter="|",
|
|
157
|
+
names=table["headers"],
|
|
158
|
+
dtype=table["dtype"],
|
|
159
|
+
index_col=False,
|
|
160
|
+
)
|
|
161
|
+
df.to_parquet(parquet_path / f"{rrf_path.stem}/{rrf_path.stem}.parquet")
|
|
162
|
+
|
|
163
|
+
def prepare_queries(
|
|
164
|
+
self,
|
|
165
|
+
config: base_utils.StudyConfig,
|
|
166
|
+
manifest: study_manifest.StudyManifest,
|
|
167
|
+
*args,
|
|
168
|
+
**kwargs,
|
|
169
|
+
):
|
|
170
|
+
download_path = pathlib.Path(__file__).resolve().parent / "downloads"
|
|
171
|
+
download_path.mkdir(exist_ok=True, parents=True)
|
|
172
|
+
parquet_path = pathlib.Path(__file__).resolve().parent / "generated_parquet"
|
|
173
|
+
parquet_path.mkdir(exist_ok=True, parents=True)
|
|
174
|
+
files, new_version, folder = self.get_rxnorm_data(
|
|
175
|
+
download_path, parquet_path, config.force_upload, config.umls_key
|
|
176
|
+
)
|
|
177
|
+
parquet_path = parquet_path / folder
|
|
178
|
+
parquet_path.mkdir(exist_ok=True, parents=True)
|
|
179
|
+
schema = base_utils.get_schema(config, manifest)
|
|
180
|
+
with base_utils.get_progress_bar() as progress:
|
|
181
|
+
task = progress.add_task(
|
|
182
|
+
None,
|
|
183
|
+
total=len(files),
|
|
184
|
+
)
|
|
185
|
+
for file in files:
|
|
186
|
+
with open(file) as f:
|
|
187
|
+
datasource, table = self.parse_ctl_file(f.readlines())
|
|
188
|
+
progress.update(task, description=f"Compressing {datasource}...")
|
|
189
|
+
rrf_path = download_path / f"./{folder}/rrf/{datasource}"
|
|
190
|
+
self.create_parquet(rrf_path, parquet_path, table, config)
|
|
191
|
+
progress.update(task, description=f"Uploading {datasource}...")
|
|
192
|
+
remote_path = config.db.upload_file(
|
|
193
|
+
file=parquet_path / f"{file.stem}/{file.stem}.parquet",
|
|
194
|
+
study="rxnorm",
|
|
195
|
+
topic=file.stem,
|
|
196
|
+
remote_filename=f"{file.stem}.parquet",
|
|
197
|
+
force_upload=config.force_upload or new_version,
|
|
198
|
+
)
|
|
199
|
+
self.queries.append(
|
|
200
|
+
base_templates.get_ctas_from_parquet_query(
|
|
201
|
+
schema_name=schema,
|
|
202
|
+
table_name=f"rxnorm__{file.stem}",
|
|
203
|
+
local_location=parquet_path / f"{file.stem}",
|
|
204
|
+
remote_location=remote_path,
|
|
205
|
+
table_cols=table["headers"],
|
|
206
|
+
remote_table_cols_types=table["parquet_types"],
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
progress.advance(task)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
version = "1.0"
|
|
3
|
+
name = "cumulus-library-rxnorm"
|
|
4
|
+
requires-python = ">= 3.11"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"cumulus-library >= 6, <7.0",
|
|
7
|
+
]
|
|
8
|
+
description = "A RxNorm study for the Cumulus project"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text="Apache License 2.0" }
|
|
11
|
+
keywords = ["FHIR", "SQL", "Health Informatics"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"License :: OSI Approved :: Apache Software License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
]
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"ruff == 0.15.20",
|
|
21
|
+
"pre-commit",
|
|
22
|
+
]
|
|
23
|
+
test = [
|
|
24
|
+
"pytest",
|
|
25
|
+
"responses"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Home = "https://smarthealthit.org/cumulus"
|
|
30
|
+
Documentation = "https://docs.smarthealthit.org/cumulus/"
|
|
31
|
+
Source = "https://github.com/smart-on-fhir/cumulus-library-rxnorm"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["flit_core >=3.4,<4"]
|
|
35
|
+
build-backend = "flit_core.buildapi"
|
|
36
|
+
|
|
37
|
+
[tool.flit.sdist]
|
|
38
|
+
include = [".sqlfluff"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
minversion = "6.0"
|
|
42
|
+
testpaths = [
|
|
43
|
+
"tests",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
target-version = "py310"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = [
|
|
51
|
+
"A", # prevent using keywords that clobber python builtins
|
|
52
|
+
"B", # bugbear: security warnings
|
|
53
|
+
"E", # pycodestyle
|
|
54
|
+
"F", # pyflakes
|
|
55
|
+
"I", # isort
|
|
56
|
+
"ISC", # implicit string concatenation
|
|
57
|
+
"PLE", # pylint errors
|
|
58
|
+
"RUF", # the ruff developer's own rules
|
|
59
|
+
"UP", # alert you when better syntax is available in your python version
|
|
60
|
+
]
|
|
61
|
+
ignore = [
|
|
62
|
+
# Recommended ingore from `ruff format` due to in-project conflicts with check.
|
|
63
|
+
# It's expected that this will be fixed in the coming months.
|
|
64
|
+
"ISC001"
|
|
65
|
+
]
|