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.
@@ -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.
@@ -0,0 +1,9 @@
1
+ study_prefix = "rxnorm"
2
+
3
+ [[stages.default]]
4
+ files = [
5
+ "rxnorm_builder.py"
6
+ ]
7
+
8
+ [advanced_options]
9
+ dedicated_schema="rxnorm"
@@ -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
+ ]