From 772b534b71a72eed254301bb77d5ca0243efc5b5 Mon Sep 17 00:00:00 2001 From: Mihai Todor Date: Thu, 24 Aug 2023 02:20:25 +0100 Subject: [PATCH 1/3] Add SPDI normalization without using NCBI APIs Co-authored-by: Bob Dolin --- app/__init__.py | 4 + app/common.py | 150 +-------------------------------- app/fasta.py | 23 +++++ app/input_normalization.py | 168 +++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + utilities/__init__.py | 0 6 files changed, 199 insertions(+), 147 deletions(-) create mode 100644 app/fasta.py create mode 100644 app/input_normalization.py create mode 100644 utilities/__init__.py diff --git a/app/__init__.py b/app/__init__.py index d806ce99e..2b0d92b94 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,9 +2,13 @@ import flask from flask_cors import CORS import os +from .fasta import download_fasta def create_app(): + # First ensure we have the fasta files locally + download_fasta() + # App and API options = { 'swagger_url': '/', diff --git a/app/common.py b/app/common.py index 46819dc57..4e6ca0d52 100644 --- a/app/common.py +++ b/app/common.py @@ -2,12 +2,12 @@ from threading import Lock from uuid import uuid4 import pyliftover -import requests from datetime import datetime import pymongo from flask import abort from itertools import groupby import re +from .input_normalization import normalize # MongoDB Client URIs FHIR_genomics_data_client_uri = "mongodb+srv://download:download@cluster0.8ianr.mongodb.net/FHIRGenomicsData" @@ -116,8 +116,6 @@ def get_liftover(from_db, to_db): SUPPORTED_GENOMIC_SOURCE_CLASSES = ['germline', 'somatic'] -NCBI_VARIATION_SERVICES_BASE_URL = 'https://api.ncbi.nlm.nih.gov/variation/v0/' - CHROMOSOME_CSV_FILE = 'app/_Dict_Chromosome.csv' # Utility Functions @@ -163,26 +161,6 @@ def merge_ranges(ranges): return merged_ranges -def get_hgvs_contextuals_url(hgvs): - return f"{NCBI_VARIATION_SERVICES_BASE_URL}hgvs/{hgvs}/contextuals" - - -def get_spdi_all_equivalent_contextual_url(contextual_SPDI): - return f'{NCBI_VARIATION_SERVICES_BASE_URL}spdi/{contextual_SPDI}/all_equivalent_contextual' - - -def get_spdi_canonical_representative_url(contextual_SPDI): - return f'{NCBI_VARIATION_SERVICES_BASE_URL}spdi/{contextual_SPDI}/canonical_representative' - - -def build_spdi(seq_id, position, deleted_sequence, inserted_sequence): - return f"{seq_id}:{position}:{deleted_sequence}:{inserted_sequence}" - - -def get_spdi_elements(response_object): - return (response_object['seq_id'], response_object['position'], response_object['deleted_sequence'], response_object['inserted_sequence']) - - def validate_subject(patient_id): if not patients_db.find_one({"patientID": patient_id}): abort(400, f"Patient ({patient_id}) not found.") @@ -1002,133 +980,11 @@ def get_intersected_regions(bed_id, build, chrom, start, end, intersected_region def hgvs_2_contextual_SPDIs(hgvs): - - # convert hgvs to contextualSPDI - url = get_hgvs_contextuals_url(hgvs) - headers = {'Accept': 'application/json'} - - r = requests.get(url, headers=headers) - if r.status_code != 200: - return False - - response = r.json() - raw_data = response['data'] - raw_SPDI = raw_data['spdis'][0] - - seq_id, position, deleted_sequence, inserted_sequence = get_spdi_elements(raw_SPDI) - - contextual_SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - - # convert contextualSPDI to build37 and build38 contextual SPDIs - url = get_spdi_all_equivalent_contextual_url(contextual_SPDI) - headers = {'Accept': 'application/json'} - - r = requests.get(url, headers=headers) - if r.status_code != 200: - return False - - response = r.json() - raw_SPDI_List = response['data']['spdis'] - - b37SPDI = None - b38SPDI = None - for item in raw_SPDI_List: - if item['seq_id'].startswith("NC_"): - temp = get_build_and_chrom_by_ref_seq(item['seq_id']) - if temp: - seq_id, position, deleted_sequence, inserted_sequence = get_spdi_elements(item) - - if temp['build'] == 'GRCh37': - b37SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - elif temp['build'] == 'GRCh38': - b38SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - else: - return False - - return {"GRCh37": b37SPDI, "GRCh38": b38SPDI} - - -def hgvs_2_canonical_SPDI(hgvs): - - # convert hgvs to contextualSPDI - url = get_hgvs_contextuals_url(hgvs) - headers = {'Accept': 'application/json'} - - r = requests.get(url, headers=headers) - if r.status_code != 200: - return False - - response = r.json() - raw_data = response['data'] - raw_SPDI = raw_data['spdis'][0] - - seq_id, position, deleted_sequence, inserted_sequence = get_spdi_elements(raw_SPDI) - - contextual_SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - - # convert contextualSPDI to canonical SPDI - url = get_spdi_canonical_representative_url(contextual_SPDI) - headers = {'Accept': 'application/json'} - - r = requests.get(url, headers=headers) - if r.status_code != 200: - return False - - response = r.json() - raw_SPDI = response['data'] - - seq_id, position, deleted_sequence, inserted_sequence = get_spdi_elements(raw_SPDI) - - canonical_SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - - return {"canonicalSPDI": canonical_SPDI} + return normalize(hgvs) def SPDI_2_contextual_SPDIs(spdi): - url = get_spdi_all_equivalent_contextual_url(spdi) - headers = {'Accept': 'application/json'} - - r = requests.get(url, headers=headers) - if r.status_code != 200: - return False - - response = r.json() - raw_SPDI_List = response['data']['spdis'] - - b37SPDI = None - b38SPDI = None - for item in raw_SPDI_List: - if item['seq_id'].startswith("NC_"): - temp = get_build_and_chrom_by_ref_seq(item['seq_id']) - if temp: - seq_id, position, deleted_sequence, inserted_sequence = get_spdi_elements(item) - - if temp['build'] == 'GRCh37': - b37SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - elif temp['build'] == 'GRCh38': - b38SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - else: - return False - - return {"GRCh37": b37SPDI, "GRCh38": b38SPDI} - - -def SPDI_2_canonical_SPDI(spdi): - url = get_spdi_canonical_representative_url(spdi) - headers = {'Accept': 'application/json'} - - r = requests.get(url, headers=headers) - if r.status_code != 200: - return False - - response = r.json() - raw_SPDI = response['data'] - - seq_id, position, deleted_sequence, inserted_sequence = get_spdi_elements(raw_SPDI) - - canonical_SPDI = build_spdi(seq_id, position, deleted_sequence, inserted_sequence) - - return {"canonicalSPDI": canonical_SPDI} + return normalize(spdi) def query_clinvar_by_variants(normalized_variant_list, code_list, query, population=False): diff --git a/app/fasta.py b/app/fasta.py new file mode 100644 index 000000000..0045f1d05 --- /dev/null +++ b/app/fasta.py @@ -0,0 +1,23 @@ +from pathlib import Path +from pyfastx import Fasta +from urllib.request import urlretrieve + + +def download_fasta(): + try: + # Make sure the parent folder exists + Path('FASTA').mkdir(exist_ok=True) + + for build in ['GRCh37', 'GRCh38']: + filename = build + '_latest_genomic.fna.gz' + filepath = 'FASTA/' + filename + + # Download files + if not Path(filepath).is_file(): + urlretrieve('https://ftp.ncbi.nlm.nih.gov/refseq/H_sapiens/annotation/' + build + '_latest/refseq_identifiers/' + filename, filepath) + + # Build indexes + if not Path(filepath + '.fxi').is_file(): + Fasta(filepath) + except Exception as error: + print(error) diff --git a/app/input_normalization.py b/app/input_normalization.py new file mode 100644 index 000000000..5cb38f14f --- /dev/null +++ b/app/input_normalization.py @@ -0,0 +1,168 @@ +import hgvs.parser +import hgvs.dataproviders.uta +import hgvs.assemblymapper +from utilities.SPDI_Normalization import get_normalized_spdi + +hgvsParser = hgvs.parser.Parser() +hgvsDataProvider = hgvs.dataproviders.uta.connect( + db_url="postgresql://anonymous:anonymous@uta.biocommons.org/uta/uta_20210129") +b37hgvsAssemblyMapper = hgvs.assemblymapper.AssemblyMapper( + hgvsDataProvider, assembly_name='GRCh37', alt_aln_method='splign', replace_reference=True) +b38hgvsAssemblyMapper = hgvs.assemblymapper.AssemblyMapper( + hgvsDataProvider, assembly_name='GRCh38', alt_aln_method='splign', replace_reference=True) + +# ------------- point to latest data source ------------------------ +# at unix command line: export UTA_DB_URL=postgresql://anonymous:anonymous@uta.biocommons.org/uta/uta_20210129 + +# ------------------ PARSE ------------- + + +def parse_variant(variant): + parsed_variant_dict = dict() + parsed_variant_dict['parsed'] = hgvsParser.parse_hgvs_variant(variant) + return parsed_variant_dict + +# ------------------ PROJECT ------------- + + +def project_variant(parsed_variant): + projected_variant_dict = dict() + projected_variant_dict['b37projected'] = b37hgvsAssemblyMapper.c_to_g( + parsed_variant) + projected_variant_dict['b38projected'] = b38hgvsAssemblyMapper.c_to_g( + parsed_variant) + return projected_variant_dict + +# ---------------- NORMALIZE to canonical SPDIs --------------- + + +def normalize_variant(parsed_variant, build): + pos = parsed_variant.posedit.pos.start.base-1 + if parsed_variant.posedit.edit.ref: + ref = parsed_variant.posedit.edit.ref + else: # ref is blank for insertions + ref = '' + pos = pos+1 + if str(parsed_variant.posedit.edit) == 'dup': + alt = ref+ref + elif parsed_variant.posedit.edit.alt: + alt = parsed_variant.posedit.edit.alt + else: # alt is blank for deletions + alt = '' + return get_normalized_spdi(parsed_variant.ac, pos, ref, alt, build) + +# ---------------- CONVERT NM_HGVS to canonical SPDIs --------------- + + +def process_NM_HGVS(NM_HGVS): + parsed_variant_dict = parse_variant(NM_HGVS) + print(f"parsed: {parsed_variant_dict['parsed']}") + + projected_variant_dict = project_variant(parsed_variant_dict['parsed']) + print( + f"b37projected: {projected_variant_dict['b37projected']}; b38projected: {projected_variant_dict['b38projected']}") + + b37SPDI = normalize_variant( + projected_variant_dict['b37projected'], 'GRCh37') + b38SPDI = normalize_variant( + projected_variant_dict['b38projected'], 'GRCh38') + print(f"b37normalized: {b37SPDI}; b38normalized: {b38SPDI}") + + return {"GRCh37": b37SPDI, "GRCh38": b38SPDI} + + +# ---------------- CONVERT NC_HGVS to canonical SPDIs --------------- + + +def process_NC_HGVS(NC_HGVS): + parsed_variant_dict = parse_variant(NC_HGVS) + parsed_variant = parsed_variant_dict['parsed'] + print(f"parsed: {parsed_variant_dict['parsed']}") + + var_c = b38hgvsAssemblyMapper.g_to_c( + parsed_variant, b38hgvsAssemblyMapper.relevant_transcripts(parsed_variant)[0]) + + projected_variant_dict = project_variant(var_c) + print( + f"b37projected: {projected_variant_dict['b37projected']}; b38projected: {projected_variant_dict['b38projected']}") + + b37SPDI = normalize_variant( + projected_variant_dict['b37projected'], 'GRCh37') + b38SPDI = normalize_variant( + projected_variant_dict['b38projected'], 'GRCh38') + print(f"b37normalized: {b37SPDI}; b38normalized: {b38SPDI}") + + return {"GRCh37": b37SPDI, "GRCh38": b38SPDI} + +# ---------------- CONVERT NM_SPDI to canonical SPDIs --------------- + + +def process_NM_SPDI(NM_SPDI): + # convert SPDI into NM_HGVS then use NM_HGVS pipeline + refSeq = NM_SPDI.split(":")[0] + pos = int(NM_SPDI.split(":")[1])+1 + ref = NM_SPDI.split(":")[2] + alt = NM_SPDI.split(":")[3] + + if len(ref) == len(alt) == 1: # SNV + var_n = hgvsParser.parse_hgvs_variant( + refSeq+":n."+str(pos)+ref+">"+alt) + elif len(ref) == 0: # INS (e.g. NM_007294.3:c.5533_5534insG) + start = pos-1 + end = start+1 + var_n = hgvsParser.parse_hgvs_variant( + refSeq+":n."+str(start)+"_"+str(end)+'ins'+alt) + elif len(alt) == 0: # DEL (e.g. NM_000527.5:c.1350_1355del) + start = pos + end = start+len(ref)-1 + var_n = hgvsParser.parse_hgvs_variant( + refSeq+":n."+str(start)+"_"+str(end)+'del') + elif len(alt) != 0 and len(ref) != 0: # DELINS (e.g. NM_007294.3:c.5359_5363delinsAGTGA) + start = pos + end = start+len(ref)-1 + var_n = hgvsParser.parse_hgvs_variant( + refSeq+":n."+str(start)+"_"+str(end)+'delins'+alt) + NM_HGVS = b38hgvsAssemblyMapper.n_to_c(var_n) + + return process_NM_HGVS(str(NM_HGVS)) + +# ---------------- CONVERT NC_SPDI to canonical SPDIs --------------- + + +def process_NC_SPDI(NC_SPDI): + # convert SPDI into NC_HGVS then use NC_HGVS pipeline + refSeq = NC_SPDI.split(":")[0] + pos = int(NC_SPDI.split(":")[1])+1 + ref = NC_SPDI.split(":")[2] + alt = NC_SPDI.split(":")[3] + + if len(ref) == len(alt) == 1: # SNV + NC_HGVS = (refSeq+":g."+str(pos)+ref+">"+alt) + elif len(ref) == 0: # INS (e.g. NM_007294.3:c.5533_5534insG) + start = pos-1 + end = start+1 + NC_HGVS = (refSeq+":g."+str(start)+"_"+str(end)+'ins'+alt) + elif len(alt) == 0: # DEL (e.g. NM_000527.5:c.1350_1355del) + start = pos + end = start+len(ref)-1 + NC_HGVS = (refSeq+":g."+str(start)+"_"+str(end)+'del') + elif len(alt) != 0 and len(ref) != 0: # DELINS (e.g. NM_007294.3:c.5359_5363delinsAGTGA) + start = pos + end = start+len(ref)-1 + NC_HGVS = (refSeq+":g."+str(start)+"_"+str(end)+'delins'+alt) + + return process_NC_HGVS(str(NC_HGVS)) + + +def normalize(variant): + print(f"submitted: {variant}") + if variant.upper().startswith('NM'): + if variant.count(':') == 3: + return process_NM_SPDI(variant) + else: + return process_NM_HGVS(variant) + elif variant.upper().startswith('NC'): + if variant.count(':') == 3: + return process_NC_SPDI(variant) + else: + return process_NC_HGVS(variant) diff --git a/requirements.txt b/requirements.txt index 776b6ae64..aa262d654 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ dnspython==2.2.1 Flask==2.2.2 flask_cors==4.0.0 gunicorn==20.1.0 +hgvs==1.5.4 pandas==1.3.5 pyfastx==0.9.1 pyliftover==0.4 diff --git a/utilities/__init__.py b/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb From 9e19325be1b98042b0499d0910253796b3902ba6 Mon Sep 17 00:00:00 2001 From: Mihai Todor Date: Thu, 24 Aug 2023 17:07:37 +0100 Subject: [PATCH 2/3] Pick a relevant transcript that starts with NM_ --- app/input_normalization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/input_normalization.py b/app/input_normalization.py index 5cb38f14f..3221f153c 100644 --- a/app/input_normalization.py +++ b/app/input_normalization.py @@ -79,8 +79,10 @@ def process_NC_HGVS(NC_HGVS): parsed_variant = parsed_variant_dict['parsed'] print(f"parsed: {parsed_variant_dict['parsed']}") + transcripts = b38hgvsAssemblyMapper.relevant_transcripts(parsed_variant) + relevantTranscript = next((tr for tr in transcripts if tr.startswith("NM_"))) var_c = b38hgvsAssemblyMapper.g_to_c( - parsed_variant, b38hgvsAssemblyMapper.relevant_transcripts(parsed_variant)[0]) + parsed_variant, relevantTranscript) projected_variant_dict = project_variant(var_c) print( From 34b8d1e00b8f35dc6dbedd53d063b15c001aef63 Mon Sep 17 00:00:00 2001 From: Mihai Todor Date: Thu, 31 Aug 2023 03:01:24 +0100 Subject: [PATCH 3/3] Catch HGVSDataNotAvailableError exception This can occur when the gene orientation changes between builds --- app/common.py | 12 +---- app/endpoints.py | 50 ++++++++++++++++--- .../find_population_specific_variants/2.json | 50 ------------------- .../test_population_genotype_operations.py | 6 ++- 4 files changed, 49 insertions(+), 69 deletions(-) delete mode 100644 tests/expected_outputs/find_population_specific_variants/2.json diff --git a/app/common.py b/app/common.py index 4e6ca0d52..18b68b051 100644 --- a/app/common.py +++ b/app/common.py @@ -174,7 +174,7 @@ def get_variant(variant): variant = variant.lstrip() if variant.count(":") == 1: # HGVS expression - SPDIs = hgvs_2_contextual_SPDIs(variant) + SPDIs = normalize(variant) if not SPDIs: abort(400, f'Cannot normalize variant: {variant}') elif not SPDIs["GRCh37"] and not SPDIs["GRCh38"]: @@ -183,7 +183,7 @@ def get_variant(variant): normalized_variant = {"variant": variant, "GRCh37": SPDIs["GRCh37"], "GRCh38": SPDIs["GRCh38"]} elif variant.count(":") == 3: # SPDI expression - SPDIs = SPDI_2_contextual_SPDIs(variant) + SPDIs = normalize(variant) if not SPDIs: abort(400, f'Cannot normalize variant: {variant}') elif not SPDIs["GRCh37"] and not SPDIs["GRCh38"]: @@ -979,14 +979,6 @@ def get_intersected_regions(bed_id, build, chrom, start, end, intersected_region intersected_regions.append(f'{ref_seq}:{max(start, csePair["Start"])}-{min(end, csePair["End"])}') -def hgvs_2_contextual_SPDIs(hgvs): - return normalize(hgvs) - - -def SPDI_2_contextual_SPDIs(spdi): - return normalize(spdi) - - def query_clinvar_by_variants(normalized_variant_list, code_list, query, population=False): variant_list = [] for item in normalized_variant_list: diff --git a/app/endpoints.py b/app/endpoints.py index b04c53abf..7582a8693 100644 --- a/app/endpoints.py +++ b/app/endpoints.py @@ -179,7 +179,11 @@ def find_subject_specific_variants( subject = subject.strip() common.validate_subject(subject) - variants = list(map(common.get_variant, variants)) + try: + variants = list(map(common.get_variant, variants)) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') # Query query = {} @@ -833,13 +837,22 @@ def find_subject_tx_implications( if ranges: ranges = list(map(common.get_range, ranges)) common.get_lift_over_range(ranges) - variants = common.get_variants(ranges, query) + try: + variants = common.get_variants(ranges, query) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') + if not variants: return jsonify({"resourceType": "Parameters"}) normalized_variants = [{variant["BUILD"]: variant["SPDI"]} for variant in variants] if variants and not ranges: - normalized_variants = list(map(common.get_variant, variants)) + try: + normalized_variants = list(map(common.get_variant, variants)) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') # Result Object result = OrderedDict() @@ -1105,13 +1118,22 @@ def find_subject_dx_implications( if ranges: ranges = list(map(common.get_range, ranges)) common.get_lift_over_range(ranges) - variants = common.get_variants(ranges, query) + try: + variants = common.get_variants(ranges, query) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') + if not variants: return jsonify({"resourceType": "Parameters"}) normalized_variants = [{variant["BUILD"]: variant["SPDI"]} for variant in variants] if variants and not ranges: - normalized_variants = list(map(common.get_variant, variants)) + try: + normalized_variants = list(map(common.get_variant, variants)) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') # Result Object result = OrderedDict() @@ -1373,7 +1395,11 @@ def find_population_specific_variants( # Parameters variants = list(map(lambda x: x.strip().split(","), variants)) for i in range(len(variants)): - variants[i] = list(map(common.get_variant, variants[i])) + try: + variants[i] = list(map(common.get_variant, variants[i])) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') # Query query = {} @@ -1884,7 +1910,11 @@ def find_population_tx_implications( return jsonify({"resourceType": "Parameters"}) if variants: - variants = list(map(common.get_variant, variants)) + try: + variants = list(map(common.get_variant, variants)) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') condition_code_list = [] if conditions: @@ -2090,7 +2120,11 @@ def find_population_dx_implications( return jsonify({"resourceType": "Parameters"}) if variants: - variants = list(map(common.get_variant, variants)) + try: + variants = list(map(common.get_variant, variants)) + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + abort(422, 'Failed LiftOver') condition_code_list = [] if conditions: diff --git a/tests/expected_outputs/find_population_specific_variants/2.json b/tests/expected_outputs/find_population_specific_variants/2.json deleted file mode 100644 index a141ff1aa..000000000 --- a/tests/expected_outputs/find_population_specific_variants/2.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "resourceType": "Parameters", - "parameter": [ - { - "name": "variants", - "part": [ - { - "name": "variantItem", - "valueString": "NC_000001.10:144931726:G:A AND NC_000001.10:145532548:T:C AND NC_000001.10:145592072:A:T" - }, - { - "name": "numerator", - "valueQuantity": { - "value": 6 - } - }, - { - "name": "denominator", - "valueQuantity": { - "value": 1112 - } - }, - { - "name": "subject", - "valueString": "NA18498" - }, - { - "name": "subject", - "valueString": "NA18499" - }, - { - "name": "subject", - "valueString": "NA18871" - }, - { - "name": "subject", - "valueString": "NA19238" - }, - { - "name": "subject", - "valueString": "NA19239" - }, - { - "name": "subject", - "valueString": "NA19240" - } - ] - } - ] -} \ No newline at end of file diff --git a/tests/integration_tests/test_population_genotype_operations.py b/tests/integration_tests/test_population_genotype_operations.py index c3327d55b..d2f5e2343 100644 --- a/tests/integration_tests/test_population_genotype_operations.py +++ b/tests/integration_tests/test_population_genotype_operations.py @@ -22,11 +22,15 @@ def test_population_specific_variants_1(client): def test_population_specific_variants_2(client): + # PDE4DIP gene is on minus strand in build 37, but is on positive strand in build 38. + # The hgvs library returns "HGVSDataNotAvailableError: No alignments for NM_001002811.1 in GRCh38 using splign" in + # this case. + url = tu.find_population_specific_variants_query( 'subject=HG00403&variants=NC_000001.10:144931726:G:A&variants=NC_000001.10:145532548:T:C&variants=NC_000001.10:145592072:A:T&includePatientList=true') response = client.get(url) - tu.compare_actual_and_expected_output(f'{tu.FIND_POPULATION_SPECIFIC_VARIANTS_OUTPUT_DIR}2.json', response.json) + assert response.status_code == 422 # def test_population_specific_variants_3(client):