diff --git a/pyproject.toml b/pyproject.toml index a1040a4..c84b324 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,7 @@ ignore = [ # B011 - assert-false # INP001 - implicit-namespace-package # D103 - missing-docstring-public-function -"tests/*" = ["ANN001", "ANN2", "ANN102", "S101", "B011", "INP001", "D103"] +"tests/*" = ["ANN001", "ANN2", "ANN102", "S101", "B011", "INP001", "D103", "D100"] [tool.ruff.format] docstring-code-format = true diff --git a/src/regbot/fetch/drugsfda.py b/src/regbot/fetch/drugsfda.py index 240d40c..bb86954 100644 --- a/src/regbot/fetch/drugsfda.py +++ b/src/regbot/fetch/drugsfda.py @@ -635,7 +635,7 @@ def _get_result(data: dict, normalize: bool) -> Result: ) -def get_drugsfda_results( +def make_drugsatfda_request( url: str, normalize: bool = False, limit: int = 500 ) -> list[Result] | None: """Get Drugs@FDA data given an API query URL. @@ -677,7 +677,7 @@ def get_anda_results(anda: str, normalize: bool = False) -> list[Result] | None: :return: list of Drugs@FDA ``Result``s if successful """ url = f"https://api.fda.gov/drug/drugsfda.json?search=openfda.application_number:ANDA{anda}" - return get_drugsfda_results(url, normalize) + return make_drugsatfda_request(url, normalize) def get_nda_results(nda: str, normalize: bool = False) -> list[Result] | None: @@ -689,4 +689,4 @@ def get_nda_results(nda: str, normalize: bool = False) -> list[Result] | None: :return: list of Drugs@FDA ``Result``s if successful """ url = f"https://api.fda.gov/drug/drugsfda.json?search=openfda.application_number:NDA{nda}" - return get_drugsfda_results(url, normalize) + return make_drugsatfda_request(url, normalize) diff --git a/src/regbot/fetch/rxclass.py b/src/regbot/fetch/rxclass.py index 2337960..fc34ef6 100644 --- a/src/regbot/fetch/rxclass.py +++ b/src/regbot/fetch/rxclass.py @@ -1,4 +1,4 @@ -"""todo""" +"""Fetch data from RxClass API.""" import logging from collections import namedtuple @@ -118,37 +118,63 @@ def _missing_(cls, value): # noqa: ANN001 ANN206 ) -def _get_concept(concept_raw: dict) -> DrugConcept: - """TODO""" +def _get_concept(concept_raw: dict, normalize: bool) -> DrugConcept: return DrugConcept( concept_id=f"rxcui:{concept_raw['rxcui']}", name=concept_raw["name"], - term_type=TermType[concept_raw["tty"]], + term_type=TermType[concept_raw["tty"]] if normalize else concept_raw["tty"], ) -def _get_classification(classification_raw: dict) -> DrugClassification: - """TODO""" +def _get_classification( + classification_raw: dict, normalize: bool +) -> DrugClassification: return DrugClassification( class_id=classification_raw["classId"], class_name=classification_raw["className"], - class_type=classification_raw["classType"], + class_type=ClassType(classification_raw["classType"].lower()) + if normalize + else classification_raw["classType"], class_url=classification_raw.get("classUrl"), ) -def _get_rxclass_entry(drug_info: dict) -> RxClassEntry: - """Todo""" +def _get_relation(raw_value: str | None, normalize: bool) -> str | Relation | None: + if raw_value is None: + return None + if normalize: + return Relation(raw_value.lower()) + return raw_value + + +def _get_relation_source(raw_value: str, normalize: bool) -> str | RelationSource: + return RelationSource(raw_value.lower()) if normalize else raw_value + + +def _get_rxclass_entry(drug_info: dict, normalize: bool) -> RxClassEntry: return RxClassEntry( - concept=_get_concept(drug_info["minConcept"]), - drug_classification=_get_classification(drug_info["rxclassMinConceptItem"]), - relation=Relation(drug_info["rela"].lower()) if drug_info["rela"] else None, - relation_source=RelationSource(drug_info["relaSource"].lower()), + concept=_get_concept(drug_info["minConcept"], normalize), + drug_classification=_get_classification( + drug_info["rxclassMinConceptItem"], normalize + ), + relation=_get_relation(drug_info.get("rela"), normalize), + relation_source=_get_relation_source(drug_info["relaSource"], normalize), ) -def make_rxclass_request(url: str, include_snomedt: bool = False) -> list[RxClassEntry]: - """TODO""" +def make_rxclass_request( + url: str, include_snomedt: bool = False, normalize: bool = False +) -> list[RxClassEntry]: + """Issue an API request to RxClass. + + :param url: RxClass API URL to request + :param include_snomedct: if ``True``, include class claims provided by SNOMEDCT. + These are provided under a different license from the rest of the data and + may present publishability issues for data consumers. + :param normalize: if ``True``, try to normalize values to controlled enumerations + and appropriate Python datatypes + :return: processed list of drug class descriptions from RxClass + """ with requests.get(url, timeout=30) as r: try: r.raise_for_status() @@ -156,8 +182,10 @@ def make_rxclass_request(url: str, include_snomedt: bool = False) -> list[RxClas _logger.warning("Request to %s returned status code %s", url, r.status_code) raise e raw_data = r.json() + if not raw_data: + return [] processed_results = [ - _get_rxclass_entry(entry) + _get_rxclass_entry(entry, normalize) for entry in raw_data["rxclassDrugInfoList"]["rxclassDrugInfo"] ] if not include_snomedt: @@ -167,9 +195,23 @@ def make_rxclass_request(url: str, include_snomedt: bool = False) -> list[RxClas return processed_results -def get_drug_info(drug: str) -> list[RxClassEntry]: - """TODO""" +def get_drug_class_info( + drug: str, include_snomedct: bool = False, normalize: bool = False +) -> list[RxClassEntry]: + """Get RxClass-provided drug info. + + See also RxClass getClassByRxNormDrugName API: + https://lhncbc.nlm.nih.gov/RxNav/APIs/api-RxClass.getClassByRxNormDrugName.html + + :param drug: RxNorm-provided drug name + :param include_snomedct: if ``True``, include class claims provided by SNOMEDCT. + These are provided under a different license from the rest of the data and + may present publishability issues for data consumers. + :param normalize: if ``True``, try to normalize values to controlled enumerations + and appropriate Python datatypes + :return: list of drug class descriptions from RxClass + """ url = ( f"https://rxnav.nlm.nih.gov/REST/rxclass/class/byDrugName.json?drugName={drug}" ) - return make_rxclass_request(url) + return make_rxclass_request(url, include_snomedct, normalize) diff --git a/tests/fixtures/fetch_rxclass_imatinib.json b/tests/fixtures/fetch_rxclass_imatinib.json new file mode 100644 index 0000000..57b4fe9 --- /dev/null +++ b/tests/fixtures/fetch_rxclass_imatinib.json @@ -0,0 +1,618 @@ +{ + "rxclassDrugInfoList": { + "rxclassDrugInfo": [ + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "129557000", + "className": "Protein-tyrosine kinase inhibitor-containing product", + "classType": "DISPOS", + "classUrl": "http://snomed.info/id/129557000" + }, + "rela": "isa_disposition", + "relaSource": "SNOMEDCT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "27867009", + "className": "Antineoplastic agent", + "classType": "THERAP", + "classUrl": "http://snomed.info/id/27867009" + }, + "rela": "isa_therapeutic", + "relaSource": "SNOMEDCT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "768609001", + "className": "Pyrimidine-containing product", + "classType": "STRUCT", + "classUrl": "http://snomed.info/id/768609001" + }, + "rela": "isa_structure", + "relaSource": "SNOMEDCT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "860929000", + "className": "Benzamide structure-containing product", + "classType": "STRUCT", + "classUrl": "http://snomed.info/id/860929000" + }, + "rela": "isa_structure", + "relaSource": "SNOMEDCT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D000068877", + "className": "Imatinib Mesylate", + "classType": "CHEM" + }, + "rela": "has_ingredient", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D007945", + "className": "Leukemia, Lymphoid", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D007951", + "className": "Leukemia, Myeloid", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D017681", + "className": "Hypereosinophilic Syndrome", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D018223", + "className": "Dermatofibrosarcoma", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D034721", + "className": "Mastocytosis, Systemic", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "D054437", + "className": "Myelodysplastic-Myeloproliferative Diseases", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATC" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000009689", + "className": "Increased T Lymphocyte Destruction", + "classType": "PE" + }, + "rela": "has_pe", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000020009", + "className": "Bcr-Abl Tyrosine Kinase Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "DAILYMED" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000020009", + "className": "Bcr-Abl Tyrosine Kinase Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000020009", + "className": "Bcr-Abl Tyrosine Kinase Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "FDASPL" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000175605", + "className": "Kinase Inhibitor", + "classType": "EPC" + }, + "rela": "has_epc", + "relaSource": "DAILYMED" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000175605", + "className": "Kinase Inhibitor", + "classType": "EPC" + }, + "rela": "has_epc", + "relaSource": "FDASPL" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000175672", + "className": "Cellular Proliferation Alteration", + "classType": "PE" + }, + "rela": "has_pe", + "relaSource": "MEDRT" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000182137", + "className": "Cytochrome P450 2D6 Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "FDASPL" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000182137", + "className": "Cytochrome P450 2D6 Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "DAILYMED" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000182141", + "className": "Cytochrome P450 3A4 Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "FDASPL" + }, + { + "minConcept": { "rxcui": "282388", "name": "imatinib", "tty": "IN" }, + "rxclassMinConceptItem": { + "classId": "N0000182141", + "className": "Cytochrome P450 3A4 Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "DAILYMED" + }, + { + "minConcept": { + "rxcui": "284206", + "name": "imatinib 100 MG Oral Capsule", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D000068877", + "className": "Imatinib Mesylate", + "classType": "CHEM" + }, + "rela": "has_ingredient", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D001752", + "className": "Blast Crisis", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D004342", + "className": "Drug Hypersensitivity", + "classType": "DISEASE" + }, + "rela": "ci_with", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D005770", + "className": "Gastrointestinal Neoplasms", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D007945", + "className": "Leukemia, Lymphoid", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D007951", + "className": "Leukemia, Myeloid", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D011247", + "className": "Pregnancy", + "classType": "DISEASE" + }, + "rela": "ci_with", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D015465", + "className": "Leukemia, Myeloid, Accelerated Phase", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D017681", + "className": "Hypereosinophilic Syndrome", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D018223", + "className": "Dermatofibrosarcoma", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D034721", + "className": "Mastocytosis, Systemic", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "D054437", + "className": "Myelodysplastic-Myeloproliferative Diseases", + "classType": "DISEASE" + }, + "rela": "may_treat", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "N0000008373", + "className": "Connective Tissue Alteration", + "classType": "PE" + }, + "rela": "has_pe", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "N0000009689", + "className": "Increased T Lymphocyte Destruction", + "classType": "PE" + }, + "rela": "has_pe", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "N0000020009", + "className": "Bcr-Abl Tyrosine Kinase Inhibitors", + "classType": "MOA" + }, + "rela": "has_moa", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "284924", + "name": "imatinib mesylate", + "tty": "PIN" + }, + "rxclassMinConceptItem": { + "classId": "N0000175672", + "className": "Cellular Proliferation Alteration", + "classType": "PE" + }, + "rela": "has_pe", + "relaSource": "MEDRT" + }, + { + "minConcept": { + "rxcui": "403878", + "name": "imatinib 100 MG Oral Tablet", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "AN900", + "className": "ANTINEOPLASTIC,OTHER", + "classType": "VA" + }, + "rela": "has_VAClass", + "relaSource": "VA" + }, + { + "minConcept": { + "rxcui": "403878", + "name": "imatinib 100 MG Oral Tablet", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + }, + { + "minConcept": { + "rxcui": "403879", + "name": "imatinib 400 MG Oral Tablet", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "AN900", + "className": "ANTINEOPLASTIC,OTHER", + "classType": "VA" + }, + "rela": "has_VAClass", + "relaSource": "VA" + }, + { + "minConcept": { + "rxcui": "403879", + "name": "imatinib 400 MG Oral Tablet", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + }, + { + "minConcept": { + "rxcui": "404588", + "name": "imatinib 100 MG Oral Tablet [Gleevec]", + "tty": "SBD" + }, + "rxclassMinConceptItem": { + "classId": "AN900", + "className": "ANTINEOPLASTIC,OTHER", + "classType": "VA" + }, + "rela": "has_VAClass_extended", + "relaSource": "VA" + }, + { + "minConcept": { + "rxcui": "404588", + "name": "imatinib 100 MG Oral Tablet [Gleevec]", + "tty": "SBD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + }, + { + "minConcept": { + "rxcui": "404589", + "name": "imatinib 400 MG Oral Tablet [Gleevec]", + "tty": "SBD" + }, + "rxclassMinConceptItem": { + "classId": "AN900", + "className": "ANTINEOPLASTIC,OTHER", + "classType": "VA" + }, + "rela": "has_VAClass_extended", + "relaSource": "VA" + }, + { + "minConcept": { + "rxcui": "404589", + "name": "imatinib 400 MG Oral Tablet [Gleevec]", + "tty": "SBD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + }, + { + "minConcept": { + "rxcui": "411240", + "name": "imatinib 50 MG Oral Capsule", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + }, + { + "minConcept": { + "rxcui": "483431", + "name": "imatinib 400 MG Oral Capsule", + "tty": "SCD" + }, + "rxclassMinConceptItem": { + "classId": "L01EA", + "className": "BCR-ABL tyrosine kinase inhibitors", + "classType": "ATC1-4" + }, + "rela": "", + "relaSource": "ATCPROD" + } + ] + } +} diff --git a/tests/test_fetch_rxclass.py b/tests/test_fetch_rxclass.py new file mode 100644 index 0000000..8506dc2 --- /dev/null +++ b/tests/test_fetch_rxclass.py @@ -0,0 +1,25 @@ +from pathlib import Path + +import requests_mock + +from regbot.fetch.rxclass import get_drug_class_info + + +def test_get_rxclass(fixtures_dir: Path): + with requests_mock.Mocker() as m: + m.get( + "https://rxnav.nlm.nih.gov/REST/rxclass/class/byDrugName.json?drugName=not_a_drug", + text="{}", + ) + results = get_drug_class_info("not_a_drug") + assert results == [] + + with ( + requests_mock.Mocker() as m, + (fixtures_dir / "fetch_rxclass_imatinib.json").open() as json_response, + ): + m.get( + "https://rxnav.nlm.nih.gov/REST/rxclass/class/byDrugName.json?drugName=imatinib", + text=json_response.read(), + ) + results = get_drug_class_info("imatinib", normalize=True)