Repository: cancervariants/therapy-normalization (language: python)

Issue:
Update CI/CD, add stronger styling/autoformat
- [x] Standardize testing https://tox.wiki/en/latest/
- [x] Flake8 enforce double quotes https://github.com/zheller/flake8-quotes/
- [x] Flake8 enforce import order https://github.com/PyCQA/flake8-import-order
- [x] ~Autoformat https://github.com/psf/black/actions/runs/17913292/workflow~


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index e7a7bd52..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,7 +0,0 @@
-[run]
-source =
-   metakb/
-   tests/
-omit =
-   setup.py
-   docs/*
\ No newline at end of file
diff --git a/.flake8 b/.flake8
index cee4ee88..42ad32f3 100644
--- a/.flake8
+++ b/.flake8
@@ -1,5 +1,6 @@
 [flake8]
-ignore = D205, D400
+ignore = D205, D400, I101, ANN101, ANN002, ANN003
+max-line-length = 88
 exclude =
   .git
   venv
@@ -7,4 +8,15 @@ exclude =
   source
   outputs
   docs/*
+  analysis/*
+  codebuild/*
   therapy/version.py
+inline-quotes = "
+import-order-style = pep8
+application-import-names =
+  therapy
+  tests
+per-file-ignores =
+  tests/*:ANN001, ANN2
+  *__init__.py:F401
+mypy-init-return = true
diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml
index 64f64812..8432261e 100644
--- a/.github/workflows/github-actions.yml
+++ b/.github/workflows/github-actions.yml
@@ -8,7 +8,7 @@ jobs:
             AWS_SECRET_ACCESS_KEY: ${{ secrets.DUMMY_AWS_SECRET_ACCESS_KEY }}
             AWS_DEFAULT_REGION: us-east-2
             AWS_DEFAULT_OUTPUT: text
-            THERAPY_NORM_DB_URL: http://localhost:8000
+            THERAPY_NORM_DB_URL: http://localhost:8001
             TEST: THERAPY_TEST
         steps:
             - uses: actions/checkout@v2
@@ -25,8 +25,14 @@ jobs:
 
             - name: Build local DynamoDB
               run: |
-                  chmod +x ./tests/unit/dynamodb_build.bash
-                  ./tests/unit/dynamodb_build.bash
+                  chmod +x ./tests/scripts/dynamodb_build.bash
+                  ./tests/scripts/dynamodb_build.bash
 
-            - run: pipenv run flake8
-            - run: pipenv run pytest tests/
+            - name: Check style
+              run: pipenv run tox -e py38-lint
+
+            - name: Check type correctness
+              run: pipenv run tox -e py38-mypy
+
+            - name: Run tests
+              run: pipenv run tox -e githubactions
diff --git a/.gitignore b/.gitignore
index a51eb5a6..baab3e6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,7 +98,7 @@ ipython_config.py
 #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 #   install all needed dependencies.
-#Pipfile.lock
+Pipfile.lock
 
 # PEP 582; used by e.g. github.com/David-OConnor/pyflow
 __pypackages__/
@@ -146,18 +146,15 @@ cython_debug/
 # allow pytest override with pytest.ini
 pytest.ini
 
-# ignore data dir
+# data directories
 data/*
 therapy/data/
 
+# dynamodb
+dynamodb_local_latest/*
+tests/scripts/dynamodb_local/*
+
 # IDE materials
 .idea/
 .vim/
 *.swp
-
-# DynamoDB Local
-dynamodb_local_latest/*
-
-# Build files
-Pipfile.lock
-pyproject.toml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4c7e7618..a1b72af1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,6 +5,6 @@ repos:
     rev: v1.4.0
     hooks:
     - id: flake8
-      additional_dependencies: [flake8-docstrings]
+      additional_dependencies: [flake8-docstrings, flake8-quotes, flake8-import-order, flake8-annotations]
     - id: check-added-large-files
     - id: detect-private-key
diff --git a/EBSampleApp-Python.iml b/EBSampleApp-Python.iml
deleted file mode 100644
index db04778c..00000000
--- a/EBSampleApp-Python.iml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="PYTHON_MODULE" version="4">
-  <component name="GoLibraries">
-    <option name="urls">
-      <set>
-        <option value="file://$MODULE_DIR$" />
-      </set>
-    </option>
-  </component>
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <excludeFolder url="file://$MODULE_DIR$/build" />
-    </content>
-    <orderEntry type="jdk" jdkName="Python 3.6 (NEWRELEASE)" jdkType="Python SDK" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..819034e3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+.PHONY: clean, run
+
+run:
+	uvicorn therapy.main:app --reload --port=$(or $(port), 8000)
+
+reqs: Pipfile
+	pipenv lock --requirements > requirements.txt
+	pipenv lock --dev --requirements > requirements-dev.txt
+
+clean:
+	@find . -type f -name '*.pyc' -delete
+	@find . -type d -name '__pycache__' | xargs rm -rf
+	@rm -rf build/
+	@rm -rf dist/
+	@rm -f src/*.egg*
diff --git a/Pipfile b/Pipfile
index 25a8424d..8d59cfe2 100644
--- a/Pipfile
+++ b/Pipfile
@@ -3,21 +3,8 @@ name = "pypi"
 url = "https://pypi.org/simple"
 verify_ssl = true
 
-[dev-packages]
-pytest = "*"
-pre-commit = "*"
-flake8 = "*"
-flake8-docstrings = "*"
-coverage = "*"
-pytest-cov = "*"
-jupyterlab = "*"
-thera-py = {editable = true, path = "."}
-coveralls = "*"
-civicpy = "*"
-
 [packages]
 pydantic = ">=1.8.2"
-twine = "*"
 fastapi = ">=0.65.2"
 urllib3 = ">=1.26.5"
 uvicorn = "*"
@@ -38,3 +25,20 @@ bs4 = "*"
 pyyaml = "*"
 disease-normalizer = ">=0.2.10"
 "ga4gh.vrsatile.pydantic" = ">=0.0.3"
+
+[dev-packages]
+thera-py = {editable = true, path = "."}
+pytest = "*"
+pytest-cov = "*"
+pre-commit = "*"
+flake8 = "*"
+flake8-docstrings = "*"
+flake8-quotes = "*"
+flake8-import-order = "*"
+flake8-annotations = "*"
+jupyterlab = "*"
+civicpy = "*"
+mypy = "*"
+types-requests = "*"
+types-pyyaml = "*"
+tox = "*"
diff --git a/Precision oncology coding systems.xlsx b/Precision oncology coding systems.xlsx
deleted file mode 100644
index a0d45789..00000000
Binary files a/Precision oncology coding systems.xlsx and /dev/null differ
diff --git a/README.md b/README.md
index b375c32e..d854978b 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,9 @@ pipenv sync
 
 ### Deploying DynamoDB Locally
 
-We use Amazon DynamoDB for our database. To deploy locally, follow [these instructions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html).
+We use Amazon DynamoDB for data storage. To deploy locally, follow [these instructions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html).
 
-### Init coding style tests
+### Initialize development environment
 
 Code style is managed by [flake8](https://github.com/PyCQA/flake8) and checked prior to commit.
 
@@ -27,26 +27,32 @@ We use [pre-commit](https://pre-commit.com/#usage) to run conformance tests.
 
 This ensures:
 
-* Check code style
-* Check for added large files
-* Detect AWS Credentials
-* Detect Private Key
+* Style correctness
+* No large files
+* AWS credentials are present
+* Private key is present
 
-Before first commit run:
+Pre-commit *must* be installed before your first commit. Use the following command:
 
 ```commandline
 pre-commit install
 ```
 
-### Running unit tests
+### Running tests
 
-Running unit tests is as easy as pytest.
+Unit tests are run with pytest.
 
 ```commandline
 pipenv run pytest
 ```
 
-### Updating the therapy normalization database
+We also provide [Tox](https://tox.wiki/en/latest/index.html) settings to test in multiple environments and check for proper type annotations and code style. If interpreters for Python 3.8 and Python 3.9 are present, the following will run all tests for all environments:
+
+```commandline
+tox
+```
+
+### Updating the database
 
 Before you use the CLI to update the database, run the following in a separate terminal to start DynamoDB on `port 8000`:
 
@@ -64,7 +70,15 @@ export RXNORM_API_KEY={rxnorm_api_key}
 ```
 
 #### Update source(s)
-The Therapy Normalizer currently aggregates therapy data from [ChEMBL](https://www.ebi.ac.uk/chembl/), [NCIt](https://ncithesaurus.nci.nih.gov/ncitbrowser/), [RxNorm](https://www.nlm.nih.gov/research/umls/rxnorm/index.html), [DrugBank](https://go.drugbank.com/) (currently using CC0 data only), [ChemIDPlus](https://chem.nlm.nih.gov/chemidplus/), [Wikidata](https://www.wikidata.org/wiki/Wikidata:Main_Page), and [HemOnc.org](https://hemonc.org/wiki/Main_Page).
+The Therapy Normalizer currently aggregates therapy data from:
+* [ChEMBL](https://www.ebi.ac.uk/chembl/)
+* [ChemIDPlus](https://chem.nlm.nih.gov/chemidplus/)
+* [DrugBank](https://go.drugbank.com/) (currently using CC0 data only)
+* [The IUPHAR/BPS Guide to PHARMACOLOGY](https://www.guidetopharmacology.org/)
+* [HemOnc.org](https://hemonc.org/wiki/Main_Page).
+* [The National Cancer Institute Thesaurus](https://ncithesaurus.nci.nih.gov/ncitbrowser/)
+* [RxNorm](https://www.nlm.nih.gov/research/umls/rxnorm/index.html)
+* [Wikidata](https://www.wikidata.org/wiki/Wikidata:Main_Page)
 
 To update source(s), simply set `--normalizer` to the source(s) you wish to update separated by spaces. For example, the following command updates ChEMBL and Wikidata:
 
@@ -78,7 +92,7 @@ You can update all sources at once with the `--update_all` flag:
 python3 -m therapy.cli --update_all
 ```
 
-The `data/` subdirectory within the application should include all source data. The normalizer is capable of acquiring most of these files automatically; the exception is the HemOnc.org data, which must be manually downloaded from the [Harvard Dataverse](https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/9CY9C6) and placed within the `data/hemonc` subdirectory. Files for all sources should follow the naming convention demonstrated below (with version numbers/dates changed where applicable).
+The `data/` subdirectory within the application should house all desired source data. The normalizer is capable of acquiring most of these files automatically; the exception is the HemOnc.org data, which must be manually downloaded from the [Harvard Dataverse](https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/9CY9C6) and placed within the `data/hemonc` subdirectory. Files for all sources should follow the naming convention demonstrated below (with version numbers/dates changed where applicable).
 
 ```
 therapy/data
@@ -88,6 +102,9 @@ therapy/data
 │   └── chemidplus_20200327.xml
 ├── drugbank
 │   └── drugbank_5.1.8.csv
+├── guidetopharmacology
+│   ├── guidetopharmacology_ligand_id_mapping_2021.3.tsv
+│   └── guidetopharmacology_ligands_2021.3.tsv
 ├── hemonc
 │   ├── hemonc_concepts_20210225.csv
 │   ├── hemonc_rels_20210225.csv
@@ -103,7 +120,6 @@ therapy/data
 
 Updates to the HemOnc source depend on the [Disease Normalizer](https://github.com/cancervariants/disease-normalization) service. If the Disease Normalizer database appears to be empty or incomplete, updates to HemOnc will also trigger a refresh of the Disease Normalizer database. See its README for additional data requirements.
 
-
 ### Create Merged Concept Groups
 The `/normalize` endpoint relies on merged concept groups.  The `--update_merged` flag generates these groups:
 
diff --git a/cron.yaml b/cron.yaml
deleted file mode 100644
index 675190d5..00000000
--- a/cron.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-version: 1
-cron: 
-  - name: "task1"
-    url: "/scheduled"
-    schedule: "* * * * *"
diff --git a/dynamodb_revisions/README.md b/dynamodb_revisions/README.md
deleted file mode 100644
index 09011e69..00000000
--- a/dynamodb_revisions/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# DynamoDB Revisions
-Files in this directory are used to make updates to the `therapy_concepts` and/or `therapy_metadata` tables in DynamoDB.
\ No newline at end of file
diff --git a/dynamodb_revisions/add_item_type.py b/dynamodb_revisions/add_item_type.py
deleted file mode 100644
index 1d0e91a6..00000000
--- a/dynamodb_revisions/add_item_type.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""Add item_type attribute to all items."""
-import sys
-from pathlib import Path
-from timeit import default_timer as timer
-import click
-from botocore.exceptions import ClientError
-import logging
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-sys.path.append(f"{PROJECT_ROOT}")
-
-from therapy.database import Database  # noqa: E402
-
-
-logger = logging.getLogger('therapy')
-logger.setLevel(logging.DEBUG)
-
-db = Database()
-
-
-def add_item_type(label_and_type: str, concept_id: str, item_type: str):
-    """Add item_type to individual db item."""
-    key = {
-        'label_and_type': label_and_type,
-        'concept_id': concept_id
-    }
-    update_expression = "set item_type=:r"
-    update_values = {':r': item_type}
-    try:
-        db.therapies.update_item(Key=key,
-                                 UpdateExpression=update_expression,
-                                 ExpressionAttributeValues=update_values)
-    except ClientError as e:
-        logger.error(f"boto3 client error in `database.update_record()`: "
-                     f"{e.response['Error']['Message']}")
-
-
-VALID_TYPES = {'identity', 'label', 'trade_name', 'rx_brand', 'alias',
-               'other_id', 'xref', 'merger'}
-
-
-def add_item_types():
-    """Add item_type attribute to all items."""
-    last_evaluated_key = None
-    while True:
-        if last_evaluated_key:
-            response = db.therapies.scan(ExclusiveStartKey=last_evaluated_key)
-        else:
-            response = db.therapies.scan()
-
-        records = response['Items']
-        for record in records:
-            label_and_type = record['label_and_type']
-            item_type = label_and_type.split('##')[-1]
-            if item_type in VALID_TYPES:
-                add_item_type(label_and_type, record['concept_id'], item_type)
-            else:
-                logger.error(f"Couldn't parse item type for record: {record}")
-
-        last_evaluated_key = response.get('LastEvaluatedKey')
-        if not last_evaluated_key:
-            break
-
-
-if __name__ == '__main__':
-    click.echo("Adding item_types...")
-    start = timer()
-    add_item_types()
-    end = timer()
-    click.echo(f"finished adding item_types in {end - start:.5f}s.")
diff --git a/dynamodb_revisions/add_merge.py b/dynamodb_revisions/add_merge.py
deleted file mode 100644
index 6406e49e..00000000
--- a/dynamodb_revisions/add_merge.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""Add merged records and references to database."""
-from timeit import default_timer as timer
-from pathlib import Path
-import sys
-import logging
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-sys.path.append(f"{PROJECT_ROOT}")
-
-from therapy.database import Database  # noqa: E402
-from therapy.etl.merge import Merge  # noqa: E402
-from therapy import ACCEPTED_SOURCES  # noqa: #402
-
-logger = logging.getLogger('therapy')
-logger.setLevel(logging.DEBUG)
-
-
-def main():
-    """Execute scan on DB to gather concept IDs and add merged concepts."""
-    start = timer()
-    db = Database()
-
-    logger.info("Compiling concept IDs...")
-    concept_ids = []
-    done = False
-    start_key = None
-    args = dict()
-    while not done:
-        if start_key:
-            args['ExclusiveStartKey'] = start_key
-        response = db.therapies.scan(**args)
-        items = response['Items']
-        for item in items:
-            if item['label_and_type'].endswith('##identity'):
-                if item['src_name'].lower() in ACCEPTED_SOURCES:
-                    concept_ids.append(item['concept_id'])
-
-        start_key = response.get('LastEvaluatedKey', None)
-        done = start_key is None
-
-    logger.info("Concept ID scan successful.")
-    logger.info(f"{len(concept_ids)} concept IDs gathered.")
-
-    merge = Merge(db)
-
-    merge.create_merged_concepts(concept_ids)
-    end = timer()
-    logger.info(f"Merge generation successful, time: {end - start}")
-
-
-if __name__ == '__main__':
-    main()
diff --git a/dynamodb_revisions/add_xref_lookup.py b/dynamodb_revisions/add_xref_lookup.py
deleted file mode 100644
index 2a97aa6d..00000000
--- a/dynamodb_revisions/add_xref_lookup.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""Add xref reference to current concepts."""
-import sys
-from pathlib import Path
-from timeit import default_timer as timer
-import click
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-sys.path.append(f"{PROJECT_ROOT}")
-
-from therapy.database import Database  # noqa: E402
-
-
-def add_xref_refs():
-    """Add reference for xref attribute."""
-    db = Database()
-    batch = db.therapies.batch_writer()
-
-    last_evaluated_key = None
-    while True:
-        if last_evaluated_key:
-            response = db.therapies.scan(ExclusiveStartKey=last_evaluated_key)
-        else:
-            response = db.therapies.scan()
-        last_evaluated_key = response.get('LastEvaluatedKey')
-
-        records = response['Items']
-        for record in records:
-            if record['label_and_type'].endswith('##identity'):
-                for xref in record.get('xrefs', []):
-                    batch.put_item(Item={
-                        'label_and_type': f"{xref.lower()}##xref",
-                        'concept_id': record['concept_id'].lower(),
-                        'src_name': record['src_name']
-                    })
-
-        if not last_evaluated_key:
-            break
-
-
-if __name__ == '__main__':
-    click.echo("Adding xref references...")
-    start = timer()
-    add_xref_refs()
-    end = timer()
-    click.echo(f"finished adding xref references in {end - start:.5f}s.")
diff --git a/dynamodb_revisions/delete_merged_records.py b/dynamodb_revisions/delete_merged_records.py
deleted file mode 100644
index 1dcc6964..00000000
--- a/dynamodb_revisions/delete_merged_records.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Delete merged records from therapy_concepts DynamoDB Table."""
-import sys
-from pathlib import Path
-import click
-from timeit import default_timer as timer
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-sys.path.append(f"{PROJECT_ROOT}")
-
-from therapy.database import Database  # noqa: E402
-
-
-def delete_merged_records():
-    """Scan the database to delete merged records."""
-    db = Database()
-    done = False
-    start_key = None
-    args = dict()
-    while not done:
-        if start_key:
-            args['ExclusiveStartKey'] = start_key
-        response = db.therapies.scan(**args)
-        records = response['Items']
-
-        with db.therapies.batch_writer(
-                overwrite_by_pkeys=['label_and_type', 'concept_id']
-        ) as batch:
-            for record in records:
-                if record['label_and_type'].endswith('##merger'):
-                    batch.delete_item(
-                        Key={
-                            'label_and_type': record['label_and_type'],
-                            'concept_id': record['concept_id']
-                        }
-                    )
-
-        start_key = response.get('LastEvaluatedKey', None)
-        done = start_key is None
-
-
-if __name__ == '__main__':
-    click.echo('Deleting merged records...')
-    start = timer()
-    delete_merged_records()
-    end = timer()
-    click.echo(f"Deleted merged records in {end-start} seconds.")
diff --git a/dynamodb_revisions/other_id_ref.py b/dynamodb_revisions/other_id_ref.py
deleted file mode 100644
index c798137e..00000000
--- a/dynamodb_revisions/other_id_ref.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""Other_id ref, update normalized data for production DynamoDB instance."""
-from therapy.database import Database
-from timeit import default_timer as timer
-from boto3.dynamodb.conditions import Attr
-from therapy.schemas import ACCEPTED_SOURCES
-
-db = Database()
-db.batch = db.therapies.batch_writer(overwrite_by_pkeys=['label_and_type',
-                                                         'concept_id'])
-
-
-def perform_updates():
-    """Update records:
-    * Change merged record IDs from serialized concept IDs to main IDs. Also
-      update references from identity records.
-    * Add other_id refs for all records in `other_identifiers` field in
-      identity record.
-    """
-    # update merge_refs and add other_id refs for normalized sources
-    last_evaluated_key = None
-    while True:
-        if last_evaluated_key:
-            response = db.therapies.scan(ExclusiveStartKey=last_evaluated_key)
-        else:
-            response = db.therapies.scan()
-        records = response.get('Items', [])
-
-        for record in records:
-            if record['label_and_type'].endswith('identity'):
-                concept_id = record['concept_id']
-                if record['src_name'].lower() in ACCEPTED_SOURCES:
-                    old_ref = record['merge_ref']
-                    if '|' in old_ref:
-                        new_ref = old_ref.split('|', 1)[0]
-                        db.update_record(concept_id, 'merge_ref', new_ref)
-
-                for other_id in record.get('other_identifiers', []):
-                    db.add_ref_record(other_id, concept_id, 'other_id')
-
-        last_evaluated_key = response.get('LastEvaluatedKey')
-        if not last_evaluated_key:
-            break
-
-    # update concept IDs for merged records
-    last_evaluated_key = None
-    merge_filter = Attr('src_name').not_exists()
-    while True:
-        if last_evaluated_key:
-            response = db.therapies.scan(ExclusiveStartKey=last_evaluated_key,
-                                         FilterExpression=merge_filter)
-        else:
-            response = db.therapies.scan(FilterExpression=merge_filter)
-
-        records = response.get('Items', [])
-
-        for record in records:
-            old_label_and_type = record['label_and_type']
-            if old_label_and_type.endswith('merger'):
-                old_concept_id = record['concept_id']
-
-                new_concept_id = old_concept_id.split('|', 1)[0]
-                if new_concept_id != old_concept_id:
-                    db.batch.delete_item(Key={
-                        'label_and_type': old_label_and_type,
-                        'concept_id': old_concept_id,
-                    })
-                    record['concept_id'] = new_concept_id
-                    record['label_and_type'] = \
-                        f"{new_concept_id.lower()}##merger"
-                    db.add_record(record, 'merger')
-
-        last_evaluated_key = response.get('LastEvaluatedKey')
-        if not last_evaluated_key:
-            break
-    db.flush_batch()
-
-
-if __name__ == '__main__':
-    print("Performing updates...")
-    start = timer()
-    perform_updates()
-    end = timer()
-    print(f"Updated records in {end - start:.5f} seconds")
diff --git a/dynamodb_revisions/update_meta.py b/dynamodb_revisions/update_meta.py
deleted file mode 100644
index 8125870c..00000000
--- a/dynamodb_revisions/update_meta.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""Update source metadata in prod"""
-import sys
-from pathlib import Path
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-sys.path.append(f"{PROJECT_ROOT}")
-
-from therapy.database import Database  # noqa: E402
-from therapy.schemas import SourceName  # noqa: E402
-
-
-def main():
-    """Update DB with new source meta info."""
-    db = Database()
-    updates = {
-        SourceName.CHEMBL.value: {
-            "rdp_url": "http://reusabledata.org/chembl.html",
-            "data_license_attributes": {
-                "non_commercial": False,
-                "share_alike": True,
-                "attribution": True
-            }
-        },
-        SourceName.DRUGBANK.value: {
-            "rdp_url": "http://reusabledata.org/drugbank.html",
-            "data_license_attributes": {
-                "non_commercial": True,
-                "share_alike": False,
-                "attribution": True
-            }
-        },
-        SourceName.NCIT.value: {
-            "rdp_url": "http://reusabledata.org/ncit.html",
-            "data_license_attributes": {
-                "non_commercial": False,
-                "share_alike": False,
-                "attribution": True
-            }
-        },
-        SourceName.WIKIDATA.value: {
-            "rdp_url": None,
-            "data_license_attributes": {
-                "non_commercial": False,
-                "share_alike": False,
-                "attribution": False
-            }
-        }
-    }
-
-    for src_name in updates.keys():
-        attrs = {k: {'Value': v} for k, v in updates[src_name].items()}
-        db.metadata.update_item(
-            Key={'src_name': src_name},
-            AttributeUpdates=attrs
-        )
-
-
-if __name__ == '__main__':
-    main()
diff --git a/dynamodb_revisions/update_xrefs_other_ids.py b/dynamodb_revisions/update_xrefs_other_ids.py
deleted file mode 100644
index 32e4652c..00000000
--- a/dynamodb_revisions/update_xrefs_other_ids.py
+++ /dev/null
@@ -1,128 +0,0 @@
-"""Update xrefs and other_identifiers attribute."""
-import sys
-from pathlib import Path
-import click
-from timeit import default_timer as timer
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-sys.path.append(f"{PROJECT_ROOT}")
-
-from therapy.database import Database  # noqa: E402
-from therapy import PREFIX_LOOKUP  # noqa: E402
-
-
-def update_xrefs_other_ids(db):
-    """Update xrefs and other_identifiers in therapy identity concepts.
-
-    :param Database db: DynamoDB object
-    """
-    last_evaluated_key = None
-
-    while True:
-        if last_evaluated_key:
-            response = db.dynamodb_client.scan(
-                TableName=db.therapies.name,
-                ExclusiveStartKey=last_evaluated_key,
-                FilterExpression='src_name <> :src_name',
-                ExpressionAttributeValues={
-                    ':src_name': {'S': 'ChEMBL'}
-                }
-            )
-        else:
-            response = db.dynamodb_client.scan(
-                TableName=db.therapies.name,
-                FilterExpression='src_name <> :src_name',
-                ExpressionAttributeValues={
-                    ':src_name': {'S': 'ChEMBL'}
-                }
-            )
-        last_evaluated_key = response.get('LastEvaluatedKey')
-
-        records = response['Items']
-        for record in records:
-            record_identity = record['label_and_type']['S']
-            record_concept_id = record['concept_id']['S']
-            if '##identity' in record_identity:
-                other_ids = []
-                xrefs = []
-                for attr in ['other_identifiers', 'xrefs']:
-                    _add_xrefs_other_ids(record, attr, other_ids, xrefs)
-                update_item(db, record_identity, record_concept_id,
-                            other_ids, xrefs)
-
-        if not last_evaluated_key:
-            break
-
-
-def _add_xrefs_other_ids(record, attr, other_ids, xrefs):
-    """Add xrefs or other_identifiers to the corresponding list.
-
-    :param dict record: The therapy identity concept
-    :param str attr: `other_identifiers` or `xrefs`
-    :param list other_ids: The other identifiers for the therapy concept
-    :param list xrefs: The xrefs for the therapy concept
-    """
-    if attr in record:
-        if 'NULL' not in record[attr]:
-            prev_ids = record[attr]['L']
-            for prev_id in prev_ids:
-                other_id_xref = prev_id['S']
-                if other_id_xref.split(':')[0] in PREFIX_LOOKUP:
-                    other_ids.append(other_id_xref)
-                else:
-                    xrefs.append(other_id_xref)
-
-
-def update_item(db, record_identity, record_concept_id, other_ids, xrefs):
-    """Update therapy identity concept with associated other_identifiers
-    and xrefs attributes.
-
-    :param Database db: DynamoDB object
-    :param str record_identity: The therapy identity concept label_and_type
-                                value
-    :param str record_concept_id: The therapy identity concept id
-    :param list other_ids: The other identifiers for the therapy concept
-    :param list xrefs: The xrefs for the therapy concept
-    """
-    db.therapies.update_item(
-        Key={
-            'label_and_type': record_identity,
-            'concept_id': record_concept_id
-        },
-        UpdateExpression="set other_identifiers=:o, xrefs=:x",
-        ExpressionAttributeValues={
-            ':o': other_ids,
-            ':x': xrefs,
-        },
-        ReturnValues="UPDATED_NEW"
-    )
-
-    # Delete empty lists
-
-    if not xrefs:
-        db.therapies.update_item(
-            Key={
-                'label_and_type': record_identity,
-                'concept_id': record_concept_id
-            },
-            UpdateExpression="remove xrefs",
-            ReturnValues="UPDATED_NEW"
-        )
-
-    if not other_ids:
-        db.therapies.update_item(
-            Key={
-                'label_and_type': record_identity,
-                'concept_id': record_concept_id
-            },
-            UpdateExpression="remove other_identifiers",
-            ReturnValues="UPDATED_NEW"
-        )
-
-
-if __name__ == '__main__':
-    click.echo("Updating xrefs and other_identifiers...")
-    start = timer()
-    update_xrefs_other_ids(Database())
-    end = timer()
-    click.echo(f"Updated xrefs and other_identifiers in {end-start} seconds.")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..864b334a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta:__legacy__"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index a9bdbaa0..55acbb22 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -10,7 +10,7 @@
 
 -i https://pypi.org/simple
 -e .
-anyio==3.3.2; python_full_version >= '3.6.2'
+anyio==3.3.4; python_full_version >= '3.6.2'
 appnope==0.1.2; sys_platform == 'darwin' and platform_system == 'Darwin'
 argon2-cffi==21.1.0; python_version >= '3.5'
 asgiref==3.4.1; python_version >= '3.6'
@@ -22,38 +22,36 @@ backports-datetime-fromisoformat==1.0.0
 backports.entry-points-selectable==1.1.0; python_version >= '2.7'
 beautifulsoup4==4.10.0; python_version >= '3.1'
 bleach==4.1.0; python_version >= '3.6'
-boto3==1.18.55
-botocore==1.21.55; python_version >= '3.6'
+boto3==1.18.63
+botocore==1.21.63; python_version >= '3.6'
 bs4==0.0.1
-certifi==2021.5.30
-cffi==1.14.6
+certifi==2021.10.8
+cffi==1.15.0
 cfgv==3.3.1; python_full_version >= '3.6.1'
-charset-normalizer==2.0.6; python_version >= '3'
+charset-normalizer==2.0.7; python_version >= '3'
 civicpy==1.1.3
-click==8.0.1
-colorama==0.4.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-coverage[toml]==5.5
-coveralls==3.2.0
+click==8.0.3
+coverage[toml]==6.0.2; python_version >= '3.6'
 cython==0.29.24
 debugpy==1.5.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-decorator==5.1.0; python_version >= '3.5'
+decorator==5.1.0; python_full_version >= '3.5.0'
 defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
 deprecation==2.1.0
 disease-normalizer==0.2.10
 distlib==0.3.3
-docopt==0.6.2
-docutils==0.17.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
 entrypoints==0.3; python_version >= '2.7'
-fastapi==0.68.2
-filelock==3.3.0; python_version >= '3.6'
+fastapi==0.70.0
+filelock==3.3.1; python_version >= '3.6'
+flake8-annotations==2.7.0
 flake8-docstrings==1.6.0
-flake8==3.9.2
+flake8-import-order==0.18.1
+flake8-quotes==3.3.0
+flake8==4.0.1
 ga4gh.vrsatile.pydantic==0.0.3
 h11==0.12.0; python_version >= '3.6'
 httptools==0.3.0
 identify==2.3.0; python_full_version >= '3.6.1'
-idna==3.2; python_version >= '3'
-importlib-metadata==4.8.1; python_version >= '3.6'
+idna==3.3; python_version >= '3'
 iniconfig==1.1.1
 ipykernel==6.4.1
 ipython-genutils==0.2.0
@@ -63,39 +61,39 @@ jedi==0.18.0; python_version >= '3.6'
 jinja2==3.0.2; python_version >= '3.6'
 jmespath==0.10.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
 json5==0.9.6
-jsonschema==4.0.1; python_version >= '3.7'
+jsonschema==4.1.0; python_version >= '3.7'
 jupyter-client==7.0.6; python_full_version >= '3.6.1'
 jupyter-core==4.8.1; python_version >= '3.6'
 jupyter-server==1.11.1; python_version >= '3.6'
 jupyterlab-pygments==0.1.2
 jupyterlab-server==2.8.2; python_version >= '3.6'
-jupyterlab==3.1.17
-keyring==23.2.1; python_version >= '3.6'
+jupyterlab==3.2.0
 lxml==4.6.3
 markupsafe==2.0.1; python_version >= '3.6'
-matplotlib-inline==0.1.3; python_version >= '3.5'
+matplotlib-inline==0.1.3; python_full_version >= '3.5.0'
 mccabe==0.6.1
 mistune==0.8.4
 mwoauth==0.3.7
+mypy-extensions==0.4.3
+mypy==0.910
 nbclassic==0.3.2; python_version >= '3.6'
 nbclient==0.5.4; python_full_version >= '3.6.1'
 nbconvert==6.2.0; python_version >= '3.7'
 nbformat==5.1.3; python_version >= '3.5'
-nest-asyncio==1.5.1; python_version >= '3.5'
+nest-asyncio==1.5.1; python_full_version >= '3.5.0'
 networkx==2.6.3; python_version >= '3.7'
 nodeenv==1.6.0
 notebook==6.4.4; python_version >= '3.6'
-numpy==1.21.2; python_version < '3.11' and python_version >= '3.7'
+numpy==1.21.2; python_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64'
 oauthlib==3.1.1; python_version >= '3.6'
 obonet==0.3.0; python_version >= '3.5'
-owlready2==0.34
+owlready2==0.35
 packaging==21.0; python_version >= '3.6'
-pandas==1.3.3; python_full_version >= '3.7.1'
+pandas==1.3.4; python_full_version >= '3.7.1'
 pandocfilters==1.5.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
 parso==0.8.2; python_version >= '3.6'
 pexpect==4.8.0; sys_platform != 'win32'
 pickleshare==0.7.5
-pkginfo==1.7.1
 platformdirs==2.4.0; python_version >= '3.6'
 pluggy==1.0.0; python_version >= '3.6'
 pre-commit==2.15.0
@@ -103,12 +101,12 @@ prometheus-client==0.11.0; python_version >= '2.7' and python_version not in '3.
 prompt-toolkit==3.0.20; python_full_version >= '3.6.2'
 ptyprocess==0.7.0
 py==1.10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-pycodestyle==2.7.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+pycodestyle==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
 pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
 pydantic==1.8.2
 pydocstyle==6.1.1; python_version >= '3.6'
-pyflakes==2.3.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-pygments==2.10.0; python_version >= '3.5'
+pyflakes==2.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+pygments==2.10.0; python_full_version >= '3.5.0'
 pyjwt==1.7.1
 pyparsing==2.4.7
 pyrsistent==0.18.0; python_version >= '3.6'
@@ -117,30 +115,29 @@ pytest-cov==3.0.0
 pytest==6.2.5
 python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
 pytz==2021.3
-pyyaml==5.4.1
+pyyaml==6.0
 pyzmq==22.3.0; python_version >= '3.6'
-rdflib==6.0.1
-readme-renderer==30.0
+rdflib==6.0.2
 requests-oauthlib==1.3.0
-requests-toolbelt==0.9.1
 requests-unixsocket==0.2.0
 requests==2.26.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
-rfc3986==1.5.0
 s3transfer==0.5.0; python_version >= '3.6'
 send2trash==1.8.0
 simplejson==3.17.5; python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'
 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-sniffio==1.2.0; python_version >= '3.5'
+sniffio==1.2.0; python_full_version >= '3.5.0'
 snowballstemmer==2.1.0
 soupsieve==2.2.1; python_version >= '3.6'
-starlette==0.14.2; python_version >= '3.6'
+starlette==0.16.0; python_version >= '3.6'
 terminado==0.12.1; python_version >= '3.6'
 testpath==0.5.0; python_version >= '3.5'
 toml==0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
-tornado==6.1; python_version >= '3.5'
-tqdm==4.62.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+tomli==1.2.1; python_version >= '3.6'
+tornado==6.1; python_full_version >= '3.5.0'
+tox==3.24.4
 traitlets==5.1.0; python_version >= '3.7'
-twine==3.4.2
+types-pyyaml==5.4.12
+types-requests==2.25.11
 typing-extensions==3.10.0.2
 urllib3==1.26.7
 uvicorn==0.15.0
@@ -152,4 +149,3 @@ webencodings==0.5.1
 websocket-client==1.2.1; python_version >= '3.6'
 websockets==10.0
 wikibaseintegrator==0.11.0
-zipp==3.6.0; python_version >= '3.6'
diff --git a/requirements.txt b/requirements.txt
index 7158d6e4..881b0ce3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,31 +6,28 @@
 #
 
 -i https://pypi.org/simple
+anyio==3.3.4; python_full_version >= '3.6.2'
 appnope==0.1.2; sys_platform == 'darwin' and platform_system == 'Darwin'
 asgiref==3.4.1; python_version >= '3.6'
 backcall==0.2.0
 backoff==1.11.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
 beautifulsoup4==4.10.0; python_version >= '3.1'
-bleach==4.1.0; python_version >= '3.6'
-boto3==1.18.55
-botocore==1.21.55; python_version >= '3.6'
+boto3==1.18.63
+botocore==1.21.63; python_version >= '3.6'
 bs4==0.0.1
-certifi==2021.5.30
-charset-normalizer==2.0.6; python_version >= '3'
-click==8.0.1
-colorama==0.4.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
+certifi==2021.10.8
+charset-normalizer==2.0.7; python_version >= '3'
+click==8.0.3
 cython==0.29.24
 debugpy==1.5.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-decorator==5.1.0; python_version >= '3.5'
+decorator==5.1.0; python_full_version >= '3.5.0'
 disease-normalizer==0.2.10
-docutils==0.17.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
 entrypoints==0.3; python_version >= '2.7'
-fastapi==0.68.2
+fastapi==0.70.0
 ga4gh.vrsatile.pydantic==0.0.3
 h11==0.12.0; python_version >= '3.6'
 httptools==0.3.0
-idna==3.2; python_version >= '3'
-importlib-metadata==4.8.1; python_version >= '3.6'
+idna==3.3; python_version >= '3'
 ipykernel==6.4.1
 ipython-genutils==0.2.0
 ipython==7.28.0; python_version >= '3.7'
@@ -39,48 +36,39 @@ jedi==0.18.0; python_version >= '3.6'
 jmespath==0.10.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
 jupyter-client==7.0.6; python_full_version >= '3.6.1'
 jupyter-core==4.8.1; python_version >= '3.6'
-keyring==23.2.1; python_version >= '3.6'
 lxml==4.6.3
-matplotlib-inline==0.1.3; python_version >= '3.5'
+matplotlib-inline==0.1.3; python_full_version >= '3.5.0'
 mwoauth==0.3.7
-nest-asyncio==1.5.1; python_version >= '3.5'
+nest-asyncio==1.5.1; python_full_version >= '3.5.0'
 oauthlib==3.1.1; python_version >= '3.6'
-owlready2==0.34
-packaging==21.0; python_version >= '3.6'
+owlready2==0.35
 parso==0.8.2; python_version >= '3.6'
 pexpect==4.8.0; sys_platform != 'win32'
 pickleshare==0.7.5
-pkginfo==1.7.1
 prompt-toolkit==3.0.20; python_full_version >= '3.6.2'
 ptyprocess==0.7.0
 pydantic==1.8.2
-pygments==2.10.0; python_version >= '3.5'
+pygments==2.10.0; python_full_version >= '3.5.0'
 pyjwt==1.7.1
 pyparsing==2.4.7
 python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-pyyaml==5.4.1
+pyyaml==6.0
 pyzmq==22.3.0; python_version >= '3.6'
-rdflib==6.0.1
-readme-renderer==30.0
+rdflib==6.0.2
 requests-oauthlib==1.3.0
-requests-toolbelt==0.9.1
 requests==2.26.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
-rfc3986==1.5.0
 s3transfer==0.5.0; python_version >= '3.6'
 simplejson==3.17.5; python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'
 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+sniffio==1.2.0; python_full_version >= '3.5.0'
 soupsieve==2.2.1; python_version >= '3.6'
-starlette==0.14.2; python_version >= '3.6'
-tornado==6.1; python_version >= '3.5'
-tqdm==4.62.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+starlette==0.16.0; python_version >= '3.6'
+tornado==6.1; python_full_version >= '3.5.0'
 traitlets==5.1.0; python_version >= '3.7'
-twine==3.4.2
 typing-extensions==3.10.0.2
 urllib3==1.26.7
 uvicorn==0.15.0
 uvloop==0.16.0
 wcwidth==0.2.5
-webencodings==0.5.1
 websockets==10.0
 wikibaseintegrator==0.11.0
-zipp==3.6.0; python_version >= '3.6'
diff --git a/setup.cfg b/setup.cfg
index 91435904..17baa96c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -15,10 +15,13 @@ zip_safe = False
 include_package_data = True
 
 install_requires =
-    fastapi
+    fastapi >= 0.65.2
+    urllib3 >= 1.26.5
     uvicorn
+    uvloop
+    websockets >= 9.1
     click
-    lxml
+    lxml >= 4.6.3
     boto3
     cython
     owlready2
@@ -28,7 +31,9 @@ install_requires =
     bs4
     pyyaml
     pydantic
-    disease-normalizer
+    pydantic >= 1.8.2
+    disease-normalizer >= 0.2.10
+    ga4gh.vrsatile.pydantic >= 0.0.3
 
 tests_require =
     pytest
@@ -40,3 +45,15 @@ therapy =
 
 [tool:pytest]
 addopts = --ignore setup.py --ignore=codebuild/ --ignore=analysis/ --ignore=dynamodb_revisions/ --doctest-modules --cov-report term-missing --cov .
+
+[mypy]
+plugins = pydantic.mypy
+ignore_missing_imports = True
+
+[coverage:run]
+source =
+   therapy/
+   tests/
+omit =
+   setup.py
+   docs/*
diff --git a/setup.py b/setup.py
index 66118618..03d3083e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,5 @@
 """Defines how metakb is packaged and distributed."""
 from setuptools import setup
 
-exec(open('therapy/version.py').read())
+exec(open("therapy/version.py").read())
 setup(version=__version__)  # noqa: F821
diff --git a/therapy/__init__.py b/therapy/__init__.py
index 47667b4d..91829919 100644
--- a/therapy/__init__.py
+++ b/therapy/__init__.py
@@ -1,25 +1,26 @@
 """The VICC library for normalizing therapies."""
-from .version import __version__  # noqa: F401
 from pathlib import Path
 import logging
 
-PROJECT_ROOT = Path(__file__).resolve().parents[0]
+from .version import __version__
+
+PROJECT_ROOT: Path = Path(__file__).resolve().parents[0]
 logging.basicConfig(
-    filename='therapy.log',
-    format='[%(asctime)s] - %(name)s - %(levelname)s : %(message)s')
-logger = logging.getLogger('therapy')
+    filename="therapy.log",
+    format="[%(asctime)s] - %(name)s - %(levelname)s : %(message)s")
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
 class DownloadException(Exception):
     """Exception for failures relating to source file downloads."""
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, **kwargs):  # noqa: ANN204
         """Initialize exception."""
         super().__init__(*args, **kwargs)
 
 
-from therapy.schemas import SourceName, NamespacePrefix, SourceIDAfterNamespace, ItemTypes  # noqa: E402, E501
+from therapy.schemas import SourceName, NamespacePrefix, SourceIDAfterNamespace, ItemTypes  # noqa: E402, E501, I100, I202
 ITEM_TYPES = {k.lower(): v.value for k, v in ItemTypes.__members__.items()}
 
 # Sources we import directly
@@ -36,15 +37,15 @@ def __init__(self, *args, **kwargs):
 # e.g. {'q': 'wikidata'}
 NAMESPACE_LOOKUP = {v.value.lower(): NamespacePrefix[k].value
                     for k, v in SourceIDAfterNamespace.__members__.items()
-                    if v.value != ''}
+                    if v.value != ""}
 
 # Sources that we import directly
 XREF_SOURCES = {source for source in SourceName.__members__}
 
 # Sources that are found in data from imported sources
-ASSOC_WITH_SOURCES = {source for source in NamespacePrefix.__members__} - XREF_SOURCES  # noqa: E501
+ASSOC_WITH_SOURCES = {source for source in NamespacePrefix.__members__} - XREF_SOURCES
 
-from therapy.etl import ChEMBL, Wikidata, DrugBank, NCIt, ChemIDplus, RxNorm, HemOnc, GuideToPHARMACOLOGY  # noqa: F401, E402, E501
+from therapy.etl import ChEMBL, Wikidata, DrugBank, NCIt, ChemIDplus, RxNorm, HemOnc, GuideToPHARMACOLOGY  # noqa: E402, E501, I202
 # used to get source class name from string
 SOURCES_CLASS = \
     {s.value.lower(): eval(s.value) for s in SourceName.__members__.values()}
diff --git a/therapy/cli.py b/therapy/cli.py
index de9d0109..5346710f 100644
--- a/therapy/cli.py
+++ b/therapy/cli.py
@@ -1,19 +1,22 @@
 """This module provides a CLI util to make updates to normalizer database."""
+from os import environ
+import logging
+from timeit import default_timer as timer
+from typing import List, Optional
+
 import click
 from botocore.exceptions import ClientError
-from therapy.etl.merge import Merge
-from timeit import default_timer as timer
 from boto3.dynamodb.conditions import Key
-from therapy.schemas import SourceName
-from therapy import SOURCES_CLASS, SOURCES
-from therapy.database import Database
 from disease.database import Database as DiseaseDatabase
 from disease.cli import CLI as DiseaseCLI
 from disease.schemas import SourceName as DiseaseSources
-from os import environ
-import logging
 
-logger = logging.getLogger('therapy')
+from therapy import SOURCES_CLASS, SOURCES
+from therapy.schemas import SourceName
+from therapy.database import Database
+from therapy.etl.merge import Merge
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
@@ -23,44 +26,50 @@ class CLI:
     @staticmethod
     @click.command()
     @click.option(
-        '--normalizer',
+        "--normalizer",
         help="The normalizer(s) you wish to update separated by spaces."
     )
     @click.option(
-        '--prod',
+        "--prod",
         is_flag=True,
         help="Working in production environment."
     )
     @click.option(
-        '--db_url',
+        "--db_url",
         help="URL endpoint for the application database."
     )
     @click.option(
-        '--update_all',
+        "--update_all",
         is_flag=True,
-        help='Update all normalizer sources.'
+        help="Update all normalizer sources."
     )
     @click.option(
-        '--update_merged',
+        "--update_merged",
         is_flag=True,
-        help='Update concepts for normalize endpoint from accepted sources.'
+        help="Update concepts for normalize endpoint from accepted sources."
     )
-    def update_normalizer_db(normalizer, prod, db_url, update_all,
-                             update_merged):
-        """Update select normalizer source(s) in the therapy database."""
+    def update_normalizer_db(normalizer: str, prod: bool, db_url: str,
+                             update_all: bool, update_merged: bool) -> None:
+        """Update select normalizer source(s) in the therapy database.
+        :param str normalizer: comma-separated string listing source names
+        :param bool prod: if true, utilize production environment settings
+        :param str db_url: DynamoDB endpoint URL (usually only needed locally)
+        :param bool update_all: if true, update all sources
+        :param bool update_merged: if true, update normalized group results
+        """
         endpoint_url = None
         if prod:
-            environ['THERAPY_NORM_PROD'] = "TRUE"
-            environ['DISEASE_NORM_PROD'] = "TRUE"
+            environ["THERAPY_NORM_PROD"] = "TRUE"
+            environ["DISEASE_NORM_PROD"] = "TRUE"
             db: Database = Database()
         else:
             if db_url:
                 endpoint_url = db_url
-            elif 'THERAPY_NORM_DB_URL' in environ.keys():
-                endpoint_url = environ['THERAPY_NORM_DB_URL']
+            elif "THERAPY_NORM_DB_URL" in environ.keys():
+                endpoint_url = environ["THERAPY_NORM_DB_URL"]
             else:
-                endpoint_url = 'http://localhost:8000'
-            db: Database = Database(db_url=endpoint_url)
+                endpoint_url = "http://localhost:8000"
+            db = Database(db_url=endpoint_url)
 
         if update_all:
             normalizers = list(src for src in SOURCES)
@@ -85,60 +94,70 @@ def update_normalizer_db(normalizer, prod, db_url, update_all,
             CLI()._check_disease_normalizer(normalizers, endpoint_url)
             CLI()._update_normalizers(normalizers, db, update_merged)
 
-    def _check_disease_normalizer(self, normalizers, endpoint_url):
-        """When loading HemOnc source, perform rudimentary check of Disease
-            Normalizer tables, and reload them if necessary.
+    def _check_disease_normalizer(self, normalizers: List[str],
+                                  endpoint_url: Optional[str]) -> None:
+        """When loading HemOnc source, perform rudimentary check of Disease Normalizer
+        tables, and reload them if necessary.
 
         :param list normalizers: List of sources to load
-        :param str endpoint_url: Therapy endpoint URL
+        :param Optional[str] endpoint_url: Therapy endpoint URL. If none, tries to
+            connect to cloud DynamoDB instance.
         """
-        def _load_diseases():
+        def _load_diseases() -> None:
             msg = "Disease Normalizer not loaded. " \
                   "Loading Disease Normalizer..."
             logger.debug(msg)
             click.echo(msg)
             try:
                 DiseaseCLI().update_normalizer_db(
-                    ['--update_all', '--update_merged',
-                     '--db_url', endpoint_url]
+                    ["--update_all", "--update_merged", "--db_url", endpoint_url]
                 )
             except Exception as e:
                 logger.error(e)
                 raise Exception(e)
             except:  # noqa: E722
+                # TODO: what does this do?
                 pass
 
-        if 'THERAPY_NORM_PROD' not in environ and 'hemonc' in normalizers:
-            db = DiseaseDatabase(db_url=endpoint_url)
+        if "THERAPY_NORM_PROD" not in environ and "hemonc" in normalizers:
+            db = DiseaseDatabase(db_url=endpoint_url)  # type: ignore
             current_tables = {table.name for table in db.dynamodb.tables.all()}
             if not all(name in current_tables
-                       for name in ('disease_concepts', 'disease_metadata')):
+                       for name in ("disease_concepts", "disease_metadata")):
                 _load_diseases()
-            elif db.diseases.scan()['Count'] == 0 or \
-                    db.metadata.scan()['Count'] < len(DiseaseSources):
+            elif db.diseases.scan()["Count"] == 0 or \
+                    db.metadata.scan()["Count"] < len(DiseaseSources):
                 _load_diseases()
 
     @staticmethod
-    def _help_msg():
+    def _help_msg() -> None:
         """Display help message."""
         ctx = click.get_current_context()
         click.echo(
-            "Must either enter 1 or more sources, or use `--update_all` parameter")  # noqa: E501
+            "Must either enter 1 or more sources, or use `--update_all` parameter")
         click.echo(ctx.get_help())
         ctx.exit()
 
     @staticmethod
-    def _update_normalizers(normalizers, db, update_merged):
-        """Update selected normalizer sources."""
+    def _update_normalizers(normalizers: List[str], db: Database,
+                            update_merged: bool) -> None:
+        """Update selected normalizer sources.
+        :param List[str] normalizers: list of source names to update
+        :param Database db: database instance to use
+        :param bool update_merged: if true, store concept IDs as they're processed and
+            produce normalized records
+        """
         processed_ids = list()
         for n in normalizers:
             msg = f"Deleting {n}..."
             click.echo(f"\n{msg}")
             logger.info(msg)
+
             start_delete = timer()
             CLI()._delete_data(n, db)
             end_delete = timer()
             delete_time = end_delete - start_delete
+
             msg = f"Deleted {n} in {delete_time:.5f} seconds."
             click.echo(f"{msg}\n")
             logger.info(msg)
@@ -146,14 +165,17 @@ def _update_normalizers(normalizers, db, update_merged):
             msg = f"Loading {n}..."
             click.echo(msg)
             logger.info(msg)
+
             start_load = timer()
             source = SOURCES_CLASS[n](database=db)
             processed_ids += source.perform_etl()
             end_load = timer()
             load_time = end_load - start_load
+
             msg = f"Loaded {n} in {load_time:.5f} seconds."
             click.echo(msg)
             logger.info(msg)
+
             msg = f"Total time for {n}: " \
                   f"{(delete_time + load_time):.5f} seconds."
             click.echo(msg)
@@ -162,90 +184,99 @@ def _update_normalizers(normalizers, db, update_merged):
         if update_merged:
             CLI()._update_merged(db, processed_ids)
 
-    def _update_merged(self, db, processed_ids):
-        """Update merged concepts"""
+    def _update_merged(self, db: Database, processed_ids: List[str]) -> None:
+        """Build and upload merged records. Will construct list of IDs if given an empty
+        processed_ids list.
+        :param Database db: DB instance to use
+        :param List[str] processed_ids: List of IDs to create merged groups from
+        """
         start_merge = timer()
         if not processed_ids:
             CLI()._delete_normalized_data(db)
             processed_ids = db.get_ids_for_merge()
         merge = Merge(database=db)
         click.echo("Constructing normalized records...")
-        merge.create_merged_concepts(processed_ids)
+        merge.create_merged_concepts(set(processed_ids))
         end_merge = timer()
         click.echo(f"Merged concept generation completed in"
                    f" {(end_merge - start_merge):.5f} seconds.")
 
     @staticmethod
-    def _delete_normalized_data(database):
+    def _delete_normalized_data(database: Database) -> None:
         click.echo("\nDeleting normalized records...")
         start_delete = timer()
         try:
             while True:
                 with database.therapies.batch_writer(
-                        overwrite_by_pkeys=['label_and_type', 'concept_id']) \
+                        overwrite_by_pkeys=["label_and_type", "concept_id"]) \
                         as batch:
                     response = database.therapies.query(
-                        IndexName='item_type_index',
-                        KeyConditionExpression=Key('item_type').eq('merger'),
+                        IndexName="item_type_index",
+                        KeyConditionExpression=Key("item_type").eq("merger"),
                     )
-                    records = response['Items']
+                    records = response["Items"]
                     if not records:
                         break
                     for record in records:
                         batch.delete_item(Key={
-                            'label_and_type': record['label_and_type'],
-                            'concept_id': record['concept_id']
+                            "label_and_type": record["label_and_type"],
+                            "concept_id": record["concept_id"]
                         })
         except ClientError as e:
-            click.echo(e.response['Error']['Message'])
+            click.echo(e.response["Error"]["Message"])
         end_delete = timer()
         delete_time = end_delete - start_delete
         click.echo(f"Deleted normalized records in {delete_time:.5f} seconds.")
 
     @staticmethod
-    def _delete_data(source, database):
-        # Delete source's metadata
+    def _delete_data(source: str, database: Database) -> None:
+        """Delete all data (records + metadata) from given source in database.
+        :param str source: name of source to delete
+        :param Database database: db instance
+        """
+        source_name = SourceName[f"{source.upper()}"].value
+        # Delete source"s metadata first
         try:
             metadata = database.metadata.query(
                 KeyConditionExpression=Key(
-                    'src_name').eq(SourceName[f"{source.upper()}"].value)
+                    "src_name").eq(source_name)
             )
-            if metadata['Items']:
+            if metadata["Items"]:
                 database.metadata.delete_item(
-                    Key={'src_name': metadata['Items'][0]['src_name']},
+                    Key={"src_name": metadata["Items"][0]["src_name"]},
                     ConditionExpression="src_name = :src",
                     ExpressionAttributeValues={
-                        ':src': SourceName[f"{source.upper()}"].value}
+                        ":src": source_name}
                 )
         except ClientError as e:
-            click.echo(e.response['Error']['Message'])
+            click.echo(e.response["Error"]["Message"])
 
         try:
             while True:
                 response = database.therapies.query(
-                    IndexName='src_index',
-                    KeyConditionExpression=Key('src_name').eq(
-                        SourceName[f"{source.upper()}"].value),
+                    IndexName="src_index",
+                    KeyConditionExpression=Key("src_name").eq(
+                        source_name),
                 )
 
-                records = response['Items']
+                records = response["Items"]
                 if not records:
                     break
 
                 with database.therapies.batch_writer(
-                        overwrite_by_pkeys=['label_and_type', 'concept_id']) \
+                        overwrite_by_pkeys=["label_and_type", "concept_id"]) \
                         as batch:
 
                     for record in records:
                         batch.delete_item(
                             Key={
-                                'label_and_type': record['label_and_type'],
-                                'concept_id': record['concept_id']
+                                "label_and_type": record["label_and_type"],
+                                "concept_id": record["concept_id"]
                             }
                         )
         except ClientError as e:
-            click.echo(e.response['Error']['Message'])
+            click.echo(e.response["Error"]["Message"])
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     CLI().update_normalizer_db()
diff --git a/therapy/database.py b/therapy/database.py
index a3986db4..4873bb35 100644
--- a/therapy/database.py
+++ b/therapy/database.py
@@ -1,36 +1,38 @@
 """This module creates the database."""
-import boto3
-from therapy import PREFIX_LOOKUP
-from botocore.exceptions import ClientError
-from boto3.dynamodb.conditions import Key
 from os import environ
 from typing import Optional, Dict, List, Any
 import logging
-import click
 import sys
 
-logger = logging.getLogger('therapy')
+import click
+import boto3
+from boto3.dynamodb.conditions import Key
+from botocore.exceptions import ClientError
+
+from therapy import PREFIX_LOOKUP
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
 class Database:
     """The database class."""
 
-    def __init__(self, db_url: str = '', region_name: str = 'us-east-2'):
-        """Initialize Database class.
+    def __init__(self, db_url: str = "", region_name: str = "us-east-2"):
+        """Initialize Database class instance.
 
         :param str db_url: database endpoint URL to connect to
         :param str region_name: AWS region name to use
         """
-        if 'THERAPY_NORM_PROD' in environ or 'THERAPY_NORM_EB_PROD' in environ:
+        if "THERAPY_NORM_PROD" in environ or "THERAPY_NORM_EB_PROD" in environ:
             boto_params = {
-                'region_name': region_name
+                "region_name": region_name
             }
-            self.endpoint_url = ''
-            if 'THERAPY_NORM_PROD' in environ:
-                environ['DISEASE_NORM_PROD'] = "true"
-            if 'THERAPY_NORM_EB_PROD' in environ:
-                environ['DISEASE_NORM_EB_PROD'] = "true"
+            self.endpoint_url = ""
+            if "THERAPY_NORM_PROD" in environ:
+                environ["DISEASE_NORM_PROD"] = "true"
+            if "THERAPY_NORM_EB_PROD" in environ:
+                environ["DISEASE_NORM_EB_PROD"] = "true"
             else:
                 # EB Instance should not have to confirm.
                 # This is used only for using production via CLI
@@ -43,173 +45,170 @@ def __init__(self, db_url: str = '', region_name: str = 'us-east-2'):
         else:
             if db_url:
                 self.endpoint_url = db_url
-            elif 'THERAPY_NORM_DB_URL' in environ:
-                self.endpoint_url = environ['THERAPY_NORM_DB_URL']
+            elif "THERAPY_NORM_DB_URL" in environ:
+                self.endpoint_url = environ["THERAPY_NORM_DB_URL"]
             else:
-                self.endpoint_url = 'http://localhost:8000'
+                self.endpoint_url = "http://localhost:8000"
             click.echo(f"***Using Therapy Database Endpoint: "
                        f"{self.endpoint_url}***")
-            environ['DISEASE_NORM_DB_URL'] = self.endpoint_url
+            environ["DISEASE_NORM_DB_URL"] = self.endpoint_url
 
             boto_params = {
-                'region_name': region_name,
-                'endpoint_url': self.endpoint_url
+                "region_name": region_name,
+                "endpoint_url": self.endpoint_url
             }
 
-        self.dynamodb = boto3.resource('dynamodb', **boto_params)
-        self.dynamodb_client = boto3.client('dynamodb', **boto_params)
+        self.dynamodb = boto3.resource("dynamodb", **boto_params)
+        self.dynamodb_client = boto3.client("dynamodb", **boto_params)
 
         # Table creation for local database
-        if 'THERAPY_NORM_PROD' not in environ and \
-                'THERAPY_NORM_EB_PROD' not in environ:
-            existing_tables = self.dynamodb_client.list_tables()['TableNames']
+        if "THERAPY_NORM_PROD" not in environ and \
+                "THERAPY_NORM_EB_PROD" not in environ:
+            existing_tables = self.dynamodb_client.list_tables()["TableNames"]
             self.create_therapies_table(existing_tables)
             self.create_meta_data_table(existing_tables)
 
-        self.therapies = self.dynamodb.Table('therapy_concepts')
-        self.metadata = self.dynamodb.Table('therapy_metadata')
+        self.therapies = self.dynamodb.Table("therapy_concepts")
+        self.metadata = self.dynamodb.Table("therapy_metadata")
         self.batch = self.therapies.batch_writer()
-        self.cached_sources = {}
+        self.cached_sources: Dict[str, Any] = {}
 
-    def create_therapies_table(self, existing_tables: List):
-        """Create Therapies table if it doesn't already exist.
+    def create_therapies_table(self, existing_tables: List[str]) -> None:
+        """Create Therapies table if it doesn"t already exist.
 
-        :param List existing_tables: list of existing table names
+        :param List[str] existing_tables: list of existing table names
         """
-        table_name = 'therapy_concepts'
+        table_name = "therapy_concepts"
         if table_name not in existing_tables:
             self.dynamodb.create_table(
                 TableName=table_name,
                 KeySchema=[
                     {
-                        'AttributeName': 'label_and_type',
-                        'KeyType': 'HASH'  # Partition key
+                        "AttributeName": "label_and_type",
+                        "KeyType": "HASH"  # Partition key
                     },
                     {
-                        'AttributeName': 'concept_id',
-                        'KeyType': 'RANGE'  # Sort key
+                        "AttributeName": "concept_id",
+                        "KeyType": "RANGE"  # Sort key
                     }
                 ],
                 AttributeDefinitions=[
                     {
-                        'AttributeName': 'label_and_type',
-                        'AttributeType': 'S'
+                        "AttributeName": "label_and_type",
+                        "AttributeType": "S"
                     },
                     {
-                        'AttributeName': 'concept_id',
-                        'AttributeType': 'S'
+                        "AttributeName": "concept_id",
+                        "AttributeType": "S"
                     },
                     {
-                        'AttributeName': 'src_name',
-                        'AttributeType': 'S'
+                        "AttributeName": "src_name",
+                        "AttributeType": "S"
                     },
                     {
-                        'AttributeName': 'item_type',
-                        'AttributeType': 'S'
+                        "AttributeName": "item_type",
+                        "AttributeType": "S"
                     }
                 ],
                 GlobalSecondaryIndexes=[
                     {
-                        'IndexName': 'src_index',
-                        'KeySchema': [
+                        "IndexName": "src_index",
+                        "KeySchema": [
                             {
-                                'AttributeName': 'src_name',
-                                'KeyType': 'HASH'
+                                "AttributeName": "src_name",
+                                "KeyType": "HASH"
                             }
                         ],
-                        'Projection': {
-                            'ProjectionType': 'KEYS_ONLY'
+                        "Projection": {
+                            "ProjectionType": "KEYS_ONLY"
                         },
-                        'ProvisionedThroughput': {
-                            'ReadCapacityUnits': 10,
-                            'WriteCapacityUnits': 10
+                        "ProvisionedThroughput": {
+                            "ReadCapacityUnits": 10,
+                            "WriteCapacityUnits": 10
                         }
                     },
                     {
-                        'IndexName': 'item_type_index',
-                        'KeySchema': [
+                        "IndexName": "item_type_index",
+                        "KeySchema": [
                             {
-                                'AttributeName': 'item_type',
-                                'KeyType': 'HASH'
+                                "AttributeName": "item_type",
+                                "KeyType": "HASH"
                             }
                         ],
-                        'Projection': {
-                            'ProjectionType': 'KEYS_ONLY'
+                        "Projection": {
+                            "ProjectionType": "KEYS_ONLY"
                         },
-                        'ProvisionedThroughput': {
-                            'ReadCapacityUnits': 10,
-                            'WriteCapacityUnits': 10
+                        "ProvisionedThroughput": {
+                            "ReadCapacityUnits": 10,
+                            "WriteCapacityUnits": 10
                         }
                     }
                 ],
                 ProvisionedThroughput={
-                    'ReadCapacityUnits': 10,
-                    'WriteCapacityUnits': 10
+                    "ReadCapacityUnits": 10,
+                    "WriteCapacityUnits": 10
                 }
             )
 
-    def create_meta_data_table(self, existing_tables: List):
+    def create_meta_data_table(self, existing_tables: List[str]) -> None:
         """Create MetaData table if not exists.
 
         :param List existing_tables: list of existing table names
         """
-        table_name = 'therapy_metadata'
+        table_name = "therapy_metadata"
         if table_name not in existing_tables:
             self.dynamodb.create_table(
                 TableName=table_name,
                 KeySchema=[
                     {
-                        'AttributeName': 'src_name',
-                        'KeyType': 'HASH'  # Partition key
+                        "AttributeName": "src_name",
+                        "KeyType": "HASH"  # Partition key
                     }
                 ],
                 AttributeDefinitions=[
                     {
-                        'AttributeName': 'src_name',
-                        'AttributeType': 'S'
+                        "AttributeName": "src_name",
+                        "AttributeType": "S"
                     },
                 ],
                 ProvisionedThroughput={
-                    'ReadCapacityUnits': 10,
-                    'WriteCapacityUnits': 10
+                    "ReadCapacityUnits": 10,
+                    "WriteCapacityUnits": 10
                 }
             )
 
-    def get_record_by_id(self, concept_id: str,
-                         case_sensitive: bool = True,
+    def get_record_by_id(self, concept_id: str, case_sensitive: bool = True,
                          merge: bool = False) -> Optional[Dict]:
         """Fetch record corresponding to provided concept ID
 
         :param str concept_id: concept ID for therapy record
-        :param bool case_sensitive: if true, performs exact lookup, which is
-            more efficient. Otherwise, performs filter operation, which
-            doesn't require correct casing.
+        :param bool case_sensitive: if true, performs exact lookup, which is more
+            efficient. Otherwise, performs filter operation, which doesn"t require
+            correct casing.
         :param bool merge: if true, look for merged concept
         :return: complete therapy record, if match is found; None otherwise
         """
         try:
             if merge:
-                pk = f'{concept_id.lower()}##merger'
+                pk = f"{concept_id.lower()}##merger"
             else:
-                pk = f'{concept_id.lower()}##identity'
+                pk = f"{concept_id.lower()}##identity"
             if case_sensitive:
                 match = self.therapies.get_item(Key={
-                    'label_and_type': pk,
-                    'concept_id': concept_id
+                    "label_and_type": pk,
+                    "concept_id": concept_id
                 })
-                return match['Item']
+                return match["Item"]
             else:
-                exp = Key('label_and_type').eq(pk)
+                exp = Key("label_and_type").eq(pk)
                 response = self.therapies.query(KeyConditionExpression=exp)
-                return response['Items'][0]
+                return response["Items"][0]
         except ClientError as e:
             logger.error(f"boto3 client error on get_records_by_id for "
                          f"search term {concept_id}: "
                          f"{e.response['Error']['Message']}")
             return None
-        except KeyError:  # record doesn't exist
-            return None
-        except IndexError:  # record doesn't exist
+        except (KeyError, IndexError):  # record doesn"t exist
             return None
 
     def get_records_by_type(self, query: str,
@@ -223,11 +222,11 @@ def get_records_by_type(self, query: str,
             lookup
         :return: list of matching records. Empty if lookup fails.
         """
-        pk = f'{query}##{match_type.lower()}'
-        filter_exp = Key('label_and_type').eq(pk)
+        pk = f"{query}##{match_type.lower()}"
+        filter_exp = Key("label_and_type").eq(pk)
         try:
             matches = self.therapies.query(KeyConditionExpression=filter_exp)
-            return matches.get('Items', None)
+            return matches.get("Items", None)
         except ClientError as e:
             logger.error(f"boto3 client error on get_records_by_type for "
                          f"search term {query}: "
@@ -242,7 +241,7 @@ def get_ids_for_merge(self) -> List[str]:
         last_evaluated_key = None
         concept_ids = []
         params = {
-            'ProjectionExpression': 'concept_id',
+            "ProjectionExpression": "concept_id",
         }
         while True:
             if last_evaluated_key:
@@ -251,63 +250,62 @@ def get_ids_for_merge(self) -> List[str]:
                 )
             else:
                 response = self.therapies.scan(**params)
-            records = response['Items']
+            records = response["Items"]
             for record in records:
-                concept_id = record['concept_id']
+                concept_id = record["concept_id"]
                 concept_ids.append(concept_id)
-            last_evaluated_key = response.get('LastEvaluatedKey')
+            last_evaluated_key = response.get("LastEvaluatedKey")
             if not last_evaluated_key:
                 break
-        return set(concept_ids)
+        return concept_ids
 
-    def add_record(self, record: Dict, record_type: str = "identity"):
+    def add_record(self, record: Dict, record_type: str = "identity") -> None:
         """Add new record to database.
 
         :param Dict record: record to upload
-        :param str record_type: type of record, one of {'identity', 'merger'}
+        :param str record_type: type of record, one of {"identity", "merger"}
         """
-        id_prefix = record['concept_id'].split(':')[0].lower()
-        record['src_name'] = PREFIX_LOOKUP[id_prefix]
-        label_and_type = f'{record["concept_id"].lower()}##{record_type}'
-        record['label_and_type'] = label_and_type
-        record['item_type'] = record_type
+        id_prefix = record["concept_id"].split(":")[0].lower()
+        record["src_name"] = PREFIX_LOOKUP[id_prefix]
+        label_and_type = f"{record['concept_id'].lower()}##{record_type}"
+        record["label_and_type"] = label_and_type
+        record["item_type"] = record_type
         try:
             self.batch.put_item(Item=record)
         except ClientError as e:
-            if (len((record['label_and_type']).encode('utf-8')) >= 2048) or \
-                    (len((record['concept_id']).encode('utf-8')) >= 1024):
+            if (len((record["label_and_type"]).encode("utf-8")) >= 2048) or \
+                    (len((record["concept_id"]).encode("utf-8")) >= 1024):
                 logger.info(f"{record['concept_id']}: An error occurred "
-                            "(ValidationException) when calling the "
-                            "BatchWriteItem operation: Hash primary key "
-                            "values must be under 2048 bytes, and range "
-                            "primary key values must be under 1024 "
+                            "(ValidationException) when calling the BatchWriteItem "
+                            "operation: Hash primary key values must be under 2048 "
+                            "bytes, and range primary key values must be under 1024 "
                             "bytes.")
             else:
                 logger.error("boto3 client error on add_record for "
                              f"{record['concept_id']}: "
                              f"{e.response['Error']['Message']}")
 
-    def add_ref_record(self, term: str, concept_id: str, ref_type: str):
+    def add_ref_record(self, term: str, concept_id: str, ref_type: str) -> None:
         """Add auxiliary/reference record to database.
 
         :param str term: referent term
         :param str concept_id: concept ID to refer to
-        :param str ref_type: one of {'alias', 'label', 'trade_name',
-            'xref', 'associated_with', 'rx_brand'}
+        :param str ref_type: one of {"alias", "label", "trade_name",
+            "xref", "associated_with", "rx_brand"}
         """
-        label_and_type = f'{term.lower()}##{ref_type}'
-        src_name = PREFIX_LOOKUP[concept_id.split(':')[0].lower()]
+        label_and_type = f"{term.lower()}##{ref_type}"
+        src_name = PREFIX_LOOKUP[concept_id.split(":")[0].lower()]
         record = {
-            'label_and_type': label_and_type,
-            'concept_id': concept_id.lower(),
-            'src_name': src_name,
-            'item_type': ref_type,
+            "label_and_type": label_and_type,
+            "concept_id": concept_id.lower(),
+            "src_name": src_name,
+            "item_type": ref_type,
         }
         try:
             self.batch.put_item(Item=record)
         except ClientError as e:
-            if (len((record['label_and_type']).encode('utf-8')) >= 2048) or \
-                    (len((record['concept_id']).encode('utf-8')) >= 1024):
+            if (len((record["label_and_type"]).encode("utf-8")) >= 2048) or \
+                    (len((record["concept_id"]).encode("utf-8")) >= 1024):
                 logger.info(f"{record['concept_id']}: An error occurred "
                             "(ValidationException) when calling the "
                             "BatchWriteItem operation: Hash primary key "
@@ -320,20 +318,20 @@ def add_ref_record(self, term: str, concept_id: str, ref_type: str):
                              f"{e.response['Error']['Message']}")
 
     def update_record(self, concept_id: str, field: str, new_value: Any,
-                      item_type: str = 'identity'):
+                      item_type: str = "identity") -> None:
         """Update the field of an individual record to a new value.
 
         :param str concept_id: record to update
         :param str field: name of field to update
         :param Any new_value: new value
-        :param str item_type: record type, one of {'identity', 'merger'}
+        :param str item_type: record type, one of {"identity", "merger"}
         """
         key = {
-            'label_and_type': f'{concept_id.lower()}##identity',
-            'concept_id': concept_id
+            "label_and_type": f"{concept_id.lower()}##{item_type}",
+            "concept_id": concept_id
         }
         update_expression = f"set {field}=:r"
-        update_values = {':r': new_value}
+        update_values = {":r": new_value}
         try:
             self.therapies.update_item(Key=key,
                                        UpdateExpression=update_expression,
@@ -342,7 +340,7 @@ def update_record(self, concept_id: str, field: str, new_value: Any,
             logger.error(f"boto3 client error in `database.update_record()`: "
                          f"{e.response['Error']['Message']}")
 
-    def flush_batch(self):
+    def flush_batch(self) -> None:
         """Flush internal batch_writer."""
         self.batch.__exit__(*sys.exc_info())
         self.batch = self.therapies.batch_writer()
diff --git a/therapy/etl/__init__.py b/therapy/etl/__init__.py
index b4fe8944..5125b28c 100644
--- a/therapy/etl/__init__.py
+++ b/therapy/etl/__init__.py
@@ -1,10 +1,10 @@
 """Module to load and init namespace at package level"""
-from .wikidata import Wikidata  # noqa: F401
-from .chembl import ChEMBL  # noqa: F401
-from .ncit import NCIt  # noqa: F401
-from .drugbank import DrugBank  # noqa: F401
-from .chemidplus import ChemIDplus  # noqa: F401
-from .rxnorm import RxNorm  # noqa: F401
-from .hemonc import HemOnc  # noqa: F401
-from .guidetopharmacology import GuideToPHARMACOLOGY  # noqa: F401
-from .merge import Merge  # noqa: F401
+from .wikidata import Wikidata
+from .chembl import ChEMBL
+from .ncit import NCIt
+from .drugbank import DrugBank
+from .chemidplus import ChemIDplus
+from .rxnorm import RxNorm
+from .hemonc import HemOnc
+from .guidetopharmacology import GuideToPHARMACOLOGY
+from .merge import Merge
diff --git a/therapy/etl/base.py b/therapy/etl/base.py
index ac5a3f1c..25b4dfb9 100644
--- a/therapy/etl/base.py
+++ b/therapy/etl/base.py
@@ -2,20 +2,23 @@
 from abc import ABC, abstractmethod
 from ftplib import FTP
 from pathlib import Path
-from typing import List, Dict
+from typing import List, Dict, Union, Set
+import logging
+
 from therapy import PROJECT_ROOT, ITEM_TYPES
 from therapy.schemas import Drug
-import logging
+from therapy.database import Database
 
 
-logger = logging.getLogger('therapy')
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
 class Base(ABC):
     """The ETL base class."""
 
-    def __init__(self, database, data_path=PROJECT_ROOT / 'data'):
+    def __init__(self, database: Database,
+                 data_path: Path = PROJECT_ROOT / "data"):
         """Extract from sources.
 
         :param Database database: application database object
@@ -24,7 +27,7 @@ def __init__(self, database, data_path=PROJECT_ROOT / 'data'):
         name = self.__class__.__name__.lower()
         self.database = database
         self._src_data_dir = data_path / name
-        self._added_ids = []
+        self._added_ids: List[str] = []
 
     def perform_etl(self) -> List[str]:
         """Public-facing method to begin ETL procedures on given data.
@@ -40,15 +43,15 @@ def perform_etl(self) -> List[str]:
         self._transform_data()
         return self._added_ids
 
-    def _download_data(self, *args, **kwargs):
+    def _download_data(self) -> None:
         raise NotImplementedError
 
     def _ftp_download(self, host: str, data_dir: str, source_dir: Path,
                       data_fn: str) -> None:
         """Download data file from FTP site.
-        :param str host: Source's FTP host name
+        :param str host: Source"s FTP host name
         :param str data_dir: Data directory located on FTP site
-        :param Path source_dir: Source's data directory
+        :param Path source_dir: Source"s data directory
         :param str data_fn: Filename on FTP site to be downloaded
         """
         try:
@@ -56,16 +59,16 @@ def _ftp_download(self, host: str, data_dir: str, source_dir: Path,
                 ftp.login()
                 logger.debug(f"FTP login to {host} was successful")
                 ftp.cwd(data_dir)
-                with open(source_dir / data_fn, 'wb') as fp:
-                    ftp.retrbinary(f'RETR {data_fn}', fp.write)
+                with open(source_dir / data_fn, "wb") as fp:
+                    ftp.retrbinary(f"RETR {data_fn}", fp.write)
         except Exception as e:
-            logger.error(f'FTP download failed: {e}')
+            logger.error(f"FTP download failed: {e}")
             raise Exception(e)
 
-    def _extract_data(self):
+    def _extract_data(self) -> None:
         """Get source file from data directory."""
         self._src_data_dir.mkdir(exist_ok=True, parents=True)
-        src_file_prefix = f'{type(self).__name__.lower()}_'
+        src_file_prefix = f"{type(self).__name__.lower()}_"
         dir_files = [f for f in self._src_data_dir.iterdir()
                      if f.name.startswith(src_file_prefix)]
         if len(dir_files) == 0:
@@ -73,52 +76,49 @@ def _extract_data(self):
             dir_files = [f for f in self._src_data_dir.iterdir()
                          if f.name.startswith(src_file_prefix)]
         self._src_file = sorted(dir_files, reverse=True)[0]
-        self._version = self._src_file.stem.split('_', 1)[1]
+        self._version = self._src_file.stem.split("_", 1)[1]
 
     @abstractmethod
-    def _transform_data(self, *args, **kwargs):
+    def _transform_data(self) -> None:
         raise NotImplementedError
 
-    def _load_therapy(self, therapy: Dict):
+    def _load_therapy(self, therapy: Dict) -> None:
         """Load individual therapy record.
 
         :param Dict therapy: valid therapy object.
         """
         assert Drug(**therapy)
-        concept_id = therapy['concept_id']
+        concept_id = therapy["concept_id"]
 
         for attr_type, item_type in ITEM_TYPES.items():
             if attr_type in therapy:
                 value = therapy[attr_type]
                 if value is not None and value != []:
                     if isinstance(value, str):
-                        items = [value.lower()]
+                        items: Union[List, Set] = [value.lower()]
                     else:
                         therapy[attr_type] = list(set(value))
                         items = {item.lower() for item in value}
-                        if attr_type in ['aliases', 'trade_names']:
+                        if attr_type in ["aliases", "trade_names"]:
                             # remove duplicates
-                            if 'label' in therapy:
-                                therapy[attr_type] = list(set(therapy[attr_type]) - {therapy['label']})  # noqa: E501
+                            if "label" in therapy:
+                                therapy[attr_type] = list(set(therapy[attr_type]) - {therapy["label"]})  # noqa: E501
 
-                            if attr_type == 'aliases' and \
-                                    'trade_names' in therapy:
-                                therapy[attr_type] = list(set(therapy[attr_type]) - set(therapy['trade_names']))  # noqa: E501
+                            if attr_type == "aliases" and "trade_names" in therapy:
+                                therapy[attr_type] = list(set(therapy[attr_type]) - set(therapy["trade_names"]))  # noqa: E501
 
                             if len(items) > 20:
-                                logger.debug(f"{concept_id} has > 20"
-                                             f" {attr_type}.")
+                                logger.debug(f"{concept_id} has > 20 {attr_type}.")
                                 del therapy[attr_type]
                                 continue
 
                     for item in items:
-                        self.database.add_ref_record(item, concept_id,
-                                                     item_type)
+                        self.database.add_ref_record(item, concept_id, item_type)
                 else:
                     del therapy[attr_type]
 
         # handle detail fields
-        approval_attrs = ('approval_status', 'approval_year', 'fda_indication')
+        approval_attrs = ("approval_status", "approval_year", "fda_indication")
         for field in approval_attrs:
             if approval_attrs in therapy and therapy[field] is None:
                 del therapy[field]
@@ -127,5 +127,5 @@ def _load_therapy(self, therapy: Dict):
         self._added_ids.append(concept_id)
 
     @abstractmethod
-    def _load_meta(self, *args, **kwargs):
+    def _load_meta(self) -> None:
         raise NotImplementedError
diff --git a/therapy/etl/chembl.py b/therapy/etl/chembl.py
index a2292967..b729f8ac 100644
--- a/therapy/etl/chembl.py
+++ b/therapy/etl/chembl.py
@@ -1,41 +1,42 @@
 """This module defines the ChEMBL ETL methods."""
-from .base import Base
-from therapy import PROJECT_ROOT
-from therapy.schemas import SourceName, NamespacePrefix, ApprovalStatus, \
-    SourceMeta
 import logging
 import tarfile
 import sqlite3
 import os
 import shutil
 
-logger = logging.getLogger('therapy')
+from therapy.etl.base import Base
+from therapy import PROJECT_ROOT
+from therapy.schemas import SourceName, NamespacePrefix, ApprovalStatus, \
+    SourceMeta
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
 class ChEMBL(Base):
     """ETL the ChEMBL source into therapy.db."""
 
-    def _extract_data(self, *args, **kwargs):
+    def _extract_data(self, *args, **kwargs) -> None:
         """Extract data from the ChEMBL source."""
-        logger.info('Extracting chembl_27.db...')
-        if 'data_path' in kwargs:
-            chembl_db = kwargs['data_path']
+        logger.info("Extracting chembl_27.db...")
+        if "data_path" in kwargs:
+            chembl_db = kwargs["data_path"]
         else:
-            chembl_db = self._src_data_dir / 'chembl_27.db'
+            chembl_db = self._src_data_dir / "chembl_27.db"
         if not chembl_db.exists():
-            chembl_archive = self._src_data_dir / 'chembl_27_sqlite.tar.gz'
+            chembl_archive = self._src_data_dir / "chembl_27_sqlite.tar.gz"
             chembl_archive.parent.mkdir(exist_ok=True, parents=True)
             self._download_data()
             tar = tarfile.open(chembl_archive)
-            tar.extractall(path=PROJECT_ROOT / 'data' / 'chembl')
+            tar.extractall(path=PROJECT_ROOT / "data" / "chembl")
             tar.close()
 
             # Remove unused directories and files
-            chembl_27_dir = self._src_data_dir / 'chembl_27'
-            temp_chembl = chembl_27_dir / 'chembl_27_sqlite' / 'chembl_27.db'
-            chembl_db = self._src_data_dir / 'chembl_27.db'
-            shutil.move(temp_chembl, chembl_db)
+            chembl_27_dir = self._src_data_dir / "chembl_27"
+            temp_chembl = chembl_27_dir / "chembl_27_sqlite" / "chembl_27.db"
+            chembl_db = self._src_data_dir / "chembl_27.db"
+            shutil.move(str(temp_chembl), str(chembl_db))
             os.remove(chembl_archive)
             shutil.rmtree(chembl_27_dir)
         conn = sqlite3.connect(chembl_db)
@@ -43,18 +44,18 @@ def _extract_data(self, *args, **kwargs):
         self._conn = conn
         self._cursor = conn.cursor()
         assert chembl_db.exists()
-        logger.info('Finished extracting chembl_27.db.')
+        logger.info("Finished extracting chembl_27.db.")
 
-    def _download_data(self, *args, **kwargs):
+    def _download_data(self, *args, **kwargs) -> None:
         """Download ChEMBL data from FTP."""
         logger.info(
-            'Downloading ChEMBL v27, this will take a few minutes.')
-        self._ftp_download('ftp.ebi.ac.uk',
-                           'pub/databases/chembl/ChEMBLdb/releases/chembl_27',
+            "Downloading ChEMBL v27, this will take a few minutes.")
+        self._ftp_download("ftp.ebi.ac.uk",
+                           "pub/databases/chembl/ChEMBLdb/releases/chembl_27",
                            self._src_data_dir,
-                           'chembl_27_sqlite.tar.gz')
+                           "chembl_27_sqlite.tar.gz")
 
-    def _transform_data(self, *args, **kwargs):
+    def _transform_data(self, *args, **kwargs) -> None:
         """Transform SQLite data to JSON."""
         self._create_dictionary_synonyms_table()
         self._create_trade_names_table()
@@ -67,7 +68,7 @@ def _transform_data(self, *args, **kwargs):
         self._conn.commit()
         self._conn.close()
 
-    def _create_dictionary_synonyms_table(self):
+    def _create_dictionary_synonyms_table(self) -> None:
         """Create temporary table to store drugs and their synonyms."""
         create_dictionary_synonyms_table = f"""
             CREATE TEMPORARY TABLE DictionarySynonyms AS
@@ -98,7 +99,7 @@ def _create_dictionary_synonyms_table(self):
         """
         self._cursor.execute(create_dictionary_synonyms_table)
 
-    def _create_trade_names_table(self):
+    def _create_trade_names_table(self) -> None:
         """Create temporary table to store trade name data."""
         create_trade_names_table = """
             CREATE TEMPORARY TABLE TradeNames AS
@@ -114,7 +115,7 @@ def _create_trade_names_table(self):
         """
         self._cursor.execute(create_trade_names_table)
 
-    def _create_temp_table(self):
+    def _create_temp_table(self) -> None:
         """Create temporary table to store therapies data."""
         create_temp_table = """
             CREATE TEMPORARY TABLE temp(concept_id, label, approval_status,
@@ -168,7 +169,7 @@ def _create_temp_table(self):
         """
         self._cursor.execute(insert_temp)
 
-    def _load_json(self):
+    def _load_json(self) -> None:
         """Load ChEMBL data into database."""
         chembl_data = """
             SELECT
@@ -185,23 +186,23 @@ def _load_json(self):
         self._cursor.execute("DROP TABLE temp;")
 
         for record in result:
-            for attr in ['aliases', 'trade_names']:
+            for attr in ["aliases", "trade_names"]:
                 if attr in record and record[attr]:
-                    record[attr] = record[attr].split('||')
+                    record[attr] = record[attr].split("||")
             self._load_therapy(record)
 
-    def _load_meta(self, *args, **kwargs):
+    def _load_meta(self, *args, **kwargs) -> None:
         """Add ChEMBL metadata."""
-        metadata = SourceMeta(data_license='CC BY-SA 3.0',
-                              data_license_url='https://creativecommons.org/licenses/by-sa/3.0/',  # noqa: E501
-                              version='27',
-                              data_url='http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/',  # noqa: E501
-                              rdp_url='http://reusabledata.org/chembl.html',
+        metadata = SourceMeta(data_license="CC BY-SA 3.0",
+                              data_license_url="https://creativecommons.org/licenses/by-sa/3.0/",  # noqa: E501
+                              version="27",
+                              data_url="http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/",  # noqa: E501
+                              rdp_url="http://reusabledata.org/chembl.html",
                               data_license_attributes={
-                                  'non_commercial': False,
-                                  'share_alike': True,
-                                  'attribution': True
+                                  "non_commercial": False,
+                                  "share_alike": True,
+                                  "attribution": True
                               })
         params = dict(metadata)
-        params['src_name'] = SourceName.CHEMBL.value
+        params["src_name"] = SourceName.CHEMBL.value
         self.database.metadata.put_item(Item=params)
diff --git a/therapy/etl/chemidplus.py b/therapy/etl/chemidplus.py
index e7565ecf..22baa42f 100644
--- a/therapy/etl/chemidplus.py
+++ b/therapy/etl/chemidplus.py
@@ -2,32 +2,35 @@
 
 Courtesy of the U.S. National Library of Medicine.
 """
-from .base import Base
-from therapy import PROJECT_ROOT
-from therapy.schemas import Drug, NamespacePrefix, SourceMeta, SourceName, \
-    DataLicenseAttributes
+from typing import List, Generator
 from pathlib import Path
 import xml.etree.ElementTree as ET
 import logging
 import re
 
+from therapy.etl.base import Base
+from therapy import PROJECT_ROOT
+from therapy.database import Database
+from therapy.schemas import Drug, NamespacePrefix, SourceMeta, SourceName, \
+    DataLicenseAttributes, RecordParams
+
 
-logger = logging.getLogger('therapy')
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
-TAGS_REGEX = r' \[.*\]'
+TAGS_REGEX = r" \[.*\]"
 
 
 class ChemIDplus(Base):
     """Core ChemIDplus ETL class."""
 
     def __init__(self,
-                 database,
-                 data_path: Path = PROJECT_ROOT / 'data',
-                 src_server: str = 'ftp.nlm.nih.gov',
-                 src_dir_path: str = 'nlmdata/.chemidlease/',
-                 src_fname: str = 'CurrentChemID.xml'):
+                 database: Database,
+                 data_path: Path = PROJECT_ROOT / "data",
+                 src_server: str = "ftp.nlm.nih.gov",
+                 src_dir_path: str = "nlmdata/.chemidlease/",
+                 src_fname: str = "CurrentChemID.xml"):
         """Initialize class instance.
 
         :param Database database: application database object
@@ -36,17 +39,17 @@ def __init__(self,
         :param str src_dir_path: The directory to the chemidplus release
         :param str src_fname: name of file as stored in src_dir.
 
-        If the source file is provided locally in the data_path directory,
-        it's unnecessary to provide `src_dir` and `src_fname` args.
+        If the source file is provided locally in the data_path directory, it's
+        unnecessary to provide `src_dir` and `src_fname` args.
         """
         super().__init__(database, data_path)
         self._src_server = src_server
         self._src_dir_path = src_dir_path
         self._src_fname = src_fname
 
-    def _download_data(self):
+    def _download_data(self) -> None:
         """Download source data from default location."""
-        logger.info('Downloading ChemIDplus data...')
+        logger.info("Downloading ChemIDplus data...")
         outfile_path = self._src_data_dir / self._src_fname
 
         self._ftp_download(self._src_server,
@@ -54,13 +57,13 @@ def _download_data(self):
                            self._src_data_dir,
                            self._src_fname)
 
-        parser = ET.iterparse(outfile_path, ('start', 'end'))
-        date = next(parser)[1].attrib['date']
-        version = date.replace('-', '')
-        outfile_path.rename(self._src_data_dir / f'chemidplus_{version}.xml')
-        logger.info('Finished downloading ChemIDplus data')
+        parser = ET.iterparse(outfile_path, ("start", "end"))
+        date = next(parser)[1].attrib["date"]
+        version = date.replace("-", "")
+        outfile_path.rename(self._src_data_dir / f"chemidplus_{version}.xml")
+        logger.info("Finished downloading ChemIDplus data")
 
-    def _extract_data(self):
+    def _extract_data(self) -> None:
         """Acquire ChemIDplus dataset.
 
         :arg pathlib.Path data_path: directory containing source data
@@ -71,93 +74,92 @@ def _extract_data(self):
         if len(dir_files) == 0:
             file = self._get_file()
         else:
-            file = sorted([f for f in dir_files
-                           if f.name.startswith('chemidplus')])
+            file = sorted([f for f in dir_files if f.name.startswith("chemidplus")])
             if not file:
                 file = self._get_file()
 
         self._data_src = file[-1]
-        self._version = self._data_src.stem.split('_')[1]
+        self._version = self._data_src.stem.split("_")[1]
 
-    def _get_file(self):
+    def _get_file(self) -> List[Path]:
+        """Acquire file paths to ChemIDplus data source.
+        :return: List of source file Paths
+        """
         self._download_data()
         dir_files = list(self._src_data_dir.iterdir())
         return sorted([f for f in dir_files
-                       if f.name.startswith('chemidplus')])
+                       if f.name.startswith("chemidplus")])
 
     @staticmethod
-    def parse_xml(path: Path, tag: str):
+    def parse_xml(path: Path, tag: str) -> Generator:
         """Parse XML file and retrieve elements with matching tag value.
         :param Path path: path to XML file
         :param str tag: XML tag
         :return: generator yielding elements of corresponding tag
         """
-        context = iter(ET.iterparse(path, events=('start', 'end')))
+        context = iter(ET.iterparse(path, events=("start", "end")))
         _, root = next(context)
         for event, elem in context:
-            if event == 'end' and elem.tag == tag:
+            if event == "end" and elem.tag == tag:
                 yield elem
                 root.clear()
 
-    def _transform_data(self):
+    def _transform_data(self) -> None:
         """Open dataset and prepare for loading into database."""
-        parser = self.parse_xml(self._data_src, 'Chemical')
-        for chemical in parser:
-            if 'displayName' not in chemical.attrib:
+        parser = self.parse_xml(self._data_src, "Chemical")
+        for chemical in parser:  # type: ignore
+            if "displayName" not in chemical.attrib:
                 continue
 
             # initial setup and get label
-            display_name = chemical.attrib['displayName']
+            display_name = chemical.attrib["displayName"]
             if not display_name or not re.search(TAGS_REGEX, display_name):
                 continue
-            label = re.sub(TAGS_REGEX, '', display_name)
-            params = {
-                'label': label
-            }
+            label = re.sub(TAGS_REGEX, "", display_name)
+            params: RecordParams = {"label": label}
 
             # get concept ID
-            reg_no = chemical.find('NumberList').find("CASRegistryNumber")
+            reg_no = chemical.find("NumberList").find("CASRegistryNumber")
             if not reg_no:
                 continue
-            params['concept_id'] = \
-                f'{NamespacePrefix.CASREGISTRY.value}:{reg_no.text}'
+            params["concept_id"] = f"{NamespacePrefix.CASREGISTRY.value}:{reg_no.text}"
 
             # get aliases
             aliases = []
             label_l = label.lower()
-            name_list = chemical.find('NameList')
+            name_list = chemical.find("NameList")
             if name_list:
-                for name in name_list.findall('NameOfSubstance'):
+                for name in name_list.findall("NameOfSubstance"):
                     text = name.text
                     if text != display_name and text.lower() != label_l:
-                        aliases.append(re.sub(TAGS_REGEX, '', text))
-            params['aliases'] = aliases
+                        aliases.append(re.sub(TAGS_REGEX, "", text))
+            params["aliases"] = aliases
 
             # get xrefs and associated_with
-            params['xrefs'] = []
-            params['associated_with'] = []
-            locator_list = chemical.find('LocatorList')
+            params["xrefs"] = []
+            params["associated_with"] = []
+            locator_list = chemical.find("LocatorList")
             if locator_list:
-                for loc in locator_list.findall('InternetLocator'):
-                    if loc.text == 'DrugBank':
-                        db = f'{NamespacePrefix.DRUGBANK.value}:' \
-                             f'{loc.attrib["url"].split("/")[-1]}'
-                        params['xrefs'].append(db)
-                    elif loc.text == 'FDA SRS':
-                        unii = f'{NamespacePrefix.UNII.value}:' \
-                               f'{loc.attrib["url"].split("/")[-1]}'
-                        params['associated_with'].append(unii)
+                for loc in locator_list.findall("InternetLocator"):
+                    if loc.text == "DrugBank":
+                        db = f"{NamespacePrefix.DRUGBANK.value}:" \
+                             f"{loc.attrib['url'].split('/')[-1]}"
+                        params["xrefs"].append(db)  # type: ignore
+                    elif loc.text == "FDA SRS":
+                        unii = f"{NamespacePrefix.UNII.value}:" \
+                               f"{loc.attrib['url'].split('/')[-1]}"
+                        params["associated_with"].append(unii)  # type: ignore
 
             # double-check and load full record
             assert Drug(**params)
             self._load_therapy(params)
 
-    def _load_meta(self):
+    def _load_meta(self) -> None:
         """Add source metadata."""
         meta = SourceMeta(data_license="custom",
                           data_license_url="https://www.nlm.nih.gov/databases/download/terms_and_conditions.html",  # noqa: E501
                           version=self._version,
-                          data_url="ftp://ftp.nlm.nih.gov/nlmdata/.chemidlease/",  # noqa: E501
+                          data_url="ftp://ftp.nlm.nih.gov/nlmdata/.chemidlease/",
                           rdp_url=None,
                           data_license_attributes=DataLicenseAttributes(
                               non_commercial=False,
@@ -165,5 +167,5 @@ def _load_meta(self):
                               attribution=True
                           ))
         item = dict(meta)
-        item['src_name'] = SourceName.CHEMIDPLUS.value
+        item["src_name"] = SourceName.CHEMIDPLUS.value
         self.database.metadata.put_item(Item=item)
diff --git a/therapy/etl/drugbank.py b/therapy/etl/drugbank.py
index 5bdad20f..311b0a2e 100644
--- a/therapy/etl/drugbank.py
+++ b/therapy/etl/drugbank.py
@@ -1,39 +1,45 @@
 """This module defines the DrugBank ETL methods."""
-from therapy import DownloadException
-from therapy.schemas import SourceName, SourceMeta, NamespacePrefix
-from therapy.etl.base import Base
+from typing import Dict, Any
 import logging
 import csv
-import bs4
-import requests
 import re
 import zipfile
 import shutil
 from io import BytesIO
 
-logger = logging.getLogger('therapy')
+import bs4
+import requests
+
+from therapy import DownloadException
+from therapy.schemas import SourceName, SourceMeta, NamespacePrefix
+from therapy.etl.base import Base
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
 class DrugBank(Base):
     """ETL the DrugBank source into therapy.db."""
 
-    def _download_data(self):
+    def _download_data(self) -> None:
         """Download DrugBank source data."""
         logger.info("Downloading DrugBank source data...")
 
         # get newest version number
-        r = requests.get('https://go.drugbank.com/release_notes')
+        r = requests.get("https://go.drugbank.com/release_notes")
         if r.status_code == 200:
-            soup = bs4.BeautifulSoup(r.content, features='lxml')
+            soup = bs4.BeautifulSoup(r.content, features="lxml")
         else:
-            logger.error(f'DrugBank version fetch failed with status code: '
-                         f'{r.status_code}')
+            logger.error(f"DrugBank version fetch failed with status code: "
+                         f"{r.status_code}")
             raise DownloadException
-        most_recent = soup.find('div', {'class': 'card-header'})
-        version = re.search(r'[0-9]+\.[0-9]+\.[0-9]+',
-                            most_recent.contents[0]).group()
-        url = f'https://go.drugbank.com/releases/{version.replace(".", "-")}/downloads/all-drugbank-vocabulary'  # noqa: E501
+        most_recent = soup.find("div", {"class": "card-header"})
+        search_result = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", most_recent.contents[0])
+        if search_result:
+            version = search_result.group()
+        else:
+            raise DownloadException("Unable to parse DrugBank version.")
+        url = f"https://go.drugbank.com/releases/{version.replace('.', '-')}/downloads/all-drugbank-vocabulary"  # noqa: E501
 
         # download file
         r = requests.get(url)
@@ -45,73 +51,73 @@ def _download_data(self):
             raise requests.HTTPError(r.status_code)
 
         # unpack file
-        temp_dir = self._src_data_dir / 'temp_drugbank'
+        temp_dir = self._src_data_dir / "temp_drugbank"
         zip_file.extractall(temp_dir)
-        temp_file = temp_dir / 'drugbank vocabulary.csv'
-        csv_file = self._src_data_dir / f'drugbank_{version}.csv'
-        shutil.move(temp_file, csv_file)
+        temp_file = temp_dir / "drugbank vocabulary.csv"
+        csv_file = self._src_data_dir / f"drugbank_{version}.csv"
+        shutil.move(str(temp_file), str(csv_file))
         shutil.rmtree(temp_dir)
         logger.info("DrugBank source data download complete.")
 
-    def _load_meta(self):
+    def _load_meta(self) -> None:
         """Add DrugBank metadata."""
         meta = {
-            'data_license': 'CC0 1.0',
-            'data_license_url': 'https://creativecommons.org/publicdomain/zero/1.0/',  # noqa: E501
-            'version': self._version,
-            'data_url': 'https://go.drugbank.com/releases/latest#open-data',
-            'rdp_url': 'http://reusabledata.org/drugbank.html',
-            'data_license_attributes': {
-                'non_commercial': False,
-                'share_alike': False,
-                'attribution': False,
+            "data_license": "CC0 1.0",
+            "data_license_url": "https://creativecommons.org/publicdomain/zero/1.0/",
+            "version": self._version,
+            "data_url": "https://go.drugbank.com/releases/latest#open-data",
+            "rdp_url": "http://reusabledata.org/drugbank.html",
+            "data_license_attributes": {
+                "non_commercial": False,
+                "share_alike": False,
+                "attribution": False,
             },
         }
         assert SourceMeta(**meta)
-        meta['src_name'] = SourceName.DRUGBANK.value
+        meta["src_name"] = SourceName.DRUGBANK.value
         self.database.metadata.put_item(Item=meta)
 
-    def _transform_data(self):
+    def _transform_data(self) -> None:
         """Transform the DrugBank source."""
-        with open(self._src_file, 'r') as file:
+        with open(self._src_file, "r") as file:
             reader = csv.reader(file)
             next(reader)  # skip header
             for row in reader:
                 # get concept ID
-                params = {
-                    'concept_id': f'{NamespacePrefix.DRUGBANK.value}:{row[0]}',
+                params: Dict[str, Any] = {
+                    "concept_id": f"{NamespacePrefix.DRUGBANK.value}:{row[0]}",
                 }
 
                 # get label
                 label = row[2]
                 if label:
-                    params['label'] = label
+                    params["label"] = label
 
                 # get aliases
                 aliases = [
-                    a for a in row[1].split(' | ') + row[5].split(' | ') if a
+                    a for a in row[1].split(" | ") + row[5].split(" | ") if a
                 ]
                 if aliases:
-                    params['aliases'] = aliases
+                    params["aliases"] = aliases
 
                 # get CAS reference
                 cas_ref = row[3]
                 if cas_ref:
-                    params['xrefs'] = [
-                        f'{NamespacePrefix.CHEMIDPLUS.value}:{row[3]}'
+                    params["xrefs"] = [
+                        f"{NamespacePrefix.CHEMIDPLUS.value}:{row[3]}"
                     ]
 
-                params['associated_with'] = []
+                params["associated_with"] = []
                 # get inchi key
                 if len(row) >= 7:
                     inchi_key = row[6]
                     if inchi_key:
-                        inchi_id = f'{NamespacePrefix.INCHIKEY.value}:{inchi_key}'  # noqa: E501
-                        params['associated_with'].append(inchi_id)
+                        inchi_id = f"{NamespacePrefix.INCHIKEY.value}:{inchi_key}"
+                        params["associated_with"].append(inchi_id)
                 # get UNII id
                 unii = row[4]
                 if unii:
-                    unii_id = f'{NamespacePrefix.UNII.value}:{unii}'
-                    params['associated_with'].append(unii_id)
+                    unii_id = f"{NamespacePrefix.UNII.value}:{unii}"
+                    params["associated_with"].append(unii_id)
 
                 self._load_therapy(params)
diff --git a/therapy/etl/guidetopharmacology.py b/therapy/etl/guidetopharmacology.py
index a0e04c72..cb375122 100644
--- a/therapy/etl/guidetopharmacology.py
+++ b/therapy/etl/guidetopharmacology.py
@@ -1,21 +1,24 @@
 """Module for Guide to PHARMACOLOGY ETL methods."""
-from typing import Optional
-from therapy import logger, PROJECT_ROOT, DownloadException
-from therapy.etl.base import Base
-from therapy.schemas import SourceMeta, SourceName, NamespacePrefix, \
-    ApprovalStatus
+from typing import Optional, Dict, Any, List, Union
 from pathlib import Path
-import requests
-import bs4
 import re
 import csv
 import html
 
+import requests
+import bs4
+
+from therapy import logger, PROJECT_ROOT, DownloadException
+from therapy.database import Database
+from therapy.schemas import SourceMeta, SourceName, NamespacePrefix, \
+    ApprovalStatus
+from therapy.etl.base import Base
+
 
 class GuideToPHARMACOLOGY(Base):
     """Class for Guide to PHARMACOLOGY ETL methods."""
 
-    def __init__(self, database,
+    def __init__(self, database: Database,
                  data_path: Path = PROJECT_ROOT / "data") -> None:
         """Initialize GuideToPHARMACOLOGY ETL class.
 
@@ -25,10 +28,8 @@ def __init__(self, database,
         super().__init__(database, data_path)
         self._data_url = "https://www.guidetopharmacology.org/download.jsp"
         self._version = self._find_version()
-        self._ligands_data_url = "https://www.guidetopharmacology.org/DATA/ligands.tsv"  # noqa: E501
+        self._ligands_data_url = "https://www.guidetopharmacology.org/DATA/ligands.tsv"
         self._ligand_id_mapping_data_url = "https://www.guidetopharmacology.org/DATA/ligand_id_mapping.tsv"  # noqa: E501
-        self._ligands_file = None
-        self._ligand_id_mapping_file = None
 
     def _find_version(self) -> str:
         """Find most recent data version.
@@ -44,7 +45,8 @@ def _find_version(self) -> str:
                          f" status code: {status_code}")
             raise DownloadException
         data = soup.find("a", {"name": "data"}).find_next("div").find_next("div").find_next("b")  # noqa: E501
-        return re.search(r"\d{4}.\d+", data.contents[0]).group()
+        result = re.search(r"\d{4}.\d+", data.contents[0])  # type: ignore
+        return result.group()  # type: ignore
 
     def _extract_data(self) -> None:
         """Extract data from Guide to PHARMACOLOGY."""
@@ -62,7 +64,7 @@ def _download_data(self) -> None:
             for f in dir_files:
                 if f.name == f"{prefix}_ligands_{self._version}.tsv":
                     self._ligands_file = f
-                elif f.name == f"{prefix}_ligand_id_mapping_{self._version}.tsv":  # noqa: E501
+                elif f.name == f"{prefix}_ligand_id_mapping_{self._version}.tsv":
                     self._ligand_id_mapping_file = f
 
         if self._ligands_file is None:
@@ -91,13 +93,13 @@ def _download_file(self, file_url: str, fn: str) -> None:
 
     def _transform_data(self) -> None:
         """Transform Guide To PHARMACOLOGY data."""
-        data = dict()
+        data: Dict[str, Any] = dict()
         self._transform_ligands(data)
         self._transform_ligand_id_mappings(data)
         for param in data.values():
             self._load_therapy(param)
 
-    def _transform_ligands(self, data: dict) -> None:
+    def _transform_ligands(self, data: Dict) -> None:
         """Transform ligands data file and add this data to `data`.
 
         :param dict data: Transformed data
@@ -107,9 +109,9 @@ def _transform_ligands(self, data: dict) -> None:
             next(rows)
 
             for row in rows:
-                params = {
+                params: Dict[str, Union[List[str], str]] = {
                     "concept_id":
-                        f"{NamespacePrefix.GUIDETOPHARMACOLOGY.value}:{row[0]}",  # noqa: E501
+                        f"{NamespacePrefix.GUIDETOPHARMACOLOGY.value}:{row[0]}",
                     "label": row[1],
                     "src_name": SourceName.GUIDETOPHARMACOLOGY.value
                 }
@@ -153,13 +155,13 @@ def _transform_ligands(self, data: dict) -> None:
 
                 data[params["concept_id"]] = params
 
-    def _transform_ligand_id_mappings(self, data: dict):
+    def _transform_ligand_id_mappings(self, data: Dict) -> None:
         """Transform ligand_id_mappings and add this data to `data`
         All ligands found in this file should already be in data
 
         :param dict data: Transformed data
         """
-        with open(self._ligand_id_mapping_file, "r") as f:
+        with open(self._ligand_id_mapping_file.absolute(), "r") as f:
             rows = csv.reader(f, delimiter="\t")
             for row in rows:
                 concept_id = f"{NamespacePrefix.GUIDETOPHARMACOLOGY.value}:{row[0]}"  # noqa: E501
@@ -176,7 +178,7 @@ def _transform_ligand_id_mappings(self, data: dict):
                     # CHEBI
                     associated_with.append(row[7])
                 if row[11]:
-                    xrefs.append(f"{NamespacePrefix.CASREGISTRY.value}:{row[11]}")  # noqa: E501
+                    xrefs.append(f"{NamespacePrefix.CASREGISTRY.value}:{row[11]}")
                 if row[12]:
                     xrefs.append(f"{NamespacePrefix.DRUGBANK.value}:{row[12]}")
                 if row[13]:
@@ -188,17 +190,17 @@ def _transform_ligand_id_mappings(self, data: dict):
                     params["associated_with"] = associated_with
 
     def _set_approval_status(self, approved: str,
-                             withdrawn: str) -> Optional[ApprovalStatus]:
+                             withdrawn: str) -> Optional[str]:
         """Set approval status.
 
-        :param str approved: The drug is or has in the past been approved for
-            human clinical use by a regulatory agency
-        :param str withdrawn: The drug is no longer approved for its original
-            clinical use in one or more countries
+        :param str approved: The drug is or has in the past been approved for human
+            clinical use by a regulatory agency
+        :param str withdrawn: The drug is no longer approved for its original clinical
+            use in one or more countries
         :return: Approval status
         """
         if approved and not withdrawn:
-            approval_status = ApprovalStatus.APPROVED.value
+            approval_status: Optional[str] = ApprovalStatus.APPROVED.value
         elif withdrawn:
             approval_status = ApprovalStatus.WITHDRAWN.value
         else:
diff --git a/therapy/etl/hemonc.py b/therapy/etl/hemonc.py
index 3dc82235..38a105ab 100644
--- a/therapy/etl/hemonc.py
+++ b/therapy/etl/hemonc.py
@@ -1,23 +1,27 @@
 """Provide ETL methods for HemOnc.org data."""
-from therapy import DownloadException, PROJECT_ROOT
-from therapy.etl.base import Base
-from therapy.schemas import NamespacePrefix, SourceMeta, SourceName, \
-    ApprovalStatus
-from disease.query import QueryHandler as DiseaseNormalizer
 from pathlib import Path
 from typing import Dict, Tuple
 import csv
 import logging
 
+from disease.query import QueryHandler as DiseaseNormalizer
+
+from therapy import DownloadException, PROJECT_ROOT
+from therapy.database import Database
+from therapy.schemas import NamespacePrefix, SourceMeta, SourceName, RecordParams, \
+    ApprovalStatus
+from therapy.etl.base import Base
+
 
-logger = logging.getLogger('therapy')
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
 class HemOnc(Base):
     """Docstring"""
 
-    def __init__(self, database, data_path: Path = PROJECT_ROOT / 'data'):
+    def __init__(self, database: Database,
+                 data_path: Path = PROJECT_ROOT / "data"):
         """Initialize HemOnc instance.
 
         :param therapy.database.Database database: application database
@@ -26,7 +30,7 @@ def __init__(self, database, data_path: Path = PROJECT_ROOT / 'data'):
         super().__init__(database, data_path)
         self.disease_normalizer = DiseaseNormalizer(self.database.endpoint_url)
 
-    def _download_data(self):
+    def _download_data(self) -> None:
         """Download HemOnc.org source data.
 
         Raises download exception for now -- HTTP authorization may be
@@ -35,12 +39,12 @@ def _download_data(self):
         raise DownloadException("No download for HemOnc data available -- "
                                 "must place manually in data/ directory.")
 
-    def _extract_data(self):
+    def _extract_data(self) -> None:
         """Get source files from data directory."""
         self._src_data_dir.mkdir(exist_ok=True, parents=True)
         self._src_files = []
-        for item_type in ('concepts', 'rels', 'synonyms'):
-            src_file_prefix = f'hemonc_{item_type}_'
+        for item_type in ("concepts", "rels", "synonyms"):
+            src_file_prefix = f"hemonc_{item_type}_"
             dir_files = [f for f in self._src_data_dir.iterdir()
                          if f.name.startswith(src_file_prefix)]
             if len(dir_files) == 0:
@@ -48,35 +52,35 @@ def _extract_data(self):
                 dir_files = [f for f in self._src_data_dir.iterdir()
                              if f.name.startswith(src_file_prefix)]
             self._src_files.append(sorted(dir_files, reverse=True)[0])
-        self._version = self._src_files[0].stem.split('_', 2)[-1]
+        self._version = self._src_files[0].stem.split("_", 2)[-1]
 
-    def _load_meta(self):
+    def _load_meta(self) -> None:
         """Add HemOnc metadata."""
         meta = {
-            'data_license': 'CC BY 4.0',
-            'data_license_url': 'https://creativecommons.org/licenses/by/4.0/legalcode',  # noqa: E501
-            'version': self._version,
-            'data_url': 'https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/9CY9C6',  # noqa: E501
-            'rdp_url': None,
-            'data_license_attributes': {
-                'non_commercial': False,
-                'share_alike': False,
-                'attribution': True,
+            "data_license": "CC BY 4.0",
+            "data_license_url": "https://creativecommons.org/licenses/by/4.0/legalcode",
+            "version": self._version,
+            "data_url": "https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/9CY9C6",  # noqa: E501
+            "rdp_url": None,
+            "data_license_attributes": {
+                "non_commercial": False,
+                "share_alike": False,
+                "attribution": True,
             },
         }
         assert SourceMeta(**meta)
-        meta['src_name'] = SourceName.HEMONC.value
+        meta["src_name"] = SourceName.HEMONC.value
         self.database.metadata.put_item(Item=meta)
 
     def _get_concepts(self) -> Tuple[Dict, Dict, Dict]:
         """Get therapy, brand name, and disease concepts from concepts file.
         :return: Tuple of dicts mapping ID to object for each type of concept
         """
-        therapies = {}  # hemonc id -> record
-        brand_names = {}  # hemonc id -> brand name
-        conditions = {}  # hemonc id -> condition name
+        therapies: RecordParams = {}  # hemonc id -> record
+        brand_names: Dict[str, str] = {}  # hemonc id -> brand name
+        conditions: Dict[str, str] = {}  # hemonc id -> condition name
 
-        concepts_file = open(self._src_files[0], 'r')
+        concepts_file = open(self._src_files[0], "r")
         concepts_reader = csv.reader(concepts_file)
         next(concepts_reader)  # skip header
         for row in concepts_reader:
@@ -84,18 +88,18 @@ def _get_concepts(self) -> Tuple[Dict, Dict, Dict]:
                 continue  # skip if deprecated/invalid
 
             row_type = row[2]
-            if row_type == 'Component':
-                concept_id = f'{NamespacePrefix.HEMONC.value}:{row[3]}'
+            if row_type == "Component":
+                concept_id = f"{NamespacePrefix.HEMONC.value}:{row[3]}"
                 therapies[row[3]] = {
-                    'concept_id': concept_id,
-                    'label': row[0],
-                    'trade_names': [],
-                    'aliases': [],
-                    'xrefs': [],
+                    "concept_id": concept_id,
+                    "label": row[0],
+                    "trade_names": [],
+                    "aliases": [],
+                    "xrefs": [],
                 }
-            elif row_type == 'Brand Name':
+            elif row_type == "Brand Name":
                 brand_names[row[3]] = row[0]
-            elif row_type == 'Condition':
+            elif row_type == "Condition":
                 conditions[row[3]] = row[0]
         concepts_file.close()
 
@@ -105,8 +109,7 @@ def _get_concepts(self) -> Tuple[Dict, Dict, Dict]:
     def _id_to_yr(hemonc_id: str) -> int:
         """Get year from HemOnc ID corresponding to year concept.
         :param str hemonc_id: HemOnc ID to get year for
-        :return: int representing year. Raises TypeError if HemOnc ID not
-            valid.
+        :return: int representing year. Raises TypeError if HemOnc ID not valid.
         """
         id_int = int(hemonc_id)
         if id_int == 780:
@@ -122,15 +125,15 @@ def _id_to_yr(hemonc_id: str) -> int:
 
     def _get_rels(self, therapies: Dict, brand_names: Dict,
                   conditions: Dict) -> Dict:
-        """Gather relations to provide associations between therapies, brand
-        names, and conditions.
+        """Gather relations to provide associations between therapies, brand names,
+        and conditions.
 
         :param dict therapies: mapping from IDs to therapy concepts
         :param dict brand_names: mapping from IDs to therapy brand names
         :param dict conditions: mapping from IDs to disease conditions
         :return: therapies dict updated with brand names and conditions
         """
-        rels_file = open(self._src_files[1], 'r')
+        rels_file = open(self._src_files[1], "r")
         rels_reader = csv.reader(rels_file)
         next(rels_reader)  # skip header
 
@@ -145,15 +148,15 @@ def _get_rels(self, therapies: Dict, brand_names: Dict,
             if rel_type == "Maps to":
                 src_raw = row[3]
                 if src_raw == "RxNorm":
-                    xref = f'{NamespacePrefix.RXNORM.value}:{row[1]}'
-                    record['xrefs'].append(xref)
+                    xref = f"{NamespacePrefix.RXNORM.value}:{row[1]}"
+                    record["xrefs"].append(xref)
                 elif src_raw == "RxNorm Extension":
                     continue  # skip
                 else:
                     logger.warning(f"Unrecognized `Maps To` source: {src_raw}")
 
             elif rel_type == "Has brand name":
-                record['trade_names'].append(brand_names[row[1]])
+                record["trade_names"].append(brand_names[row[1]])
 
             elif rel_type == "Was FDA approved yr":
                 try:
@@ -165,27 +168,27 @@ def _get_rels(self, therapies: Dict, brand_names: Dict,
                 if year == 9999:
                     logger.warning(f"HemOnc ID {row[0]} has FDA approval year"
                                    f" 9999")
-                record['approval_status'] = ApprovalStatus.APPROVED
-                if 'approval_year' in record:
-                    record['approval_year'].append(year)
+                record["approval_status"] = ApprovalStatus.APPROVED
+                if "approval_year" in record:
+                    record["approval_year"].append(year)
                 else:
-                    record['approval_year'] = [year]
+                    record["approval_year"] = [year]
 
             elif rel_type == "Has FDA indication":
                 label = conditions[row[1]]
                 norm_response = self.disease_normalizer.search_groups(label)
-                if norm_response['match_type'] > 0:
-                    ncit_id = norm_response['disease_descriptor']['disease_id']
+                if norm_response["match_type"] > 0:
+                    ncit_id = norm_response["disease_descriptor"]["disease_id"]
                 else:
                     ncit_id = ""
-                    logger.warning(f'Normalization of condition id: {row[1]}'
-                                   f', {label}, failed.')
-                hemonc_concept_id = f"{NamespacePrefix.HEMONC.value}:{row[1]}"  # noqa: E501
+                    logger.warning(f"Normalization of condition id: {row[1]}"
+                                   f", {label}, failed.")
+                hemonc_concept_id = f"{NamespacePrefix.HEMONC.value}:{row[1]}"
                 indication = [hemonc_concept_id, label, ncit_id]
-                if 'fda_indication' in record:
-                    record['fda_indication'].append(indication)
+                if "fda_indication" in record:
+                    record["fda_indication"].append(indication)
                 else:
-                    record['fda_indication'] = [indication]
+                    record["fda_indication"] = [indication]
 
         rels_file.close()
         return therapies
@@ -196,7 +199,7 @@ def _get_synonyms(self, therapies: Dict) -> Dict:
         :param dict therapies: mapping of IDs to therapy objects
         :return: therapies dict with synonyms added as aliases
         """
-        synonyms_file = open(self._src_files[2], 'r')
+        synonyms_file = open(self._src_files[2], "r")
         synonyms_reader = csv.reader(synonyms_file)
         next(synonyms_reader)
         for row in synonyms_reader:
@@ -204,12 +207,12 @@ def _get_synonyms(self, therapies: Dict) -> Dict:
             if therapy_code in therapies:
                 therapy = therapies[therapy_code]
                 alias = row[0]
-                if alias != therapy.get('label'):
-                    therapies[therapy_code]['aliases'].append(row[0])
+                if alias != therapy.get("label"):
+                    therapies[therapy_code]["aliases"].append(row[0])
         synonyms_file.close()
         return therapies
 
-    def _transform_data(self):
+    def _transform_data(self) -> None:
         """Prepare dataset for loading into normalizer database."""
         therapies, brand_names, conditions = self._get_concepts()
         therapies = self._get_rels(therapies, brand_names, conditions)
diff --git a/therapy/etl/merge.py b/therapy/etl/merge.py
index 899b1521..630f5024 100644
--- a/therapy/etl/merge.py
+++ b/therapy/etl/merge.py
@@ -1,11 +1,12 @@
 """Create concept groups and merged records."""
-from therapy.schemas import SourcePriority
-from therapy.database import Database
-from typing import Set, Dict
+from typing import Set, Dict, Tuple
 import logging
 from timeit import default_timer as timer
 
-logger = logging.getLogger('therapy')
+from therapy.schemas import SourcePriority
+from therapy.database import Database
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
@@ -15,21 +16,18 @@ class Merge:
     def __init__(self, database: Database):
         """Initialize Merge instance.
 
-        :param Database database: db instance to use for record retrieval
-            and creation.
+        :param Database database: db instance to use for record retrieval and creation.
         """
         self._database = database
-        self._groups = {}  # dict keying concept IDs to group Sets
+        self._groups: Dict[str, Set[str]] = {}
 
-    def create_merged_concepts(self, record_ids: Set[str]):
-        """Create concept groups, generate merged concept records, and
-        update database.
+    def create_merged_concepts(self, record_ids: Set[str]) -> None:
+        """Create concept groups, generate merged concept records, and update database.
 
-        :param Set[str] record_ids: concept identifiers from which groups
-            should be generated. Should *not* include any records from
-            excluded sources.
+        :param Set[str] record_ids: concept identifiers from which groups should be
+            generated.
         """
-        logger.info('Generating record ID sets...')
+        logger.info("Generating record ID sets...")
         start = timer()
         for record_id in record_ids:
             new_group = self._create_record_id_set(record_id)
@@ -37,11 +35,11 @@ def create_merged_concepts(self, record_ids: Set[str]):
                 for concept_id in new_group:
                     self._groups[concept_id] = new_group
         end = timer()
-        logger.debug(f'Built record ID sets in {end - start} seconds')
+        logger.debug(f"Built record ID sets in {end - start} seconds")
 
         self._groups = {k: v for k, v in self._groups.items() if len(v) > 1}
 
-        logger.info('Creating merged records and updating database...')
+        logger.info("Creating merged records and updating database...")
         uploaded_ids = set()
         start = timer()
         for record_id, group in self._groups.items():
@@ -50,7 +48,7 @@ def create_merged_concepts(self, record_ids: Set[str]):
             merged_record = self._generate_merged_record(group)
 
             # add group merger item to DB
-            self._database.add_record(merged_record, 'merger')
+            self._database.add_record(merged_record, "merger")
 
             # add updated references
             for concept_id in group:
@@ -58,24 +56,22 @@ def create_merged_concepts(self, record_ids: Set[str]):
                     logger.error(f"Updating nonexistent record: {concept_id} "
                                  f"for {merged_record['label_and_type']}")
                 else:
-                    merge_ref = merged_record['concept_id'].lower()
-                    self._database.update_record(concept_id, 'merge_ref',
-                                                 merge_ref)
+                    merge_ref = merged_record["concept_id"].lower()
+                    self._database.update_record(concept_id, "merge_ref", merge_ref)
             uploaded_ids |= group
-        logger.info('Merged concept generation successful.')
+        logger.info("Merged concept generation successful.")
         end = timer()
-        logger.debug(f'Generated and added concepts in {end - start} seconds')
+        logger.debug(f"Generated and added concepts in {end - start} seconds")
 
     def _get_xrefs(self, record: Dict) -> Set[str]:
         """Extract references to entries in other sources from a record.
-        Crucially, filter to allowed sources only.
 
         :param Dict record: record to process
         :return: Set of xref values
         :rtype: Set
         """
         xrefs = set()
-        for xref in record.get('xrefs', []):
+        for xref in record.get("xrefs", []):
             xrefs.add(xref)
         return xrefs
 
@@ -93,13 +89,13 @@ def _create_record_id_set(self, record_id: str,
             if not db_record:
                 # attempt RxNorm brand lookup
                 brand_lookup = self._database.get_records_by_type(record_id,
-                                                                  'rx_brand')
+                                                                  "rx_brand")
                 if len(brand_lookup) == 1:
-                    lookup_id = brand_lookup[0]['concept_id']
+                    lookup_id = brand_lookup[0]["concept_id"]
                     db_record = self._database.get_record_by_id(lookup_id,
                                                                 False)
                     if db_record:
-                        record_id = db_record['concept_id']
+                        record_id = db_record["concept_id"]
                         return self._create_record_id_set(record_id,
                                                           observed_id_set)
                     else:
@@ -113,7 +109,7 @@ def _create_record_id_set(self, record_id: str,
 
             local_id_set = self._get_xrefs(db_record)
             if not local_id_set:
-                return observed_id_set | {db_record['concept_id']}
+                return observed_id_set | {db_record["concept_id"]}
             merged_id_set = {record_id} | observed_id_set
             for local_record_id in local_id_set - observed_id_set:
                 merged_id_set |= self._create_record_id_set(local_record_id,
@@ -122,9 +118,8 @@ def _create_record_id_set(self, record_id: str,
 
     def _generate_merged_record(self, record_id_set: Set[str]) -> Dict:
         """Generate merged record from provided concept ID group.
-        Where attributes are sets, they should be merged, and where they are
-        scalars, assign from the highest-priority source where that attribute
-        is non-null.
+        Where attributes are sets, they should be merged, and where they are scalars,
+        assign from the highest-priority source where that attribute is non-null.
 
         Priority is:
         RxNorm > NCIt > HemOnc.org > DrugBank > GuideToPHARMACOLOGY >
@@ -142,57 +137,59 @@ def _generate_merged_record(self, record_id_set: Set[str]) -> Dict:
                 logger.error(f"Merge record generator could not retrieve "
                              f"record for {record_id} in {record_id_set}")
 
-        def record_order(record):
-            """Provide priority values of concepts for sort function."""
-            src = record['src_name'].upper()
+        def record_order(record: Dict) -> Tuple[int, str]:
+            """Provide priority values of concepts for sort function.
+            :param Dict record: individual record to sort
+            """
+            src = record["src_name"].upper()
             if src in SourcePriority.__members__:
                 source_rank = SourcePriority[src].value
             else:
                 raise Exception(f"Prohibited source: {src} in concept_id "
                                 f"{record['concept_id']}")
-            return (source_rank, record['concept_id'])
+            return (source_rank, record["concept_id"])
         records.sort(key=record_order)
 
         # initialize merged record
         merged_attrs = {
-            'concept_id': records[0]['concept_id'],
-            'label': None,
-            'aliases': set(),
-            'trade_names': set(),
-            'associated_with': set(),
-            'approval_status': None,
-            'approval_year': set(),
-            'fda_indication': [],
+            "concept_id": records[0]["concept_id"],
+            "label": None,
+            "aliases": set(),
+            "trade_names": set(),
+            "associated_with": set(),
+            "approval_status": None,
+            "approval_year": set(),
+            "fda_indication": [],
         }
         if len(records) > 1:
-            merged_attrs['xrefs'] = [r['concept_id'] for r in records[1:]]
+            merged_attrs["xrefs"] = [r["concept_id"] for r in records[1:]]
 
         # merge from constituent records
-        set_fields = ['aliases', 'trade_names', 'associated_with',
-                      'approval_year']
+        set_fields = ["aliases", "trade_names", "associated_with",
+                      "approval_year"]
         for record in records:
             for field in set_fields:
                 merged_attrs[field] |= set(record.get(field, set()))
-            if merged_attrs['label'] is None:
-                merged_attrs['label'] = record.get('label')
-            if merged_attrs['approval_status'] is None:
-                merged_attrs['approval_status'] = record.get('approval_status')
-            for ind in record.get('fda_indication', []):
-                if ind not in merged_attrs['fda_indication']:
-                    merged_attrs['fda_indication'].append(ind)
+            if merged_attrs["label"] is None:
+                merged_attrs["label"] = record.get("label")
+            if merged_attrs["approval_status"] is None:
+                merged_attrs["approval_status"] = record.get("approval_status")
+            for ind in record.get("fda_indication", []):
+                if ind not in merged_attrs["fda_indication"]:
+                    merged_attrs["fda_indication"].append(ind)
 
         # clear unused fields
-        for field in set_fields + ['fda_indication']:
+        for field in set_fields + ["fda_indication"]:
             field_value = merged_attrs[field]
             if field_value:
                 merged_attrs[field] = list(field_value)
             else:
                 del merged_attrs[field]
-        for field in ['label', 'approval_status']:
+        for field in ["label", "approval_status"]:
             if merged_attrs[field] is None:
                 del merged_attrs[field]
 
-        merged_attrs['label_and_type'] = \
-            f'{merged_attrs["concept_id"].lower()}##merger'
-        merged_attrs['item_type'] = 'merger'
+        merged_attrs["label_and_type"] = \
+            f"{merged_attrs['concept_id'].lower()}##merger"
+        merged_attrs["item_type"] = "merger"
         return merged_attrs
diff --git a/therapy/etl/ncit.py b/therapy/etl/ncit.py
index 7f80e29c..05eb2fa3 100644
--- a/therapy/etl/ncit.py
+++ b/therapy/etl/ncit.py
@@ -1,17 +1,20 @@
 """ETL methods for NCIt source"""
-from .base import Base
-from therapy import PROJECT_ROOT
-from therapy.schemas import SourceName, NamespacePrefix, Therapy, SourceMeta
 import logging
-import owlready2 as owl
-from owlready2.entity import ThingClass
 from typing import Set
 from pathlib import Path
-import requests
 import zipfile
 from os import remove, rename
 
-logger = logging.getLogger('therapy')
+import requests
+import owlready2 as owl
+from owlready2.entity import ThingClass
+
+from therapy import PROJECT_ROOT
+from therapy.database import Database
+from therapy.schemas import SourceName, NamespacePrefix, SourceMeta, RecordParams
+from therapy.etl.base import Base
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 
@@ -25,11 +28,11 @@ class NCIt(Base):
     """
 
     def __init__(self,
-                 database,
+                 database: Database,
                  src_dir: str = "https://evs.nci.nih.gov/ftp1/NCI_Thesaurus/archive/2020/20.09d_Release/",  # noqa F401
                  src_fname: str = "Thesaurus_20.09d.OWL.zip",
-                 data_path: Path = PROJECT_ROOT / 'data',
-                 chemidplus_path: Path = PROJECT_ROOT / 'data' / 'chemidplus'):
+                 data_path: Path = PROJECT_ROOT / "data",
+                 chemidplus_path: Path = PROJECT_ROOT / "data" / "chemidplus"):
         """Override base class init method. Call ETL methods.
 
         :param therapy.database.Database database: app database instance
@@ -42,35 +45,35 @@ def __init__(self,
         self._SRC_FNAME = src_fname
         self._chemidplus_path = chemidplus_path
 
-    def _download_data(self):
+    def _download_data(self) -> None:
         """Download NCI thesaurus source file for loading into normalizer."""
-        logger.info('Downloading NCI Thesaurus...')
+        logger.info("Downloading NCI Thesaurus...")
         url = self._SRC_DIR + self._SRC_FNAME
-        zip_path = self._src_data_dir / 'ncit.zip'
+        zip_path = self._src_data_dir / "ncit.zip"
         response = requests.get(url, stream=True)
         handle = open(zip_path, "wb")
         for chunk in response.iter_content(chunk_size=512):
             if chunk:
                 handle.write(chunk)
         handle.close()
-        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+        with zipfile.ZipFile(zip_path, "r") as zip_ref:
             zip_ref.extractall(self._src_data_dir)
         remove(zip_path)
-        version = self._SRC_DIR.split('/')[-2].split('_')[0]
-        rename(self._src_data_dir / 'Thesaurus.owl', self._src_data_dir / f'ncit_{version}.owl')  # noqa: E501
-        logger.info('Finished downloading NCI Thesaurus')
+        version = self._SRC_DIR.split("/")[-2].split("_")[0]
+        rename(self._src_data_dir / "Thesaurus.owl", self._src_data_dir / f"ncit_{version}.owl")  # noqa: E501
+        logger.info("Finished downloading NCI Thesaurus")
 
     def _get_desc_nodes(self, node: ThingClass,
                         uq_nodes: Set[ThingClass]) -> Set[ThingClass]:
         """Create set of unique subclasses of node parameter.
         Should be originally called on ncit:C1909: Pharmacologic Substance.
 
-        :param owlready2.entity.ThingClass node: concept node to either
-            retrieve descendants of, or to normalize and add to DB
-        :param Set[owlready2.entity.ThingClass] uq_nodes: set of unique class
-            nodes found so far from recursive tree exploration
-        :return: the uq_nodes set, updated with any class nodes found from
-            recursive exploration of this branch of the class tree
+        :param owlready2.entity.ThingClass node: concept node to either retrieve
+            descendants of, or to normalize and add to DB
+        :param Set[owlready2.entity.ThingClass] uq_nodes: set of unique class nodes
+            found so far from recursive tree exploration
+        :return: the uq_nodes set, updated with any class nodes found from recursive
+            exploration of this branch of the class tree
         :rtype: Set[owlready2.entity.ThingClass]
         """
         children = node.descendants()
@@ -85,39 +88,38 @@ def _get_typed_nodes(self, uq_nodes: Set[ThingClass],
                          ncit: owl.namespace.Ontology) -> Set[ThingClass]:
         """Get all nodes with semantic_type Pharmacologic Substance
 
-        :param Set[owlready2.entity.ThingClass] uq_nodes: set of unique class
-            nodes found so far.
-        :param owl.namespace.Ontology ncit: owlready2 Ontology instance for
-            NCI Thesaurus.
-        :return: uq_nodes, with the addition of all classes found to have
-            semantic_type Pharmacologic Substance and not of type
-            Retired_Concept
+        :param Set[owlready2.entity.ThingClass] uq_nodes: set of unique class nodes
+            found so far.
+        :param owl.namespace.Ontology ncit: owlready2 Ontology instance for NCIt.
+        :return: uq_nodes, with the addition of all classes found to have semantic_type
+            Pharmacologic Substance and not of type Retired_Concept
         :rtype: Set[owlready2.entity.ThingClass]
         """
         graph = owl.default_world.as_rdflib_graph()
 
-        query_str = '''SELECT ?x WHERE {
+        query_str = """SELECT ?x WHERE {
             ?x <http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#P106>
-            "Pharmacologic Substance"
-        }'''
+            \"Pharmacologic Substance\"
+        }
+        """
         typed_results = set(graph.query(query_str))
 
-        retired_query_str = '''SELECT ?x WHERE {
+        retired_query_str = """SELECT ?x WHERE {
             ?x <http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#P310>
-            "Retired_Concept"
+            \"Retired_Concept\"
         }
-        '''
+        """
         retired_results = set(graph.query(retired_query_str))
 
         typed_results = typed_results - retired_results
 
         for result in typed_results:
             # parse result as URI and get ThingClass object back from NCIt
-            class_object = ncit[result[0].toPython().split('#')[1]]
+            class_object = ncit[result[0].toPython().split("#")[1]]
             uq_nodes.add(class_object)
         return uq_nodes
 
-    def _transform_data(self):
+    def _transform_data(self) -> None:
         """Get data from file and construct objects for loading"""
         ncit = owl.get_ontology(self._src_file.absolute().as_uri())
         ncit.load()
@@ -150,31 +152,30 @@ def _transform_data(self):
                                        f"{node.P320.first()}")
             if node.P368:
                 iri = node.P368.first()
-                if ':' in iri:
-                    iri = iri.split(':')[1]
+                if ":" in iri:
+                    iri = iri.split(":")[1]
                 associated_with.append(f"{NamespacePrefix.CHEBI.value}:{iri}")
-            params = {
-                'concept_id': concept_id,
-                'label': label,
-                'aliases': aliases,
-                'xrefs': xrefs,
-                'associated_with': associated_with
+            params: RecordParams = {  # type: ignore
+                "concept_id": concept_id,
+                "label": label,
+                "aliases": aliases,
+                "xrefs": xrefs,
+                "associated_with": associated_with
             }
-            assert Therapy(**params)
             self._load_therapy(params)
 
-    def _load_meta(self):
+    def _load_meta(self) -> None:
         """Load metadata"""
         metadata = SourceMeta(data_license="CC BY 4.0",
                               data_license_url="https://creativecommons.org/licenses/by/4.0/legalcode",  # noqa F401
                               version=self._version,
                               data_url=self._SRC_DIR,
-                              rdp_url='http://reusabledata.org/ncit.html',
+                              rdp_url="http://reusabledata.org/ncit.html",
                               data_license_attributes={
-                                  'non_commercial': False,
-                                  'share_alike': False,
-                                  'attribution': True
+                                  "non_commercial": False,
+                                  "share_alike": False,
+                                  "attribution": True
                               })
         params = dict(metadata)
-        params['src_name'] = SourceName.NCIT.value
+        params["src_name"] = SourceName.NCIT.value
         self.database.metadata.put_item(Item=params)
diff --git a/therapy/etl/rxnorm.py b/therapy/etl/rxnorm.py
index 832836de..2f1f0b22 100644
--- a/therapy/etl/rxnorm.py
+++ b/therapy/etl/rxnorm.py
@@ -5,41 +5,44 @@
  and Human Services; NLM is not responsible for the product and does not
  endorse or recommend this or any other product."
 """
-from .base import Base
-from therapy import PROJECT_ROOT, DownloadException, XREF_SOURCES, \
-    ASSOC_WITH_SOURCES, ITEM_TYPES
-import therapy
-from therapy.database import Database
-from therapy.schemas import SourceName, NamespacePrefix, SourceMeta, Drug, \
-    ApprovalStatus
 import csv
 import datetime
 import logging
-from os import environ, remove
 import subprocess
 import shutil
 import zipfile
 import re
-import yaml
+from os import environ, remove
 from pathlib import Path
+from typing import List, Dict
+
+import yaml
+from boto3.dynamodb.table import BatchWriter
+
+import therapy
+from therapy import PROJECT_ROOT, DownloadException, XREF_SOURCES, \
+    ASSOC_WITH_SOURCES, ITEM_TYPES
+from therapy.etl.base import Base
+from therapy.database import Database
+from therapy.schemas import SourceName, NamespacePrefix, SourceMeta, Drug, \
+    ApprovalStatus, RecordParams
 
-logger = logging.getLogger('therapy')
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 # Designated Alias, Designated Syn, Tall Man Syn, Machine permutation
 # Generic Drug Name, Designated Preferred Name, Preferred Entry Term,
 # Clinical Drug, Entry Term, Rxnorm Preferred
-ALIASES = ['SYN', 'SY', 'TMSY', 'PM',
-           'GN', 'PT', 'PEP', 'CD', 'ET', 'RXN_PT']
+ALIASES = ["SYN", "SY", "TMSY", "PM", "GN", "PT", "PEP", "CD", "ET", "RXN_PT"]
 
 # Fully-specified drug brand name that can be prescribed
 # Fully-specified drug brand name that can not be prescribed,
 # Semantic branded drug
-TRADE_NAMES = ['BD', 'BN', 'SBD']
+TRADE_NAMES = ["BD", "BN", "SBD"]
 
 # Allowed rxnorm xrefs that have Source Level Restriction 0 or 1
-RXNORM_XREFS = ['ATC', 'CVX', 'DRUGBANK', 'MMSL', 'MSH', 'MTHCMSFRF', 'MTHSPL',
-                'RXNORM', 'USP', 'VANDF']
+RXNORM_XREFS = ["ATC", "CVX", "DRUGBANK", "MMSL", "MSH", "MTHCMSFRF", "MTHSPL",
+                "RXNORM", "USP", "VANDF"]
 
 
 class RxNorm(Base):
@@ -47,9 +50,9 @@ class RxNorm(Base):
 
     def __init__(self,
                  database: Database,
-                 data_path=PROJECT_ROOT / 'data',
-                 data_url='https://www.nlm.nih.gov/research/umls/'
-                          'rxnorm/docs/rxnormfiles.html'):
+                 data_path: Path = PROJECT_ROOT / "data",
+                 data_url: str = "https://www.nlm.nih.gov/research/umls/"
+                                 "rxnorm/docs/rxnormfiles.html"):
         """Initialize the RxNorm ETL class.
 
         :param Database database: Access to DynamoDB.
@@ -59,13 +62,13 @@ def __init__(self,
         super().__init__(database, data_path)
         self._data_url = data_url
 
-    def _extract_data(self, *args, **kwargs):
+    def _extract_data(self, *args, **kwargs) -> None:
         """Extract data from the RxNorm source."""
-        logger.info('Extracting RxNorm...')
-        if 'data_path' in kwargs:
-            self._data_src = kwargs['data_path']
+        logger.info("Extracting RxNorm...")
+        if "data_path" in kwargs:
+            self._data_src = kwargs["data_path"]
         else:
-            rxn_dir = PROJECT_ROOT / 'data' / 'rxnorm'
+            rxn_dir = PROJECT_ROOT / "data" / "rxnorm"
             rxn_dir.mkdir(exist_ok=True, parents=True)
             rxn_files = list(rxn_dir.iterdir())
             if len(rxn_files) == 0:
@@ -73,13 +76,13 @@ def _extract_data(self, *args, **kwargs):
 
             # file might already exist
             files = sorted([fn for fn in rxn_dir.iterdir() if fn.name.
-                           startswith('rxnorm_') and fn.name.endswith('.RRF')],
+                           startswith("rxnorm_") and fn.name.endswith(".RRF")],
                            reverse=True)
             file_found = False
             for file in files:
-                version = str(file).split('_')[-1].split('.')[0]
+                version = str(file).split("_")[-1].split(".")[0]
                 try:
-                    datetime.datetime.strptime(version, '%Y%m%d')
+                    datetime.datetime.strptime(version, "%Y%m%d")
                     self._data_src = file
                     self._version = version
                     file_found = True
@@ -89,140 +92,140 @@ def _extract_data(self, *args, **kwargs):
             if not file_found:
                 self._download_data(rxn_dir)
             self._create_drug_form_yaml(rxn_dir)
-        logger.info('Successfully extracted RxNorm.')
+        logger.info("Successfully extracted RxNorm.")
 
-    def _download_data(self, rxn_dir):
+    def _download_data(self, rxn_dir: Path) -> None:  # type: ignore
         """Download RxNorm data file.
 
         :param Path rxn_dir: Path to RxNorm data directory.
         """
-        logger.info('Downloading RxNorm...')
-        if 'RXNORM_API_KEY' in environ.keys():
-            uri = 'https://download.nlm.nih.gov/umls/kss/' \
-                  'rxnorm/RxNorm_full_current.zip'
+        logger.info("Downloading RxNorm...")
+        if "RXNORM_API_KEY" in environ.keys():
+            uri = "https://download.nlm.nih.gov/umls/kss/" \
+                  "rxnorm/RxNorm_full_current.zip"
 
-            rxnorm_path = str(Path(therapy.__file__).resolve().parents[0] / 'data' / 'rxnorm' / 'RxNorm_full_current.zip')  # noqa: E501
-            environ['RXNORM_PATH'] = rxnorm_path
+            rxnorm_path = str(Path(therapy.__file__).resolve().parents[0] / "data" / "rxnorm" / "RxNorm_full_current.zip")  # noqa: E501
+            environ["RXNORM_PATH"] = rxnorm_path
 
             # Source:
             # https://documentation.uts.nlm.nih.gov/automating-downloads.html
-            subprocess.call(['bash', f'{PROJECT_ROOT}/etl/'
-                                     f'rxnorm_download.sh', uri])
+            subprocess.call(["bash", f"{PROJECT_ROOT}/etl/"
+                                     f"rxnorm_download.sh", uri])
 
-            with zipfile.ZipFile(rxnorm_path, 'r') as zf:
+            with zipfile.ZipFile(rxnorm_path, "r") as zf:
                 zf.extractall(rxn_dir)
 
             remove(rxnorm_path)
-            shutil.rmtree(rxn_dir / 'prescribe')
-            shutil.rmtree(rxn_dir / 'scripts')
+            shutil.rmtree(rxn_dir / "prescribe")
+            shutil.rmtree(rxn_dir / "scripts")
 
             readme = sorted([fn for fn in rxn_dir.iterdir() if fn.name.
-                            startswith('Readme')])[0]
+                            startswith("Readme")])[0]
 
             # get version
-            version = str(readme).split('/')[-1].split('.')[0].split('_')[-1]
+            version = str(readme).split("/")[-1].split(".")[0].split("_")[-1]
 
             self._version = datetime.datetime.strptime(
-                version, '%m%d%Y').strftime('%Y%m%d')
+                version, "%m%d%Y").strftime("%Y%m%d")
             remove(readme)
 
-            temp_file = rxn_dir / 'rrf' / 'RXNCONSO.RRF'
+            temp_file = rxn_dir / "rrf" / "RXNCONSO.RRF"
             self._data_src = rxn_dir / f"rxnorm_{self._version}.RRF"
-            shutil.move(temp_file, self._data_src)
-            shutil.rmtree(rxn_dir / 'rrf')
+            shutil.move(str(temp_file), str(self._data_src))
+            shutil.rmtree(rxn_dir / "rrf")
         else:
-            logger.error('Could not find RXNORM_API_KEY in environment '
-                         'variables.')
+            logger.error("Could not find RXNORM_API_KEY in environment variables.")
             raise DownloadException("RXNORM_API_KEY not found.")
 
-    def _create_drug_form_yaml(self, rxn_dir):
+    def _create_drug_form_yaml(self, rxn_dir: Path) -> None:
         """Create a YAML file containing RxNorm drug form values.
 
         :param Path rxn_dir: Path to RxNorm data directory.
         """
-        self._drug_forms_file = rxn_dir / 'drug_forms.yaml'
+        self._drug_forms_file = rxn_dir / "drug_forms.yaml"
         if not self._drug_forms_file.exists():
             dfs = list()
             with open(self._data_src) as f:
-                data = csv.reader(f, delimiter='|')
+                data = csv.reader(f, delimiter="|")
                 for row in data:
-                    if row[12] == 'DF' and row[11] == 'RXNORM':
+                    if row[12] == "DF" and row[11] == "RXNORM":
                         if row[14] not in dfs:
                             dfs.append(row[14])
 
-            with open(self._drug_forms_file, 'w') as file:
+            with open(self._drug_forms_file, "w") as file:
                 yaml.dump(dfs, file)
 
-    def _transform_data(self):
+    def _transform_data(self) -> None:
         """Transform the RxNorm source."""
-        with open(self._drug_forms_file, 'r') as file:
+        with open(self._drug_forms_file, "r") as file:
             drug_forms = yaml.safe_load(file)
 
         with open(self._data_src) as f:
-            rff_data = csv.reader(f, delimiter='|')
-            ingredient_brands = dict()  # Link ingredient to brand
-            precise_ingredient = dict()  # Link precise ingredient to get brand
-            data = dict()  # Transformed therapy records
-            sbdfs = dict()  # Link ingredient to brand
-            brands = dict()  # Get RXNORM|BN to concept_id
+            rff_data = csv.reader(f, delimiter="|")
+            # Link ingredient to brand
+            ingredient_brands: Dict[str, str] = dict()
+            # Link precise ingredient to get brand
+            precise_ingredient: Dict[str, str] = dict()
+            # Transformed therapy records
+            data: Dict[str, Dict] = dict()
+            # Link ingredient to brand
+            sbdfs: Dict[str, str] = dict()
+            # Get RXNORM|BN to concept_id
+            brands: Dict[str, str] = dict()
             for row in rff_data:
                 if row[11] in RXNORM_XREFS:
                     concept_id = f"{NamespacePrefix.RXNORM.value}:{row[0]}"
-                    if row[12] == 'BN' and row[11] == 'RXNORM':
+                    if row[12] == "BN" and row[11] == "RXNORM":
                         brands[row[14]] = concept_id
-                    if row[12] == 'SBDC' and row[11] == 'RXNORM':
+                    if row[12] == "SBDC" and row[11] == "RXNORM":
                         # Semantic Branded Drug Component
                         self._get_brands(row, ingredient_brands)
                     else:
                         if concept_id not in data.keys():
-                            params = dict()
-                            params['concept_id'] = concept_id
-                            self._add_str_field(params, row,
-                                                precise_ingredient,
+                            params: RecordParams = {}
+                            params["concept_id"] = concept_id
+                            self._add_str_field(params, row, precise_ingredient,
                                                 drug_forms, sbdfs)
                             self._add_xref_assoc(params, row)
                             data[concept_id] = params
                         else:
                             # Concept already created
                             params = data[concept_id]
-                            self._add_str_field(params, row,
-                                                precise_ingredient,
+                            self._add_str_field(params, row, precise_ingredient,
                                                 drug_forms, sbdfs)
                             self._add_xref_assoc(params, row)
 
             with self.database.therapies.batch_writer() as batch:
-                for key, value in data.items():
-                    if 'label' in value:
+                for value in data.values():
+                    if "label" in value:
                         self._get_trade_names(value, precise_ingredient,
                                               ingredient_brands, sbdfs)
                         self._load_brand_concepts(value, brands, batch)
 
-                        params = {
-                            'concept_id': value['concept_id']
-                        }
+                        params = {"concept_id": value["concept_id"]}
 
-                        for field in list(ITEM_TYPES.keys()) + ['approval_status']:  # noqa: E501
+                        for field in list(ITEM_TYPES.keys()) + ["approval_status"]:
                             field_value = value.get(field)
                             if field_value:
                                 params[field] = field_value
                         assert Drug(**params)
                         self._load_therapy(params)
 
-    def _get_brands(self, row, ingredient_brands):
+    def _get_brands(self, row: List, ingredient_brands: Dict) -> None:
         """Add ingredient and brand to ingredient_brands.
 
-        :param list row: A row in the RxNorm data file.
-        :param dict ingredient_brands: Store brands for each ingredient
+        :param List row: A row in the RxNorm data file.
+        :param Dict ingredient_brands: Store brands for each ingredient
         """
         # SBDC: Ingredient(s) + Strength + [Brand Name]
         term = row[14]
         ingredients_brand = \
             re.sub(r"(\d*)(\d*\.)?\d+ (MG|UNT|ML)?(/(ML|HR|MG))?",
                    "", term)
-        brand = term.split('[')[-1].split(']')[0]
-        ingredients = ingredients_brand.replace(f"[{brand}]", '')
-        if '/' in ingredients:
-            ingredients = ingredients.split('/')
+        brand = term.split("[")[-1].split("]")[0]
+        ingredients = ingredients_brand.replace(f"[{brand}]", "")
+        if "/" in ingredients:
+            ingredients = ingredients.split("/")
             for ingredient in ingredients:
                 self._add_term(ingredient_brands, brand,
                                ingredient.strip())
@@ -230,103 +233,103 @@ def _get_brands(self, row, ingredient_brands):
             self._add_term(ingredient_brands, brand,
                            ingredients.strip())
 
-    def _get_trade_names(self, value, precise_ingredient, ingredient_brands,
-                         sbdfs):
+    def _get_trade_names(self, value: Dict, precise_ingredient: Dict,
+                         ingredient_brands: Dict, sbdfs: Dict) -> None:
         """Get trade names for a given ingredient.
 
-        :param dict value: Therapy attributes
-        :param dict precise_ingredient: Brand names for precise ingredient
-        :param dict ingredient_brands: Brand names for ingredient
-        :param dict sbdfs: Brand names for ingredient from SBDF row
+        :param Dict value: Therapy attributes
+        :param Dict precise_ingredient: Brand names for precise ingredient
+        :param Dict ingredient_brands: Brand names for ingredient
+        :param Dict sbdfs: Brand names for ingredient from SBDF row
         """
-        record_label = value['label'].lower()
+        record_label = value["label"].lower()
         labels = [record_label]
 
-        if 'PIN' in value and value['PIN'] \
+        if "PIN" in value and value["PIN"] \
                 in precise_ingredient:
-            for pin in precise_ingredient[value['PIN']]:
+            for pin in precise_ingredient[value["PIN"]]:
                 labels.append(pin.lower())
 
         for label in labels:
-            trade_names = \
+            trade_names: List[str] = \
                 [val for key, val in ingredient_brands.items()
                  if label == key.lower()]
-            trade_names = {val for sublist in trade_names
-                           for val in sublist}
-            for tn in trade_names:
-                self._add_term(value, tn, 'trade_names')
+            trade_names_uq = {val for sublist in trade_names
+                              for val in sublist}
+            for tn in trade_names_uq:
+                self._add_term(value, tn, "trade_names")
 
         if record_label in sbdfs:
             for tn in sbdfs[record_label]:
-                self._add_term(value, tn, 'trade_names')
+                self._add_term(value, tn, "trade_names")
 
     @staticmethod
-    def _load_brand_concepts(value, brands, batch):
+    def _load_brand_concepts(value: Dict, brands: Dict, batch: BatchWriter) -> None:
         """Connect brand names to a concept and load into the database.
 
         :params dict value: A transformed therapy record
         :params dict brands: Connects brand names to concept records
         :param BatchWriter batch: Object to write data to DynamoDB.
         """
-        if 'trade_names' in value:
-            for tn in value['trade_names']:
+        if "trade_names" in value:
+            for tn in value["trade_names"]:
                 if brands.get(tn):
                     batch.put_item(Item={
-                        'label_and_type':
+                        "label_and_type":
                             f"{brands.get(tn)}##rx_brand",
-                        'concept_id': value['concept_id'],
-                        'src_name': SourceName.RXNORM.value,
-                        'item_type': 'rx_brand'
+                        "concept_id": value["concept_id"],
+                        "src_name": SourceName.RXNORM.value,
+                        "item_type": "rx_brand"
                     })
 
-    def _add_str_field(self, params, row, precise_ingredient, drug_forms,
-                       sbdfs):
+    def _add_str_field(self, params: Dict, row: List, precise_ingredient: Dict,
+                       drug_forms: List, sbdfs: Dict) -> None:
         """Differentiate STR field.
 
-        :param dict params: A transformed therapy record.
-        :param list row: A row in the RxNorm data file.
-        :param dict precise_ingredient: Precise ingredient information
-        :param list drug_forms: RxNorm Drug Form values
-        :param dict sbdfs: Brand names for precise ingredient
+        :param Dict params: A transformed therapy record.
+        :param List row: A row in the RxNorm data file.
+        :param Dict precise_ingredient: Precise ingredient information
+        :param List drug_forms: RxNorm Drug Form values
+        :param Dict sbdfs: Brand names for precise ingredient
         """
         term = row[14]
         term_type = row[12]
         source = row[11]
 
-        if (term_type == 'IN' or term_type == 'PIN') and source == 'RXNORM':
-            params['label'] = term
-            if row[17] == '4096':
-                params['approval_status'] = ApprovalStatus.APPROVED.value
+        if (term_type == "IN" or term_type == "PIN") and source == "RXNORM":
+            params["label"] = term
+            if row[17] == "4096":
+                params["approval_status"] = ApprovalStatus.APPROVED.value
         elif term_type in ALIASES:
-            self._add_term(params, term, 'aliases')
+            self._add_term(params, term, "aliases")
         elif term_type in TRADE_NAMES:
-            self._add_term(params, term, 'trade_names')
+            self._add_term(params, term, "trade_names")
 
-        if source == 'RXNORM':
-            if term_type == 'SBDF':
-                brand = term.split('[')[-1].split(']')[0]
-                ingredient_strength = term.replace(f"[{brand}]", '')
+        if source == "RXNORM":
+            if term_type == "SBDF":
+                brand = term.split("[")[-1].split("]")[0]
+                ingredient_strength = term.replace(f"[{brand}]", "")
                 for df in drug_forms:
                     if df in ingredient_strength:
                         ingredient = \
-                            ingredient_strength.replace(df, '').strip()
+                            ingredient_strength.replace(df, "").strip()
                         self._add_term(sbdfs, brand, ingredient.lower())
                         break
 
-        if source == 'MSH':
-            if term_type == 'MH':
+        if source == "MSH":
+            if term_type == "MH":
                 # Get ID for accessing precise ingredient
-                params['PIN'] = row[13]
-            elif term_type == 'PEP':
+                params["PIN"] = row[13]
+            elif term_type == "PEP":
                 self._add_term(precise_ingredient, term, row[13])
 
     @staticmethod
-    def _add_term(params, term, label_type):
+    def _add_term(params: Dict, term: str, label_type: str) -> None:
         """Add a single term to a therapy record in an associated field.
 
-        :param dict params: A transformed therapy record.
-        :param str term: The term to be added
-        :param str label_type: The type of term
+        :param Dict params: A transformed therapy record.
+        :param Str term: The term to be added
+        :param Str label_type: The type of term
         """
         if label_type in params and params[label_type]:
             if term not in params[label_type]:
@@ -334,45 +337,45 @@ def _add_term(params, term, label_type):
         else:
             params[label_type] = [term]
 
-    def _add_xref_assoc(self, params, row):
+    def _add_xref_assoc(self, params: Dict, row: List) -> None:
         """Add xref or associated_with to therapy.
 
-        :param dict params: A transformed therapy record.
-        :param list row: A row in the RxNorm data file.
+        :param Dict params: A transformed therapy record.
+        :param List row: A row in the RxNorm data file.
         """
         ref = row[11]
         if ref:
-            if ref == 'MTHSPL':
-                xref_assoc = 'UNII'
+            if ref == "MTHSPL":
+                xref_assoc = "UNII"
             else:
                 xref_assoc = row[11].upper()
 
             if xref_assoc in XREF_SOURCES:
                 source_id =\
                     f"{NamespacePrefix[xref_assoc].value}:{row[13]}"
-                if source_id != params['concept_id']:
+                if source_id != params["concept_id"]:
                     # Sometimes concept_id is included in the source field
-                    self._add_term(params, source_id, 'xrefs')
+                    self._add_term(params, source_id, "xrefs")
             elif xref_assoc in ASSOC_WITH_SOURCES:
                 source_id = f"{NamespacePrefix[xref_assoc].value}:{row[13]}"
-                self._add_term(params, source_id, 'associated_with')
+                self._add_term(params, source_id, "associated_with")
             else:
                 logger.info(f"{xref_assoc} not in NameSpacePrefix.")
 
-    def _load_meta(self):
+    def _load_meta(self) -> None:
         """Add RxNorm metadata."""
         meta = SourceMeta(
-            data_license='UMLS Metathesaurus',
-            data_license_url='https://www.nlm.nih.gov/research/umls/rxnorm/docs/termsofservice.html',  # noqa: E501
+            data_license="UMLS Metathesaurus",
+            data_license_url="https://www.nlm.nih.gov/research/umls/rxnorm/docs/termsofservice.html",  # noqa: E501
             version=self._version,
             data_url=self._data_url,
             rdp_url=None,
             data_license_attributes={
-                'non_commercial': False,
-                'share_alike': False,
-                'attribution': True
+                "non_commercial": False,
+                "share_alike": False,
+                "attribution": True
             }
         )
         params = dict(meta)
-        params['src_name'] = SourceName.RXNORM.value
+        params["src_name"] = SourceName.RXNORM.value
         self.database.metadata.put_item(Item=params)
diff --git a/therapy/etl/wikidata.py b/therapy/etl/wikidata.py
index fc6890f4..705935ad 100644
--- a/therapy/etl/wikidata.py
+++ b/therapy/etl/wikidata.py
@@ -1,27 +1,31 @@
 """This module defines the Wikidata ETL methods."""
-from .base import Base
-from therapy import PROJECT_ROOT
-from therapy.schemas import SourceName, NamespacePrefix, \
-    SourceIDAfterNamespace, SourceMeta
 import json
 import logging
 from pathlib import Path
-from wikibaseintegrator.wbi_functions import execute_sparql_query
 import datetime
+from typing import Dict, Any
+
+from wikibaseintegrator.wbi_functions import execute_sparql_query
 
-logger = logging.getLogger('therapy')
+from therapy import PROJECT_ROOT
+from therapy.database import Database
+from therapy.schemas import SourceName, NamespacePrefix, RecordParams, \
+    SourceIDAfterNamespace, SourceMeta
+from therapy.etl.base import Base
+
+logger = logging.getLogger("therapy")
 logger.setLevel(logging.DEBUG)
 
 # Prefixes for translating ID namespaces
 IDENTIFIER_PREFIXES = {
-    'casRegistry': NamespacePrefix.CASREGISTRY.value,
-    'ChemIDplus': NamespacePrefix.CHEMIDPLUS.value,
-    'pubchemCompound': NamespacePrefix.PUBCHEMCOMPOUND.value,
-    'pubchemSubstance': NamespacePrefix.PUBCHEMSUBSTANCE.value,
-    'chembl': NamespacePrefix.CHEMBL.value,
-    'rxnorm': NamespacePrefix.RXNORM.value,
-    'drugbank': NamespacePrefix.DRUGBANK.value,
-    'wikidata': NamespacePrefix.WIKIDATA.value,
+    "casRegistry": NamespacePrefix.CASREGISTRY.value,
+    "ChemIDplus": NamespacePrefix.CHEMIDPLUS.value,
+    "pubchemCompound": NamespacePrefix.PUBCHEMCOMPOUND.value,
+    "pubchemSubstance": NamespacePrefix.PUBCHEMSUBSTANCE.value,
+    "chembl": NamespacePrefix.CHEMBL.value,
+    "rxnorm": NamespacePrefix.RXNORM.value,
+    "drugbank": NamespacePrefix.DRUGBANK.value,
+    "wikidata": NamespacePrefix.WIKIDATA.value,
 }
 
 
@@ -32,7 +36,7 @@
       ?item (wdt:P31/(wdt:P279*)) wd:Q12140.
       OPTIONAL {
         ?item skos:altLabel ?alias.
-        FILTER((LANG(?alias)) = "en")
+        FILTER((LANG(?alias)) = \"en\")
       }
       OPTIONAL { ?item p:P231 ?wds1.
                  ?wds1 ps:P231 ?casRegistry.
@@ -53,7 +57,7 @@
                  ?wds6 ps:P715 ?drugbank
                }
       SERVICE wikibase:label {
-        bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en".
+        bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],en\".
       }
     }
 """
@@ -63,69 +67,64 @@ class Wikidata(Base):
     """Extract, transform, and load the Wikidata source into therapy.db."""
 
     def __init__(self,
-                 database,
-                 data_path: Path = PROJECT_ROOT / 'data'):
+                 database: Database,
+                 data_path: Path = PROJECT_ROOT / "data"):
         """Initialize wikidata ETL class.
 
-        :param therapy.database.Database: DB instance to use
+        :param Database: DB instance to use
         :param Path data_path: path to app data directory
         """
         super().__init__(database, data_path)
 
-    def _extract_data(self):
+    def _extract_data(self) -> None:
         """Extract data from the Wikidata source."""
         self._src_data_dir.mkdir(exist_ok=True, parents=True)
 
-        data = execute_sparql_query(SPARQL_QUERY)['results']['bindings']
+        data = execute_sparql_query(SPARQL_QUERY)["results"]["bindings"]
 
-        transformed_data = list()
+        transformed_data = []
         for item in data:
-            params = dict()
+            params: RecordParams = {}
             for attr in item:
-                params[attr] = item[attr]['value']
+                params[attr] = item[attr]["value"]
             transformed_data.append(params)
 
-        self._version = datetime.datetime.today().strftime('%Y%m%d')
-        with open(f"{self._src_data_dir}/wikidata_{self._version}.json",
-                  'w+') as f:
+        self._version = datetime.datetime.today().strftime("%Y%m%d")
+        with open(f"{self._src_data_dir}/wikidata_{self._version}.json", "w+") as f:
             json.dump(transformed_data, f)
         self._data_src = sorted(list(self._src_data_dir.iterdir()))[-1]
-        logger.info('Successfully extracted Wikidata.')
+        logger.info("Successfully extracted Wikidata.")
 
-    def _load_meta(self):
+    def _load_meta(self) -> None:
         """Add Wikidata metadata."""
         metadata = SourceMeta(src_name=SourceName.WIKIDATA.value,
-                              data_license='CC0 1.0',
-                              data_license_url='https://creativecommons.org/publicdomain/zero/1.0/',  # noqa: E501
+                              data_license="CC0 1.0",
+                              data_license_url="https://creativecommons.org/publicdomain/zero/1.0/",  # noqa: E501
                               version=self._version,
                               data_url=None,
                               rdp_url=None,
                               data_license_attributes={
-                                  'non_commercial': False,
-                                  'share_alike': False,
-                                  'attribution': False
+                                  "non_commercial": False,
+                                  "share_alike": False,
+                                  "attribution": False
                               })
         params = dict(metadata)
-        params['src_name'] = SourceName.WIKIDATA.value
+        params["src_name"] = SourceName.WIKIDATA.value
         self.database.metadata.put_item(Item=params)
 
-    def _transform_data(self):
+    def _transform_data(self) -> None:
         """Transform the Wikidata source data."""
         from therapy import XREF_SOURCES
-        with open(self._data_src, 'r') as f:
+        with open(self._data_src, "r") as f:
             records = json.load(f)
 
-            items = dict()
+            items: Dict[str, Any] = dict()
 
             for record in records:
-                record_id = record['item'].split('/')[-1]
+                record_id = record["item"].split("/")[-1]
                 concept_id = f"{NamespacePrefix.WIKIDATA.value}:{record_id}"
                 if concept_id not in items.keys():
-                    item = dict()
-                    item['label_and_type'] = f"{concept_id.lower()}##identity"
-                    item['item_type'] = 'identity'
-                    item['concept_id'] = concept_id
-                    item['src_name'] = SourceName.WIKIDATA.value
+                    item: Dict[str, Any] = {"concept_id": concept_id}
 
                     xrefs = list()
                     associated_with = list()
@@ -133,31 +132,29 @@ def _transform_data(self):
                         if key in record.keys():
                             ref = record[key]
 
-                            if key.upper() == 'CASREGISTRY':
+                            if key.upper() == "CASREGISTRY":
                                 key = SourceName.CHEMIDPLUS.value
 
                             if key.upper() in XREF_SOURCES:
-                                if key != 'chembl':
+                                if key != "chembl":
                                     fmted_xref = \
                                         f"{IDENTIFIER_PREFIXES[key]}:{SourceIDAfterNamespace[key.upper()].value}{ref}"  # noqa: E501
                                 else:
-                                    fmted_xref = \
-                                        f"{IDENTIFIER_PREFIXES[key]}:{ref}"
+                                    fmted_xref = f"{IDENTIFIER_PREFIXES[key]}:{ref}"
                                 xrefs.append(fmted_xref)
                             else:
-                                fmted_assoc = f"{IDENTIFIER_PREFIXES[key]}:" \
-                                              f"{ref}"
+                                fmted_assoc = f"{IDENTIFIER_PREFIXES[key]}:{ref}"
                                 associated_with.append(fmted_assoc)
-                    item['xrefs'] = xrefs
-                    item['associated_with'] = associated_with
-                    if 'itemLabel' in record.keys():
-                        item['label'] = record['itemLabel']
+                    item["xrefs"] = xrefs
+                    item["associated_with"] = associated_with
+                    if "itemLabel" in record.keys():
+                        item["label"] = record["itemLabel"]
                     items[concept_id] = item
-                if 'alias' in record.keys():
-                    if 'aliases' in items[concept_id].keys():
-                        items[concept_id]['aliases'].append(record['alias'])
+                if "alias" in record.keys():
+                    if "aliases" in items[concept_id].keys():
+                        items[concept_id]["aliases"].append(record["alias"])
                     else:
-                        items[concept_id]['aliases'] = [record['alias']]
+                        items[concept_id]["aliases"] = [record["alias"]]
 
         for item in items.values():
             self._load_therapy(item)
diff --git a/therapy/main.py b/therapy/main.py
index d7339751..f9f46c66 100644
--- a/therapy/main.py
+++ b/therapy/main.py
@@ -1,17 +1,19 @@
 """Main application for FastAPI"""
+import html
+from typing import Dict
+
+from fastapi import FastAPI, HTTPException, Query
+from fastapi.openapi.utils import get_openapi
+
 from therapy import __version__
 from therapy.query import QueryHandler, InvalidParameterException
 from therapy.schemas import SearchService, NormalizationService
-from fastapi import FastAPI, HTTPException, Query
-from fastapi.openapi.utils import get_openapi
-import html
-from typing import Optional
 
 query_handler = QueryHandler()
-app = FastAPI(docs_url='/therapy', openapi_url='/therapy/openapi.json')
+app = FastAPI(docs_url="/therapy", openapi_url="/therapy/openapi.json")
 
 
-def custom_openapi():
+def custom_openapi() -> Dict:
     """Generate custom fields for OpenAPI response"""
     if app.openapi_schema:
         return app.openapi_schema
@@ -22,11 +24,11 @@ def custom_openapi():
         description="Normalize drugs and other therapy terms.",
         routes=app.routes
     )
-#    openapi_schema['info']['license'] = {  # TODO
+#    openapi_schema["info"]["license"] = {  # TODO
 #        "name": "Name-of-license",
 #        "url": "http://www.to-be-determined.com"
 #    }
-    openapi_schema['info']['contact'] = {  # TODO
+    openapi_schema["info"]["contact"] = {  # TODO
         "name": "Alex H. Wagner",
         "email": "Alex.Wagner@nationwidechildrens.org"
     }
@@ -34,27 +36,24 @@ def custom_openapi():
     return app.openapi_schema
 
 
-app.openapi = custom_openapi
+app.openapi = custom_openapi  # type: ignore
 
 # endpoint description text
-get_matches_summary = ("Given query, provide highest matches from "
-                       "each source.")
+get_matches_summary = ("Given query, provide highest matches from each source.")
 response_descr = "A response to a validly-formed query."
 q_descr = "Therapy to search."
-keyed_descr = ("If true, return response as key-value pairs of "
-               "sources to source matches.")
-incl_descr = ("Comma-separated list of source names to include in "
-              "response. Will exclude all other sources. Will return HTTP "
-              "status code 422: Unprocessable Entity if both 'incl' and "
-              "'excl' parameters are given.")
-excl_descr = ("Comma-separated list of source names to exclude in "
-              "response. Will include all other sources. Will return HTTP "
-              "status code 422: Unprocessable Entity if both 'incl' and"
-              "'excl' parameters are given.")
-search_description = ("For each source, return strongest-match concepts "
-                      "for query string provided by user")
-normalize_description = ("Return merged strongest-match concept for query "
-                         "string provided by user.")
+keyed_descr = ("If true, return response as key-value pairs of sources to source "
+               "matches.")
+incl_descr = ("Comma-separated list of source names to include in response. Will "
+              "exclude all other sources. Will return HTTP status code 422: "
+              'Unprocessable Entity if both "incl" and "excl" parameters are given.')
+excl_descr = ("Comma-separated list of source names to exclude in response. Will "
+              "include all other sources. Will return HTTP status code 422: "
+              'Unprocessable Entity if both "incl" and "excl" parameters are given.')
+search_description = ("For each source, return strongest-match concepts for query "
+                      "string provided by user")
+normalize_description = ("Return merged strongest-match concept for query string "
+                         "provided by user.")
 
 
 @app.get("/therapy/search",
@@ -64,24 +63,24 @@ def custom_openapi():
          response_model=SearchService,
          description=search_description)
 def search(q: str = Query(..., description=q_descr),
-           keyed: Optional[bool] = Query(False, description=keyed_descr),
-           incl: Optional[str] = Query(None, description=incl_descr),
-           excl: Optional[str] = Query(None, description=excl_descr)):
+           keyed: bool = Query(False, description=keyed_descr),
+           incl: str = Query("", description=incl_descr),
+           excl: str = Query("", description=excl_descr)) -> Dict:
     """For each source, return strongest-match concepts for query string
     provided by user.
 
-        :param q: therapy search term
-        :param keyed: if true, response is structured as key/value pair of
-            sources to source match lists.
-        :param incl: comma-separated list of sources to include, with all
-            others excluded. Raises HTTPException if both `incl` and
-            `excl` are given.
-        :param excl: comma-separated list of sources exclude, with all others
-            included. Raises HTTPException if both `incl` and `excl` are given.
-        :returns: JSON response with matched records and source metadata
+    :param q: therapy search term
+    :param keyed: if true, response is structured as key/value pair of sources to
+        source match lists.
+    :param incl: comma-separated list of sources to include, with all others excluded.
+        Raises HTTPException if both `incl` and `excl` are given.
+    :param excl: comma-separated list of sources exclude, with all others included.
+        Raises HTTPException if both `incl` and `excl` are given.
+    :returns: JSON response with matched records and source metadata
     """
     try:
-        response = query_handler.search_sources(html.unescape(q), keyed=keyed,
+        response = query_handler.search_sources(html.unescape(q),
+                                                keyed=keyed,  # type: ignore
                                                 incl=incl, excl=excl)
     except InvalidParameterException as e:
         raise HTTPException(status_code=422, detail=str(e))
@@ -99,9 +98,9 @@ def search(q: str = Query(..., description=q_descr),
          response_description=merged_response_descr,
          response_model=NormalizationService,
          description=normalize_description)
-def normalize(q: str = Query(..., description=merged_q_descr)):
-    """Return merged strongest-match concept for query string provided by
-    user.
+def normalize(q: str = Query(..., description=merged_q_descr)) -> Dict:
+    """Normalize query or concept ID provided by user. Return strongest-match merged
+    concept.
 
     :param q: therapy search term
     """
diff --git a/therapy/query.py b/therapy/query.py
index 4b21b499..23cd938f 100644
--- a/therapy/query.py
+++ b/therapy/query.py
@@ -1,22 +1,23 @@
 """This module provides methods for handling queries."""
 import re
-from typing import List, Dict, Set, Optional, Tuple
-from .version import __version__
-from therapy import SOURCES, NAMESPACE_LOOKUP, \
-    PREFIX_LOOKUP, ITEM_TYPES
+from typing import Dict, Set, Optional, Tuple, Union, Any
+from urllib.parse import quote
+from datetime import datetime
+
 from uvicorn.config import logger
+from botocore.exceptions import ClientError
+
+from therapy import SOURCES, NAMESPACE_LOOKUP, PREFIX_LOOKUP, ITEM_TYPES
 from therapy.database import Database
 from therapy.schemas import Drug, SourceMeta, MatchType, ServiceMeta, \
     HasIndication, SourcePriority, SearchService, NormalizationService
-from botocore.exceptions import ClientError
-from urllib.parse import quote
-from datetime import datetime
+from .version import __version__
 
 
 class InvalidParameterException(Exception):
     """Exception for invalid parameter args provided by the user."""
 
-    def __init__(self, message):
+    def __init__(self, message: str) -> None:
         """Create new instance
 
         :param str message: string describing the nature of the error
@@ -25,11 +26,11 @@ def __init__(self, message):
 
 
 class QueryHandler:
-    """Class for normalizer management. Stores reference to database instance
-    and normalizes query input.
+    """Class for normalizer management. Stores reference to database instance and
+    normalizes query input.
     """
 
-    def __init__(self, db_url: str = '', db_region: str = 'us-east-2'):
+    def __init__(self, db_url: str = "", db_region: str = "us-east-2"):
         """Initialize Normalizer instance.
 
         :param str db_url: URL to database source.
@@ -37,20 +38,20 @@ def __init__(self, db_url: str = '', db_region: str = 'us-east-2'):
         """
         self.db = Database(db_url=db_url, region_name=db_region)
 
-    def _emit_warnings(self, query_str) -> Optional[Dict]:
+    def _emit_warnings(self, query_str: str) -> Optional[Dict]:
         """Emit warnings if query contains non breaking space characters.
 
         :param str query_str: query string
         :return: dict keying warning type to warning description
         """
         warnings = None
-        nbsp = re.search('\xa0|&nbsp;', query_str)
+        nbsp = re.search("\xa0|&nbsp;", query_str)
         if nbsp:
             warnings = {
-                'nbsp': 'Query contains non breaking space characters.'
+                "nbsp": "Query contains non breaking space characters."
             }
             logger.warning(
-                f'Query ({query_str}) contains non breaking space characters.'
+                f"Query ({query_str}) contains non breaking space characters."
             )
         return warnings
 
@@ -64,14 +65,13 @@ def _fetch_meta(self, src_name: str) -> SourceMeta:
             return self.db.cached_sources[src_name]
         else:
             try:
-                db_response = self.db.metadata.get_item(
-                    Key={'src_name': src_name}
-                )
-                response = SourceMeta(**db_response['Item'])
+                db_response = self.db.metadata.get_item(Key={"src_name": src_name})
+                response = SourceMeta(**db_response["Item"])
                 self.db.cached_sources[src_name] = response
                 return response
             except ClientError as e:
-                logger.error(e.response['Error']['Message'])
+                logger.error(e.response["Error"]["Message"])
+                raise Exception(f"Unable to retrieve metadata for {src_name}")
 
     def _add_record(self,
                     response: Dict[str, Dict],
@@ -79,34 +79,33 @@ def _add_record(self,
                     match_type: str) -> Tuple[Dict, str]:
         """Add individual record (i.e. Item in DynamoDB) to response object
 
-        :param Dict[str, Dict] response: in-progress response object to return
-            to client
+        :param Dict[str, Dict] response: in-progress response object
         :param Dict item: Item retrieved from DynamoDB
         :param MatchType match_type: type of query match
         :return: Tuple containing updated response object, and string
             containing name of the source of the match
         """
-        inds = item.get('fda_indication')
+        inds = item.get("fda_indication")
         if inds:
-            item['has_indication'] = [HasIndication(disease_id=i[0],
+            item["has_indication"] = [HasIndication(disease_id=i[0],
                                                     disease_label=i[1],
                                                     normalized_disease_id=i[2])
                                       for i in inds]
 
         drug = Drug(**item)
-        src_name = item['src_name']
+        src_name = item["src_name"]
 
-        matches = response['source_matches']
+        matches = response["source_matches"]
         if src_name not in matches.keys():
             pass
         elif matches[src_name] is None:
             matches[src_name] = {
-                'match_type': MatchType[match_type.upper()],
-                'records': [drug],
-                'source_meta_': self._fetch_meta(src_name)
+                "match_type": MatchType[match_type.upper()],
+                "records": [drug],
+                "source_meta_": self._fetch_meta(src_name)
             }
-        elif matches[src_name]['match_type'] == MatchType[match_type.upper()]:
-            matches[src_name]['records'].append(drug)
+        elif matches[src_name]["match_type"] == MatchType[match_type.upper()]:
+            matches[src_name]["records"].append(drug)
 
         return response, src_name
 
@@ -114,57 +113,53 @@ def _fetch_records(self,
                        response: Dict[str, Dict],
                        concept_ids: Set[str],
                        match_type: str) -> Tuple[Dict, Set]:
-        """Return matched Drug records as a structured response for a given
-        collection of concept IDs.
-
-        :param Dict[str, Dict] response: in-progress response object to return
-            to client.
-        :param List[str] concept_ids: List of concept IDs to build from.
-            Should be all lower-case.
-        :param str match_type: record should be assigned this type of
-            match.
-        :return: response Dict with records filled in via provided concept
-            IDs, and Set of source names of matched records
+        """Return matched Drug records as a structured response for a given collection
+        of concept IDs.
+
+        :param Dict[str, Dict] response: in-progress response object
+        :param List[str] concept_ids: List of concept IDs to build from.  Should be all
+            lower-case.
+        :param str match_type: record should be assigned this type of match.
+        :return: response Dict with records filled in via provided concept IDs, and Set
+            of source names of matched records
         """
         matched_sources = set()
         for concept_id in concept_ids:
             try:
                 match = self.db.get_record_by_id(concept_id.lower(),
                                                  case_sensitive=False)
+                assert match, f"Unable to retrieve record for {concept_id}"
                 (response, src) = self._add_record(response, match, match_type)
                 matched_sources.add(src)
             except ClientError as e:
-                logger.error(e.response['Error']['Message'])
+                logger.error(e.response["Error"]["Message"])
 
         return response, matched_sources
 
-    def _fill_no_matches(self, resp: Dict[str, Dict]) -> Dict:
+    def _fill_no_matches(self, resp: Dict[str, Any]) -> Dict:
         """Fill all empty source_matches slots with NO_MATCH results.
 
         :param Dict[str, Dict] resp: incoming response object
-        :return: response object with empty source slots filled with
-                NO_MATCH results and corresponding source metadata
+        :return: response object with empty source slots filled with NO_MATCH results
+            and corresponding source metadata
         """
-        for src_name in resp['source_matches'].keys():
-            if resp['source_matches'][src_name] is None:
-                resp['source_matches'][src_name] = {
-                    'match_type': MatchType.NO_MATCH,
-                    'records': [],
-                    'source_meta_': self._fetch_meta(src_name)
+        for src_name in resp["source_matches"].keys():
+            if resp["source_matches"][src_name] is None:
+                resp["source_matches"][src_name] = {
+                    "match_type": MatchType.NO_MATCH,
+                    "records": [],
+                    "source_meta_": self._fetch_meta(src_name)
                 }
         return resp
 
-    def _check_concept_id(self,
-                          query: str,
-                          resp: Dict,
+    def _check_concept_id(self, query: str, resp: Dict,
                           sources: Set[str]) -> Tuple[Dict, Set]:
         """Check query for concept ID match. Should only find 0 or 1 matches.
 
         :param str query: search string
         :param Dict resp: in-progress response object to return to client
         :param Set[str] sources: remaining unmatched sources
-        :return: Tuple with updated resp object and updated set of unmatched
-            sources
+        :return: Tuple with updated resp object and updated set of unmatched sources
         """
         concept_id_items = []
         if [p for p in PREFIX_LOOKUP.keys() if query.startswith(p)]:
@@ -173,7 +168,7 @@ def _check_concept_id(self,
                 concept_id_items.append(record)
         for prefix in [p for p in NAMESPACE_LOOKUP.keys()
                        if query.startswith(p)]:
-            concept_id = f'{NAMESPACE_LOOKUP[prefix]}:{query}'
+            concept_id = f"{NAMESPACE_LOOKUP[prefix]}:{query}"
             id_lookup = self.db.get_record_by_id(concept_id, False)
             if id_lookup:
                 concept_id_items.append(id_lookup)
@@ -183,44 +178,41 @@ def _check_concept_id(self,
             sources = sources - {src_name}
         return resp, sources
 
-    def _check_match_type(self,
-                          query: str,
-                          resp: Dict,
-                          sources: Set[str],
+    def _check_match_type(self, query: str, resp: Dict, sources: Set[str],
                           match_type: str) -> Tuple[Dict, Set]:
         """Check query for selected match type.
         :param str query: search string
         :param Dict resp: in-progress response object to return to client
         :param Set[str] sources: remaining unmatched sources
         :param str match_type: Match type to check for. Should be one of
-            {'trade_name', 'label', 'alias', 'xref', 'associated_with'}
+        {'trade_name', 'label', 'alias', 'xref', 'associated_with'}
         :return: Tuple with updated resp object and updated set of unmatched
-                 sources
+            sources
         """
         matches = self.db.get_records_by_type(query, match_type)
         if matches:
-            concept_ids = {i['concept_id'] for i in matches}
-            (resp, matched_srcs) = self._fetch_records(resp, concept_ids,
-                                                       match_type)
+            concept_ids = {i["concept_id"] for i in matches}
+            (resp, matched_srcs) = self._fetch_records(resp, concept_ids, match_type)
             sources = sources - matched_srcs
         return resp, sources
 
     def _response_keyed(self, query: str, sources: Set[str]) -> Dict:
-        """Return response as dict where key is source name and value
-        is a list of records. Corresponds to `keyed=true` API parameter.
+        """Return response as dict where key is source name and value is a list of
+        records. Corresponds to `keyed=true` API parameter.
 
         :param str query: string to match against
         :param Set[str] sources: sources to match from
+
         :return: completed response object to return to client
         """
-        response = {
-            'query': query,
-            'warnings': self._emit_warnings(query),
-            'source_matches': {
+        response: Dict[str, Union[None, str, Dict]] = {
+            "query": query,
+            "warnings": self._emit_warnings(query),
+            "source_matches": {
                 source: None for source in sources
             }
         }
-        if query == '':
+        if query == "":
             response = self._fill_no_matches(response)
             return response
         query = query.lower()
@@ -231,17 +223,17 @@ def _response_keyed(self, query: str, sources: Set[str]) -> Dict:
             return response
 
         for match_type in ITEM_TYPES.values():
-            (response, sources) = self._check_match_type(query, response,
-                                                         sources, match_type)
+            (response, sources) = self._check_match_type(query, response, sources,
+                                                         match_type)
             if len(sources) == 0:
                 return response
 
         # remaining sources get no match
         return self._fill_no_matches(response)
 
-    def _response_list(self, query: str, sources: List[str]) -> Dict:
-        """Return response as list, where the first key-value in each item
-        is the source name. Corresponds to `keyed=false` API parameter.
+    def _response_list(self, query: str, sources: Set[str]) -> Dict:
+        """Return response as list, where the first key-value in each item is the source
+        name. Corresponds to `keyed=false` API parameter.
 
         :param str query: string to match against
         :param List[str] sources: sources to match from
@@ -249,36 +241,35 @@ def _response_list(self, query: str, sources: List[str]) -> Dict:
         """
         response_dict = self._response_keyed(query, sources)
         source_list = []
-        for src_name in response_dict['source_matches'].keys():
-            src = {
-                "source": src_name,
-            }
-            to_merge = response_dict['source_matches'][src_name]
+        for src_name in response_dict["source_matches"].keys():
+            src = {"source": src_name}
+            to_merge = response_dict["source_matches"][src_name]
             src.update(to_merge)
 
             source_list.append(src)
-        response_dict['source_matches'] = source_list
+        response_dict["source_matches"] = source_list
 
         return response_dict
 
-    def search_sources(self, query_str, keyed=False, incl='', excl='') -> Dict:
+    def search_sources(self, query_str: str, keyed: bool = False,
+                       incl: str = "", excl: str = "") -> Dict:
         """Fetch normalized therapy objects.
 
         :param str query_str: query, a string, to search for
-        :param bool keyed: if true, return response as dict keying source names
-            to source objects; otherwise, return list of source objects
-        :param str incl: str containing comma-separated names of sources to
-            use. Will exclude all other sources. Case-insensitive.
-        :param str excl: str containing comma-separated names of source to
-            exclude. Will include all other source. Case-insensitive.
+        :param bool keyed: if true, return response as dict keying source names to
+            source objects; otherwise, return list of source objects
+        :param str incl: str containing comma-separated names of sources to use. Will
+            exclude all other sources. Case-insensitive.
+        :param str excl: str containing comma-separated names of source to exclude.
+            Will include all other source. Case-insensitive.
         :return: dict containing all matches found in sources.
         :rtype: dict
-        :raises InvalidParameterException: if both incl and excl args are
-            provided, or if invalid source names are given.
+        :raises InvalidParameterException: if both incl and excl args are provided, or
+            if invalid source names are given.
         """
         sources = dict()
         for k, v in SOURCES.items():
-            if self.db.metadata.get_item(Key={'src_name': v}).get('Item'):
+            if self.db.metadata.get_item(Key={"src_name": v}).get("Item"):
                 sources[k] = v
         if not incl and not excl:
             query_sources = set(sources.values())
@@ -286,7 +277,7 @@ def search_sources(self, query_str, keyed=False, incl='', excl='') -> Dict:
             detail = "Cannot request both source inclusions and exclusions."
             raise InvalidParameterException(detail)
         elif incl:
-            req_sources = [n.strip() for n in incl.split(',')]
+            req_sources = [n.strip() for n in incl.split(",")]
             invalid_sources = []
             query_sources = set()
             for source in req_sources:
@@ -298,7 +289,7 @@ def search_sources(self, query_str, keyed=False, incl='', excl='') -> Dict:
                 detail = f"Invalid source name(s): {invalid_sources}"
                 raise InvalidParameterException(detail)
         else:
-            req_exclusions = [n.strip() for n in excl.lower().split(',')]
+            req_exclusions = [n.strip() for n in excl.lower().split(",")]
             req_excl_dict = {r.lower(): r for r in req_exclusions}
             invalid_sources = []
             query_sources = set()
@@ -319,7 +310,7 @@ def search_sources(self, query_str, keyed=False, incl='', excl='') -> Dict:
         else:
             response = self._response_list(query_str, query_sources)
 
-        response['service_meta_'] = ServiceMeta(
+        response["service_meta_"] = ServiceMeta(
             version=__version__,
             response_datetime=datetime.now(),
             url="https://github.com/cancervariants/therapy-normalization"
@@ -333,14 +324,14 @@ def _add_merged_meta(self, response: Dict) -> Dict:
         :return: completed response object.
         """
         sources_meta = {}
-        vod = response['therapy_descriptor']
-        ids = [vod['therapy_id']] + vod.get('xrefs', [])
+        vod = response["therapy_descriptor"]
+        ids = [vod["therapy_id"]] + vod.get("xrefs", [])
         for concept_id in ids:
-            prefix = concept_id.split(':')[0]
+            prefix = concept_id.split(":")[0]
             src_name = PREFIX_LOOKUP[prefix.lower()]
             if src_name not in sources_meta:
                 sources_meta[src_name] = self._fetch_meta(src_name)
-        response['source_meta_'] = sources_meta
+        response["source_meta_"] = sources_meta
         return response
 
     def _record_order(self, record: Dict) -> Tuple[int, str]:
@@ -349,9 +340,9 @@ def _record_order(self, record: Dict) -> Tuple[int, str]:
         :param Dict record: individual record item in iterable to sort
         :return: tuple with rank value and concept ID
         """
-        src = record['src_name'].upper()
+        src = record["src_name"].upper()
         source_rank = SourcePriority[src]
-        return source_rank, record['concept_id']
+        return source_rank, record["concept_id"]
 
     def _add_vod(self, response: Dict, record: Dict, query: str,
                  match_type: MatchType) -> Dict:
@@ -363,30 +354,30 @@ def _add_vod(self, response: Dict, record: Dict, query: str,
         :return: completed response object ready to return to user
         """
         vod = {
-            'id': f'normalize.therapy:{quote(query.strip())}',
-            'type': 'TherapyDescriptor',
-            'therapy_id': record['concept_id'],
-            'label': record.get('label'),
-            'extensions': [],
+            "id": f"normalize.therapy:{quote(query.strip())}",
+            "type": "TherapyDescriptor",
+            "therapy_id": record["concept_id"],
+            "label": record.get("label"),
+            "extensions": [],
         }
 
-        if 'xrefs' in record:
-            vod["xrefs"] = record['xrefs']
-        if 'aliases' in record:
-            vod['alternate_labels'] = record['aliases']
+        if "xrefs" in record:
+            vod["xrefs"] = record["xrefs"]
+        if "aliases" in record:
+            vod["alternate_labels"] = record["aliases"]
 
-        if any(filter(lambda f: f in record, ('approval_status',
-                                              'approval_year',
-                                              'fda_indication'))):
-            fda_approv = {
+        if any(filter(lambda f: f in record, ("approval_status",
+                                              "approval_year",
+                                              "fda_indication"))):
+            fda_approv: Dict[str, Union[str, Dict]] = {
                 "name": "fda_approval",
-                "value": {}
             }
-            for field in ('approval_status', 'approval_year'):
+            fda_approv_value: Dict = {}
+            for field in ("approval_status", "approval_year"):
                 value = record.get(field)
                 if value:
-                    fda_approv['value'][field] = value
-            inds = record.get('fda_indication', [])
+                    fda_approv_value[field] = value
+            inds = record.get("fda_indication", [])
             inds_list = []
             for ind in inds:
                 ind_obj = {
@@ -395,34 +386,36 @@ def _add_vod(self, response: Dict, record: Dict, query: str,
                     "label": ind[1]
                 }
                 if ind[2]:
-                    ind_obj['disease_id'] = ind[2]
+                    ind_obj["disease_id"] = ind[2]
                     inds_list.append(ind_obj)
                 else:
                     logger.warning(f"{ind[0]} has no disease ID")
             if inds_list:
-                fda_approv['value']['has_indication'] = inds_list
-            vod['extensions'].append(fda_approv)
+                fda_approv_value["has_indication"] = inds_list
+            fda_approv["value"] = fda_approv_value
+            vod["extensions"].append(fda_approv)
 
-        for field, name in (('trade_names', 'trade_names'),
-                            ('associated_with', 'associated_with')):
+        for field, name in (("trade_names", "trade_names"),
+                            ("associated_with", "associated_with")):
             values = record.get(field)
 
             if values:
-                vod['extensions'].append({
-                    'type': 'Extension',
-                    'name': name,
-                    'value': values
+                vod["extensions"].append({
+                    "type": "Extension",
+                    "name": name,
+                    "value": values
                 })
 
-        if not vod['extensions']:
-            del vod['extensions']
+        if not vod["extensions"]:
+            del vod["extensions"]
 
-        response['match_type'] = match_type
-        response['therapy_descriptor'] = vod
+        response["match_type"] = match_type
+        response["therapy_descriptor"] = vod
         response = self._add_merged_meta(response)
         return response
 
-    def _handle_failed_merge_ref(self, record, response, query) -> Dict:
+    def _handle_failed_merge_ref(self, record: Dict, response: Dict,
+                                 query: str) -> Dict:
         """Log + fill out response for a failed merge reference lookup.
 
         :param Dict record: record containing failed merge_ref
@@ -432,7 +425,7 @@ def _handle_failed_merge_ref(self, record, response, query) -> Dict:
         """
         logger.error(f"Merge ref lookup failed for ref {record['merge_ref']} "
                      f"in record {record['concept_id']} from query `{query}`")
-        response['match_type'] = MatchType.NO_MATCH
+        response["match_type"] = MatchType.NO_MATCH
         return response
 
     def search_groups(self, query: str) -> Dict:
@@ -441,39 +434,35 @@ def search_groups(self, query: str) -> Dict:
         :param str query: string to search against
         """
         # prepare basic response
-        response = {
-            'query': query,
-            'warnings': self._emit_warnings(query),
-            'service_meta_': ServiceMeta(
+        response: Dict[str, Any] = {
+            "query": query,
+            "warnings": self._emit_warnings(query),
+            "service_meta_": ServiceMeta(
                 version=__version__,
                 response_datetime=datetime.now(),
-                url="https://github.com/cancervariants/therapy-normalization"  # noqa: E501
+                url="https://github.com/cancervariants/therapy-normalization"
             ).dict()
         }
-        if query == '':
-            response['match_type'] = MatchType.NO_MATCH
+        if query == "":
+            response["match_type"] = MatchType.NO_MATCH.value
             return response
         query_str = query.lower().strip()
 
         # check merged concept ID match
-        record = self.db.get_record_by_id(query_str, case_sensitive=False,
-                                          merge=True)
+        record = self.db.get_record_by_id(query_str, case_sensitive=False, merge=True)
         if record:
             return self._add_vod(response, record, query, MatchType.CONCEPT_ID)
 
         # check concept ID match
         record = self.db.get_record_by_id(query_str, case_sensitive=False)
         if record:
-            merge_ref = record.get('merge_ref')
+            merge_ref = record.get("merge_ref")
             if not merge_ref:
-                return self._add_vod(response, record, query,
-                                     MatchType.CONCEPT_ID)
-            merge = self.db.get_record_by_id(merge_ref,
-                                             case_sensitive=False,
+                return self._add_vod(response, record, query, MatchType.CONCEPT_ID)
+            merge = self.db.get_record_by_id(merge_ref, case_sensitive=False,
                                              merge=True)
             if merge is None:
-                return self._handle_failed_merge_ref(record, response,
-                                                     query_str)
+                return self._handle_failed_merge_ref(record, response, query_str)
             else:
                 return self._add_vod(response, merge, query,
                                      MatchType.CONCEPT_ID)
@@ -483,19 +472,20 @@ def search_groups(self, query: str) -> Dict:
             # get matches list for match tier
             matching_refs = self.db.get_records_by_type(query_str, match_type)
             matching_records = \
-                [self.db.get_record_by_id(m['concept_id'], False)
+                [self.db.get_record_by_id(m["concept_id"], False)
                  for m in matching_refs]
-            matching_records.sort(key=self._record_order)
+            matching_records.sort(key=self._record_order)  # type: ignore
 
             # attempt merge ref resolution until successful
             for match in matching_records:
-                record = self.db.get_record_by_id(match['concept_id'], False)
+                assert match is not None
+                record = self.db.get_record_by_id(match["concept_id"], False)
                 if record:
-                    merge_ref = record.get('merge_ref')
+                    merge_ref = record.get("merge_ref")
                     if not merge_ref:
                         return self._add_vod(response, record, query,
                                              MatchType[match_type.upper()])
-                    merge = self.db.get_record_by_id(record['merge_ref'],
+                    merge = self.db.get_record_by_id(record["merge_ref"],
                                                      case_sensitive=False,
                                                      merge=True)
                     if merge is None:
@@ -505,6 +495,6 @@ def search_groups(self, query: str) -> Dict:
                         return self._add_vod(response, merge, query,
                                              MatchType[match_type.upper()])
 
-        if not matching_records:
-            response['match_type'] = MatchType.NO_MATCH
+        if not matching_records:  # type: ignore
+            response["match_type"] = MatchType.NO_MATCH.value
         return NormalizationService(**response).dict()
diff --git a/therapy/schemas.py b/therapy/schemas.py
index fd6365f0..d0fe752d 100644
--- a/therapy/schemas.py
+++ b/therapy/schemas.py
@@ -1,31 +1,14 @@
-"""This module contains data models for representing VICC normalized
-therapy records.
-"""
-from typing import List, Optional, Dict, Union, Any, Type
-from pydantic import BaseModel, StrictBool
+"""This module contains data models for representing VICC therapy records."""
+from typing import List, Optional, Dict, Union, Any, Type, Set
 from enum import Enum, IntEnum
 from datetime import datetime
-from ga4gh.vrsatile.pydantic.vrsatile_model import ValueObjectDescriptor
-
-
-class Therapy(BaseModel):
-    """A procedure or substance used in the treatment of a disease."""
 
-    label: str
-    concept_id: str
-    aliases: Optional[List[str]] = []
-    xrefs: Optional[List[str]] = []
-    associated_with: Optional[List[str]] = []
+from ga4gh.vrsatile.pydantic.vrsatile_model import ValueObjectDescriptor
+from pydantic import BaseModel, StrictBool
 
-    class Config:
-        """Configure model"""
 
-        @staticmethod
-        def schema_extra(schema: Dict[str, Any],
-                         model: Type['Therapy']) -> None:
-            """Configure OpenAPI schema"""
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
+# Working structure for object in preparation for upload to DB
+RecordParams = Dict[str, Union[List, Set, str, Dict[str, Any]]]
 
 
 class ApprovalStatus(str, Enum):
@@ -58,13 +41,13 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['Drug']) -> None:
+                         model: Type["Drug"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = [
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = [
                 {
                     "disease_id": "hemonc:671",
                     "disease_label": "Testicular cancer",
@@ -83,27 +66,31 @@ def schema_extra(schema: Dict[str, Any],
             ]
 
 
-class Drug(Therapy):
+class Drug(BaseModel):
     """A pharmacologic substance used to treat a medical condition."""
 
+    concept_id: str
+    label: Optional[str] = None
+    aliases: Optional[List[str]] = []
+    trade_names: Optional[List[str]] = []
+    xrefs: Optional[List[str]] = []
+    associated_with: Optional[List[str]] = []
     approval_status: Optional[ApprovalStatus] = None
     approval_year: Optional[List[int]] = []
     has_indication: Optional[List[HasIndication]] = []
-    trade_names: Optional[List[str]] = []
-    label: Optional[str] = None
 
     class Config:
         """Configure Drug class"""
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['Drug']) -> None:
+                         model: Type["Drug"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
                 "label": "CISPLATIN",
                 "concept_id": "chembl:CHEMBL11359",
                 "aliases": [
@@ -127,14 +114,6 @@ def schema_extra(schema: Dict[str, Any],
             }
 
 
-class DrugGroup(Therapy):
-    """A grouping of drugs based on common pharmacological attributes."""
-
-    description: str
-    type_identifier: str
-    drugs: List[Drug]
-
-
 class MatchType(IntEnum):
     """Define string constraints for use in Match Type attributes.
 
@@ -242,11 +221,11 @@ class ItemTypes(str, Enum):
     """Item types used in DynamoDB."""
 
     # Must be in descending MatchType order.
-    LABEL = 'label'
-    TRADE_NAMES = 'trade_name'
-    ALIASES = 'alias'
-    XREFS = 'xref'
-    ASSOCIATED_WITH = 'associated_with'
+    LABEL = "label"
+    TRADE_NAMES = "trade_name"
+    ALIASES = "alias"
+    XREFS = "xref"
+    ASSOCIATED_WITH = "associated_with"
 
 
 class SourceMeta(BaseModel):
@@ -264,24 +243,24 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['SourceMeta']) -> None:
+                         model: Type["SourceMeta"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
-                'data_license': 'CC BY-SA 3.0',
-                'data_license_url':
-                    'https://creativecommons.org/licenses/by-sa/3.0/',
-                'version': '27',
-                'data_url':
-                    'http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/',  # noqa: E501
-                'rdp_url': 'http://reusabledata.org/chembl.html',
-                'data_license_attributes': {
-                    'non_commercial': False,
-                    'share_alike': True,
-                    'attribution': True
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
+                "data_license": "CC BY-SA 3.0",
+                "data_license_url":
+                    "https://creativecommons.org/licenses/by-sa/3.0/",
+                "version": "27",
+                "data_url":
+                    "http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/",  # noqa: E501
+                "rdp_url": "http://reusabledata.org/chembl.html",
+                "data_license_attributes": {
+                    "non_commercial": False,
+                    "share_alike": True,
+                    "attribution": True
                 }
             }
 
@@ -300,27 +279,27 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['MatchesKeyed']) -> None:
+                         model: Type["MatchesKeyed"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
-                'match_type': 0,
-                'records': [],
-                'source_meta_': {
-                    'data_license': 'CC BY-SA 3.0',
-                    'data_license_url':
-                        'https://creativecommons.org/licenses/by-sa/3.0/',
-                    'version': '27',
-                    'data_url':
-                        'http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/',  # noqa: E501
-                    'rdp_url': 'http://reusabledata.org/chembl.html',
-                    'data_license_attributes': {
-                        'non_commercial': False,
-                        'share_alike': True,
-                        'attribution': True
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
+                "match_type": 0,
+                "records": [],
+                "source_meta_": {
+                    "data_license": "CC BY-SA 3.0",
+                    "data_license_url":
+                        "https://creativecommons.org/licenses/by-sa/3.0/",
+                    "version": "27",
+                    "data_url":
+                        "http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/",  # noqa: E501
+                    "rdp_url": "http://reusabledata.org/chembl.html",
+                    "data_license_attributes": {
+                        "non_commercial": False,
+                        "share_alike": True,
+                        "attribution": True
                     }
                 },
             }
@@ -341,28 +320,28 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['MatchesListed']) -> None:
+                         model: Type["MatchesListed"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
-                'normalizer': 'ChEMBL',
-                'match_type': 0,
-                'records': [],
-                'source_meta_': {
-                    'data_license': 'CC BY-SA 3.0',
-                    'data_license_url':
-                        'https://creativecommons.org/licenses/by-sa/3.0/',
-                    'version': '27',
-                    'data_url':
-                        'http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/',  # noqa: E501
-                    'rdp_url': 'http://reusabledata.org/chembl.html',
-                    'data_license_attributes': {
-                        'non_commercial': False,
-                        'share_alike': True,
-                        'attribution': True
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
+                "normalizer": "ChEMBL",
+                "match_type": 0,
+                "records": [],
+                "source_meta_": {
+                    "data_license": "CC BY-SA 3.0",
+                    "data_license_url":
+                        "https://creativecommons.org/licenses/by-sa/3.0/",
+                    "version": "27",
+                    "data_url":
+                        "http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/",  # noqa: E501
+                    "rdp_url": "http://reusabledata.org/chembl.html",
+                    "data_license_attributes": {
+                        "non_commercial": False,
+                        "share_alike": True,
+                        "attribution": True
                     }
                 },
             }
@@ -389,17 +368,17 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['SourceMeta']) -> None:
+                         model: Type["SourceMeta"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
-                'name': 'thera-py',
-                'version': '0.1.0',
-                'response_datetime': '2021-04-05T16:44:15.367831',
-                'url': 'https://github.com/cancervariants/therapy-normalization'  # noqa: E501
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
+                "name": "thera-py",
+                "version": "0.1.0",
+                "response_datetime": "2021-04-05T16:44:15.367831",
+                "url": "https://github.com/cancervariants/therapy-normalization"
             }
 
 
@@ -418,13 +397,13 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['NormalizationService']) -> None:
+                         model: Type["NormalizationService"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
                 "query": "cisplatin",
                 "warnings": None,
                 "match_type": 80,
@@ -533,10 +512,10 @@ def schema_extra(schema: Dict[str, Any],
                     }
                 },
                 "service_meta_": {
-                    'name': 'thera-py',
-                    'version': '0.1.0',
-                    'response_datetime': '2021-04-05T16:44:15.367831',
-                    'url': 'https://github.com/cancervariants/therapy-normalization'  # noqa: E501
+                    "name": "thera-py",
+                    "version": "0.1.0",
+                    "response_datetime": "2021-04-05T16:44:15.367831",
+                    "url": "https://github.com/cancervariants/therapy-normalization"
                 }
             }
 
@@ -554,13 +533,13 @@ class Config:
 
         @staticmethod
         def schema_extra(schema: Dict[str, Any],
-                         model: Type['SearchService']) -> None:
+                         model: Type["SearchService"]) -> None:
             """Configure OpenAPI schema"""
-            if 'title' in schema.keys():
-                schema.pop('title', None)
-            for prop in schema.get('properties', {}).values():
-                prop.pop('title', None)
-            schema['example'] = {
+            if "title" in schema.keys():
+                schema.pop("title", None)
+            for prop in schema.get("properties", {}).values():
+                prop.pop("title", None)
+            schema["example"] = {
                 "query": "cisplatin",
                 "warnings": None,
                 "source_matches": [
@@ -585,7 +564,7 @@ def schema_extra(schema: Dict[str, Any],
                             "data_license": "custom",
                             "data_license_url": "https://www.nlm.nih.gov/databases/download/terms_and_conditions.html",  # noqa: E501
                             "version": "20210204",
-                            "data_url": "ftp://ftp.nlm.nih.gov/nlmdata/.chemidlease/",  # noqa: E501
+                            "data_url": "ftp://ftp.nlm.nih.gov/nlmdata/.chemidlease/",
                             "rdp_url": None,
                             "data_license_attributes": {
                                 "non_commercial": False,
@@ -725,9 +704,9 @@ def schema_extra(schema: Dict[str, Any],
                     }
                 ],
                 "service_meta_": {
-                    'name': 'thera-py',
-                    'version': '0.1.0',
-                    'response_datetime': '2021-04-05T16:44:15.367831',
-                    'url': 'https://github.com/cancervariants/therapy-normalization'  # noqa: E501
+                    "name": "thera-py",
+                    "version": "0.1.0",
+                    "response_datetime": "2021-04-05T16:44:15.367831",
+                    "url": "https://github.com/cancervariants/therapy-normalization"
                 }
             }
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..021ed10a
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,39 @@
+[tox]
+isolated_build = True
+envlist =
+    {py38,py39}-{test,lint,mypy}
+skipsdist = False
+
+[testenv]
+deps =
+    test: pytest
+    test: pytest-cov
+    lint: flake8
+    lint: flake8-docstrings
+    lint: flake8-quotes
+    lint: flake8-import-order
+    lint: flake8-annotations
+    mypy: mypy
+    mypy: types-PyYAML
+    mypy: types-requests
+setenv =
+    test: PY_IGNORE_IMPORTMISMATCH=1
+commands =
+    test: python3 -m pytest {posargs}
+    lint: flake8 therapy/ tests/ setup.py
+    mypy: mypy --ignore-missing-imports therapy/
+
+[testenv:githubactions]
+description="GitHub Actions-specific testing environment"
+setenv =
+    PY_IGNORE_IMPORTMISMATCH=1
+    TEST=TRUE
+    THERAPY_NORM_DB_URL=http://localhost:8001
+passenv =
+    AWS_ACCESS_KEY_ID
+    AWS_SECRET_ACCESS_KEY
+    AWS_DEFAULT_REGION
+    AWS_DEFAULT_OUTPUT
+commands =
+    {toxinidir}/tests/scripts/dynamodb_build.bash
+    pytest {posargs}


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 2cc07957..6144b0c3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,46 +1,46 @@
 """Pytest test config tools."""
-from therapy.schemas import Drug, MatchType
-from therapy.database import Database
 import os
 from typing import Dict, Any, Optional, List
 import json
-import pytest
 from pathlib import Path
 
+import pytest
+
+from therapy.schemas import Drug, MatchType
+from therapy.database import Database
+
 TEST_ROOT = Path(__file__).resolve().parents[1]
 
 
-@pytest.fixture(scope='module', autouse=True)
+@pytest.fixture(scope="module", autouse=True)
 def db():
     """Create a DynamoDB test fixture."""
 
     class DB:
         def __init__(self):
             self.db = Database()
-            if os.environ.get('TEST') is not None:
+            if os.environ.get("TEST") is not None:
                 self.load_test_data()
 
-        def load_test_data(self):
-            with open(f'{TEST_ROOT}/tests/unit/'
-                      f'data/therapies.json', 'r') as f:
+        def load_test_data(self) -> None:
+            with open(f"{TEST_ROOT}/tests/unit/"
+                      f"data/therapies.json", "r") as f:
                 therapies = json.load(f)
                 with self.db.therapies.batch_writer() as batch:
                     for therapy in therapies:
                         batch.put_item(Item=therapy)
-                f.close()
 
-            with open(f'{TEST_ROOT}/tests/unit/'
-                      f'data/metadata.json', 'r') as f:
+            with open(f"{TEST_ROOT}/tests/unit/"
+                      f"data/metadata.json", "r") as f:
                 metadata = json.load(f)
                 with self.db.metadata.batch_writer() as batch:
                     for m in metadata:
                         batch.put_item(Item=m)
-                f.close()
 
     return DB().db
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def mock_database():
     """Return MockDatabase object."""
 
@@ -48,22 +48,22 @@ class MockDatabase(Database):
         """Mock database object to use in test cases."""
 
         def __init__(self):
-            """Initialize mock database object. This class's method's shadow the
-            actual Database class methods.
+            """Initialize mock database object. This class's methods shadow the actual
+            Database class methods.
 
             `self.records` loads preexisting DB items.
-            `self.added_records` stores add record requests, with the
-            concept_id as the key and the complete record as the value.
-            `self.updates` stores update requests, with the concept_id as the
-            key, and a dict of {new_attribute: new_value} as the value.
+            `self.added_records` stores add record requests, with the concept_id as the
+            key and the complete record as the value.
+            `self.updates` stores update requests, with the concept_id as the key, and
+            a dict of {new_attribute: new_value} as the value.
             """
-            infile = TEST_ROOT / 'tests' / 'unit' / 'data' / 'therapies.json'
+            infile = TEST_ROOT / "tests" / "unit" / "data" / "therapies.json"
             self.records = {}
-            with open(infile, 'r') as f:
+            with open(infile, "r") as f:
                 records_json = json.load(f)
             for record in records_json:
-                label_and_type = record['label_and_type']
-                concept_id = record['concept_id']
+                label_and_type = record["label_and_type"]
+                concept_id = record["concept_id"]
                 if self.records.get(label_and_type):
                     self.records[label_and_type][concept_id] = record
                 else:
@@ -71,14 +71,14 @@ def __init__(self):
             self.added_records: Dict[str, Dict[Any, Any]] = {}
             self.updates: Dict[str, Dict[Any, Any]] = {}
 
-            meta = TEST_ROOT / 'tests' / 'unit' / 'data' / 'metadata.json'
-            with open(meta, 'r') as f:
+            meta = TEST_ROOT / "tests" / "unit" / "data" / "metadata.json"
+            with open(meta, "r") as f:
                 meta_json = json.load(f)
             self.cached_sources = {}
             for src in meta_json:
-                name = src['src_name']
+                name = src["src_name"]
                 self.cached_sources[name] = src
-                del self.cached_sources[name]['src_name']
+                del self.cached_sources[name]["src_name"]
 
         def get_record_by_id(self, record_id: str,
                              case_sensitive: bool = True,
@@ -86,21 +86,21 @@ def get_record_by_id(self, record_id: str,
             """Fetch record corresponding to provided concept ID.
 
             :param str record_id: concept ID for therapy record
-            :param bool case_sensitive: if true, performs exact lookup, which
-                is more efficient. Otherwise, performs filter operation, which
-                doesn't require correct casing.
+            :param bool case_sensitive: if true, performs exact lookup, which is more
+                efficient. Otherwise, performs filter operation, which doesn"t require
+                correct casing.
             :param bool merge: if true, retrieve merged record
             :return: complete therapy record, if match is found; None otherwise
             """
             if merge:
-                label_and_type = f'{record_id.lower()}##merger'
+                label_and_type = f"{record_id.lower()}##merger"
                 record_lookup = self.records.get(label_and_type)
                 if record_lookup:
                     return list(record_lookup.values())[0].copy()
                 else:
                     return None
             else:
-                label_and_type = f'{record_id.lower()}##identity'
+                label_and_type = f"{record_id.lower()}##identity"
             record_lookup = self.records.get(label_and_type, None)
             if record_lookup:
                 if case_sensitive:
@@ -123,16 +123,16 @@ def get_records_by_type(self, query: str,
                 "associated_with"} (use get_record_by_id for concept ID lookup)
             :return: list of matching records. Empty if lookup fails.
             """
-            assert match_type in ('alias', 'trade_name', 'label', 'rx_brand',
-                                  'xref', 'associated_with')
-            label_and_type = f'{query}##{match_type.lower()}'
+            assert match_type in ("alias", "trade_name", "label", "rx_brand",
+                                  "xref", "associated_with")
+            label_and_type = f"{query}##{match_type.lower()}"
             records_lookup = self.records.get(label_and_type, None)
             if records_lookup:
                 return [v.copy() for v in records_lookup.values()]
             else:
                 return []
 
-        def add_record(self, record: Dict, record_type: str):
+        def add_record(self, record: Dict, record_type: str) -> None:
             """Store add record request sent to database.
 
             :param Dict record: record (of any type) to upload. Must include
@@ -140,7 +140,7 @@ def add_record(self, record: Dict, record_type: str):
                 concept_id must be correctly-cased.
             :param str record_type: ignored by this function
             """
-            self.added_records[record['concept_id']] = record
+            self.added_records[record["concept_id"]] = record
 
         def update_record(self, concept_id: str, attribute: str,
                           new_value: Any):
@@ -150,31 +150,31 @@ def update_record(self, concept_id: str, attribute: str,
             :param str attribute: name of field to update
             :param str new_value: new value
             """
-            assert f'{concept_id.lower()}##identity' in self.records
+            assert f"{concept_id.lower()}##identity" in self.records
             self.updates[concept_id] = {attribute: new_value}
 
     return MockDatabase
 
 
-def compare_records(actual: Drug, fixture: Drug):
+def compare_records(actual: Dict, fixt: Drug):
     """Check that identity records are identical."""
-    fixture = fixture.dict()
-    assert actual['concept_id'] == fixture['concept_id']
-    assert actual['label'] == fixture['label']
-    assert set(actual['aliases']) == set(fixture['aliases'])
-    assert set(actual['trade_names']) == set(fixture['trade_names'])
-    assert set(actual['xrefs']) == set(fixture['xrefs'])
-    assert set(actual['associated_with']) == set(fixture['associated_with'])
-    if actual['approval_status'] or fixture['approval_status']:
-        assert actual['approval_status'] == fixture['approval_status']
-    if actual['approval_year'] or fixture['approval_year']:
-        assert set(actual['approval_year']) == set(fixture['approval_year'])
-    if actual['has_indication'] or fixture['has_indication']:
-        actual_inds = actual['has_indication'].copy()
-        fixture_inds = fixture['has_indication'].copy()
+    fixture = fixt.dict()
+    assert actual["concept_id"] == fixture["concept_id"]
+    assert actual["label"] == fixture["label"]
+    assert set(actual["aliases"]) == set(fixture["aliases"])
+    assert set(actual["trade_names"]) == set(fixture["trade_names"])
+    assert set(actual["xrefs"]) == set(fixture["xrefs"])
+    assert set(actual["associated_with"]) == set(fixture["associated_with"])
+    if actual["approval_status"] or fixture["approval_status"]:
+        assert actual["approval_status"] == fixture["approval_status"]
+    if actual["approval_year"] or fixture["approval_year"]:
+        assert set(actual["approval_year"]) == set(fixture["approval_year"])
+    if actual["has_indication"] or fixture["has_indication"]:
+        actual_inds = actual["has_indication"].copy()
+        fixture_inds = fixture["has_indication"].copy()
         assert len(actual_inds) == len(fixture_inds)
-        actual_inds.sort(key=lambda x: x['disease_id'])
-        fixture_inds.sort(key=lambda x: x['disease_id'])
+        actual_inds.sort(key=lambda x: x["disease_id"])
+        fixture_inds.sort(key=lambda x: x["disease_id"])
         for i in range(len(actual_inds)):
             assert actual_inds[i] == fixture_inds[i]
 
@@ -183,17 +183,15 @@ def compare_response(response: Dict, match_type: MatchType,
                      fixture: Drug = None, fixture_list: List[Drug] = None,
                      num_records: int = 0):
     """Check that test response is correct. Only 1 of {fixture, fixture_list}
-    should be passed as arguments. num_records should only be passed with
-    fixture_list.
+    should be passed as arguments. num_records should only be passed with fixture_list.
 
     :param Dict response: response object returned by QueryHandler
     :param MatchType match_type: expected match type
     :param Drug fixture: single Drug object to match response against
-    :param List[Drug] fixture_list: multiple Drug objects to match response
-        against
-    :param int num_records: expected number of records in response. If not
-        given, tests for number of fixture Drugs given (ie, 1 for single
-        fixture and length of fixture_list otherwise)
+    :param List[Drug] fixture_list: multiple Drug objects to match response against
+    :param int num_records: expected number of records in response. If not given, tests
+        for number of fixture Drugs given (ie, 1 for single fixture and length of
+        fixture_list otherwise)
     """
     if fixture and fixture_list:
         raise Exception("Args provided for both `fixture` and `fixture_list`")
@@ -203,17 +201,17 @@ def compare_response(response: Dict, match_type: MatchType,
         raise Exception("`num_records` should only be given with "
                         "`fixture_list`.")
 
-    assert response['match_type'] == match_type
+    assert response["match_type"] == match_type
     if fixture:
-        assert len(response['records']) == 1
-        compare_records(response['records'][0], fixture)
+        assert len(response["records"]) == 1
+        compare_records(response["records"][0], fixture)
     elif fixture_list:
         if not num_records:
-            assert len(response['records']) == len(fixture_list)
+            assert len(response["records"]) == len(fixture_list)
         else:
-            assert len(response['records']) == num_records
+            assert len(response["records"]) == num_records
         for fixt in fixture_list:
-            for record in response['records']:
+            for record in response["records"]:
                 if fixt.concept_id == record.concept_id:
                     compare_records(record, fixt)
                     break
diff --git a/tests/scripts/dynamodb_build.bash b/tests/scripts/dynamodb_build.bash
new file mode 100755
index 00000000..ed93da77
--- /dev/null
+++ b/tests/scripts/dynamodb_build.bash
@@ -0,0 +1,14 @@
+#! /bin/bash
+# check if dynamodb process already on port 8001
+IN_USE=$(ps aux | grep -E 'DynamoDBLocal.jar.*-port 8001' | wc -l)
+if [ $IN_USE -lt 2 ]; then
+    if [ ! -d tests/scripts/dynamodb_local ]; then
+        mkdir tests/scripts/dynamodb_local
+    fi
+    cd tests/scripts/dynamodb_local
+    if [ ! -e DynamoDBLocal.jar ]; then
+        curl -O "https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.tar.gz"
+        tar -xvzf dynamodb_local_latest.tar.gz; rm dynamodb_local_latest.tar.gz
+    fi
+    java -jar DynamoDBLocal.jar -port 8001 &
+fi
diff --git a/tests/unit/data/metadata.json b/tests/unit/data/metadata.json
index dc595f16..12afa65b 100644
--- a/tests/unit/data/metadata.json
+++ b/tests/unit/data/metadata.json
@@ -27,8 +27,8 @@
     "src_name": "NCIt",
     "data_license": "CC BY 4.0",
     "data_license_url": "https://creativecommons.org/licenses/by/4.0/legalcode",
-    "version": "20.09d",
-    "data_url": "https://evs.nci.nih.gov/ftp1/NCI_Thesaurus/archive/2020/20.09d_Release/",
+    "version": "21.08e",
+    "data_url": "https://evs.nci.nih.gov/ftp1/NCI_Thesaurus/archive/2021/21.08e_Release/",
     "rdp_url": "http://reusabledata.org/ncit.html",
     "data_license_attributes": {
       "non_commercial": false,
diff --git a/tests/unit/data/therapies.json b/tests/unit/data/therapies.json
index 9245d93f..b2a04c7d 100644
--- a/tests/unit/data/therapies.json
+++ b/tests/unit/data/therapies.json
@@ -136,13 +136,7 @@
     "label_and_type": "drugbank:db00522##identity",
     "label": "Bentiromide",
     "concept_id": "drugbank:DB00522",
-    "aliases": [
-      "APRD00818",
-      "Bentiromido",
-      "Bentiromidum",
-      "BTPABA",
-      "PFT"
-    ],
+    "aliases": ["APRD00818", "Bentiromido", "Bentiromidum", "BTPABA", "PFT"],
     "xrefs": ["chemidplus:37106-97-1"],
     "src_name": "DrugBank",
     "associated_with": [
@@ -830,6 +824,86 @@
     ],
     "src_name": "RxNorm"
   },
+  {
+    "src_name": "RxNorm",
+    "label_and_type": "rxcui:107044##identity",
+    "item_type": "identity",
+    "label": "rabbit anti-human T-lymphocyte globulin",
+    "concept_id": "rxcui:107044",
+    "aliases": [
+      "Anti-human thymocyte immunoglobulin, rabbit",
+      "Rabbit anti-human thymocyte globulin (rATG)",
+      "Rabbit ATG",
+      "anti-thymocyte globulin (rabbit)",
+      "Anti-thymocyte globulin rabbit",
+      "Anti-thymocyte globulin (rabbit)",
+      "Rabbit anti-human thymocyte immunoglobulin",
+      "rATG",
+      "Rabbit antithymocyte globulin",
+      "Anti-T-lymphocyte immune globulin, rabbit",
+      "Rabbit anti-thymocyte immunoglobulin",
+      "Rabbit anti-human thymocyte globulin",
+      "Lapine T-lymphocyte immune globulin",
+      "Antithymocyte immunoglobulin"
+    ],
+    "xrefs": ["drugbank:DB00098"],
+    "associated_with": [
+      "atc:L04AA04",
+      "mesh:C512542",
+      "mthspl:D7RD81HE4W",
+      "mmsl:21336",
+      "mmsl:d07642"
+    ],
+    "approval_status": "approved",
+    "approval_year": [],
+    "has_indication": [],
+    "trade_names": ["Thymoglobulin"]
+  },
+  {
+    "item_type": "trade_name",
+    "label_and_type": "thymoglobulin##trade_name",
+    "concept_id": "rxcui:107044",
+    "src_name": "RxNorm"
+  },
+  {
+    "src_name": "RxNorm",
+    "label_and_type": "rxcui:91601##identity",
+    "item_type": "identity",
+    "label": "lymphocyte immune globulin, anti-thymocyte globulin, equine",
+    "concept_id": "rxcui:91601",
+    "aliases": [
+      "Lymphocyte immune globulin, anti-thymocyte globulin",
+      "equine ATG",
+      "Anti-thymocyte globulin equine",
+      "Lymphocyte Immune Globulin, Anti-Thymocyte Globulin (Equine)",
+      "Horse ATG",
+      "eATG",
+      "lymphocyte immune globulin, anti-thy (equine)",
+      "Anti-thymocyte Globulin (Equine)",
+      "hATG",
+      "Equine thymocyte immune globulin",
+      "Lymphocyte immune globulin anti-thymocyte globulin (equine)",
+      "Horse anti-thymocyte globulin"
+    ],
+    "xrefs": ["drugbank:DB09312"],
+    "associated_with": [
+      "mthspl:475247QF1Z",
+      "mmsl:27222",
+      "mesh:D000961",
+      "mmsl:d07641",
+      "atc:L04AA03"
+    ],
+    "approval_status": "approved",
+    "approval_year": [],
+    "has_indication": [],
+    "trade_names": ["ATGAM"]
+  },
+  {
+    "label_and_type": "atgam##trade_name",
+    "item_type": "trade_name",
+    "src_name": "RxNorm",
+    "concept_id": "rxcui:91601"
+  },
   {
     "label_and_type": "lymphocyte immune globulin, anti-thymocyte globulin##label",
     "concept_id": "rxcui:1011",
@@ -961,7 +1035,8 @@
       "3-Aminopropionic Acid"
     ],
     "associated_with": ["mesh:D015091", "vandf:4028377", "unii:11P2JDE17B"],
-    "src_name": "RxNorm"
+    "src_name": "RxNorm",
+    "item_type": "identity"
   },
   {
     "label_and_type": "beta-alanine##label",
@@ -1005,7 +1080,6 @@
     "aliases": [
       "3,5,3',5'-Tetraiodo-L-thyronine",
       "Thyroxine",
-      "thyroxine",
       "Thyroid Hormone, T4",
       "Thyroxin",
       "T4 Thyroid Hormone",
@@ -1072,7 +1146,7 @@
     "concept_id": "rxcui:10582",
     "src_name": "RxNorm"
   },
-    {
+  {
     "label_and_type": "chembl:chembl40##identity",
     "label": "PHENOBARBITAL",
     "concept_id": "chembl:CHEMBL40",
@@ -1373,12 +1447,7 @@
       "drugbank:DB12117",
       "chembl:CHEMBL11359"
     ],
-    "trade_names": [
-      "Cisplatin",
-      "Platinol",
-      "PLATINOL",
-      "PLATINOL-AQ"
-    ],
+    "trade_names": ["Cisplatin", "Platinol", "PLATINOL", "PLATINOL-AQ"],
     "aliases": [
       "1,2-Diaminocyclohexaneplatinum II citrate",
       "APRD00359",
@@ -1445,10 +1514,7 @@
     "label_and_type": "ncit:c839##merger",
     "item_type": "merger",
     "concept_id": "ncit:C839",
-    "xrefs": [
-      "chemidplus:8025-81-8",
-      "wikidata:Q422265"
-    ],
+    "xrefs": ["chemidplus:8025-81-8", "wikidata:Q422265"],
     "label": "Spiramycin",
     "aliases": [
       "SPIRAMYCIN",
@@ -1954,8 +2020,9 @@
       "TREAT",
       "Treatment",
       "TX",
-      "therapeutic intervention",
-      "treatment"
+      "treatment",
+      "treatment_or_therapy",
+      "treatment or therapy"
     ]
   },
   {
@@ -2023,14 +2090,8 @@
       "Avermectin A1a, 5-O-demethyl-25-de(1-methylpropyl)-22,23-dihydro-25-(1-methylethyl)-",
       "Sklice"
     ],
-    "xrefs": [
-      "chemidplus:70288-86-7"
-    ],
-    "associated_with": [
-      "umls:C0022322",
-      "unii:8883YP2R6D",
-      "CHEBI:6078"
-    ]
+    "xrefs": ["chemidplus:70288-86-7"],
+    "associated_with": ["umls:C0022322", "unii:8883YP2R6D", "CHEBI:6078"]
   },
   {
     "label_and_type": "iuphar.ligand:5343##identity",
@@ -2040,18 +2101,16 @@
     "label": "cisplatin",
     "approval_status": "approved",
     "xrefs": [
-        "chembl:CHEMBL11359",
-        "chemidplus:15663-27-1",
-        "drugbank:DB00515"
+      "chembl:CHEMBL11359",
+      "chemidplus:15663-27-1",
+      "drugbank:DB00515"
     ],
     "associated_with": [
-        "pubchem.substance:178102005",
-        "pubchem.compound:441203",
-        "CHEBI:27899"
+      "pubchem.substance:178102005",
+      "pubchem.compound:441203",
+      "CHEBI:27899"
     ],
-    "aliases": [
-        "Platinol"
-    ]
+    "aliases": ["Platinol"]
   },
   {
     "label_and_type": "cisplatin##label",
@@ -2089,19 +2148,17 @@
     "item_type": "identity",
     "concept_id": "iuphar.ligand:2169",
     "label": "arginine vasotocin",
-    "xrefs": [
-        "chemidplus:113-80-4"
-    ],
+    "xrefs": ["chemidplus:113-80-4"],
     "associated_with": [
-        "pubchem.substance:135652004",
-        "pubchem.compound:68649",
-        "inchikey:OXDZADMCOWPSOC-ICBIOJHSSA-N"
+      "pubchem.substance:135652004",
+      "pubchem.compound:68649",
+      "inchikey:OXDZADMCOWPSOC-ICBIOJHSSA-N"
     ],
     "aliases": [
-        "L-cysteinyl-L-tyrosyl-(3S)-DL-isoleucyl-L-glutaminyl-L-asparagyl-L-cysteinyl-DL-prolyl-L-arginyl-glycinamide (1->6)-disulfide",
-        "argiprestocin",
-        "[Arg<sup>8</sup>]vasotocin",
-        "AVT"
+      "L-cysteinyl-L-tyrosyl-(3S)-DL-isoleucyl-L-glutaminyl-L-asparagyl-L-cysteinyl-DL-prolyl-L-arginyl-glycinamide (1->6)-disulfide",
+      "argiprestocin",
+      "[Arg<sup>8</sup>]vasotocin",
+      "AVT"
     ]
   },
   {
@@ -2141,26 +2198,22 @@
     "concept_id": "iuphar.ligand:2804",
     "label": "phenobarbital",
     "approval_status": "approved",
-    "xrefs": [
-        "chembl:CHEMBL40",
-        "chemidplus:50-06-6",
-        "drugbank:DB01174"
-    ],
+    "xrefs": ["chembl:CHEMBL40", "chemidplus:50-06-6", "drugbank:DB01174"],
     "associated_with": [
-        "pubchem.substance:135650817",
-        "pubchem.compound:4763",
-        "CHEBI:8069",
-        "inchikey:DDBREPKUVSBGFI-UHFFFAOYSA-N",
-        "drugcentral:2134"
+      "pubchem.substance:135650817",
+      "pubchem.compound:4763",
+      "CHEBI:8069",
+      "inchikey:DDBREPKUVSBGFI-UHFFFAOYSA-N",
+      "drugcentral:2134"
     ],
     "aliases": [
-        "5-ethyl-5-phenyl-1,3-diazinane-2,4,6-trione",
-        "fenobarbital",
-        "Luminal",
-        "phenobarb",
-        "phenobarbital sodium",
-        "phenobarbitone",
-        "phenylethylbarbiturate"
+      "5-ethyl-5-phenyl-1,3-diazinane-2,4,6-trione",
+      "fenobarbital",
+      "Luminal",
+      "phenobarb",
+      "phenobarbital sodium",
+      "phenobarbitone",
+      "phenylethylbarbiturate"
     ],
     "merge_ref": "rxcui:8134"
   },
@@ -2227,22 +2280,18 @@
     "concept_id": "iuphar.ligand:240",
     "label": "cisapride",
     "approval_status": "withdrawn",
-    "xrefs": [
-        "chembl:CHEMBL1729",
-        "chemidplus:81098-60-4",
-        "drugbank:DB00604"
-    ],
+    "xrefs": ["chembl:CHEMBL1729", "chemidplus:81098-60-4", "drugbank:DB00604"],
     "associated_with": [
-        "pubchem.substance:135650104",
-        "pubchem.compound:2769",
-        "CHEBI:151790",
-        "inchikey:DCSUBABJRXZOMT-UHFFFAOYSA-N",
-        "drugcentral:660"
+      "pubchem.substance:135650104",
+      "pubchem.compound:2769",
+      "CHEBI:151790",
+      "inchikey:DCSUBABJRXZOMT-UHFFFAOYSA-N",
+      "drugcentral:660"
     ],
     "aliases": [
-        "4-amino-5-chloro-N-[1-[3-(4-fluorophenoxy)propyl]-3-methoxypiperidin-4-yl]-2-methoxybenzamide",
-        "Prepulsid",
-        "Propulsid"
+      "4-amino-5-chloro-N-[1-[3-(4-fluorophenoxy)propyl]-3-methoxypiperidin-4-yl]-2-methoxybenzamide",
+      "Prepulsid",
+      "Propulsid"
     ]
   },
   {
diff --git a/tests/unit/dynamodb_build.bash b/tests/unit/dynamodb_build.bash
deleted file mode 100644
index a476be6f..00000000
--- a/tests/unit/dynamodb_build.bash
+++ /dev/null
@@ -1,6 +0,0 @@
-#! /bin/bash
-mkdir ./tests/unit/dynamodb_local
-cd ./tests/unit/dynamodb_local
-curl -O "https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.tar.gz"
-tar -xvzf dynamodb_local_latest.tar.gz; rm dynamodb_local_latest.tar.gz
-java -jar DynamoDBLocal.jar &
diff --git a/tests/unit/test_chembl_source.py b/tests/unit/test_chembl_source.py
index c66a9ac7..b4a6a2ca 100644
--- a/tests/unit/test_chembl_source.py
+++ b/tests/unit/test_chembl_source.py
@@ -1,11 +1,12 @@
 """Test that the therapy normalizer works as intended for the ChEMBL source."""
 import pytest
+
 from therapy.schemas import Drug, MatchType
 from therapy.query import QueryHandler
 from tests.conftest import compare_records
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def chembl():
     """Build ChEMBL normalizer test fixture."""
     class QueryGetter:
@@ -15,67 +16,67 @@ def __init__(self):
 
         def search(self, query_str):
             resp = self.query_handler.search_sources(query_str, keyed=True,
-                                                     incl='chembl')
-            return resp['source_matches']['ChEMBL']
+                                                     incl="chembl")
+            return resp["source_matches"]["ChEMBL"]
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def cisplatin():
     """Create a cisplatin drug fixture."""
     params = {
-        'label': 'CISPLATIN',
-        'concept_id': 'chembl:CHEMBL11359',
-        'aliases': [
-            'Cisplatin',
-            'Cis-Platinum II',
-            'Cisplatinum',
-            'cis-diamminedichloroplatinum(II)',
-            'CIS-DDP',
-            'INT-230-6 COMPONENT CISPLATIN',
-            'INT230-6 COMPONENT CISPLATIN',
-            'NSC-119875',
-            'Platinol',
-            'Platinol-Aq'
+        "label": "CISPLATIN",
+        "concept_id": "chembl:CHEMBL11359",
+        "aliases": [
+            "Cisplatin",
+            "Cis-Platinum II",
+            "Cisplatinum",
+            "cis-diamminedichloroplatinum(II)",
+            "CIS-DDP",
+            "INT-230-6 COMPONENT CISPLATIN",
+            "INT230-6 COMPONENT CISPLATIN",
+            "NSC-119875",
+            "Platinol",
+            "Platinol-Aq"
         ],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [],
-        'trade_names': [
-            'PLATINOL',
-            'PLATINOL-AQ'
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [],
+        "trade_names": [
+            "PLATINOL",
+            "PLATINOL-AQ"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def l745870():
     """Create a L-745870 drug fixture."""
     params = {
-        'label': 'L-745870',
-        'concept_id': 'chembl:CHEMBL267014',
-        'aliases': [],
-        'approval_status': None,
-        'xrefs': [],
-        'associated_with': [],
-        'trade_names': []
+        "label": "L-745870",
+        "concept_id": "chembl:CHEMBL267014",
+        "aliases": [],
+        "approval_status": None,
+        "xrefs": [],
+        "associated_with": [],
+        "trade_names": []
     }
     return Drug(**params)
 
 
 # Test with aliases and trade names > 20
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def aspirin():
     """Create an aspirin drug fixture."""
     params = {
-        'label': 'ASPIRIN',
-        'concept_id': 'chembl:CHEMBL25',
-        'aliases': [],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [],
-        'trade_names': []
+        "label": "ASPIRIN",
+        "concept_id": "chembl:CHEMBL25",
+        "aliases": [],
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [],
+        "trade_names": []
     }
     return Drug(**params)
 
@@ -84,192 +85,190 @@ def test_concept_id_cisplatin(cisplatin, chembl):
     """Test that cisplatin drug normalizes to correct drug concept
     as a CONCEPT_ID match.
     """
-    response = chembl.search('chembl:CHEMBL11359')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = chembl.search('CHEMBL11359')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = chembl.search('chembl:chembl11359')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = chembl.search('cHEmbl:chembl11359')
-    assert response['match_type'] == \
+    response = chembl.search("chembl:CHEMBL11359")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = chembl.search("CHEMBL11359")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = chembl.search("chembl:chembl11359")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = chembl.search("cHEmbl:chembl11359")
+    assert response["match_type"] == \
            MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = chembl.search('cHEmbl:CHEMBL11359')
-    assert response['match_type'] == \
+    response = chembl.search("cHEmbl:CHEMBL11359")
+    assert response["match_type"] == \
            MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
 
 def test_cisplatin_label(cisplatin, chembl):
     """Test that cisplatin drug normalizes to correct drug concept
     as a LABEL match.
     """
-    response = chembl.search('CISPLATIN')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 2
-    if len(response['records'][0]['aliases']) >\
-            len(response['records'][1]['aliases']):
+    response = chembl.search("CISPLATIN")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 2
+    if len(response["records"][0]["aliases"]) > len(response["records"][1]["aliases"]):
         ind = 0
     else:
         ind = 1
-    compare_records(response['records'][ind], cisplatin)
+    compare_records(response["records"][ind], cisplatin)
 
-    response = chembl.search('cisplatin')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 2
-    if len(response['records'][0]['aliases']) >\
-            len(response['records'][1]['aliases']):
+    response = chembl.search("cisplatin")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 2
+    if len(response["records"][0]["aliases"]) > len(response["records"][1]["aliases"]):
         ind = 0
     else:
         ind = 1
-    compare_records(response['records'][ind], cisplatin)
+    compare_records(response["records"][ind], cisplatin)
 
 
 def test_cisplatin_alias(cisplatin, chembl):
     """Test that alias term normalizes to correct drug concept as an
     ALIAS match.
     """
-    response = chembl.search('cis-diamminedichloroplatinum(II)')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = chembl.search("cis-diamminedichloroplatinum(II)")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = chembl.search('INT230-6 COMPONENT CISPLATIn')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = chembl.search("INT230-6 COMPONENT CISPLATIn")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
 
 def test_no_match(chembl):
     """Test that a term normalizes to correct drug concept as a NO match."""
-    response = chembl.search('cisplati')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = chembl.search("cisplati")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
     # Test white space in between label
-    response = chembl.search('L - 745870')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = chembl.search("L - 745870")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Test empty query
-    response = chembl.search('')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = chembl.search("")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
 
 def test_l745870_concept_id(l745870, chembl):
     """Test that L-745870 drug normalizes to correct drug concept
     as a CONCEPT_ID match.
     """
-    response = chembl.search('chembl:CHEMBL267014')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("chembl:CHEMBL267014")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
-    response = chembl.search('CHEMBL267014')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("CHEMBL267014")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
-    response = chembl.search('chembl:chembl267014')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("chembl:chembl267014")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
-    response = chembl.search('cHEmbl:chembl267014')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("cHEmbl:chembl267014")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
-    response = chembl.search('cHEmbl:CHEMBL267014')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("cHEmbl:CHEMBL267014")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
 
 def test_l745870_label(l745870, chembl):
     """Test that L-745870 drug normalizes to correct drug concept
     as a LABEL match.
     """
-    response = chembl.search('L-745870')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("L-745870")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
-    response = chembl.search('l-745870')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], l745870)
+    response = chembl.search("l-745870")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], l745870)
 
 
 def test_aspirin_concept_id(aspirin, chembl):
     """Test that L-745870 drug normalizes to correct drug concept
     as a CONCEPT_ID match.
     """
-    response = chembl.search('chembl:CHEMBL25')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    response = chembl.search("chembl:CHEMBL25")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
-    response = chembl.search('CHEMBL25')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    response = chembl.search("CHEMBL25")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
-    response = chembl.search('chembl:chembl25')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    response = chembl.search("chembl:chembl25")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
-    response = chembl.search('cHEmbl:chembl25')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    response = chembl.search("cHEmbl:chembl25")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
-    response = chembl.search('cHEmbl:CHEMBL25')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    response = chembl.search("cHEmbl:CHEMBL25")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
 
 def test_aspirin_label(aspirin, chembl):
     """Test that L-745870 drug normalizes to correct drug concept
     as a LABEL match.
     """
-    response = chembl.search('ASPIRIN')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    response = chembl.search("ASPIRIN")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
-    response = chembl.search('aspirin')
-    assert response['match_type'] ==\
+    response = chembl.search("aspirin")
+    assert response["match_type"] ==\
            MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], aspirin)
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
 
-def test_meta_info(cisplatin, chembl):
+def test_meta_info(chembl):
     """Test that the meta field is correct."""
-    response = chembl.search('cisplatin')
-    assert response['source_meta_']['data_license'] == 'CC BY-SA 3.0'
-    assert response['source_meta_']['data_license_url'] == \
-           'https://creativecommons.org/licenses/by-sa/3.0/'
-    assert response['source_meta_']['version'] == '27'
-    assert response['source_meta_']['data_url'] == \
-           'http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/'  # noqa: E501
-    assert response['source_meta_']['rdp_url'] == 'http://reusabledata.org/chembl.html'  # noqa: E501
-    assert response['source_meta_']['data_license_attributes'] == {
+    response = chembl.search("cisplatin")
+    assert response["source_meta_"]["data_license"] == "CC BY-SA 3.0"
+    assert response["source_meta_"]["data_license_url"] == \
+           "https://creativecommons.org/licenses/by-sa/3.0/"
+    assert response["source_meta_"]["version"] == "27"
+    assert response["source_meta_"]["data_url"] == \
+           "http://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases/chembl_27/"
+    assert response["source_meta_"]["rdp_url"] == "http://reusabledata.org/chembl.html"  # noqa: E501
+    assert response["source_meta_"]["data_license_attributes"] == {
         "non_commercial": False,
         "share_alike": True,
         "attribution": True
diff --git a/tests/unit/test_chemidplus_source.py b/tests/unit/test_chemidplus_source.py
index 6228ad9d..87960ff9 100644
--- a/tests/unit/test_chemidplus_source.py
+++ b/tests/unit/test_chemidplus_source.py
@@ -1,12 +1,14 @@
 """Test ChemIDplus ETL methods."""
+import datetime
+
+import pytest
+
 from therapy.query import QueryHandler
 from therapy.schemas import Drug, MatchType
-import pytest
 from tests.conftest import compare_records
-import datetime
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def chemidplus():
     """Build ETL test fixture."""
     class QueryGetter:
@@ -16,19 +18,19 @@ def __init__(self):
 
         def search(self, query_str):
             resp = self.normalizer.search_sources(query_str, keyed=True,
-                                                  incl='chemidplus')
-            return resp['source_matches']['ChemIDplus']
+                                                  incl="chemidplus")
+            return resp["source_matches"]["ChemIDplus"]
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def penicillin_v():
     """Build test fixture for Penicillin V drug."""
     return Drug(**{
         "concept_id": "chemidplus:87-08-1",
         "label": "Penicillin V",
         "aliases": [
-            'Phenoxymethylpenicillin'
+            "Phenoxymethylpenicillin"
         ],
         "trade_names": [],
         "xrefs": [
@@ -40,7 +42,7 @@ def penicillin_v():
     })
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def imatinib():
     """Build test fixture for Imatinib."""
     return Drug(**{
@@ -57,7 +59,7 @@ def imatinib():
     })
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def other_imatinib():
     """Build test fixture for imatinib mesylate."""
     return Drug(**{
@@ -70,134 +72,133 @@ def other_imatinib():
     })
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def cisplatin():
     """Build test fixture for cisplatin."""
     return Drug(**{
-        'concept_id': 'chemidplus:15663-27-1',
-        'label': 'Cisplatin',
-        'aliases': [
-            'cis-Diaminedichloroplatinum',
-            '1,2-Diaminocyclohexaneplatinum II citrate'
+        "concept_id": "chemidplus:15663-27-1",
+        "label": "Cisplatin",
+        "aliases": [
+            "cis-Diaminedichloroplatinum",
+            "1,2-Diaminocyclohexaneplatinum II citrate"
         ],
-        'trade_names': [],
-        'xrefs': [
-            'drugbank:DB00515'
+        "trade_names": [],
+        "xrefs": [
+            "drugbank:DB00515"
         ],
         "associated_with": [
-            'unii:Q20Q21Q62J'
+            "unii:Q20Q21Q62J"
         ]
     })
 
 
 def test_penicillin(chemidplus, penicillin_v):
     """Test record retrieval for Penicillin V."""
-    response = chemidplus.search('chemidplus:87-08-1')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], penicillin_v)
+    response = chemidplus.search("chemidplus:87-08-1")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], penicillin_v)
 
-    response = chemidplus.search('CHemidplus:87-08-1')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], penicillin_v)
+    response = chemidplus.search("CHemidplus:87-08-1")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], penicillin_v)
 
-    response = chemidplus.search('Penicillin V')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], penicillin_v)
+    response = chemidplus.search("Penicillin V")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], penicillin_v)
 
-    response = chemidplus.search('Phenoxymethylpenicillin')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], penicillin_v)
+    response = chemidplus.search("Phenoxymethylpenicillin")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], penicillin_v)
 
-    response = chemidplus.search('drugbank:DB00417')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], penicillin_v)
+    response = chemidplus.search("drugbank:DB00417")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], penicillin_v)
 
-    response = chemidplus.search('unii:Z61I075U2W')
-    assert response['match_type'] == MatchType.ASSOCIATED_WITH
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], penicillin_v)
+    response = chemidplus.search("unii:Z61I075U2W")
+    assert response["match_type"] == MatchType.ASSOCIATED_WITH
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], penicillin_v)
 
-    response = chemidplus.search('87-08-1')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = chemidplus.search("87-08-1")
+    assert response["match_type"] == MatchType.NO_MATCH
 
-    response = chemidplus.search('87081')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = chemidplus.search("87081")
+    assert response["match_type"] == MatchType.NO_MATCH
 
-    response = chemidplus.search('PenicillinV')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = chemidplus.search("PenicillinV")
+    assert response["match_type"] == MatchType.NO_MATCH
 
 
 def test_imatinib(chemidplus, imatinib, other_imatinib):
     """Test record retrieval for imatinib."""
-    response = chemidplus.search('Imatinib')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], imatinib)
-
-    response = chemidplus.search('imatiniB')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], imatinib)
-
-    response = chemidplus.search('drugbank:DB00619')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 2
-    for record in response['records']:
-        if record['concept_id'] == 'chemidplus:152459-95-5':
+    response = chemidplus.search("Imatinib")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], imatinib)
+
+    response = chemidplus.search("imatiniB")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], imatinib)
+
+    response = chemidplus.search("drugbank:DB00619")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 2
+    for record in response["records"]:
+        if record["concept_id"] == "chemidplus:152459-95-5":
             compare_records(record, imatinib)
-        elif record['concept_id'] == 'chemidplus:220127-57-1':
+        elif record["concept_id"] == "chemidplus:220127-57-1":
             compare_records(record, other_imatinib)
         else:
             assert False  # retrieved incorrect record
 
-    response = chemidplus.search('unii:BKJ8M8G5HI')
-    assert response['match_type'] == MatchType.ASSOCIATED_WITH
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], imatinib)
+    response = chemidplus.search("unii:BKJ8M8G5HI")
+    assert response["match_type"] == MatchType.ASSOCIATED_WITH
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], imatinib)
 
 
 def test_cisplatin(chemidplus, cisplatin):
     """Test record retrieval for cisplatin."""
-    response = chemidplus.search('chemidplus:15663-27-1')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = chemidplus.search("chemidplus:15663-27-1")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = chemidplus.search('Cisplatin')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = chemidplus.search("Cisplatin")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = chemidplus.search('cis-Diaminedichloroplatinum')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = chemidplus.search("cis-Diaminedichloroplatinum")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = chemidplus.search('drugbank:DB00515')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = chemidplus.search("drugbank:DB00515")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = chemidplus.search('Cisplatine')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = chemidplus.search("Cisplatine")
+    assert response["match_type"] == MatchType.NO_MATCH
 
 
 def test_meta(chemidplus):
     """Test correctness of source metadata."""
-    response = chemidplus.search('incoherent-string-of-text')
-    assert response['source_meta_']['data_license'] == 'custom'
-    assert response['source_meta_']['data_license_url'] == 'https://www.nlm.nih.gov/databases/download/terms_and_conditions.html'  # noqa: E501
-    version = response['source_meta_']['version']
-    assert len(version) == 8
-    assert datetime.datetime.strptime(version, '%Y%m%d')
-    assert response['source_meta_']['data_url'] == 'ftp://ftp.nlm.nih.gov/nlmdata/.chemidlease/'  # noqa: E501
-    assert response['source_meta_']['rdp_url'] is None
-    assert response['source_meta_']['data_license_attributes'] == {
+    response = chemidplus.search("incoherent-string-of-text")
+    assert response["source_meta_"]["data_license"] == "custom"
+    assert response["source_meta_"]["data_license_url"] == "https://www.nlm.nih.gov/databases/download/terms_and_conditions.html"  # noqa: E501
+    version = response["source_meta_"]["version"]
+    assert datetime.datetime.strptime(version, "%Y%m%d")
+    assert response["source_meta_"]["data_url"] == "ftp://ftp.nlm.nih.gov/nlmdata/.chemidlease/"  # noqa: E501
+    assert response["source_meta_"]["rdp_url"] is None
+    assert response["source_meta_"]["data_license_attributes"] == {
         "non_commercial": False,
         "share_alike": False,
         "attribution": True
diff --git a/tests/unit/test_database.py b/tests/unit/test_database.py
index 0e7d220e..51937401 100644
--- a/tests/unit/test_database.py
+++ b/tests/unit/test_database.py
@@ -1,53 +1,54 @@
 """Test DynamoDB"""
 from boto3.dynamodb.conditions import Key
+
 from tests.conftest import db  # noqa: F401
 
 
 def test_tables_created(db):  # noqa: F811
     """Check that therapy_concepts and therapy_metadata are created."""
-    existing_tables = db.dynamodb_client.list_tables()['TableNames']
-    assert 'therapy_concepts' in existing_tables
-    assert 'therapy_metadata' in existing_tables
+    existing_tables = db.dynamodb_client.list_tables()["TableNames"]
+    assert "therapy_concepts" in existing_tables
+    assert "therapy_metadata" in existing_tables
 
 
 def test_item_type(db):  # noqa: F811
     """Check that objects are tagged with item_type attribute."""
-    filter_exp = Key('label_and_type').eq('chembl:chembl11359##identity')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'identity'
-
-    filter_exp = Key('label_and_type').eq('interferon alfacon-1##label')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'label'
-
-    filter_exp = Key('label_and_type').eq('unii:5x5hb3vz3z##associated_with')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'associated_with'
-
-    filter_exp = Key('label_and_type').eq('drugbank:db00515##xref')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'xref'
-
-    filter_exp = Key('label_and_type').eq('dichlorodiammineplatinum##alias')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'alias'
-
-    filter_exp = Key('label_and_type').eq('rxcui:1041527##rx_brand')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'rx_brand'
-
-    filter_exp = Key('label_and_type').eq('align##trade_name')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'trade_name'
-
-    filter_exp = Key('label_and_type').eq('ncit:c839##merger')
-    item = db.therapies.query(KeyConditionExpression=filter_exp)['Items'][0]
-    assert 'item_type' in item
-    assert item['item_type'] == 'merger'
+    filter_exp = Key("label_and_type").eq("chembl:chembl11359##identity")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "identity"
+
+    filter_exp = Key("label_and_type").eq("interferon alfacon-1##label")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "label"
+
+    filter_exp = Key("label_and_type").eq("unii:5x5hb3vz3z##associated_with")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "associated_with"
+
+    filter_exp = Key("label_and_type").eq("drugbank:db00515##xref")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "xref"
+
+    filter_exp = Key("label_and_type").eq("dichlorodiammineplatinum##alias")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "alias"
+
+    filter_exp = Key("label_and_type").eq("rxcui:1041527##rx_brand")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "rx_brand"
+
+    filter_exp = Key("label_and_type").eq("align##trade_name")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "trade_name"
+
+    filter_exp = Key("label_and_type").eq("ncit:c839##merger")
+    item = db.therapies.query(KeyConditionExpression=filter_exp)["Items"][0]
+    assert "item_type" in item
+    assert item["item_type"] == "merger"
diff --git a/tests/unit/test_drugbank_source.py b/tests/unit/test_drugbank_source.py
index 69c45258..b75ab9f7 100644
--- a/tests/unit/test_drugbank_source.py
+++ b/tests/unit/test_drugbank_source.py
@@ -1,15 +1,16 @@
 """Test that the therapy normalizer works as intended for the DrugBank
 source.
 """
+import re
+
 import pytest
 
 from tests.conftest import compare_response
 from therapy.query import QueryHandler
 from therapy.schemas import Drug, MatchType
-import re
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def drugbank():
     """Build DrugBank normalizer test fixture."""
     class QueryGetter:
@@ -19,33 +20,33 @@ def __init__(self):
 
         def search(self, query_str):
             resp = self.query_handler.search_sources(query_str, keyed=True,
-                                                     incl='drugbank')
-            return resp['source_matches']['DrugBank']
+                                                     incl="drugbank")
+            return resp["source_matches"]["DrugBank"]
 
         def fetch_meta(self):
-            return self.query_handler._fetch_meta('DrugBank')
+            return self.query_handler._fetch_meta("DrugBank")
 
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def cisplatin():
     """Create a cisplatin drug fixture."""
     params = {
-        'label': 'Cisplatin',
-        'concept_id': 'drugbank:DB00515',
-        'aliases': [
-            'CDDP',
-            'Cis-DDP',
-            'cis-diamminedichloroplatinum(II)',
-            'APRD00359',
-            'cisplatino',
+        "label": "Cisplatin",
+        "concept_id": "drugbank:DB00515",
+        "aliases": [
+            "CDDP",
+            "Cis-DDP",
+            "cis-diamminedichloroplatinum(II)",
+            "APRD00359",
+            "cisplatino",
         ],
-        'xrefs': [
-            'chemidplus:15663-27-1',
+        "xrefs": [
+            "chemidplus:15663-27-1",
         ],
-        'trade_names': [],
-        'associated_with': [
+        "trade_names": [],
+        "associated_with": [
             "unii:Q20Q21Q62J",
             "inchikey:LXZZYRPGZAFOLE-UHFFFAOYSA-L"
         ],
@@ -53,24 +54,24 @@ def cisplatin():
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def bentiromide():
     """Create a bentiromide drug fixture."""
     params = {
-        'label': 'Bentiromide',
-        'concept_id': 'drugbank:DB00522',
-        'aliases': [
-            'APRD00818',
-            'Bentiromido',
-            'Bentiromidum',
-            'BTPABA',
-            'PFT',
+        "label": "Bentiromide",
+        "concept_id": "drugbank:DB00522",
+        "aliases": [
+            "APRD00818",
+            "Bentiromido",
+            "Bentiromidum",
+            "BTPABA",
+            "PFT",
         ],
-        'xrefs': [
-            'chemidplus:37106-97-1',
+        "xrefs": [
+            "chemidplus:37106-97-1",
         ],
-        'trade_names': [],
-        'associated_with': [
+        "trade_names": [],
+        "associated_with": [
             "unii:239IF5W61J",
             "inchikey:SPPTWHFVYKCNNK-FQEVSTJZSA-N"
         ],
@@ -78,17 +79,17 @@ def bentiromide():
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def aloe_ferox_leaf():
     """Create aloe ferox leaf fixture. Record has >20 aliases, so ETL deletes
     all of them.
     """
     params = {
-        'concept_id': 'drugbank:DB14257',
-        'label': 'Aloe ferox leaf',
-        'aliases': [],
-        'xrefs': [],
-        'trade_names': [],
+        "concept_id": "drugbank:DB14257",
+        "label": "Aloe ferox leaf",
+        "aliases": [],
+        "xrefs": [],
+        "trade_names": [],
         "associated_with": ["unii:0D145J8EME"]
     }
     return Drug(**params)
@@ -96,125 +97,125 @@ def aloe_ferox_leaf():
 
 def test_concept_id_match(drugbank, cisplatin, bentiromide, aloe_ferox_leaf):
     """Test that concept ID query resolves to correct record."""
-    response = drugbank.search('drugbank:DB00515')
+    response = drugbank.search("drugbank:DB00515")
     compare_response(response, MatchType.CONCEPT_ID, cisplatin)
 
-    response = drugbank.search('DB00515')
+    response = drugbank.search("DB00515")
     compare_response(response, MatchType.CONCEPT_ID, cisplatin)
 
-    response = drugbank.search('drugbank:db00515')
+    response = drugbank.search("drugbank:db00515")
     compare_response(response, MatchType.CONCEPT_ID, cisplatin)
 
-    response = drugbank.search('Drugbank:db00515')
+    response = drugbank.search("Drugbank:db00515")
     compare_response(response, MatchType.CONCEPT_ID, cisplatin)
 
-    response = drugbank.search('druGBank:DB00515')
+    response = drugbank.search("druGBank:DB00515")
     compare_response(response, MatchType.CONCEPT_ID, cisplatin)
 
-    response = drugbank.search('drugbank:DB00522')
+    response = drugbank.search("drugbank:DB00522")
     compare_response(response, MatchType.CONCEPT_ID, bentiromide)
 
-    response = drugbank.search('DB00522')
+    response = drugbank.search("DB00522")
     compare_response(response, MatchType.CONCEPT_ID, bentiromide)
 
-    response = drugbank.search('drugbank:db00522')
+    response = drugbank.search("drugbank:db00522")
     compare_response(response, MatchType.CONCEPT_ID, bentiromide)
 
-    response = drugbank.search('Drugbank:db00522')
+    response = drugbank.search("Drugbank:db00522")
     compare_response(response, MatchType.CONCEPT_ID, bentiromide)
 
-    response = drugbank.search('druGBank:DB00522')
+    response = drugbank.search("druGBank:DB00522")
     compare_response(response, MatchType.CONCEPT_ID, bentiromide)
 
-    response = drugbank.search('drugbank:DB14257')
+    response = drugbank.search("drugbank:DB14257")
     compare_response(response, MatchType.CONCEPT_ID, aloe_ferox_leaf)
 
 
 def test_label_match(drugbank, cisplatin, bentiromide, aloe_ferox_leaf):
     """Test that label query resolves to correct record."""
-    response = drugbank.search('cisplatin')
+    response = drugbank.search("cisplatin")
     compare_response(response, MatchType.LABEL, cisplatin)
 
-    response = drugbank.search('cisplatin')
+    response = drugbank.search("cisplatin")
     compare_response(response, MatchType.LABEL, cisplatin)
 
-    response = drugbank.search('Bentiromide')
+    response = drugbank.search("Bentiromide")
     compare_response(response, MatchType.LABEL, bentiromide)
 
-    response = drugbank.search('bentiromide')
+    response = drugbank.search("bentiromide")
     compare_response(response, MatchType.LABEL, bentiromide)
 
-    response = drugbank.search('aloe ferox leaf')
+    response = drugbank.search("aloe ferox leaf")
     compare_response(response, MatchType.LABEL, aloe_ferox_leaf)
 
 
 def test_alias_match(drugbank, cisplatin, bentiromide, aloe_ferox_leaf):
     """Test that alias query resolves to correct record."""
-    response = drugbank.search('CISPLATINO')
+    response = drugbank.search("CISPLATINO")
     compare_response(response, MatchType.ALIAS, cisplatin)
 
-    response = drugbank.search('Cis-ddp')
+    response = drugbank.search("Cis-ddp")
     compare_response(response, MatchType.ALIAS, cisplatin)
 
-    response = drugbank.search('APRD00818')
+    response = drugbank.search("APRD00818")
     compare_response(response, MatchType.ALIAS, bentiromide)
 
-    response = drugbank.search('PFT')
+    response = drugbank.search("PFT")
     compare_response(response, MatchType.ALIAS, bentiromide)
 
     # verify aliases > 20 not stored
-    response = drugbank.search('Aloe Capensis')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = drugbank.search("Aloe Capensis")
+    assert response["match_type"] == MatchType.NO_MATCH
 
-    response = drugbank.search('Aloe Ferox Juice')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = drugbank.search("Aloe Ferox Juice")
+    assert response["match_type"] == MatchType.NO_MATCH
 
 
 def test_xref_match(drugbank, cisplatin, bentiromide):
     """Test that xref query resolves to correct record."""
-    response = drugbank.search('chemidplus:15663-27-1')
+    response = drugbank.search("chemidplus:15663-27-1")
     compare_response(response, MatchType.XREF, cisplatin)
 
-    response = drugbank.search('chemidplus:37106-97-1')
+    response = drugbank.search("chemidplus:37106-97-1")
     compare_response(response, MatchType.XREF, bentiromide)
 
 
 def test_assoc_with_match(drugbank, cisplatin, bentiromide, aloe_ferox_leaf):
     """Test that associated_with query resolves to correct record."""
-    response = drugbank.search('inchikey:lxzzyrpgzafole-uhfffaoysa-l')
+    response = drugbank.search("inchikey:lxzzyrpgzafole-uhfffaoysa-l")
     compare_response(response, MatchType.ASSOCIATED_WITH, cisplatin)
 
-    response = drugbank.search('unii:239if5w61j')
+    response = drugbank.search("unii:239if5w61j")
     compare_response(response, MatchType.ASSOCIATED_WITH, bentiromide)
 
-    response = drugbank.search('UNII:0D145J8EME')
+    response = drugbank.search("UNII:0D145J8EME")
     compare_response(response, MatchType.ASSOCIATED_WITH, aloe_ferox_leaf)
 
 
 def test_no_match(drugbank):
     """Test that a term normalizes to correct drug concept as a NO match."""
-    response = drugbank.search('lepirudi')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = drugbank.search("lepirudi")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
     # Test white space in between id
-    response = drugbank.search('DB 00001')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = drugbank.search("DB 00001")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Test empty query
-    response = drugbank.search('')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = drugbank.search("")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
 
 def test_meta_info(drugbank):
     """Test that the meta field is correct."""
     response = drugbank.fetch_meta()
-    assert response.data_license == 'CC0 1.0'
-    assert response.data_license_url == 'https://creativecommons.org/publicdomain/zero/1.0/'  # noqa: E501
-    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', response.version)
-    assert response.data_url == 'https://go.drugbank.com/releases/latest#open-data'  # noqa: E501
-    assert response.rdp_url == 'http://reusabledata.org/drugbank.html'  # noqa: E501
+    assert response.data_license == "CC0 1.0"
+    assert response.data_license_url == "https://creativecommons.org/publicdomain/zero/1.0/"  # noqa: E501
+    assert re.match(r"[0-9]+\.[0-9]+\.[0-9]", response.version)
+    assert response.data_url == "https://go.drugbank.com/releases/latest#open-data"
+    assert response.rdp_url == "http://reusabledata.org/drugbank.html"
     assert response.data_license_attributes == {
         "non_commercial": False,
         "share_alike": False,
diff --git a/tests/unit/test_emit_warnings.py b/tests/unit/test_emit_warnings.py
index f90196ae..f13b0ef8 100644
--- a/tests/unit/test_emit_warnings.py
+++ b/tests/unit/test_emit_warnings.py
@@ -5,23 +5,23 @@
 def test_emit_warnings():
     """Test that emit_warnings works correctly."""
     expected_warnings = {
-        'nbsp': 'Query contains non breaking space characters.'
+        "nbsp": "Query contains non breaking space characters."
     }
     query_handler = QueryHandler()
 
     # Test emit no warnings
-    actual_warnings = query_handler._emit_warnings('CISPLATIN')
+    actual_warnings = query_handler._emit_warnings("CISPLATIN")
     assert actual_warnings is None
 
     # Test emit warnings
-    actual_warnings = query_handler._emit_warnings('CIS PLATIN')
+    actual_warnings = query_handler._emit_warnings("CIS PLATIN")
     assert actual_warnings == actual_warnings
 
-    actual_warnings = query_handler._emit_warnings('CIS\u00A0platin')
+    actual_warnings = query_handler._emit_warnings("CIS\u00A0platin")
     assert expected_warnings == actual_warnings
 
-    actual_warnings = query_handler._emit_warnings('CIS&nbsp;platin')
+    actual_warnings = query_handler._emit_warnings("CIS&nbsp;platin")
     assert expected_warnings == actual_warnings
 
-    actual_warnings = query_handler._emit_warnings('CIS\xa0platin')
+    actual_warnings = query_handler._emit_warnings("CIS\xa0platin")
     assert expected_warnings == actual_warnings
diff --git a/tests/unit/test_guidetopharmacology.py b/tests/unit/test_guidetopharmacology.py
index 15d85c9a..ac5f8f0c 100644
--- a/tests/unit/test_guidetopharmacology.py
+++ b/tests/unit/test_guidetopharmacology.py
@@ -1,9 +1,11 @@
 """Test Guide to PHARMACOLOGY source."""
+import re
+
 import pytest
+
 from tests.conftest import compare_response
 from therapy.query import QueryHandler
 from therapy.schemas import Drug, MatchType
-import re
 
 
 @pytest.fixture(scope="module")
@@ -173,7 +175,7 @@ def test_alias_match(guidetopharmacology, cisplatin, arginine_vasotocin,
     resp = guidetopharmacology.search("AVT")
     compare_response(resp, MatchType.ALIAS, arginine_vasotocin)
 
-    resp = guidetopharmacology.search("5-ethyl-5-phenyl-1,3-diazinane-2,4,6-trione")  # noqa: E501
+    resp = guidetopharmacology.search("5-ethyl-5-phenyl-1,3-diazinane-2,4,6-trione")
     compare_response(resp, MatchType.ALIAS, phenobarbital)
 
     resp = guidetopharmacology.search("Prepulsid")
@@ -249,7 +251,7 @@ def test_meta_info(guidetopharmacology):
     """Test that metadata is correct."""
     resp = guidetopharmacology.fetch_meta()
     assert resp.data_license == "CC BY-SA 4.0"
-    assert resp.data_license_url == "https://creativecommons.org/licenses/by-sa/4.0/"  # noqa: E501
+    assert resp.data_license_url == "https://creativecommons.org/licenses/by-sa/4.0/"
     assert re.match(r"\d{4}.\d+", resp.version)
     assert resp.data_url == "https://www.guidetopharmacology.org/download.jsp"
     assert resp.rdp_url is None
diff --git a/tests/unit/test_hemonc_source.py b/tests/unit/test_hemonc_source.py
index 49dea45d..67d4603d 100644
--- a/tests/unit/test_hemonc_source.py
+++ b/tests/unit/test_hemonc_source.py
@@ -1,15 +1,16 @@
 """Test that the therapy normalizer works as intended for the HemOnc.org
 source.
 """
+import re
+
 import pytest
 
 from tests.conftest import compare_response
 from therapy.query import QueryHandler
 from therapy.schemas import Drug, MatchType, ApprovalStatus
-import re
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def hemonc():
     """Build HemOnc normalizer test fixture."""
     class QueryGetter:
@@ -19,16 +20,16 @@ def __init__(self):
 
         def search(self, query_str):
             resp = self.query_handler.search_sources(query_str, keyed=True,
-                                                     incl='hemonc')
-            return resp['source_matches']['HemOnc']
+                                                     incl="hemonc")
+            return resp["source_matches"]["HemOnc"]
 
         def fetch_meta(self):
-            return self.query_handler._fetch_meta('HemOnc')
+            return self.query_handler._fetch_meta("HemOnc")
 
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def cisplatin():
     """Construct cisplatin fixture."""
     return Drug(**{
@@ -68,7 +69,7 @@ def cisplatin():
     })
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def bendamustine():
     """Construct bendamustine fixture."""
     return Drug(**{
@@ -117,7 +118,7 @@ def bendamustine():
     })
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def degarelix():
     """Create fixture for degarelix drug."""
     return Drug(**{
@@ -145,74 +146,74 @@ def degarelix():
 
 def test_concept_id_match(hemonc, cisplatin, bendamustine, degarelix):
     """Test that concept ID queries resolve to correct record."""
-    response = hemonc.search('hemonc:105')
+    response = hemonc.search("hemonc:105")
     compare_response(response, MatchType.CONCEPT_ID, cisplatin)
 
-    response = hemonc.search('hemonc:65')
+    response = hemonc.search("hemonc:65")
     compare_response(response, MatchType.CONCEPT_ID, bendamustine)
 
-    response = hemonc.search('hemonc:151')
+    response = hemonc.search("hemonc:151")
     compare_response(response, MatchType.CONCEPT_ID, degarelix)
 
 
 def test_label_match(hemonc, cisplatin, bendamustine, degarelix):
     """Test that label queries resolve to correct record."""
-    response = hemonc.search('cisplatin')
+    response = hemonc.search("cisplatin")
     compare_response(response, MatchType.LABEL, cisplatin)
 
-    response = hemonc.search('Bendamustine')
+    response = hemonc.search("Bendamustine")
     compare_response(response, MatchType.LABEL, bendamustine)
 
-    response = hemonc.search('DEGARELIX')
+    response = hemonc.search("DEGARELIX")
     compare_response(response, MatchType.LABEL, degarelix)
 
 
 def test_alias_match(hemonc, cisplatin, bendamustine, degarelix):
     """Test that alias queries resolve to correct record."""
-    response = hemonc.search('ddp')
+    response = hemonc.search("ddp")
     compare_response(response, MatchType.ALIAS, cisplatin)
 
-    response = hemonc.search('dacp')
+    response = hemonc.search("dacp")
     compare_response(response, MatchType.ALIAS, cisplatin)
 
-    response = hemonc.search('nsc 119875')
+    response = hemonc.search("nsc 119875")
     compare_response(response, MatchType.ALIAS, cisplatin)
 
-    response = hemonc.search('cep-18083')
+    response = hemonc.search("cep-18083")
     compare_response(response, MatchType.ALIAS, bendamustine)
 
-    response = hemonc.search('bendamustine hydrochloride')
+    response = hemonc.search("bendamustine hydrochloride")
     compare_response(response, MatchType.ALIAS, bendamustine)
 
-    response = hemonc.search('asp3550')
+    response = hemonc.search("asp3550")
     compare_response(response, MatchType.ALIAS, degarelix)
 
 
 def test_trade_name(hemonc, bendamustine, degarelix):
     """Test that trade name queries resolve to correct record."""
-    response = hemonc.search('bendamax')
+    response = hemonc.search("bendamax")
     compare_response(response, MatchType.TRADE_NAME, bendamustine)
 
-    response = hemonc.search('purplz')
+    response = hemonc.search("purplz")
     compare_response(response, MatchType.TRADE_NAME, bendamustine)
 
-    response = hemonc.search('firmagon')
+    response = hemonc.search("firmagon")
     compare_response(response, MatchType.TRADE_NAME, degarelix)
 
     # no trade names for records with > 20
-    response = hemonc.search('platinol')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = hemonc.search("platinol")
+    assert response["match_type"] == MatchType.NO_MATCH
 
 
 def test_xref_match(hemonc, cisplatin, bendamustine, degarelix):
     """Test that xref query resolves to correct record."""
-    response = hemonc.search('rxcui:2555')
+    response = hemonc.search("rxcui:2555")
     compare_response(response, MatchType.XREF, cisplatin)
 
-    response = hemonc.search('rxcui:134547')
+    response = hemonc.search("rxcui:134547")
     compare_response(response, MatchType.XREF, bendamustine)
 
-    response = hemonc.search('rxcui:475230')
+    response = hemonc.search("rxcui:475230")
     compare_response(response, MatchType.XREF, degarelix)
 
 
@@ -221,7 +222,7 @@ def test_metadata(hemonc):
     response = hemonc.fetch_meta()
     assert response.data_license == "CC BY 4.0"
     assert response.data_license_url == "https://creativecommons.org/licenses/by/4.0/legalcode"  # noqa: E501
-    assert re.match(r'202[0-9][01][0-9][0-3][0-9]', response.version)
+    assert re.match(r"202[0-9][01][0-9][0-3][0-9]", response.version)
     assert response.data_url == "https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/9CY9C6"  # noqa: E501
     assert response.rdp_url is None
     assert response.data_license_attributes == {
diff --git a/tests/unit/test_merge.py b/tests/unit/test_merge.py
index ae4e476c..f865a352 100644
--- a/tests/unit/test_merge.py
+++ b/tests/unit/test_merge.py
@@ -1,8 +1,10 @@
 """Test merged record generation."""
+from typing import Dict
+
 import pytest
+
 from therapy.etl.merge import Merge
 from therapy.schemas import ApprovalStatus
-from typing import Dict
 
 
 @pytest.fixture(scope="module")
@@ -19,10 +21,10 @@ def create_merged_concepts(self, record_ids):
             return self.merge.create_merged_concepts(record_ids)
 
         def get_added_records(self):
-            return self.merge._database.added_records
+            return self.merge._database.added_records  # type: ignore
 
         def get_updates(self):
-            return self.merge._database.updates
+            return self.merge._database.updates  # type: ignore
 
         def create_record_id_set(self, record_id):
             return self.merge._create_record_id_set(record_id)
@@ -31,7 +33,7 @@ def generate_merged_record(self, record_id_set):
             return self.merge._generate_merged_record(record_id_set)
 
         def get_created_id_groups(self):
-            return self.merge._groups()
+            return self.merge._groups()  # type: ignore
 
     return MergeHandler()
 
@@ -91,49 +93,49 @@ def phenobarbital_merged():
             "iuphar.ligand:2804"
         ],
         "aliases": [
-            '5-Ethyl-5-phenyl-2,4,6(1H,3H,5H)-pyrimidinetrione',
-            '5-Ethyl-5-phenyl-pyrimidine-2,4,6-trione',
-            '5-Ethyl-5-phenylbarbituric acid',
-            '5-Phenyl-5-ethylbarbituric acid',
-            '5-ethyl-5-phenyl-1,3-diazinane-2,4,6-trione',
-            '5-ethyl-5-phenyl-2,4,6(1H,3H,5H)-pyrimidinetrione',
-            '5-ethyl-5-phenylpyrimidine-2,4,6(1H,3H,5H)-trione',
-            'APRD00184',
-            'Acid, Phenylethylbarbituric',
-            'Eskabarb',
-            'Fenobarbital',
-            'Luminal',
-            'Luminal®',
-            'NSC-128143',
-            'NSC-128143-',
-            'NSC-9848',
-            'Noptil',
-            'PHENO',
-            'PHENOBARBITAL',
-            'PHENYLETHYLMALONYLUREA',
-            'PHENobarbital',
-            'Phenemal',
-            'Phenobarbital',
-            'Phenobarbital civ',
-            'Phenobarbitol',
-            'Phenobarbitone',
-            'Phenobarbituric Acid',
-            'Phenylaethylbarbitursaeure',
-            'Phenylbarbital',
-            'Phenylethylbarbiturate',
-            'Phenylethylbarbituric Acid',
-            'Phenylethylbarbitursaeure',
-            'Phenylethylbarbitursäure',
-            'Phenylethylmalonylurea',
-            'Phenyläthylbarbitursäure',
-            'Solfoton',
-            'Talpheno',
-            'fenobarbital',
-            'phenobarb',
-            'phenobarbital',
-            'phenobarbital sodium',
-            'phenobarbitone',
-            'phenylethylbarbiturate'
+            "5-Ethyl-5-phenyl-2,4,6(1H,3H,5H)-pyrimidinetrione",
+            "5-Ethyl-5-phenyl-pyrimidine-2,4,6-trione",
+            "5-Ethyl-5-phenylbarbituric acid",
+            "5-Phenyl-5-ethylbarbituric acid",
+            "5-ethyl-5-phenyl-1,3-diazinane-2,4,6-trione",
+            "5-ethyl-5-phenyl-2,4,6(1H,3H,5H)-pyrimidinetrione",
+            "5-ethyl-5-phenylpyrimidine-2,4,6(1H,3H,5H)-trione",
+            "APRD00184",
+            "Acid, Phenylethylbarbituric",
+            "Eskabarb",
+            "Fenobarbital",
+            "Luminal",
+            "Luminal®",
+            "NSC-128143",
+            "NSC-128143-",
+            "NSC-9848",
+            "Noptil",
+            "PHENO",
+            "PHENOBARBITAL",
+            "PHENYLETHYLMALONYLUREA",
+            "PHENobarbital",
+            "Phenemal",
+            "Phenobarbital",
+            "Phenobarbital civ",
+            "Phenobarbitol",
+            "Phenobarbitone",
+            "Phenobarbituric Acid",
+            "Phenylaethylbarbitursaeure",
+            "Phenylbarbital",
+            "Phenylethylbarbiturate",
+            "Phenylethylbarbituric Acid",
+            "Phenylethylbarbitursaeure",
+            "Phenylethylbarbitursäure",
+            "Phenylethylmalonylurea",
+            "Phenyläthylbarbitursäure",
+            "Solfoton",
+            "Talpheno",
+            "fenobarbital",
+            "phenobarb",
+            "phenobarbital",
+            "phenobarbital sodium",
+            "phenobarbitone",
+            "phenylethylbarbiturate"
         ],
         "associated_with": [
             "pubchem.compound:4763",
diff --git a/tests/unit/test_ncit_source.py b/tests/unit/test_ncit_source.py
index 0ac53133..6e0d594c 100644
--- a/tests/unit/test_ncit_source.py
+++ b/tests/unit/test_ncit_source.py
@@ -1,11 +1,12 @@
 """Test NCIT source"""
 import pytest
+
 from therapy.schemas import Drug, MatchType
 from therapy.query import QueryHandler
 from tests.conftest import compare_response
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def ncit():
     """Build NCIt normalizer test fixture."""
     class QueryGetter:
@@ -15,64 +16,64 @@ def __init__(self):
 
         def search(self, query_str):
             resp = self.query_handler.search_sources(query_str, keyed=True,
-                                                     incl='ncit')
-            return resp['source_matches']['NCIt']
+                                                     incl="ncit")
+            return resp["source_matches"]["NCIt"]
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def voglibose():
     """Create a voglibose drug fixture.."""
     params = {
-        'label': 'Voglibose',
-        'concept_id': 'ncit:C95221',
-        'aliases': ['3,4-Dideoxy-4-((2-Hydroxy-1-(Hydroxymethyl)Ethyl)Amino)-2-C-(Hydroxymethyl)-D-Epi-Inositol',  # noqa F401
-                    'A-71100', 'AO-128', 'Basen',
-                    'N-(1,3-Dihydroxy-2-Propyl)Valiolamine', 'VOGLIBOSE'],
-        'xrefs': ['chemidplus:83480-29-9'],
-        'associated_with': ['unii:S77P977AG8', 'umls:C0532578'],
-        'approval_status': None,
-        'trade_names': []
+        "label": "Voglibose",
+        "concept_id": "ncit:C95221",
+        "aliases": ["3,4-Dideoxy-4-((2-Hydroxy-1-(Hydroxymethyl)Ethyl)Amino)-2-C-(Hydroxymethyl)-D-Epi-Inositol",  # noqa F401
+                    "A-71100", "AO-128", "Basen",
+                    "N-(1,3-Dihydroxy-2-Propyl)Valiolamine", "VOGLIBOSE"],
+        "xrefs": ["chemidplus:83480-29-9"],
+        "associated_with": ["unii:S77P977AG8", "umls:C0532578"],
+        "approval_status": None,
+        "trade_names": []
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def apricoxib():
     """Create an apricoxib drug fixture."""
     params = {
-        'label': 'Apricoxib',
-        'concept_id': 'ncit:C74021',
-        'aliases': [
-            'APRICOXIB', 'COX-2 Inhibitor TG01', 'CS-706', 'R-109339',
-            'TG01', 'TP2001'
+        "label": "Apricoxib",
+        "concept_id": "ncit:C74021",
+        "aliases": [
+            "APRICOXIB", "COX-2 Inhibitor TG01", "CS-706", "R-109339",
+            "TG01", "TP2001"
         ],
-        'xrefs': ['chemidplus:197904-84-0'],
-        'associated_with': ['unii:5X5HB3VZ3Z', 'umls:C1737955'],
-        'approval_status': None,
-        'trade_names': []
+        "xrefs": ["chemidplus:197904-84-0"],
+        "associated_with": ["unii:5X5HB3VZ3Z", "umls:C1737955"],
+        "approval_status": None,
+        "trade_names": []
     }
     return Drug(**params)
 
 
 # Test aliases > 20
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def trastuzumab():
     """Create a Trastuzumab drug fixture."""
     params = {
-        'label': 'Trastuzumab',
-        'concept_id': 'ncit:C1647',
-        'aliases': [],
-        'xrefs': ['chemidplus:180288-69-1'],
-        'associated_with': ['umls:C0728747', 'unii:P188ANX8CK'],
-        'approval_status': None,
-        'trade_names': []
+        "label": "Trastuzumab",
+        "concept_id": "ncit:C1647",
+        "aliases": [],
+        "xrefs": ["chemidplus:180288-69-1"],
+        "associated_with": ["umls:C0728747", "unii:P188ANX8CK"],
+        "approval_status": None,
+        "trade_names": []
     }
     return Drug(**params)
 
 
 # Needed for MetaKB
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def therapeutic_procedure():
     """Create a fixture for the Therapeutic Procedure class."""
     params = {
@@ -89,8 +90,9 @@ def therapeutic_procedure():
             "TREAT",
             "Treatment",
             "TX",
-            "therapeutic intervention",
-            "treatment"
+            "treatment",
+            "treatment or therapy",
+            "treatment_or_therapy",
         ],
         "trade_names": [],
         "xrefs": [],
@@ -100,7 +102,7 @@ def therapeutic_procedure():
 
 
 # test correct CHEBI naming
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def ivermectin():
     """Create a fixture for Ivermectin."""
     params = {
@@ -130,133 +132,132 @@ def ivermectin():
 def test_concept_id_match(ncit, voglibose, apricoxib, trastuzumab,
                           therapeutic_procedure, ivermectin):
     """Test that concept ID query resolves to correct record."""
-    response = ncit.search('ncit:C95221')
+    response = ncit.search("ncit:C95221")
     compare_response(response, MatchType.CONCEPT_ID, voglibose)
 
-    response = ncit.search('NCIT:C95221')
+    response = ncit.search("NCIT:C95221")
     compare_response(response, MatchType.CONCEPT_ID, voglibose)
 
-    response = ncit.search('NCIt:c95221')
+    response = ncit.search("NCIt:c95221")
     compare_response(response, MatchType.CONCEPT_ID, voglibose)
 
-    response = ncit.search('C95221')
+    response = ncit.search("C95221")
     compare_response(response, MatchType.CONCEPT_ID, voglibose)
 
-    response = ncit.search('ncit:C74021')
+    response = ncit.search("ncit:C74021")
     compare_response(response, MatchType.CONCEPT_ID, apricoxib)
 
-    response = ncit.search('NCIt:C74021')
+    response = ncit.search("NCIt:C74021")
     compare_response(response, MatchType.CONCEPT_ID, apricoxib)
 
-    response = ncit.search('ncit:c74021')
+    response = ncit.search("ncit:c74021")
     compare_response(response, MatchType.CONCEPT_ID, apricoxib)
 
-    response = ncit.search('C74021')
+    response = ncit.search("C74021")
     compare_response(response, MatchType.CONCEPT_ID, apricoxib)
 
-    response = ncit.search('ncit:C1647')
+    response = ncit.search("ncit:C1647")
     compare_response(response, MatchType.CONCEPT_ID, trastuzumab)
 
-    response = ncit.search('NCIT:C1647')
+    response = ncit.search("NCIT:C1647")
     compare_response(response, MatchType.CONCEPT_ID, trastuzumab)
 
-    response = ncit.search('NCIt:c1647')
+    response = ncit.search("NCIt:c1647")
     compare_response(response, MatchType.CONCEPT_ID, trastuzumab)
 
-    response = ncit.search('C1647')
+    response = ncit.search("C1647")
     compare_response(response, MatchType.CONCEPT_ID, trastuzumab)
 
-    response = ncit.search('ncit:C49236')
+    response = ncit.search("ncit:C49236")
     compare_response(response, MatchType.CONCEPT_ID, therapeutic_procedure)
 
-    response = ncit.search('NCIT:C61796')
+    response = ncit.search("NCIT:C61796")
     compare_response(response, MatchType.CONCEPT_ID, ivermectin)
 
 
 def test_label_match(ncit, voglibose, apricoxib, trastuzumab):
     """Test that label query resolves to correct record."""
-    response = ncit.search('voglibose')
+    response = ncit.search("voglibose")
     compare_response(response, MatchType.LABEL, voglibose)
 
-    response = ncit.search('voglibose')
+    response = ncit.search("voglibose")
     compare_response(response, MatchType.LABEL, voglibose)
 
-    response = ncit.search('trastuzumab')
+    response = ncit.search("trastuzumab")
     compare_response(response, MatchType.LABEL, trastuzumab)
 
-    response = ncit.search('Trastuzumab')
+    response = ncit.search("Trastuzumab")
     compare_response(response, MatchType.LABEL, trastuzumab)
 
-    response = ncit.search('Apricoxib')
+    response = ncit.search("Apricoxib")
     compare_response(response, MatchType.LABEL, apricoxib)
 
-    response = ncit.search('APRICOXIB')
+    response = ncit.search("APRICOXIB")
     compare_response(response, MatchType.LABEL, apricoxib)
 
 
 def test_alias_match(ncit, voglibose, apricoxib):
     """Test that alias query resolves to correct record."""
-    response = ncit.search('BASEN')
+    response = ncit.search("BASEN")
     compare_response(response, MatchType.ALIAS, voglibose)
 
-    response = ncit.search('AO-128')
+    response = ncit.search("AO-128")
     compare_response(response, MatchType.ALIAS, voglibose)
 
-    response = ncit.search('COX-2 Inhibitor TG01')
+    response = ncit.search("COX-2 Inhibitor TG01")
     compare_response(response, MatchType.ALIAS, apricoxib)
 
 
 def test_xref_match(ncit, voglibose, apricoxib, trastuzumab):
     """Test that xref query resolves to correct record."""
-    response = ncit.search('chemidplus:83480-29-9')
+    response = ncit.search("chemidplus:83480-29-9")
     compare_response(response, MatchType.XREF, voglibose)
 
-    response = ncit.search('CHEMIDPLUS:197904-84-0')
+    response = ncit.search("CHEMIDPLUS:197904-84-0")
     compare_response(response, MatchType.XREF, apricoxib)
 
-    response = ncit.search('chemidplus:180288-69-1')
+    response = ncit.search("chemidplus:180288-69-1")
     compare_response(response, MatchType.XREF, trastuzumab)
 
 
 def test_assoc_with_match(ncit, voglibose, apricoxib, trastuzumab):
     """Test that associated_with query resolves to correct record."""
-    response = ncit.search('unii:S77P977AG8')
+    response = ncit.search("unii:S77P977AG8")
     compare_response(response, MatchType.ASSOCIATED_WITH, voglibose)
 
-    response = ncit.search('unii:5X5HB3VZ3Z')
+    response = ncit.search("unii:5X5HB3VZ3Z")
     compare_response(response, MatchType.ASSOCIATED_WITH, apricoxib)
 
-    response = ncit.search('unii:P188ANX8CK')
+    response = ncit.search("unii:P188ANX8CK")
     compare_response(response, MatchType.ASSOCIATED_WITH, trastuzumab)
 
 
 def test_no_match(ncit):
     """Test that a term normalizes to NO match."""
-    response = ncit.search('voglibo')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = ncit.search("voglibo")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
     # Test white space in between label
-    response = ncit.search('Volgibo')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = ncit.search("Volgibo")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Test empty query
-    response = ncit.search('')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = ncit.search("")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
 
 def test_meta_info(ncit):
     """Test that the meta field is correct."""
-    response = ncit.search('voglibose')
-    assert response['source_meta_']['data_license'] == 'CC BY 4.0'
-    assert response['source_meta_']['data_license_url'] == \
-        'https://creativecommons.org/licenses/by/4.0/legalcode'
-    assert response['source_meta_']['version'] == '20.09d'
-    assert response['source_meta_']['data_url'] == \
-        "https://evs.nci.nih.gov/ftp1/NCI_Thesaurus/archive/2020/20.09d_Release/"  # noqa: E501
-    assert response['source_meta_']['rdp_url'] == 'http://reusabledata.org/ncit.html'  # noqa: E501
-    assert response['source_meta_']['data_license_attributes'] == {
+    response = ncit.search("voglibose")
+    assert response["source_meta_"]["data_license"] == "CC BY 4.0"
+    assert response["source_meta_"]["data_license_url"] == \
+        "https://creativecommons.org/licenses/by/4.0/legalcode"
+    assert response["source_meta_"]["version"] == "21.08e"
+    assert response["source_meta_"]["data_url"].startswith("https://evs.nci.nih.gov/ftp1/NCI_Thesaurus/archive/")  # noqa: E501
+    assert response["source_meta_"]["rdp_url"] == "http://reusabledata.org/ncit.html"
+    assert response["source_meta_"]["data_license_attributes"] == {
         "non_commercial": False,
         "share_alike": False,
         "attribution": True
diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py
index 9193dc4f..3510f4d6 100644
--- a/tests/unit/test_query.py
+++ b/tests/unit/test_query.py
@@ -1,10 +1,12 @@
 """Test the therapy querying method."""
-from therapy.query import QueryHandler, InvalidParameterException
-from therapy.schemas import MatchType
-import pytest
 from datetime import datetime
 import os
 
+import pytest
+
+from therapy.query import QueryHandler, InvalidParameterException
+from therapy.schemas import MatchType
+
 
 @pytest.fixture(scope="module")
 def query_handler():
@@ -28,7 +30,7 @@ def merge_query_handler(mock_database):
     """Provide Merge instance to test cases."""
     class QueryGetter:
         def __init__(self):
-            self.query_handler = QueryHandler(db_url="http://localhost:8000")
+            self.query_handler = QueryHandler()
             if os.environ.get("TEST") is not None:
                 self.query_handler.db = mock_database()  # replace initial DB
 
@@ -307,8 +309,9 @@ def therapeutic_procedure():
             "TREAT",
             "Treatment",
             "TX",
-            "therapeutic intervention",
-            "treatment"
+            "treatment",
+            "treatment_or_therapy",
+            "treatment or therapy",
         ],
         "extensions": [
             {
diff --git a/tests/unit/test_rxnorm_source.py b/tests/unit/test_rxnorm_source.py
index 51a2fe4b..7bd26bcc 100644
--- a/tests/unit/test_rxnorm_source.py
+++ b/tests/unit/test_rxnorm_source.py
@@ -1,512 +1,514 @@
 """Test that the therapy normalizer works as intended for the RxNorm source."""
+import os
+from datetime import datetime as dt
+
 import pytest
+from boto3.dynamodb.conditions import Key
+
 from tests.conftest import compare_records
 from therapy.schemas import Drug, MatchType
 from therapy.query import QueryHandler
 from therapy.database import Database
-import os
-from boto3.dynamodb.conditions import Key
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def rxnorm():
     """Build RxNorm normalizer test fixture."""
     class QueryGetter:
 
         def __init__(self):
             self.normalizer = QueryHandler()
-            if 'THERAPY_NORM_DB_URL' in os.environ:
-                db_url = os.environ['THERAPY_NORM_DB_URL']
+            if "THERAPY_NORM_DB_URL" in os.environ:
+                db_url = os.environ["THERAPY_NORM_DB_URL"]
             else:
-                db_url = 'http://localhost:8000'
+                db_url = "http://localhost:8000"
             self.db = Database(db_url=db_url)
 
         def search(self, query_str):
             resp = self.normalizer.search_sources(query_str, keyed=True,
-                                                  incl='rxnorm')
-            return resp['source_matches']['RxNorm']
+                                                  incl="rxnorm")
+            return resp["source_matches"]["RxNorm"]
 
         def fetch_meta(self):
-            return self.normalizer._fetch_meta('RxNorm')
+            return self.normalizer._fetch_meta("RxNorm")
 
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def bifidobacterium_infantis():
     """Create bifidobacterium infantis drug fixture."""
     params = {
-        'label': 'Bifidobacterium Infantis',
-        'concept_id': 'rxcui:100213',
-        'aliases': [
-            'bifidobacterium infantis',
-            'Bifidobacterium infantis'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB14222'
-        ],
-        'associated_with': [
-            'mmsl:d07347'
-        ],
-        'trade_names': [
-            'Align',
-            'Evivo'
+        "label": "Bifidobacterium Infantis",
+        "concept_id": "rxcui:100213",
+        "aliases": [
+            "bifidobacterium infantis",
+            "Bifidobacterium infantis"
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB14222"
+        ],
+        "associated_with": [
+            "mmsl:d07347"
+        ],
+        "trade_names": [
+            "Align",
+            "Evivo"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def cisplatin():
     """Create cisplatin drug fixture."""
     params = {
-        'label': 'cisplatin',
-        'concept_id': 'rxcui:2555',
-        'aliases': [
-            'cis-Platinum',
-            'CISplatin',
-            'cis Diamminedichloroplatinum',
-            'cis Platinum',
-            'Diamminodichloride, Platinum',
-            'cis-Diamminedichloroplatinum',
-            'DDP',
-            'Dichlorodiammineplatinum',
-            'cis-Dichlorodiammineplatinum(II)',
-            'Platinum Diamminodichloride',
-            'cis-Diamminedichloroplatinum(II)',
-            'cis-diamminedichloroplatinum(II)',
-            'CDDP',
-            'Cis-DDP',
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB00515',
-            'drugbank:DB12117'
-        ],
-        'associated_with': [
-            'usp:m17910',
-            'vandf:4018139',
-            'mesh:D002945',
-            'unii:Q20Q21Q62J',
-            'mmsl:d00195',
-            'atc:L01XA01',
-            'mmsl:31747',
-            'mmsl:4456',
-        ],
-        'trade_names': [
-            'Cisplatin',
-            'Platinol'
+        "label": "cisplatin",
+        "concept_id": "rxcui:2555",
+        "aliases": [
+            "cis-Platinum",
+            "CISplatin",
+            "cis Diamminedichloroplatinum",
+            "cis Platinum",
+            "Diamminodichloride, Platinum",
+            "cis-Diamminedichloroplatinum",
+            "DDP",
+            "Dichlorodiammineplatinum",
+            "cis-Dichlorodiammineplatinum(II)",
+            "Platinum Diamminodichloride",
+            "cis-Diamminedichloroplatinum(II)",
+            "cis-diamminedichloroplatinum(II)",
+            "CDDP",
+            "Cis-DDP",
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB00515",
+            "drugbank:DB12117"
+        ],
+        "associated_with": [
+            "usp:m17910",
+            "vandf:4018139",
+            "mesh:D002945",
+            "unii:Q20Q21Q62J",
+            "mmsl:d00195",
+            "atc:L01XA01",
+            "mmsl:31747",
+            "mmsl:4456",
+        ],
+        "trade_names": [
+            "Cisplatin",
+            "Platinol"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def amiloride_hydrochloride():
     """Create amiloride hydrochloride drug fixture."""
     params = {
-        'label': 'amiloride hydrochloride',
-        'concept_id': 'rxcui:142424',
-        'aliases': [
-            'aMILoride hydrochloride',
-            'Amiloride Hydrochloride',
-            'Hydrochloride, Amiloride'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [
-            'usp:m2650',
-            'unii:FZJ37245UC',
-            'mmsl:2658',
-            'mesh:D000584',
-            'mmsl:4166',
-            'vandf:4017935'
-        ],
-        'trade_names': [
-            'AMILoride Hydrochloride',
-            'Midamor',
-            'Aridil',
-            'Frusemek',
-            'Midamor',
-            'Moduret',
-            'Moduretic',
-            'Zida-Co'
+        "label": "amiloride hydrochloride",
+        "concept_id": "rxcui:142424",
+        "aliases": [
+            "aMILoride hydrochloride",
+            "Amiloride Hydrochloride",
+            "Hydrochloride, Amiloride"
+        ],
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [
+            "usp:m2650",
+            "unii:FZJ37245UC",
+            "mmsl:2658",
+            "mesh:D000584",
+            "mmsl:4166",
+            "vandf:4017935"
+        ],
+        "trade_names": [
+            "AMILoride Hydrochloride",
+            "Midamor",
+            "Aridil",
+            "Frusemek",
+            "Midamor",
+            "Moduret",
+            "Moduretic",
+            "Zida-Co"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def amiloride():
     """Create amiloride drug fixture."""
     params = {
-        'label': 'amiloride',
-        'concept_id': 'rxcui:644',
-        'aliases': [
-            '3,5-diamino-N-carbamimidoyl-6-chloropyrazine-2-carboxamide',
-            'aMILoride',
-            'Amipramidin',
-            'Amipramidine',
-            'N-amidino-3,5-diamino-6-chloropyrazinecarboxamide',
-            'Amyloride'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB00594'
-        ],
-        'associated_with': [
-            'mesh:D000584',
-            'vandf:4019603',
-            'unii:7DZO8EB0Z3',
-            'mmsl:d00169',
-            'atc:C03DB01'
-        ],
-        'trade_names': [
-            'Midamor',
-            'Frusemek',
-            'Moduret',
-            'Aridil',
-            'Zida-Co',
-            'Amilamont',
-            'Moduretic'
+        "label": "amiloride",
+        "concept_id": "rxcui:644",
+        "aliases": [
+            "3,5-diamino-N-carbamimidoyl-6-chloropyrazine-2-carboxamide",
+            "aMILoride",
+            "Amipramidin",
+            "Amipramidine",
+            "N-amidino-3,5-diamino-6-chloropyrazinecarboxamide",
+            "Amyloride"
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB00594"
+        ],
+        "associated_with": [
+            "mesh:D000584",
+            "vandf:4019603",
+            "unii:7DZO8EB0Z3",
+            "mmsl:d00169",
+            "atc:C03DB01"
+        ],
+        "trade_names": [
+            "Midamor",
+            "Frusemek",
+            "Moduret",
+            "Aridil",
+            "Zida-Co",
+            "Amilamont",
+            "Moduretic"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def timolol():
     """Create timolol drug fixture."""
     params = {
-        'label': 'timolol',
-        'concept_id': 'rxcui:10600',
-        'aliases': [
-            '(S)-1-((1,1-Dimethylethyl)amino)-3-((4-(4-morpholinyl)'
-            '-1,2,5-thiadazol-3-yl)oxy)-2-propanol',
-            '2-Propanol, 1-((1,1-dimethylethyl)amino)-3-((4-(4-morpholi'
-            'nyl)-1,2,5-thiadiazol-3-yl)oxy)-, (S)-',
-            '(S)-1-(tert-butylamino)-3-[(4-morpholin-4-yl-1,2,5-thiadiazol'
-            '-3-yl)oxy]propan-2-ol'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB00373'
-        ],
-        'associated_with': [
-            'vandf:4019949',
-            'unii:817W3C6175',
-            'mesh:D013999',
-            'mmsl:d00139',
-            'atc:C07AA06',
-            'atc:S01ED01'
-        ],
-        'trade_names': [
-            'Betimol',
-            'Timoptic',
-            'Istalol',
-            'Combigan',
-            'Cosopt',
-            'Timoptol LA',
-            'Blocadren',
-            'Timolide',
-            'Betim',
-            'Glau-Opt',
-            'Glaucol',
-            'Timoptol'
+        "label": "timolol",
+        "concept_id": "rxcui:10600",
+        "aliases": [
+            "(S)-1-((1,1-Dimethylethyl)amino)-3-((4-(4-morpholinyl)"
+            "-1,2,5-thiadazol-3-yl)oxy)-2-propanol",
+            "2-Propanol, 1-((1,1-dimethylethyl)amino)-3-((4-(4-morpholi"
+            "nyl)-1,2,5-thiadiazol-3-yl)oxy)-, (S)-",
+            "(S)-1-(tert-butylamino)-3-[(4-morpholin-4-yl-1,2,5-thiadiazol"
+            "-3-yl)oxy]propan-2-ol"
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB00373"
+        ],
+        "associated_with": [
+            "vandf:4019949",
+            "unii:817W3C6175",
+            "mesh:D013999",
+            "mmsl:d00139",
+            "atc:C07AA06",
+            "atc:S01ED01"
+        ],
+        "trade_names": [
+            "Betimol",
+            "Timoptic",
+            "Istalol",
+            "Combigan",
+            "Cosopt",
+            "Timoptol LA",
+            "Blocadren",
+            "Timolide",
+            "Betim",
+            "Glau-Opt",
+            "Glaucol",
+            "Timoptol"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def lymphocyte():
     """Create lymphocyte immune globulin, anti-thymocyte globulin drug
     fixture.
     """
     params = {
-        'label': 'lymphocyte immune globulin, anti-thymocyte globulin',
-        'concept_id': 'rxcui:1011',
-        'aliases': [
-            'Lymphocyte Immune Globulin, Anti-Thymocyte Globulin',
-            'Anti-Thymocyte Globulin',
-            'Antithymocyte Globulin',
-            'Antithymoglobulin',
-            'Lymphocyte Immune Globulin, Anti Thymocyte Globulin',
-            'Globulins, Antithymocyte',
-            'Antithymocyte Globulins',
-            'Antithymoglobulins',
-            'Anti-Thymocyte Globulins',
-            'Globulins, Anti-Thymocyte',
-            'Globulin, Antithymocyte',
-            'Anti Thymocyte Globulin',
-            'Globulin, Anti-Thymocyte',
-            'lymphocyte immune globulin, anti-thy (obs)'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [
-            'mesh:D000961',
-            'vandf:4022194',
-            'vandf:4018097',
-            'mmsl:d01141',
-            'mmsl:5001'
-        ],
-        'trade_names': [
-            'ATGAM',
-            'Thymoglobulin'
+        "label": "lymphocyte immune globulin, anti-thymocyte globulin",
+        "concept_id": "rxcui:1011",
+        "aliases": [
+            "Lymphocyte Immune Globulin, Anti-Thymocyte Globulin",
+            "Anti-Thymocyte Globulin",
+            "Antithymocyte Globulin",
+            "Antithymoglobulin",
+            "Lymphocyte Immune Globulin, Anti Thymocyte Globulin",
+            "Globulins, Antithymocyte",
+            "Antithymocyte Globulins",
+            "Antithymoglobulins",
+            "Anti-Thymocyte Globulins",
+            "Globulins, Anti-Thymocyte",
+            "Globulin, Antithymocyte",
+            "Anti Thymocyte Globulin",
+            "Globulin, Anti-Thymocyte",
+            "lymphocyte immune globulin, anti-thy (obs)"
+        ],
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [
+            "mesh:D000961",
+            "vandf:4022194",
+            "vandf:4018097",
+            "mmsl:d01141",
+            "mmsl:5001"
+        ],
+        "trade_names": [
+            "ATGAM",
+            "Thymoglobulin"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def aspirin():
     """Create aspirin drug fixture."""
     params = {
-        'label': 'aspirin',
-        'concept_id': 'rxcui:1191',
-        'aliases': [
-            'Salicylic acid acetate',
-            'Aspirin',
-            'Acid, Acetylsalicylic',
-            '2-(Acetyloxy)benzoic Acid',
-            '2-Acetoxybenzenecarboxylic acid',
-            'ASA',
-            'O-acetylsalicylic acid',
-            '2-Acetoxybenzoic acid',
-            'o-acetoxybenzoic acid',
-            'o-carboxyphenyl acetate',
-            'Acetylsalicylic Acid'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB00945'
-        ],
-        'associated_with': [
-            'usp:m6240',
-            'vandf:4017536',
-            'mmsl:34512',
-            'unii:R16CO5Y76E',
-            'mmsl:244',
-            'mesh:D001241',
-            'mmsl:4223',
-            'mmsl:d00170',
-            'atc:A01AD05',
-            'atc:B01AC06',
-            'atc:N02BA01'
-        ],
-        'trade_names': []
+        "label": "aspirin",
+        "concept_id": "rxcui:1191",
+        "aliases": [
+            "Salicylic acid acetate",
+            "Aspirin",
+            "Acid, Acetylsalicylic",
+            "2-(Acetyloxy)benzoic Acid",
+            "2-Acetoxybenzenecarboxylic acid",
+            "ASA",
+            "O-acetylsalicylic acid",
+            "2-Acetoxybenzoic acid",
+            "o-acetoxybenzoic acid",
+            "o-carboxyphenyl acetate",
+            "Acetylsalicylic Acid"
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB00945"
+        ],
+        "associated_with": [
+            "usp:m6240",
+            "vandf:4017536",
+            "mmsl:34512",
+            "unii:R16CO5Y76E",
+            "mmsl:244",
+            "mesh:D001241",
+            "mmsl:4223",
+            "mmsl:d00170",
+            "atc:A01AD05",
+            "atc:B01AC06",
+            "atc:N02BA01"
+        ],
+        "trade_names": []
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def mesna():
     """Create mesna drug fixture."""
     params = {
-        'label': 'mesna',
-        'concept_id': 'rxcui:44',
-        'aliases': [
-            'Mesnum',
-            'Ethanesulfonic acid, 2-mercapto-, monosodium salt',
-            'Sodium 2-Mercaptoethanesulphonate',
-            '2-Mercaptoethanesulphonate, Sodium',
-            'Sodium 2-Mercaptoethanesulphonate'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [
-            'usp:m49500',
-            'mesh:D015080',
-            'vandf:4019477',
-            'unii:NR7O1405Q9',
-            'atc:R05CB05',
-            'atc:V03AF01',
-            'mmsl:41498',
-            'mmsl:5057',
-            'mmsl:d01411'
-        ],
-        'trade_names': [
-            'Mesna',
-            'Mesnex',
-            'Mesna Nova Plus'
+        "label": "mesna",
+        "concept_id": "rxcui:44",
+        "aliases": [
+            "Mesnum",
+            "Ethanesulfonic acid, 2-mercapto-, monosodium salt",
+            "Sodium 2-Mercaptoethanesulphonate",
+            "2-Mercaptoethanesulphonate, Sodium",
+            "Sodium 2-Mercaptoethanesulphonate"
+        ],
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [
+            "usp:m49500",
+            "mesh:D015080",
+            "vandf:4019477",
+            "unii:NR7O1405Q9",
+            "atc:R05CB05",
+            "atc:V03AF01",
+            "mmsl:41498",
+            "mmsl:5057",
+            "mmsl:d01411"
+        ],
+        "trade_names": [
+            "Mesna",
+            "Mesnex",
+            "Mesna Nova Plus"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def beta_alanine():
     """Create beta-alanine drug fixture."""
     params = {
-        'label': 'beta-alanine',
-        'concept_id': 'rxcui:61',
-        'aliases': [
-            'beta Alanine',
-            '3 Aminopropionic Acid',
-            '3-Aminopropionic Acid'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [
-            'mesh:D015091',
-            'vandf:4028377',
-            'unii:11P2JDE17B'
-        ],
-        'trade_names': []
+        "label": "beta-alanine",
+        "concept_id": "rxcui:61",
+        "aliases": [
+            "beta Alanine",
+            "3 Aminopropionic Acid",
+            "3-Aminopropionic Acid"
+        ],
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [
+            "mesh:D015091",
+            "vandf:4028377",
+            "unii:11P2JDE17B"
+        ],
+        "trade_names": []
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def algestone():
     """Create algestone drug fixture."""
     params = {
-        'label': 'algestone',
-        'concept_id': 'rxcui:595',
-        'aliases': [
-            'Pregn-4-ene-3,20-dione, 16,17-dihydroxy-, (16alpha)-',
-            '16 alpha,17-Dihydroxypregn-4-ene-3,20-dione',
-            'Alphasone',
-            'Dihydroxyprogesterone'
-        ],
-        'approval_status': None,
-        'xrefs': [],
-        'associated_with': [
-            'mesh:D000523'
-        ],
-        'trade_names': []
+        "label": "algestone",
+        "concept_id": "rxcui:595",
+        "aliases": [
+            "Pregn-4-ene-3,20-dione, 16,17-dihydroxy-, (16alpha)-",
+            "16 alpha,17-Dihydroxypregn-4-ene-3,20-dione",
+            "Alphasone",
+            "Dihydroxyprogesterone"
+        ],
+        "approval_status": None,
+        "xrefs": [],
+        "associated_with": [
+            "mesh:D000523"
+        ],
+        "trade_names": []
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def levothyroxine():
     """Create levothyroxine drug fixture."""
     params = {
-        'label': 'levothyroxine',
-        'concept_id': 'rxcui:10582',
-        'aliases': [
-            '3,5,3\',5\'-Tetraiodo-L-thyronine',
-            'Thyroxine',
-            'thyroxine',
-            'Thyroid Hormone, T4',
-            'Thyroxin',
-            'T4 Thyroid Hormone',
-            'O-(4-Hydroxy-3,5-diiodophenyl)-3,5-diiodotyrosine',
-            '3,5,3\',5\'-Tetraiodothyronine',
-            'O-(4-Hydroxy-3,5-diiodophenyl)-3,5-diiodo-L-tyrosine',
-            'L-T4',
-            'LT4',
-            'T4',
-            '3,3\',5,5\'-Tetraiodo-L-thyronine',
-            'L-Thyroxine',
-            'O-(4-Hydroxy-3,5-diidophenyl)-3,5-diiodo-L-tyrosine',
-            '4-(4-Hydroxy-3,5-diiodophenoxy)-3,5-diiodo-L-phenylalanine',
-            'Levothyroxin'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB00451'
-        ],
-        'associated_with': [
-            'vandf:4022126',
-            'mesh:D013974',
-            'mmsl:d00278',
-            'unii:Q51BO43MG4'
-        ],
-        'trade_names': [
-            'Eltroxin',
-            'Estre',
-            'Euthyrox',
-            'Leventa',
-            'Levo-T',
-            'Levocrine',
-            'Levotabs',
-            'Levothroid',
-            'Levothyroid',
-            'Levoxyl',
-            'Novothyrox',
-            'SOLOXINE',
-            'Synthroid',
-            'Thyro-Tabs',
-            'ThyroKare',
-            'ThyroMed',
-            'Thyrolar',
-            'Thyrox',
-            'Tirosint',
-            'Unithroid'
+        "label": "levothyroxine",
+        "concept_id": "rxcui:10582",
+        "aliases": [
+            "3,5,3',5'-Tetraiodo-L-thyronine",
+            "Thyroxine",
+            "Thyroid Hormone, T4",
+            "Thyroxin",
+            "T4 Thyroid Hormone",
+            "O-(4-Hydroxy-3,5-diiodophenyl)-3,5-diiodotyrosine",
+            "3,5,3',5'-Tetraiodothyronine",
+            "O-(4-Hydroxy-3,5-diiodophenyl)-3,5-diiodo-L-tyrosine",
+            "L-T4",
+            "LT4",
+            "T4",
+            "3,3',5,5'-Tetraiodo-L-thyronine",
+            "L-Thyroxine",
+            "O-(4-Hydroxy-3,5-diidophenyl)-3,5-diiodo-L-tyrosine",
+            "4-(4-Hydroxy-3,5-diiodophenoxy)-3,5-diiodo-L-phenylalanine",
+            "Levothyroxin"
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB00451"
+        ],
+        "associated_with": [
+            "vandf:4022126",
+            "mesh:D013974",
+            "mmsl:d00278",
+            "unii:Q51BO43MG4"
+        ],
+        "trade_names": [
+            "Eltroxin",
+            "Estre",
+            "Euthyrox",
+            "Leventa",
+            "Levo-T",
+            "Levocrine",
+            "Levotabs",
+            "Levothroid",
+            "Levothyroid",
+            "Levoxyl",
+            "Novothyrox",
+            "SOLOXINE",
+            "Synthroid",
+            "Thyro-Tabs",
+            "ThyroKare",
+            "ThyroMed",
+            "Thyrolar",
+            "Thyrox",
+            "Tirosint",
+            "Unithroid"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def fluoxetine():
     """Create fluoxetine drug fixture."""
     params = {
-        'label': 'fluoxetine',
-        'concept_id': 'rxcui:4493',
-        'aliases': [
-            'FLUoxetine',
-            'N-Methyl-gamma-(4-(trifluoromethyl)phenoxy)'
-            'benzenepropanamine',
-            'Fluoxetin',
-            '(+-)-N-Methyl-gamma-(4-(trifluoromethyl)phenoxy)'
-            'benzenepropanamine',
-            '(+-)-N-Methyl-3-phenyl-3-((alpha,alpha,alpha-trifluoro-'
-            'P-tolyl)oxy)propylamine'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [
-            'drugbank:DB00472'
-        ],
-        'associated_with': [
-            'mesh:D005473',
-            'mmsl:17711',
-            'vandf:4019761',
-            'mmsl:d00236',
-            'atc:N06AB03',
-            'unii:01K63SUP8D'
-        ],
-        'trade_names': [
-            'Prozac',
-            'RECONCILE',
-            'Sarafem',
-            'Symbyax',
-            'Rapiflux',
-            'Selfemra'
+        "label": "fluoxetine",
+        "concept_id": "rxcui:4493",
+        "aliases": [
+            "FLUoxetine",
+            "N-Methyl-gamma-(4-(trifluoromethyl)phenoxy)"
+            "benzenepropanamine",
+            "Fluoxetin",
+            "(+-)-N-Methyl-gamma-(4-(trifluoromethyl)phenoxy)"
+            "benzenepropanamine",
+            "(+-)-N-Methyl-3-phenyl-3-((alpha,alpha,alpha-trifluoro-"
+            "P-tolyl)oxy)propylamine"
+        ],
+        "approval_status": "approved",
+        "xrefs": [
+            "drugbank:DB00472"
+        ],
+        "associated_with": [
+            "mesh:D005473",
+            "mmsl:17711",
+            "vandf:4019761",
+            "mmsl:d00236",
+            "atc:N06AB03",
+            "unii:01K63SUP8D"
+        ],
+        "trade_names": [
+            "Prozac",
+            "RECONCILE",
+            "Sarafem",
+            "Symbyax",
+            "Rapiflux",
+            "Selfemra"
         ]
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def fluoxetine_hydrochloride():
     """Create fluoxetine hydrochloride drug fixture."""
     params = {
-        'label': 'fluoxetine hydrochloride',
-        'concept_id': 'rxcui:227224',
-        'aliases': [
-            'FLUoxetine hydrochloride',
-            'Fluoxetine Hydrochloride'
-        ],
-        'approval_status': 'approved',
-        'xrefs': [],
-        'associated_with': [
-            'usp:m33780',
-            'unii:I9W7N6B1KJ',
-            'mmsl:4746',
-            'vandf:4019389',
-            'mmsl:41730',
-            'mmsl:37675',
-            'mesh:D005473'
-        ],
-        'trade_names': [
-            'FLUoxetine HCl',
-            'FLUoxetine Hydrochloride',
-            'RECONCILE'
+        "label": "fluoxetine hydrochloride",
+        "concept_id": "rxcui:227224",
+        "aliases": [
+            "FLUoxetine hydrochloride",
+            "Fluoxetine Hydrochloride"
+        ],
+        "approval_status": "approved",
+        "xrefs": [],
+        "associated_with": [
+            "usp:m33780",
+            "unii:I9W7N6B1KJ",
+            "mmsl:4746",
+            "vandf:4019389",
+            "mmsl:41730",
+            "mmsl:37675",
+            "mesh:D005473"
+        ],
+        "trade_names": [
+            "FLUoxetine HCl",
+            "FLUoxetine Hydrochloride",
+            "RECONCILE"
         ]
     }
     return Drug(**params)
@@ -517,81 +519,81 @@ def test_bifidobacterium_infantis(bifidobacterium_infantis, rxnorm):
     correct drug concept.
     """
     # Concept ID Match
-    response = rxnorm.search('RxCUI:100213')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search("RxCUI:100213")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
     # Label Match
-    response = rxnorm.search(' Bifidobacterium infantis')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search(" Bifidobacterium infantis")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
-    response = rxnorm.search('bifidobacterium infantis')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search("bifidobacterium infantis")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
     # Trade Name Match
-    response = rxnorm.search('ALIGN')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search("ALIGN")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
-    response = rxnorm.search('evivo')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search("evivo")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
     # Xref Match
-    response = rxnorm.search('drugbank:DB14222')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search("drugbank:DB14222")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
 
 def test_cisplatin(cisplatin, rxnorm):
     """Test that cisplatin drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:2555')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("RxCUI:2555")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
     # Label Match
-    response = rxnorm.search('CISPLATIN')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("CISPLATIN")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
     # Alias Match
-    response = rxnorm.search('Cis-DDP')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("Cis-DDP")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = rxnorm.search('Dichlorodiammineplatinum')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("Dichlorodiammineplatinum")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = rxnorm.search('cis Platinum')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("cis Platinum")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
     # Trade Name Match
-    response = rxnorm.search('platinol')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("platinol")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
     # Xref Match
-    response = rxnorm.search('drugbank:DB12117')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("drugbank:DB12117")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
 
 def test_amiloride_hydrochloride(amiloride_hydrochloride, rxnorm):
@@ -599,21 +601,21 @@ def test_amiloride_hydrochloride(amiloride_hydrochloride, rxnorm):
     normalizes to correct drug concept.
     """
     # Concept ID Match
-    response = rxnorm.search('RxcUI:142424')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride_hydrochloride)
+    response = rxnorm.search("RxcUI:142424")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride_hydrochloride)
 
     # Label Match
-    response = rxnorm.search('amiloride hydrochloride')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride_hydrochloride)
+    response = rxnorm.search("amiloride hydrochloride")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride_hydrochloride)
 
     # Trade Name Match
-    response = rxnorm.search('Midamor')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 2
+    response = rxnorm.search("Midamor")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 2
 
 
 def test_amiloride(amiloride, rxnorm):
@@ -621,243 +623,250 @@ def test_amiloride(amiloride, rxnorm):
     correct drug concept.
     """
     # Concept ID Match
-    response = rxnorm.search('RxcUI:644')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride)
+    response = rxnorm.search("RxcUI:644")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride)
 
     # Label Match
-    response = rxnorm.search('amiloride')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride)
+    response = rxnorm.search("amiloride")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride)
 
     # Alias Match
-    response = rxnorm.search('Amyloride')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride)
+    response = rxnorm.search("Amyloride")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride)
 
-    response = rxnorm.search('3,5-diamino-N-carbamimidoyl-6-'
-                             'chloropyrazine-2-carboxamide')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride)
+    response = rxnorm.search("3,5-diamino-N-carbamimidoyl-6-"
+                             "chloropyrazine-2-carboxamide")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride)
 
     # Trade Name Match
-    response = rxnorm.search('Midamor')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 2
+    response = rxnorm.search("Midamor")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 2
 
 
 def test_timolol(timolol, rxnorm):
     """Test that timolol drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxcUI:10600')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], timolol)
+    response = rxnorm.search("RxcUI:10600")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], timolol)
 
     # Label Match
-    response = rxnorm.search('timolol')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], timolol)
+    response = rxnorm.search("timolol")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], timolol)
 
     # Alias Match
-    response = rxnorm.search('(S)-1-(tert-butylamino)-3-[(4-'
-                             'morpholin-4-yl-1,2,5-thiadiazol'
-                             '-3-yl)oxy]propan-2-ol')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], timolol)
+    response = rxnorm.search("(S)-1-(tert-butylamino)-3-[(4-"
+                             "morpholin-4-yl-1,2,5-thiadiazol"
+                             "-3-yl)oxy]propan-2-ol")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], timolol)
 
     # Trade Name Match
-    response = rxnorm.search('Betimol')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], timolol)
+    response = rxnorm.search("Betimol")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], timolol)
 
 
 def test_lymphocyte(lymphocyte, rxnorm):
     """Test that lymphocyte drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:1011')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], lymphocyte)
+    response = rxnorm.search("RxCUI:1011")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], lymphocyte)
 
     # Label Match
-    response = rxnorm.search('lymphocyte immune globulin, '
-                             'anti-thymocyte globulin')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], lymphocyte)
+    response = rxnorm.search("lymphocyte immune globulin, "
+                             "anti-thymocyte globulin")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], lymphocyte)
 
     # Alias Match
-    response = rxnorm.search('Anti Thymocyte Globulin')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], lymphocyte)
+    response = rxnorm.search("Anti Thymocyte Globulin")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], lymphocyte)
 
-    response = rxnorm.search('Antithymoglobulin')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], lymphocyte)
+    response = rxnorm.search("Antithymoglobulin")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], lymphocyte)
 
     # Trade Name Match
-    response = rxnorm.search('Thymoglobulin')
-    assert response['match_type'] == MatchType.TRADE_NAME
+    response = rxnorm.search("Thymoglobulin")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 2
+    response["records"].sort(key=lambda r: r["concept_id"])
+    compare_records(response["records"][0], lymphocyte)
 
-    response = rxnorm.search('ATGAM')
-    assert response['match_type'] == MatchType.TRADE_NAME
+    response = rxnorm.search("ATGAM")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 2
+    response["records"].sort(key=lambda r: r["concept_id"])
+    compare_records(response["records"][0], lymphocyte)
 
 
 def test_aspirin(aspirin, rxnorm):
     """Test that aspirin drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxcUI:1191')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
+    response = rxnorm.search("RxcUI:1191")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], aspirin)
 
     # (Trade Name) No Match
-    response = rxnorm.search('Anacin')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = rxnorm.search("Anacin")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
 
 def test_mesnan(mesna, rxnorm):
     """Test that mesna drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:44')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], mesna)
+    response = rxnorm.search("RxCUI:44")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], mesna)
 
     # Label Match
-    response = rxnorm.search('mesna')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], mesna)
+    response = rxnorm.search("mesna")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], mesna)
 
     # Alias Match
-    response = rxnorm.search('Mesnum')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], mesna)
+    response = rxnorm.search("Mesnum")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], mesna)
 
-    response = rxnorm.search('Ethanesulfonic acid, 2-mercapto-, '
-                             'monosodium salt')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], mesna)
+    response = rxnorm.search("Ethanesulfonic acid, 2-mercapto-, "
+                             "monosodium salt")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], mesna)
 
     # Trade Name Match
-    response = rxnorm.search('Mesnex')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], mesna)
+    response = rxnorm.search("Mesnex")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], mesna)
 
 
 def test_beta_alanine(beta_alanine, rxnorm):
     """Test that beta_alanine drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:61')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], beta_alanine)
+    response = rxnorm.search("RxCUI:61")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], beta_alanine)
 
     # Label Match
-    response = rxnorm.search('beta-alanine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], beta_alanine)
+    response = rxnorm.search("beta-alanine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], beta_alanine)
 
     # Alias Match
-    response = rxnorm.search('3 Aminopropionic Acid')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], beta_alanine)
+    response = rxnorm.search("3 Aminopropionic Acid")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], beta_alanine)
 
 
 def test_algestone(algestone, rxnorm):
     """Test that algestone drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:595')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], algestone)
+    response = rxnorm.search("RxCUI:595")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], algestone)
 
     # Label Match
-    response = rxnorm.search('algestone')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], algestone)
+    response = rxnorm.search("algestone")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], algestone)
 
     # Alias Match
-    response = rxnorm.search('Pregn-4-ene-3,20-dione, 16,'
-                             '17-dihydroxy-, (16alpha)-')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], algestone)
+    response = rxnorm.search("Pregn-4-ene-3,20-dione, 16,"
+                             "17-dihydroxy-, (16alpha)-")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], algestone)
 
 
 def test_levothyroxine(levothyroxine, rxnorm):
     """Test that levothyroxine drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:10582')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("RxCUI:10582")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
     # Label Match
-    response = rxnorm.search('levothyroxine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("levothyroxine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
     # Alias Match
-    response = rxnorm.search('Thyroxin')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("Thyroxin")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
-    response = rxnorm.search('LT4')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("LT4")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
     # Trade Name Match
-    response = rxnorm.search('Unithroid')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("Unithroid")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
-    response = rxnorm.search('Euthyrox')
-    assert response['match_type'] == MatchType.TRADE_NAME
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("Euthyrox")
+    assert response["match_type"] == MatchType.TRADE_NAME
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
     # Xref Match
-    response = rxnorm.search('DRUGBANK:DB00451')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], levothyroxine)
+    response = rxnorm.search("DRUGBANK:DB00451")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], levothyroxine)
 
 
 def test_fluoxetine(fluoxetine, rxnorm):
     """Test that fluoxetine drug normalizes to correct drug concept."""
     # Concept ID Match
-    response = rxnorm.search('RxCUI:4493')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], fluoxetine)
+    response = rxnorm.search("RxCUI:4493")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], fluoxetine)
 
     # Label Match
-    response = rxnorm.search('fluoxetine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], fluoxetine)
+    response = rxnorm.search("fluoxetine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], fluoxetine)
 
 
 def test_fluoxetine_hydrochloride(fluoxetine_hydrochloride, rxnorm):
@@ -865,93 +874,91 @@ def test_fluoxetine_hydrochloride(fluoxetine_hydrochloride, rxnorm):
     concept.
     """
     # Concept ID Match
-    response = rxnorm.search('RxCUI:227224')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], fluoxetine_hydrochloride)
+    response = rxnorm.search("RxCUI:227224")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], fluoxetine_hydrochloride)
 
     # Label Match
-    response = rxnorm.search('fluoxetine hydrochloride')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], fluoxetine_hydrochloride)
+    response = rxnorm.search("fluoxetine hydrochloride")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], fluoxetine_hydrochloride)
 
 
 def test_no_match(rxnorm):
     """Test that a term normalizes to correct drug concept as a NO match."""
     # Misspelled name
-    response = rxnorm.search('17-hydroxycorticosteroi')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = rxnorm.search("17-hydroxycorticosteroi")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
     # Not storing foreign synonyms
-    response = rxnorm.search('cisplatino')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = rxnorm.search("cisplatino")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
     # Wrong Namespace
-    response = rxnorm.search('rxnorm:3')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = rxnorm.search("rxnorm:3")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Test white space in between id
-    response = rxnorm.search('rxcui: 3')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = rxnorm.search("rxcui: 3")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Should not store brand name concepts as identity record
-    response = rxnorm.search('rxcui:202433')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = rxnorm.search("rxcui:202433")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Test empty query
-    response = rxnorm.search('')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = rxnorm.search("")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
 
 def test_brand_name_to_concept(rxnorm):
     """Test that brand names are correctly linked to identity concept."""
     r = rxnorm.db.therapies.query(
-        KeyConditionExpression=Key('label_and_type').eq(
-            'rxcui:1041527##rx_brand')
+        KeyConditionExpression=Key("label_and_type").eq("rxcui:1041527##rx_brand")
     )
-    assert r['Items'][0]['concept_id'] == 'rxcui:161'
-    assert r['Items'][0]['concept_id'] != 'rxcui:1041527'
+    assert r["Items"][0]["concept_id"] == "rxcui:161"
+    assert r["Items"][0]["concept_id"] != "rxcui:1041527"
 
     r = rxnorm.db.therapies.query(
-        KeyConditionExpression=Key('label_and_type').eq(
-            'rxcui:218330##rx_brand')
+        KeyConditionExpression=Key("label_and_type").eq("rxcui:218330##rx_brand")
     )
-    assert r['Items'][0]['concept_id'] == 'rxcui:44'
-    assert r['Items'][0]['concept_id'] != 'rxcui:218330'
+    assert r["Items"][0]["concept_id"] == "rxcui:44"
+    assert r["Items"][0]["concept_id"] != "rxcui:218330"
 
 
 def test_xref_lookup(rxnorm, bifidobacterium_infantis, cisplatin, amiloride):
     """Test that xref lookup resolves to correct concept."""
-    response = rxnorm.search('mmsl:d07347')
-    assert response['match_type'] == MatchType.ASSOCIATED_WITH
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], bifidobacterium_infantis)
+    response = rxnorm.search("mmsl:d07347")
+    assert response["match_type"] == MatchType.ASSOCIATED_WITH
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], bifidobacterium_infantis)
 
-    response = rxnorm.search('mesh:D002945')
-    assert response['match_type'] == MatchType.ASSOCIATED_WITH
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = rxnorm.search("mesh:D002945")
+    assert response["match_type"] == MatchType.ASSOCIATED_WITH
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
-    response = rxnorm.search('atc:C03DB01')
-    assert response['match_type'] == MatchType.ASSOCIATED_WITH
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], amiloride)
+    response = rxnorm.search("atc:C03DB01")
+    assert response["match_type"] == MatchType.ASSOCIATED_WITH
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], amiloride)
 
 
 def test_meta_info(rxnorm):
     """Test that the meta field is correct."""
     response = rxnorm.fetch_meta()
-    assert response.data_license == 'UMLS Metathesaurus'
+    assert response.data_license == "UMLS Metathesaurus"
     assert response.data_license_url == \
-           'https://www.nlm.nih.gov/research/umls/rxnorm/docs/' \
-           'termsofservice.html'
-    assert response.version == '20210104'
+           "https://www.nlm.nih.gov/research/umls/rxnorm/docs/" \
+           "termsofservice.html"
+    assert dt.strptime(response.version, "%Y%m%d")
     assert response.data_url == \
-           'https://www.nlm.nih.gov/research/umls/rxnorm/docs/rxnormfiles.html'
+           "https://www.nlm.nih.gov/research/umls/rxnorm/docs/rxnormfiles.html"
     assert not response.rdp_url
     assert response.data_license_attributes == {
         "non_commercial": False,
diff --git a/tests/unit/test_wikidata_source.py b/tests/unit/test_wikidata_source.py
index c85b8d03..51e1f368 100644
--- a/tests/unit/test_wikidata_source.py
+++ b/tests/unit/test_wikidata_source.py
@@ -4,12 +4,13 @@
 from datetime import datetime
 
 import pytest
+
 from therapy.schemas import Drug, MatchType
 from therapy.query import QueryHandler
 from tests.conftest import compare_records
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def wikidata():
     """Build Wikidata normalizer test fixture."""
     class QueryGetter:
@@ -19,369 +20,369 @@ def __init__(self):
 
         def search(self, query_str):
             resp = self.query_handler.search_sources(query_str, keyed=True,
-                                                     incl='wikidata')
-            return resp['source_matches']['Wikidata']
+                                                     incl="wikidata")
+            return resp["source_matches"]["Wikidata"]
     return QueryGetter()
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def cisplatin():
     """Create a cisplatin drug fixture."""
     params = {
-        'label': 'cisplatin',
-        'concept_id': 'wikidata:Q412415',
-        'aliases': [
-            'CDDP',
-            'Cis-DDP',
-            'CIS-DDP',
-            'cis-diamminedichloroplatinum(II)',
-            'Platinol',
-            'Platinol-AQ'
+        "label": "cisplatin",
+        "concept_id": "wikidata:Q412415",
+        "aliases": [
+            "CDDP",
+            "Cis-DDP",
+            "CIS-DDP",
+            "cis-diamminedichloroplatinum(II)",
+            "Platinol",
+            "Platinol-AQ"
         ],
-        'approval_status': None,
-        'xrefs': [
-            'chembl:CHEMBL11359',
-            'drugbank:DB00515',
-            'rxcui:2555',
-            'chemidplus:15663-27-1',
+        "approval_status": None,
+        "xrefs": [
+            "chembl:CHEMBL11359",
+            "drugbank:DB00515",
+            "rxcui:2555",
+            "chemidplus:15663-27-1",
         ],
-        'associated_with': [
-            'pubchem.compound:5702198'
+        "associated_with": [
+            "pubchem.compound:5702198"
         ],
-        'trade_names': []
+        "trade_names": []
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def platinol():
     """Create a platinol drug fixture."""
     params = {
-        'label': 'Platinol',
-        'concept_id': 'wikidata:Q47522001',
-        'aliases': [],
-        'trade_names': [],
-        'approval_status': None,
-        'xrefs': ['rxcui:202856'],
-        'associated_with': []
+        "label": "Platinol",
+        "concept_id": "wikidata:Q47522001",
+        "aliases": [],
+        "trade_names": [],
+        "approval_status": None,
+        "xrefs": ["rxcui:202856"],
+        "associated_with": []
     }
     return Drug(**params)
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def interferon_alfacon_1():
     """Create an Interferon alfacon-1 drug fixture."""
     params = {
-        'label': 'Interferon alfacon-1',
-        'concept_id': 'wikidata:Q15353101',
-        'aliases': [
-            'CIFN',
-            'consensus interferon',
-            'IFN Alfacon-1',
-            'Interferon Consensus, Methionyl',
-            'methionyl interferon consensus',
-            'methionyl-interferon-consensus',
-            'rCon-IFN',
-            'Recombinant Consensus Interferon',
-            'Recombinant methionyl human consensus interferon'
+        "label": "Interferon alfacon-1",
+        "concept_id": "wikidata:Q15353101",
+        "aliases": [
+            "CIFN",
+            "consensus interferon",
+            "IFN Alfacon-1",
+            "Interferon Consensus, Methionyl",
+            "methionyl interferon consensus",
+            "methionyl-interferon-consensus",
+            "rCon-IFN",
+            "Recombinant Consensus Interferon",
+            "Recombinant methionyl human consensus interferon"
         ],
-        'approval_status': None,
-        'xrefs': [
-            'chembl:CHEMBL1201557',
-            'drugbank:DB00069',
-            'rxcui:59744',
-            'chemidplus:118390-30-0'
+        "approval_status": None,
+        "xrefs": [
+            "chembl:CHEMBL1201557",
+            "drugbank:DB00069",
+            "rxcui:59744",
+            "chemidplus:118390-30-0"
         ],
-        'associated_with': [],
-        'trade_names': []
+        "associated_with": [],
+        "trade_names": []
     }
     return Drug(**params)
 
 
 # Test drug that has > 20 aliases
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def d_methamphetamine():
     """Create a D-methamphetamine drug fixture."""
     params = {
-        'label': 'D-methamphetamine',
-        'concept_id': 'wikidata:Q191924',
-        'aliases': [],
-        'approval_status': None,
-        'xrefs': [
-            'chembl:CHEMBL1201201',
-            'drugbank:DB01577',
-            'rxcui:6816',
-            'chemidplus:537-46-2',
+        "label": "D-methamphetamine",
+        "concept_id": "wikidata:Q191924",
+        "aliases": [],
+        "approval_status": None,
+        "xrefs": [
+            "chembl:CHEMBL1201201",
+            "drugbank:DB01577",
+            "rxcui:6816",
+            "chemidplus:537-46-2",
         ],
-        'associated_with': [
-            'pubchem.compound:10836',
+        "associated_with": [
+            "pubchem.compound:10836",
         ],
-        'trade_names': []
+        "trade_names": []
     }
     return Drug(**params)
 
 
 # Test removing duplicate (case sensitive) aliases
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def atropine():
     """Create an atropine drug fixture."""
     params = {
-        'label': 'atropine',
-        'concept_id': 'wikidata:Q26272',
-        'aliases': [
-            'dl-Hyoscyamine',
-            'dl-tropyltropate',
-            'Mydriasine',
-            'Tropine tropate',
-            '(±)-atropine',
-            '(±)-hyoscyamine',
-            '(+-)-Atropine',
-            '(+-)-Hyoscyamine',
-            '(+,-)-Tropyl tropate',
-            '(3-Endo)-8-methyl-8-azabicyclo[3.2.1]oct-3-yl tropate',
-            '[(1S,5R)-8-Methyl-8-azabicyclo[3.2.1]oct-3-yl] '
-            '3-hydroxy-2-phenyl-propanoate',
-            '8-Methyl-8-azabicyclo[3.2.1]oct-3-yl '
-            '3-hydroxy-2-phenylpropanoate',
-            '8-Methyl-8-azabicyclo[3.2.1]oct-3-yl tropate'
+        "label": "atropine",
+        "concept_id": "wikidata:Q26272",
+        "aliases": [
+            "dl-Hyoscyamine",
+            "dl-tropyltropate",
+            "Mydriasine",
+            "Tropine tropate",
+            "(±)-atropine",
+            "(±)-hyoscyamine",
+            "(+-)-Atropine",
+            "(+-)-Hyoscyamine",
+            "(+,-)-Tropyl tropate",
+            "(3-Endo)-8-methyl-8-azabicyclo[3.2.1]oct-3-yl tropate",
+            "[(1S,5R)-8-Methyl-8-azabicyclo[3.2.1]oct-3-yl] "
+            "3-hydroxy-2-phenyl-propanoate",
+            "8-Methyl-8-azabicyclo[3.2.1]oct-3-yl "
+            "3-hydroxy-2-phenylpropanoate",
+            "8-Methyl-8-azabicyclo[3.2.1]oct-3-yl tropate"
         ],
-        'approval_status': None,
-        'xrefs': [
-            'drugbank:DB00572',
-            'chemidplus:51-55-8',
-            'rxcui:1223'
+        "approval_status": None,
+        "xrefs": [
+            "drugbank:DB00572",
+            "chemidplus:51-55-8",
+            "rxcui:1223"
         ],
-        'associated_with': [
-            'pubchem.compound:174174',
+        "associated_with": [
+            "pubchem.compound:174174",
         ],
-        'trade_names': []
+        "trade_names": []
     }
     return Drug(**params)
 
 
 def test_cisplatin(cisplatin, wikidata):
     """Test that cisplatin drug normalizes to correct drug concept."""
-    response = wikidata.search('wikidata:Q412415')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('wiKIdata:Q412415')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('wiKIdata:q412415')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('Q412415')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('cisplatin')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('Cisplatin')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('CDDP')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('cddp')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('chembl:CHEMBL11359')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('drugbank:DB00515')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('rxcui:2555')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('chemidplus:15663-27-1')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
-
-    response = wikidata.search('pubchem.compound:5702198')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], cisplatin)
+    response = wikidata.search("wikidata:Q412415")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("wiKIdata:Q412415")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("wiKIdata:q412415")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("Q412415")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("cisplatin")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("Cisplatin")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("CDDP")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("cddp")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("chembl:CHEMBL11359")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("drugbank:DB00515")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("rxcui:2555")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("chemidplus:15663-27-1")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
+
+    response = wikidata.search("pubchem.compound:5702198")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], cisplatin)
 
 
 def test_atropine(atropine, wikidata):
     """Test that atropine drug normalizes to correct drug concept."""
-    response = wikidata.search('wikidata:Q26272')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('wiKIdata:Q26272')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('wiKIdata:q26272')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('Q26272')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('atropine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('Atropine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('Mydriasine')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('(±)-Hyoscyamine')
-    assert response['match_type'] == MatchType.ALIAS
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('chemidplus:51-55-8')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
-
-    response = wikidata.search('pubchem.compound:174174')
-    assert response['match_type'] == MatchType.ASSOCIATED_WITH
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], atropine)
+    response = wikidata.search("wikidata:Q26272")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("wiKIdata:Q26272")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("wiKIdata:q26272")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("Q26272")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("atropine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("Atropine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("Mydriasine")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("(±)-Hyoscyamine")
+    assert response["match_type"] == MatchType.ALIAS
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("chemidplus:51-55-8")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
+
+    response = wikidata.search("pubchem.compound:174174")
+    assert response["match_type"] == MatchType.ASSOCIATED_WITH
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], atropine)
 
 
 def test_interferon_alfacon_1(interferon_alfacon_1, wikidata):
     """Test that interferon alfacon-1 drug normalizes to correct concept"""
-    response = wikidata.search('wikidata:Q15353101')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
-
-    response = wikidata.search('wiKIdata:Q15353101')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
-
-    response = wikidata.search('wiKIdata:q15353101')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
-
-    response = wikidata.search('Q15353101')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
-
-    response = wikidata.search('q15353101')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
-
-    response = wikidata.search('Interferon alfacon-1')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
-
-    response = wikidata.search('Interferon Alfacon-1')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], interferon_alfacon_1)
+    response = wikidata.search("wikidata:Q15353101")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
+
+    response = wikidata.search("wiKIdata:Q15353101")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
+
+    response = wikidata.search("wiKIdata:q15353101")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
+
+    response = wikidata.search("Q15353101")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
+
+    response = wikidata.search("q15353101")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
+
+    response = wikidata.search("Interferon alfacon-1")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
+
+    response = wikidata.search("Interferon Alfacon-1")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], interferon_alfacon_1)
 
 
 def test_d_methamphetamine(d_methamphetamine, wikidata):
     """Test that d_methamphetamine drug normalizes to correct concept."""
-    response = wikidata.search('wikidata:Q191924')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
-
-    response = wikidata.search('wiKIdata:Q191924')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
-
-    response = wikidata.search('wiKIdata:q191924')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
-
-    response = wikidata.search('Q191924')
-    assert response['match_type'] == MatchType.CONCEPT_ID
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
-
-    response = wikidata.search('D-methamphetamine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
-
-    response = wikidata.search('d-methamphetamine')
-    assert response['match_type'] == MatchType.LABEL
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
-
-    response = wikidata.search('rxcui:6816')
-    assert response['match_type'] == MatchType.XREF
-    assert len(response['records']) == 1
-    compare_records(response['records'][0], d_methamphetamine)
+    response = wikidata.search("wikidata:Q191924")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
+
+    response = wikidata.search("wiKIdata:Q191924")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
+
+    response = wikidata.search("wiKIdata:q191924")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
+
+    response = wikidata.search("Q191924")
+    assert response["match_type"] == MatchType.CONCEPT_ID
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
+
+    response = wikidata.search("D-methamphetamine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
+
+    response = wikidata.search("d-methamphetamine")
+    assert response["match_type"] == MatchType.LABEL
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
+
+    response = wikidata.search("rxcui:6816")
+    assert response["match_type"] == MatchType.XREF
+    assert len(response["records"]) == 1
+    compare_records(response["records"][0], d_methamphetamine)
 
 
 def test_case_no_match(wikidata):
     """Test that a term normalizes to NO match."""
-    response = wikidata.search('cisplati')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = wikidata.search("cisplati")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
     # Test white space in between label
-    response = wikidata.search('Interferon alfacon - 1')
-    assert response['match_type'] == MatchType.NO_MATCH
+    response = wikidata.search("Interferon alfacon - 1")
+    assert response["match_type"] == MatchType.NO_MATCH
 
     # Test empty query
-    response = wikidata.search('')
-    assert response['match_type'] == MatchType.NO_MATCH
-    assert len(response['records']) == 0
+    response = wikidata.search("")
+    assert response["match_type"] == MatchType.NO_MATCH
+    assert len(response["records"]) == 0
 
 
 def test_meta_info(wikidata):
     """Test that the meta field is correct."""
-    response = wikidata.search('cisplatin')
-    assert response['source_meta_']['data_license'] == 'CC0 1.0'
-    assert response['source_meta_']['data_license_url'] == \
-           'https://creativecommons.org/publicdomain/zero/1.0/'
-    assert datetime.strptime(response['source_meta_']['version'], "%Y%m%d")
-    assert response['source_meta_']['data_url'] is None
-    assert not response['source_meta_']['rdp_url']
-    assert response['source_meta_']['data_license_attributes'] == {
+    response = wikidata.search("cisplatin")
+    assert response["source_meta_"]["data_license"] == "CC0 1.0"
+    assert response["source_meta_"]["data_license_url"] == \
+           "https://creativecommons.org/publicdomain/zero/1.0/"
+    assert datetime.strptime(response["source_meta_"]["version"], "%Y%m%d")
+    assert response["source_meta_"]["data_url"] is None
+    assert not response["source_meta_"]["rdp_url"]
+    assert response["source_meta_"]["data_license_attributes"] == {
         "non_commercial": False,
         "share_alike": False,
         "attribution": False