diff --git a/clickhouse_search/constants.py b/clickhouse_search/constants.py index aaba9464b9..0721e16955 100644 --- a/clickhouse_search/constants.py +++ b/clickhouse_search/constants.py @@ -79,6 +79,42 @@ ('benign', 'Benign/Likely_benign', 'Benign'), ] +CLINVAR_ASSERTIONS = [ + 'Affects', + 'association', + 'association_not_found', + 'confers_sensitivity', + 'drug_response', + 'low_penetrance', + 'not_provided', + 'other', + 'protective', + 'risk_factor', + 'no_classification_for_the_single_variant', + 'no_classifications_from_unflagged_records', +] +CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY = 'Conflicting_classifications_of_pathogenicity' +CLINVAR_DEFAULT_PATHOGENICITY = 'No_pathogenic_assertion' +CLINVAR_PATHOGENICITIES = [ + 'Pathogenic', + 'Pathogenic/Likely_pathogenic', + 'Pathogenic/Likely_pathogenic/Established_risk_allele', + 'Pathogenic/Likely_pathogenic/Likely_risk_allele', + 'Pathogenic/Likely_risk_allele', + 'Likely_pathogenic', + 'Likely_pathogenic/Likely_risk_allele', + 'Established_risk_allele', + 'Likely_risk_allele', + CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY, + 'Uncertain_risk_allele', + 'Uncertain_significance/Uncertain_risk_allele', + 'Uncertain_significance', + CLINVAR_DEFAULT_PATHOGENICITY, + 'Likely_benign', + 'Benign/Likely_benign', + 'Benign', +] + HGMD_KEY = 'hgmd' HGMD_CLASS_FILTERS = [ ('disease_causing', 'DM'), diff --git a/clickhouse_search/management/commands/reload_clinvar_all_variants.py b/clickhouse_search/management/commands/reload_clinvar_all_variants.py index de9793c961..d343e9dd2e 100644 --- a/clickhouse_search/management/commands/reload_clinvar_all_variants.py +++ b/clickhouse_search/management/commands/reload_clinvar_all_variants.py @@ -15,6 +15,9 @@ from clickhouse_backend import models from clickhouse_search.models.reference_data_models import ClinvarAllVariantsGRCh37SnvIndel, ClinvarAllVariantsSnvIndel, ClinvarAllVariantsMito, \ ClinvarMvGRCh37SnvIndel, ClinvarMvSnvIndel, ClinvarMvMito, ClinvarSearchMvGRCh37SnvIndel, ClinvarSearchMvSnvIndel, ClinvarSearchMvMito +from clickhouse_search.constants import \ + CLINVAR_ASSERTIONS as CORE_CLINVAR_ASSERTIONS, CLINVAR_PATHOGENICITIES as CORE_CLINVAR_PATHOGENICITIES, CLINVAR_DEFAULT_PATHOGENICITY as CORE_CLINVAR_DEFAULT_PATHOGENICITY, \ + CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY as CORE_CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY from reference_data.models import DataVersions from seqr.utils.communication_utils import safe_post_to_slack @@ -29,11 +32,11 @@ def replace_spaces_with_underscores(value: Union[list[str], list[tuple[str, int] return [s.replace(' ', '_') for s in value] BATCH_SIZE = 1000 -CLINVAR_ASSERTIONS = replace_underscores_with_spaces(ClinvarAllVariantsSnvIndel.CLINVAR_ASSERTIONS) -CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY = replace_underscores_with_spaces([ClinvarAllVariantsSnvIndel.CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY])[0] +CLINVAR_ASSERTIONS = replace_underscores_with_spaces(CORE_CLINVAR_ASSERTIONS) +CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY = replace_underscores_with_spaces([CORE_CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY])[0] CLINVAR_CONFLICTING_DATA_FROM_SUBMITTERS = 'conflicting data from submitters' -CLINVAR_DEFAULT_PATHOGENICITY = replace_underscores_with_spaces([ClinvarAllVariantsSnvIndel.CLINVAR_DEFAULT_PATHOGENICITY])[0] -CLINVAR_PATHOGENICITIES = replace_underscores_with_spaces(ClinvarAllVariantsSnvIndel.CLINVAR_PATHOGENICITIES) +CLINVAR_DEFAULT_PATHOGENICITY = replace_underscores_with_spaces([CORE_CLINVAR_DEFAULT_PATHOGENICITY])[0] +CLINVAR_PATHOGENICITIES = replace_underscores_with_spaces(CORE_CLINVAR_PATHOGENICITIES) CLINVAR_GOLD_STARS_LOOKUP = { 'no classification for the single variant': 0, 'no classification provided': 0, diff --git a/clickhouse_search/management/tests/reload_clinvar_all_variants_tests.py b/clickhouse_search/management/tests/reload_clinvar_all_variants_tests.py index b7a18267d5..9be4e198d7 100644 --- a/clickhouse_search/management/tests/reload_clinvar_all_variants_tests.py +++ b/clickhouse_search/management/tests/reload_clinvar_all_variants_tests.py @@ -197,7 +197,7 @@ def test_batching(self, mock_logger, mock_safe_post_to_slack): call_command('reload_clinvar_all_variants') mock_logger.assert_called_with('Updating Clinvar ClickHouse tables to 2025-06-30 from 2025-06-23.') self.assertEqual(ClinvarAllVariantsSnvIndel.objects.all().count(), BATCH_SIZE * 2 + 10) - self.assertEqual(ClinvarAllVariantsSnvIndel.objects.first().pathogenicity, ClinvarAllVariantsSnvIndel.CLINVAR_DEFAULT_PATHOGENICITY) + self.assertEqual(ClinvarAllVariantsSnvIndel.objects.first().pathogenicity, 'No_pathogenic_assertion') self.assertIsNone(ClinvarAllVariantsSnvIndel.objects.first().gold_stars) @responses.activate @@ -417,7 +417,7 @@ def test_conflicting_data_from_submitters(self, mock_logger, mock_safe_post_to_s ) call_command('reload_clinvar_all_variants') self.assertEqual(ClinvarAllVariantsSnvIndel.objects.count(), 1) - self.assertEqual(ClinvarAllVariantsSnvIndel.objects.first().pathogenicity, ClinvarAllVariantsSnvIndel.CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY) + self.assertEqual(ClinvarAllVariantsSnvIndel.objects.first().pathogenicity, 'Conflicting_classifications_of_pathogenicity') mock_safe_post_to_slack.assert_called_with( SEQR_SLACK_DATA_ALERTS_NOTIFICATION_CHANNEL, 'Successfully updated Clinvar ClickHouse tables to 2025-06-30.', diff --git a/clickhouse_search/models/reference_data_models.py b/clickhouse_search/models/reference_data_models.py index d129f8de8b..783ff4934e 100644 --- a/clickhouse_search/models/reference_data_models.py +++ b/clickhouse_search/models/reference_data_models.py @@ -6,6 +6,7 @@ from clickhouse_search.backend.fields import Enum8Field, NestedField, UInt32FieldDeltaCodecField, DictKeyForeignKey from clickhouse_search.backend.table_models import FixtureLoadableClickhouseModel, Dictionary, \ RefreshableMaterializedView, RefreshableMaterializedViewMeta +from clickhouse_search.constants import CLINVAR_ASSERTIONS, CLINVAR_PATHOGENICITIES from seqr.utils.xpos_utils import CHROMOSOME_CHOICES from settings import DATABASES, PIPELINE_RUNNER_SERVER @@ -39,43 +40,6 @@ class ReferenceDataDictMeta: class BaseClinvar(FixtureLoadableClickhouseModel): - - CLINVAR_ASSERTIONS = [ - 'Affects', - 'association', - 'association_not_found', - 'confers_sensitivity', - 'drug_response', - 'low_penetrance', - 'not_provided', - 'other', - 'protective', - 'risk_factor', - 'no_classification_for_the_single_variant', - 'no_classifications_from_unflagged_records', - ] - CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY = 'Conflicting_classifications_of_pathogenicity' - CLINVAR_DEFAULT_PATHOGENICITY = 'No_pathogenic_assertion' - CLINVAR_PATHOGENICITIES = [ - 'Pathogenic', - 'Pathogenic/Likely_pathogenic', - 'Pathogenic/Likely_pathogenic/Established_risk_allele', - 'Pathogenic/Likely_pathogenic/Likely_risk_allele', - 'Pathogenic/Likely_risk_allele', - 'Likely_pathogenic', - 'Likely_pathogenic/Likely_risk_allele', - 'Established_risk_allele', - 'Likely_risk_allele', - CLINVAR_CONFLICTING_CLASSICATIONS_OF_PATHOGENICITY, - 'Uncertain_risk_allele', - 'Uncertain_significance/Uncertain_risk_allele', - 'Uncertain_significance', - CLINVAR_DEFAULT_PATHOGENICITY, - 'Likely_benign', - 'Benign/Likely_benign', - 'Benign', - ] - ASSERTIONS_CHOICES = list(enumerate(CLINVAR_ASSERTIONS)) PATHOGENICITY_CHOICES = list(enumerate(CLINVAR_PATHOGENICITIES)) diff --git a/seqr/fixtures/1kg_project.json b/seqr/fixtures/1kg_project.json index 4b2f54e9c1..dec04bf2d2 100644 --- a/seqr/fixtures/1kg_project.json +++ b/seqr/fixtures/1kg_project.json @@ -757,7 +757,7 @@ "sex": "M", "affected": "A", "display_name": "", - "notes": "", + "features": [{"id": "HP:0001508"}], "case_review_status": "", "case_review_status_last_modified_date": null, "case_review_status_last_modified_by": null, diff --git a/seqr/utils/add_data_utils.py b/seqr/utils/add_data_utils.py index 81dc2e50a7..978610b7df 100644 --- a/seqr/utils/add_data_utils.py +++ b/seqr/utils/add_data_utils.py @@ -63,7 +63,7 @@ def update_airtable_loading_tracking_status(project, status, additional_update=N update={'Status': status, **(additional_update or {})}, ) -def trigger_delete_families_search(project, family_guids, user=None): +def trigger_delete_families_search(project, family_guids, user=None, dataset_types=None): num_updated = 0 updated_families = set() for dataset in Dataset.objects.filter(active_individuals__family__guid__in=family_guids).distinct(): @@ -80,6 +80,8 @@ def trigger_delete_families_search(project, family_guids, user=None): logger.info(message, user) variables = {'project_guid': project.guid, 'family_guids': family_guids} + if dataset_types: + variables['dataset_types'] = dataset_types _enqueue_pipeline_request('delete_families', variables, user) info.append('Triggered delete family data') return info diff --git a/seqr/views/apis/anvil_workspace_api.py b/seqr/views/apis/anvil_workspace_api.py index 01bf0f1be2..7f8c8abf19 100644 --- a/seqr/views/apis/anvil_workspace_api.py +++ b/seqr/views/apis/anvil_workspace_api.py @@ -253,10 +253,10 @@ def _validate_expected_samples(vcf_samples, loaded_sample_types, loaded_individu def _trigger_add_workspace_data(project, pedigree_records, user, data_path, sample_type, previous_loaded_ids=None, get_pedigree_json=False): # add families and individuals according to the uploaded individual records pedigree_json, individual_ids = add_or_update_individuals_and_families( - project, individual_records=pedigree_records, user=user, get_update_json=get_pedigree_json, get_updated_individual_db_ids=True, + project, individual_records=pedigree_records, user=user, get_update_json=get_pedigree_json, get_individual_db_ids=True, allow_features_update=True, skip_gt_stats_rebuild=True, ) - num_updated_individuals = len(individual_ids) + num_updated_individuals = len(individual_ids - set(previous_loaded_ids or [])) individual_ids.update(previous_loaded_ids or []) # use airflow api to trigger AnVIL dags diff --git a/seqr/views/apis/anvil_workspace_api_tests.py b/seqr/views/apis/anvil_workspace_api_tests.py index cb95ae16c1..e76daeefe1 100644 --- a/seqr/views/apis/anvil_workspace_api_tests.py +++ b/seqr/views/apis/anvil_workspace_api_tests.py @@ -565,13 +565,10 @@ def setUp(self): patcher = mock.patch('seqr.utils.add_data_utils.logger') self.mock_add_data_utils_logger = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch('seqr.views.apis.anvil_workspace_api.load_uploaded_file') - self.mock_load_file = patcher.start() - self.mock_load_file.return_value = LOAD_SAMPLE_DATA - self.addCleanup(patcher.stop) patcher = mock.patch('seqr.utils.file_utils.subprocess.Popen') self.mock_subprocess = patcher.start() self.mock_subprocess.return_value.wait.return_value = 0 + self.mock_subprocess.return_value.stdout = [json.dumps(LOAD_SAMPLE_DATA).encode('utf-8')] self.addCleanup(patcher.stop) patcher = mock.patch('seqr.views.utils.export_utils.TemporaryDirectory') mock_tempdir = patcher.start() @@ -596,6 +593,9 @@ def setUp(self): super().setUp() + def _set_load_file_iter(self, data): + self.mock_subprocess.return_value.stdout = [json.dumps(data).encode('utf-8')] + @mock.patch('seqr.models.Family._compute_guid', lambda family: f'F_{family.family_id}_{family.project.workspace_name[17:]}') @mock.patch('seqr.models.Project._compute_guid', lambda project: f'P_{project.name}') @responses.activate @@ -612,8 +612,8 @@ def test_create_project_from_workspace(self): # Test valid operation responses.calls.reset() - self.mock_load_file.return_value = LOAD_SAMPLE_DATA - self.mock_subprocess.return_value.wait.side_effect = [0, 1, 0] + self._set_load_file_iter(LOAD_SAMPLE_DATA) + self.mock_subprocess.return_value.wait.side_effect = [0, 0, 1, 0] response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 200) project = Project.objects.get(workspace_namespace=TEST_WORKSPACE_NAMESPACE, workspace_name=TEST_NO_PROJECT_WORKSPACE_NAME) @@ -685,14 +685,14 @@ def test_add_workspace_data(self, mock_compute_indiv_guid): self._test_errors(url, ['uploadedFileId', 'fullDataPath', 'vcfSamples'], TEST_WORKSPACE_NAME, has_existing_data=True) # Test loading data from empty ped file - self.mock_load_file.return_value = LOAD_SAMPLE_DATA[:1] + self._set_load_file_iter(LOAD_SAMPLE_DATA[:1]) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 400) response_json = response.json() self.assertListEqual(response_json['errors'], ['No samples found in the pedigree file']) # Test Individual ID exists in an omitted family and missing loaded samples - self.mock_load_file.return_value = LOAD_SAMPLE_DATA + INVALID_ADDED_SAMPLE_DATA + self._set_load_file_iter(LOAD_SAMPLE_DATA + INVALID_ADDED_SAMPLE_DATA) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 400) response_json = response.json() @@ -705,7 +705,7 @@ def test_add_workspace_data(self, mock_compute_indiv_guid): ]) # Test project still has pending loading - self.mock_load_file.return_value = LOAD_SAMPLE_DATA + self._set_load_file_iter(LOAD_SAMPLE_DATA) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY_ADD_DATA)) self.assertEqual(response.status_code, 400) self.assertListEqual(response.json()['errors'], [ @@ -714,8 +714,8 @@ def test_add_workspace_data(self, mock_compute_indiv_guid): ]) # Test a valid operation - self.mock_subprocess.return_value.wait.side_effect = [0, 1, 0] - self.mock_load_file.return_value = LOAD_SAMPLE_DATA_ALL_PENDING + self.mock_subprocess.return_value.wait.side_effect = [0, 0, 1, 0] + self._set_load_file_iter(LOAD_SAMPLE_DATA_ALL_PENDING) mock_compute_indiv_guid.side_effect = ['I0000020_hg00735', 'I0000021_hg00736'] response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY_ADD_DATA)) self.assertEqual(response.status_code, 200) @@ -724,17 +724,17 @@ def test_add_workspace_data(self, mock_compute_indiv_guid): self.assertSetEqual(set(response_json['individualsByGuid'].keys()), { 'I0000020_hg00735', 'I000001_na19675', 'I000003_na19679', 'I0000021_hg00736', 'I000007_na20870', 'I000009_na20874', 'I000004_hg00731', 'I000010_na20875', 'I000008_na20872', 'I000005_hg00732', - 'I000006_hg00733', 'I000013_na20878', 'I000012_na20877', + 'I000006_hg00733', 'I000012_na20877', }) self.assertSetEqual(set(response_json['familiesByGuid'].keys()), { - 'F000001_1', 'F000015_21', 'F000006_6', 'F000013_13', 'F000005_5', 'F000009_9', 'F000008_8', 'F000004_4', + 'F000001_1', 'F000015_21', 'F000006_6', 'F000013_13', 'F000005_5', 'F000008_8', 'F000004_4', 'F000002_2', 'F000003_3', }) self.assertEqual(list(response_json['familyNotesByGuid'].keys()), ['FAN000005_21_c_a_new_family']) self._assert_valid_operation(Project.objects.get(guid=PROJECT1_GUID)) - self.mock_load_file.return_value = LOAD_SAMPLE_DATA_ALL_PENDING_PROJECT_2 + self._set_load_file_iter(LOAD_SAMPLE_DATA_ALL_PENDING_PROJECT_2) mock_compute_indiv_guid.side_effect = ['I0000021_na19675_1', 'I0000022_na19678', 'I0000023_hg00735'] url = reverse(add_workspace_data, args=[PROJECT2_GUID]) self._test_mv_file_and_triggering_loading_exception( @@ -749,20 +749,20 @@ def _test_errors(self, url, fields, workspace_name, has_existing_data=False): self.mock_get_ws_access_level.assert_called_with(self.manager_user, TEST_WORKSPACE_NAMESPACE, workspace_name) # test missing columns - self.mock_load_file.return_value = [['family', 'individual'], ['1', '2']] + self._set_load_file_iter([['family', 'individual'], ['1', '2']]) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 400) response_json = response.json() self.assertListEqual(response_json['errors'], ['Missing required columns: Affected, HPO Terms, Sex']) - self.mock_load_file.return_value = LOAD_SAMPLE_DATA + MISSING_REQUIRED_SAMPLE_DATA + self._set_load_file_iter(LOAD_SAMPLE_DATA + MISSING_REQUIRED_SAMPLE_DATA) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 400) response_json = response.json() self.assertListEqual(response_json['errors'], ['Missing Sex in row #4', 'Missing Affected in row #4']) # test sample data error and missing samples - self.mock_load_file.return_value = LOAD_SAMPLE_DATA + BAD_SAMPLE_DATA + self._set_load_file_iter(LOAD_SAMPLE_DATA + BAD_SAMPLE_DATA) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 400) response_json = response.json() @@ -780,7 +780,7 @@ def _test_errors(self, url, fields, workspace_name, has_existing_data=False): 'NA19681 has invalid HPO terms: HP:0100258', ]) - self.mock_load_file.return_value = LOAD_SAMPLE_DATA_NO_AFFECTED + self._set_load_file_iter(LOAD_SAMPLE_DATA_NO_AFFECTED) response = self.client.post(url, content_type='application/json', data=json.dumps(REQUEST_BODY)) self.assertEqual(response.status_code, 400) response_json = response.json() @@ -834,11 +834,13 @@ def _assert_valid_operation(self, project, test_add_data=True): gs_path = f'gs://seqr-loading-temp/v3.1/{genome_version}/SNV_INDEL/pedigrees/WES/' self.mock_subprocess.assert_has_calls([ + mock.call('gsutil ls gs://seqr-scratch-temp/temp_upload_test_temp_file_id.json.gz', stdout=-1, stderr=-2, shell=True), # nosec + mock.call().wait(), + mock.call('gsutil cat gs://seqr-scratch-temp/temp_upload_test_temp_file_id.json.gz | gunzip -c -q - ', stdout=-1, stderr=-2, shell=True), # nosec mock.call(f'gsutil mv {TEMP_PATH}/* {gs_path}', stdout=-1, stderr=-2, shell=True), # nosec mock.call().wait(), mock.call('gsutil ls gs://seqr-loading-temp/v3.1/db_id_to_gene_id.csv.gz', stdout=-1, stderr=-2, shell=True), # nosec mock.call().wait(), - mock.call().stdout.__iter__(), mock.call(f'gsutil mv {TEMP_PATH}/* gs://seqr-loading-temp/v3.1/', stdout=-1, stderr=-2, shell=True), # nosec mock.call().wait(), ]) @@ -855,7 +857,7 @@ def _assert_valid_operation(self, project, test_add_data=True): self._assert_expected_requests(variables, project, num_samples=14 if test_add_data else 3, status='Loading') self.assert_expected_airtable_headers(-1) - sample_summary = '13 new and 7 re-loaded' if test_add_data else '3 new' + sample_summary = '7 new and 7 re-loaded' if test_add_data else '3 new' self.mock_slack.assert_called_with( SEQR_SLACK_ANVIL_DATA_LOADING_CHANNEL, self._success_slack_message(project, sample_summary, genome_version, variables), @@ -913,11 +915,11 @@ def _success_slack_message(self, project, sample_summary, genome_version, variab Loading pipeline is triggered with: ```{json.dumps(variables, indent=4)}```""" - @staticmethod - def _raise_move_file_error(command, *args, **kwargs): + def _raise_move_file_error(self, command, *args, **kwargs): mock_subprocess = mock.MagicMock() - mock_subprocess.wait.return_value = 1 if 'pedigrees' in command else 0 - mock_subprocess.stdout = [b'Something wrong while moving the file.'] + is_mv_pedigree_command = 'pedigrees' in command + mock_subprocess.wait.return_value = 1 if is_mv_pedigree_command else 0 + mock_subprocess.stdout = [b'Something wrong while moving the file.'] if is_mv_pedigree_command else self.mock_subprocess.return_value.stdout return mock_subprocess def _test_mv_file_and_triggering_loading_exception(self, url, workspace, sample_data, genome_version, request_body, num_samples=None, sample_type='WES'): @@ -959,7 +961,7 @@ def _test_mv_file_and_triggering_loading_exception(self, url, workspace, sample_ self.mock_slack.assert_has_calls([ mock.call(SEQR_SLACK_LOADING_NOTIFICATION_CHANNEL, slack_message_on_failure), mock.call(SEQR_SLACK_ANVIL_DATA_LOADING_CHANNEL, self._success_slack_message( - project, '3 new' if sample_type == 'WES' else '5 new and 1 re-loaded', genome_version, variables, sample_type, + project, '3 new' if sample_type == 'WES' else '4 new and 1 re-loaded', genome_version, variables, sample_type, )), ]) self._assert_expected_requests( diff --git a/seqr/views/apis/data_manager_api.py b/seqr/views/apis/data_manager_api.py index caf655deca..aad43189fa 100644 --- a/seqr/views/apis/data_manager_api.py +++ b/seqr/views/apis/data_manager_api.py @@ -424,8 +424,11 @@ def trigger_delete_project(request): def trigger_delete_family(request): request_json = json.loads(request.body) family_guid = request_json.pop('family') + dataset_types = request_json.get('datasetTypes') or [] + if Dataset.DATASET_TYPE_SV_CALLS in dataset_types: + dataset_types.append('GCNV') project = Project.objects.get(family__guid=family_guid) - info = trigger_delete_families_search(project, [family_guid], request.user) + info = trigger_delete_families_search(project, [family_guid], request.user, dataset_types) return create_json_response({'info': info}) diff --git a/seqr/views/apis/data_manager_api_tests.py b/seqr/views/apis/data_manager_api_tests.py index 04eabf3a23..1cf8bc4b98 100644 --- a/seqr/views/apis/data_manager_api_tests.py +++ b/seqr/views/apis/data_manager_api_tests.py @@ -1325,7 +1325,7 @@ def test_trigger_delete_family(self): self.check_data_manager_login(url) Project.objects.filter(guid=PROJECT_GUID).update(genome_version='38') - response = self.client.post(url, content_type='application/json', data=json.dumps({'family': 'F000002_2'})) + response = self.client.post(url, content_type='application/json', data=json.dumps({'family': 'F000002_2', 'datasetTypes': ['SNV_INDEL', 'SV']})) self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), { 'info': [ @@ -1340,6 +1340,7 @@ def test_trigger_delete_family(self): self.assertDictEqual(json.loads(responses.calls[-1].request.body), { 'project_guid': 'R0001_1kg', 'family_guids': ['F000002_2'], + 'dataset_types': ['SNV_INDEL', 'SV', 'GCNV'], }) diff --git a/seqr/views/apis/saved_variant_api.py b/seqr/views/apis/saved_variant_api.py index 9e7b7985c4..68b5a68de8 100644 --- a/seqr/views/apis/saved_variant_api.py +++ b/seqr/views/apis/saved_variant_api.py @@ -321,7 +321,7 @@ def _delete_removed_tags(saved_variants, all_variant_guids, tag_updates, user, c existing_tag_guids = [tag['tagGuid'] for tag in tag_updates if tag.get('tagGuid')] deleted_tag_guids = [] tag_set = _get_tag_set(saved_variants[0], tag_type) - remove_tags = tag_set.exclude(guid__in=existing_tag_guids) + remove_tags = tag_set.order_by('id').exclude(guid__in=existing_tag_guids) if protected_tag_types: remove_tags = remove_tags.exclude(variant_tag_type__name__in=protected_tag_types) if not can_edit: diff --git a/seqr/views/utils/individual_utils.py b/seqr/views/utils/individual_utils.py index e396c365e9..39d9fd952c 100644 --- a/seqr/views/utils/individual_utils.py +++ b/seqr/views/utils/individual_utils.py @@ -20,7 +20,7 @@ def _get_record_individual_id(record): return record.get(JsonConstants.PREVIOUS_INDIVIDUAL_ID_COLUMN) or record[JsonConstants.INDIVIDUAL_ID_COLUMN] -def add_or_update_individuals_and_families(project, individual_records, user, get_update_json=True, get_updated_individual_db_ids=False, get_created_counts=False, allow_features_update=False, skip_gt_stats_rebuild=False): +def add_or_update_individuals_and_families(project, individual_records, user, get_update_json=True, get_individual_db_ids=False, get_created_counts=False, allow_features_update=False, skip_gt_stats_rebuild=False): updated_family_ids = set() updated_individuals = set() updated_affected = set() @@ -55,11 +55,14 @@ def add_or_update_individuals_and_families(project, individual_records, user, ge individual_id__in=[_get_record_individual_id(record) for record in individual_records]): individual_lookup[i.individual_id][i.family] = i + all_individuals = set() for record in individual_records: - created_individual = _update_from_record( + created_individual, individual_db_id = _update_from_record( record, user, families_by_id, individual_lookup, updated_family_ids, updated_individuals, updated_affected, updated_sex, updated_metadata, parent_updates, updated_note_ids, allow_features_update) if created_individual: num_created_individuals += 1 + if get_individual_db_ids: + all_individuals.add(individual_db_id) for update in parent_updates: individual = update.pop('individual') @@ -84,8 +87,8 @@ def add_or_update_individuals_and_families(project, individual_records, user, ge if get_update_json: pedigree_json = _get_updated_pedigree_json(updated_individuals, updated_family_models, updated_note_ids, user) - if get_updated_individual_db_ids: - return pedigree_json, {i.id for i in updated_individuals} + if get_individual_db_ids: + return pedigree_json, all_individuals if get_created_counts: return pedigree_json, num_created_families, num_created_individuals @@ -161,7 +164,7 @@ def _update_from_record(record, user, families_by_id, individual_lookup, updated if family.pedigree_image: updated_family_ids.add(family.id) - return created_individual + return created_individual, individual.id def delete_individuals(project, individual_guids, user): diff --git a/ui/pages/DataManagement/components/TriggerSearchDataUpdatePages.jsx b/ui/pages/DataManagement/components/TriggerSearchDataUpdatePages.jsx index c127b52f66..1ebb6c62f0 100644 --- a/ui/pages/DataManagement/components/TriggerSearchDataUpdatePages.jsx +++ b/ui/pages/DataManagement/components/TriggerSearchDataUpdatePages.jsx @@ -43,6 +43,7 @@ const FAMILY_FIELDS = [ placeholder: 'Search for a family', validate: validators.required, }, + { ...DATASET_TYPE_FIELD, name: 'datasetTypes', multiple: true, validate: null }, ] const TriggerSearchDataUpdateForm = ({ entity, fields }) => ( diff --git a/vlm/match.py b/vlm/match.py index 5d52533c81..0f038a8517 100644 --- a/vlm/match.py +++ b/vlm/match.py @@ -170,9 +170,8 @@ async def _get_match_detail_results(match: list[tuple], lift_match: Optional[lis async with ClientSession(ONTOLOGY_API_URL) as session: for f_i, (samples, has_discovery, has_excluded) in enumerate(match + (lift_match or [])): family_id = f'F_{f_i}' - proband = None - relatives = [] pedigree = [] + phenopackets = [] for s_i, (affected, sex, *sample) in enumerate(samples): individual_id = f'I_{f_i}_{s_i}' sex = SEX_LOOKUP.get(sex, 'OTHER_SEX') @@ -188,22 +187,15 @@ async def _get_match_detail_results(match: list[tuple], lift_match: Optional[lis phenopacket = await _format_phenopacket( hpo_label_map, mondo_label_map, session, individual_id, has_discovery, has_excluded, sex, *sample, ) - if affected == 'A' and proband is None: - proband = phenopacket - else: - relatives.append(phenopacket) - - if not proband: - proband = relatives[0] - relatives = relatives[1:] - - results.append({ - 'id': family_id, - 'proband': proband, - 'relatives': relatives, + phenopackets.append(phenopacket) + + results += [{ + 'id': phenopacket['id'], + 'proband': phenopacket, + 'relatives': [p for p in phenopackets if p is not phenopacket], 'pedigree': {'persons': pedigree}, 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, - }) + } for phenopacket in phenopackets] result_sets = [ (None, len(results), results), diff --git a/vlm/test_vlm.py b/vlm/test_vlm.py index fcbf1b82b7..9d64ddf050 100644 --- a/vlm/test_vlm.py +++ b/vlm/test_vlm.py @@ -31,181 +31,107 @@ def inject_fixtures(self, caplog): @aioresponses(passthrough=['http://127.0.0.1']) async def test_match(self, mocked_responses): - response = { - 'beaconHandovers': [ + meta = { + 'apiVersion': 'v1.0', + 'beaconId': 'com.gnx.beacon.v2', + 'returnedSchemas': [ { - 'handoverType': { - 'id': 'TestVLM', - 'label': 'TestVLM browser', - }, - 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=38&variantId=1-38724419-T-G', - 'email': None, + 'entityType': 'genomicVariant', + 'schema': 'ga4gh-beacon-variant-v2.0.0', } - ], - 'meta': { - 'apiVersion': 'v1.0', - 'beaconId': 'com.gnx.beacon.v2', - 'returnedSchemas': [ - { - 'entityType': 'genomicVariant', - 'schema': 'ga4gh-beacon-variant-v2.0.0', - } - ] + ] + } + results = [ + { + 'exists': True, + 'id': 'TestVLM Homozygous', + 'results': [], + 'resultsCount': 3, + 'setType': 'genomicVariant' }, - 'responseSummary': { + { 'exists': True, - 'total': 7, + 'id': 'TestVLM Heterozygous', + 'results': [], + 'resultsCount': 4, + 'setType': 'genomicVariant' }, - 'response': { - 'resultSets': [ - { - 'exists': True, - 'id': 'TestVLM Homozygous', - 'results': [], - 'resultsCount': 3, - 'setType': 'genomicVariant' - }, - { - 'exists': True, - 'id': 'TestVLM Heterozygous', - 'results': [], - 'resultsCount': 4, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Hemizygous', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Unknown', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - ], - } - } - only_37_response = { - 'beaconHandovers': [ - { - 'handoverType': { - 'id': 'TestVLM', - 'label': 'TestVLM browser', - }, - 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=37&variantId=7-143270172-A-G', - 'email': None, - } - ], - 'meta': { - 'apiVersion': 'v1.0', - 'beaconId': 'com.gnx.beacon.v2', - 'returnedSchemas': [ - { - 'entityType': 'genomicVariant', - 'schema': 'ga4gh-beacon-variant-v2.0.0', - } - ] + { + 'exists': False, + 'id': 'TestVLM Hemizygous', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' }, - 'responseSummary': { + { + 'exists': False, + 'id': 'TestVLM Unknown', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' + }, + ] + only_37_results = [ + { 'exists': True, - 'total': 1, + 'id': 'TestVLM Homozygous', + 'results': [], + 'resultsCount': 1, + 'setType': 'genomicVariant' }, - 'response': { - 'resultSets': [ - { - 'exists': True, - 'id': 'TestVLM Homozygous', - 'results': [], - 'resultsCount': 1, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Heterozygous', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Hemizygous', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Unknown', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - ], - } - } - empty_response = { - 'beaconHandovers': [ - { - 'handoverType': { - 'id': 'TestVLM', - 'label': 'TestVLM browser', - }, - 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=38&variantId=7-143270172-A-G', - 'email': None, - } - ], - 'meta': { - 'apiVersion': 'v1.0', - 'beaconId': 'com.gnx.beacon.v2', - 'returnedSchemas': [ - { - 'entityType': 'genomicVariant', - 'schema': 'ga4gh-beacon-variant-v2.0.0', - } - ] + { + 'exists': False, + 'id': 'TestVLM Heterozygous', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' }, - 'responseSummary': { + { 'exists': False, - 'total': 0, + 'id': 'TestVLM Hemizygous', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' }, - 'response': { - 'resultSets': [ - { - 'exists': False, - 'id': 'TestVLM Homozygous', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Heterozygous', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Hemizygous', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - { - 'exists': False, - 'id': 'TestVLM Unknown', - 'results': [], - 'resultsCount': 0, - 'setType': 'genomicVariant' - }, - ], - } - } - await self._test_match_endpoint('match', mocked_responses, response, only_37_response, empty_response) + { + 'exists': False, + 'id': 'TestVLM Unknown', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' + }, + ] + empty_results = [ + { + 'exists': False, + 'id': 'TestVLM Homozygous', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' + }, + { + 'exists': False, + 'id': 'TestVLM Heterozygous', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' + }, + { + 'exists': False, + 'id': 'TestVLM Hemizygous', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' + }, + { + 'exists': False, + 'id': 'TestVLM Unknown', + 'results': [], + 'resultsCount': 0, + 'setType': 'genomicVariant' + }, + ] + await self._test_match_endpoint('match', mocked_responses, meta, results, only_37_results, empty_results) @aioresponses(passthrough=['http://127.0.0.1']) async def test_match_details(self, mocked_responses): @@ -225,567 +151,707 @@ async def test_match_details(self, mocked_responses): 'definition': '', }) - response = { - 'beaconHandovers': [ + meta = { + 'apiVersion': 'v1.0', + 'beaconId': 'com.gnx.beacon.v2', + 'returnedSchemas': [ { - 'handoverType': { - 'id': 'TestVLM', - 'label': 'TestVLM browser', - }, - 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=38&variantId=1-38724419-T-G', - 'email': None, + 'entityType': 'Family', + 'schema': 'phenopacket-2.0', } - ], - 'meta': { - 'apiVersion': 'v1.0', - 'beaconId': 'com.gnx.beacon.v2', - 'returnedSchemas': [ - { - 'entityType': 'Family', - 'schema': 'phenopacket-2.0', - } - ] - }, - 'responseSummary': { + ] + } + results = [ + { 'exists': True, - 'total': 5, - }, - 'response': { - 'resultSets': [ - { - 'exists': True, - 'id': 'TestVLM', - 'setType': 'Family', - 'resultsCount': 5, - 'results': [{ - 'id': 'F_0', - 'pedigree': { - 'persons': [{ - 'affected_status': 'UNAFFECTED', - 'family_id': 'F_0', - 'individual_id': 'I_0_0', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'MALE', - }, { - 'affected_status': 'AFFECTED', - 'family_id': 'F_0', - 'individual_id': 'I_0_1', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'OTHER_SEX', - }], - }, - 'proband': { - 'id': 'I_0_1', - 'interpretations': [{ - 'diagnosis': { - 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, - }, - }, - 'interpretation_status': 'REJECTED', - 'subject_or_biosample_id': 'I_0_1', - }], + 'id': 'TestVLM', + 'setType': 'Family', + 'resultsCount': 7, + 'results': [{ + 'id': 'I_0_0', + 'pedigree': { + 'persons': [{ + 'affected_status': 'UNAFFECTED', + 'family_id': 'F_0', + 'individual_id': 'I_0_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'MALE', + }, { + 'affected_status': 'AFFECTED', + 'family_id': 'F_0', + 'individual_id': 'I_0_1', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'OTHER_SEX', + }], + }, + 'proband': { + 'id': 'I_0_0', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, + }, }, - 'id': 'I_0_1', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'REJECTED', + 'subject_or_biosample_id': 'I_0_0', }], - 'phenotypic_features': [ - {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, - {'id': 'HP:0011675', 'label': 'Arrhythmia'}, - ], - 'subject': { - 'id': 'I_0_1', - 'sex': 'OTHER_SEX', - }, - 'meta_data': { - 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }, { - 'id': 'hp', - 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', - 'name': 'Human Phenotype Ontology', - 'namespacePrefix': 'HP', - 'url': 'http://purl.obolibrary.org/obo/hp.owl', - 'version': datetime.now().strftime('%Y-%m-%d'), - }, { - 'id': 'omim', - 'iriPrefix': 'https://www.omim.org/entry/', - 'name': 'Online Mendelian Inheritance in Man', - 'namespacePrefix': 'OMIM', - 'url': 'https://www.omim.org', - 'version': datetime.now().strftime('%Y-%m-%d'), - }], - }, }, - 'relatives': [{ - 'id': 'I_0_0', - 'interpretations': [{ - 'diagnosis': { - 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, - }, - }, - 'interpretation_status': 'REJECTED', - 'subject_or_biosample_id': 'I_0_0', - }], + 'id': 'I_0_0', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_0_0', + 'sex': 'MALE', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'omim', + 'iriPrefix': 'https://www.omim.org/entry/', + 'name': 'Online Mendelian Inheritance in Man', + 'namespacePrefix': 'OMIM', + 'url': 'https://www.omim.org', + 'version': datetime.now().strftime('%Y-%m-%d'), + }], + }, + }, + 'relatives': [{ + 'id': 'I_0_1', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, + }, }, - 'id': 'I_0_0', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'REJECTED', + 'subject_or_biosample_id': 'I_0_1', }], - 'phenotypic_features': [], - 'subject': { - 'id': 'I_0_0', - 'sex': 'MALE', - }, - 'meta_data': { - 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }, { - 'id': 'omim', - 'iriPrefix': 'https://www.omim.org/entry/', - 'name': 'Online Mendelian Inheritance in Man', - 'namespacePrefix': 'OMIM', - 'url': 'https://www.omim.org', - 'version': datetime.now().strftime('%Y-%m-%d'), - }], - }, + }, + 'id': 'I_0_1', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [ + {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, + {'id': 'HP:0011675', 'label': 'Arrhythmia'}, + ], + 'subject': { + 'id': 'I_0_1', + 'sex': 'OTHER_SEX', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'hp', + 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', + 'name': 'Human Phenotype Ontology', + 'namespacePrefix': 'HP', + 'url': 'http://purl.obolibrary.org/obo/hp.owl', + 'version': datetime.now().strftime('%Y-%m-%d'), + }, { + 'id': 'omim', + 'iriPrefix': 'https://www.omim.org/entry/', + 'name': 'Online Mendelian Inheritance in Man', + 'namespacePrefix': 'OMIM', + 'url': 'https://www.omim.org', + 'version': datetime.now().strftime('%Y-%m-%d'), }], - 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, - }, { - 'id': 'F_1', - 'pedigree': { - 'persons': [{ - 'affected_status': 'AFFECTED', - 'family_id': 'F_1', - 'individual_id': 'I_1_0', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'OTHER_SEX', + }, + }], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, { + 'id': 'I_0_1', + 'pedigree': { + 'persons': [{ + 'affected_status': 'UNAFFECTED', + 'family_id': 'F_0', + 'individual_id': 'I_0_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'MALE', + }, { + 'affected_status': 'AFFECTED', + 'family_id': 'F_0', + 'individual_id': 'I_0_1', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'OTHER_SEX', + }], + }, + 'proband': { + 'id': 'I_0_1', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, + }, + }, + 'interpretation_status': 'REJECTED', + 'subject_or_biosample_id': 'I_0_1', }], }, - 'proband': { - 'id': 'I_1_0', - 'interpretations': [{ - 'diagnosis': { - 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, - }, - }, - 'interpretation_status': 'REJECTED', - 'subject_or_biosample_id': 'I_1_0', - }], + 'id': 'I_0_1', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [ + {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, + {'id': 'HP:0011675', 'label': 'Arrhythmia'}, + ], + 'subject': { + 'id': 'I_0_1', + 'sex': 'OTHER_SEX', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'hp', + 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', + 'name': 'Human Phenotype Ontology', + 'namespacePrefix': 'HP', + 'url': 'http://purl.obolibrary.org/obo/hp.owl', + 'version': datetime.now().strftime('%Y-%m-%d'), + }, { + 'id': 'omim', + 'iriPrefix': 'https://www.omim.org/entry/', + 'name': 'Online Mendelian Inheritance in Man', + 'namespacePrefix': 'OMIM', + 'url': 'https://www.omim.org', + 'version': datetime.now().strftime('%Y-%m-%d'), + }], + }, + }, + 'relatives': [{ + 'id': 'I_0_0', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, + }, }, - 'id': 'I_1_0', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'REJECTED', + 'subject_or_biosample_id': 'I_0_0', }], - 'phenotypic_features': [ - {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, - {'id': 'HP:0011675', 'label': 'Arrhythmia'}, - ], - 'subject': { - 'id': 'I_1_0', - 'sex': 'OTHER_SEX', - }, - 'meta_data': { - 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }, { - 'id': 'hp', - 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', - 'name': 'Human Phenotype Ontology', - 'namespacePrefix': 'HP', - 'url': 'http://purl.obolibrary.org/obo/hp.owl', - 'version': datetime.now().strftime('%Y-%m-%d'), - }, { - 'id': 'omim', - 'iriPrefix': 'https://www.omim.org/entry/', - 'name': 'Online Mendelian Inheritance in Man', - 'namespacePrefix': 'OMIM', - 'url': 'https://www.omim.org', - 'version': datetime.now().strftime('%Y-%m-%d'), - }], - }, }, - 'relatives': [], - 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, - }, { - 'id': 'F_2', - 'pedigree': { - 'persons': [{ - 'affected_status': 'MISSING', - 'family_id': 'F_2', - 'individual_id': 'I_2_0', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'UNKNOWN_SEX', - }, { - 'affected_status': 'MISSING', - 'family_id': 'F_2', - 'individual_id': 'I_2_1', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'FEMALE', + 'id': 'I_0_0', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_0_0', + 'sex': 'MALE', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'omim', + 'iriPrefix': 'https://www.omim.org/entry/', + 'name': 'Online Mendelian Inheritance in Man', + 'namespacePrefix': 'OMIM', + 'url': 'https://www.omim.org', + 'version': datetime.now().strftime('%Y-%m-%d'), + }], + }, + }], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, { + 'id': 'I_1_0', + 'pedigree': { + 'persons': [{ + 'affected_status': 'AFFECTED', + 'family_id': 'F_1', + 'individual_id': 'I_1_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'OTHER_SEX', + }], + }, + 'proband': { + 'id': 'I_1_0', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, + }, + }, + 'interpretation_status': 'REJECTED', + 'subject_or_biosample_id': 'I_1_0', }], }, - 'proband': { - 'id': 'I_2_0', - 'interpretations': [{ - 'diagnosis': { - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, - }, - }, - 'interpretation_status': 'CANDIDATE', - 'subject_or_biosample_id': 'I_2_0', - }], + 'id': 'I_1_0', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [ + {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, + {'id': 'HP:0011675', 'label': 'Arrhythmia'}, + ], + 'subject': { + 'id': 'I_1_0', + 'sex': 'OTHER_SEX', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'hp', + 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', + 'name': 'Human Phenotype Ontology', + 'namespacePrefix': 'HP', + 'url': 'http://purl.obolibrary.org/obo/hp.owl', + 'version': datetime.now().strftime('%Y-%m-%d'), + }, { + 'id': 'omim', + 'iriPrefix': 'https://www.omim.org/entry/', + 'name': 'Online Mendelian Inheritance in Man', + 'namespacePrefix': 'OMIM', + 'url': 'https://www.omim.org', + 'version': datetime.now().strftime('%Y-%m-%d'), + }], + }, + }, + 'relatives': [], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, { + 'id': 'I_2_0', + 'pedigree': { + 'persons': [{ + 'affected_status': 'MISSING', + 'family_id': 'F_2', + 'individual_id': 'I_2_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'UNKNOWN_SEX', + }, { + 'affected_status': 'MISSING', + 'family_id': 'F_2', + 'individual_id': 'I_2_1', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'FEMALE', + }], + }, + 'proband': { + 'id': 'I_2_0', + 'interpretations': [{ + 'diagnosis': { + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, + }, }, - 'id': 'I_2_0', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'CANDIDATE', + 'subject_or_biosample_id': 'I_2_0', }], - 'phenotypic_features': [], - 'subject': { - 'id': 'I_2_0', - 'sex': 'UNKNOWN_SEX', - }, - 'meta_data': { - 'submitted_by': '', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }], - }, }, - 'relatives': [{ - 'id': 'I_2_1', - 'interpretations': [{ - 'diagnosis': { - 'disease': {'id': 'MONDO:0044970', 'label': 'mitochondrial disease'}, - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, - }, - }, - 'interpretation_status': 'CANDIDATE', - 'subject_or_biosample_id': 'I_2_1', - }], + 'id': 'I_2_0', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_2_0', + 'sex': 'UNKNOWN_SEX', + }, + 'meta_data': { + 'submitted_by': '', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }], + }, + }, + 'relatives': [{ + 'id': 'I_2_1', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'MONDO:0044970', 'label': 'mitochondrial disease'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, + }, }, - 'id': 'I_2_1', - 'progress_status': 'SOLVED', + 'interpretation_status': 'CANDIDATE', + 'subject_or_biosample_id': 'I_2_1', }], - 'phenotypic_features': [], - 'subject': { - 'id': 'I_2_1', - 'sex': 'FEMALE', - }, - 'meta_data': { - 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }, { - 'id': 'mondo', - 'iriPrefix': 'http://purl.obolibrary.org/obo/MONDO_', - 'name': 'Mondo Disease Ontology', - 'namespacePrefix': 'MONDO', - 'url': 'http://purl.obolibrary.org/obo/mondo.owl', - 'version': datetime.now().strftime('%Y-%m-%d'), - }], - }, + }, + 'id': 'I_2_1', + 'progress_status': 'SOLVED', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_2_1', + 'sex': 'FEMALE', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'mondo', + 'iriPrefix': 'http://purl.obolibrary.org/obo/MONDO_', + 'name': 'Mondo Disease Ontology', + 'namespacePrefix': 'MONDO', + 'url': 'http://purl.obolibrary.org/obo/mondo.owl', + 'version': datetime.now().strftime('%Y-%m-%d'), }], - 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, + }], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, { + 'id': 'I_2_1', + 'pedigree': { + 'persons': [{ + 'affected_status': 'MISSING', + 'family_id': 'F_2', + 'individual_id': 'I_2_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'UNKNOWN_SEX', }, { - 'id': 'F_3', - 'pedigree': { - 'persons': [{ - 'affected_status': 'AFFECTED', - 'family_id': 'F_3', - 'individual_id': 'I_3_0', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'MALE', + 'affected_status': 'MISSING', + 'family_id': 'F_2', + 'individual_id': 'I_2_1', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'FEMALE', + }], + }, + 'proband': { + 'id': 'I_2_1', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'MONDO:0044970', 'label': 'mitochondrial disease'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, + }, + }, + 'interpretation_status': 'CANDIDATE', + 'subject_or_biosample_id': 'I_2_1', }], }, - 'proband': { - 'id': 'I_3_0', - 'interpretations': [{ - 'diagnosis': { - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': { - 'id': 'GENO:0000135', 'label': 'heterozygous'}, - }, - }, - 'interpretation_status': 'UNKNOWN_STATUS', - 'subject_or_biosample_id': 'I_3_0', - }], + 'id': 'I_2_1', + 'progress_status': 'SOLVED', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_2_1', + 'sex': 'FEMALE', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'mondo', + 'iriPrefix': 'http://purl.obolibrary.org/obo/MONDO_', + 'name': 'Mondo Disease Ontology', + 'namespacePrefix': 'MONDO', + 'url': 'http://purl.obolibrary.org/obo/mondo.owl', + 'version': datetime.now().strftime('%Y-%m-%d'), + }], + }, + }, + 'relatives': [{ + 'id': 'I_2_0', + 'interpretations': [{ + 'diagnosis': { + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, + }, }, - 'id': 'I_3_0', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'CANDIDATE', + 'subject_or_biosample_id': 'I_2_0', }], - 'phenotypic_features': [], - 'subject': { - 'id': 'I_3_0', - 'sex': 'MALE', - }, - 'meta_data': { - 'submitted_by': 'seqr-test@gmail.com,test@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }], - }, }, - 'relatives': [], - 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, - }, { - 'id': 'F_4', - 'pedigree': { - 'persons': [{ - 'affected_status': 'AFFECTED', - 'family_id': 'F_4', - 'individual_id': 'I_4_0', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'MALE', + 'id': 'I_2_0', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_2_0', + 'sex': 'UNKNOWN_SEX', + }, + 'meta_data': { + 'submitted_by': '', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }], + }, + }], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, { + 'id': 'I_3_0', + 'pedigree': { + 'persons': [{ + 'affected_status': 'AFFECTED', + 'family_id': 'F_3', + 'individual_id': 'I_3_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'MALE', + }], + }, + 'proband': { + 'id': 'I_3_0', + 'interpretations': [{ + 'diagnosis': { + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': { + 'id': 'GENO:0000135', 'label': 'heterozygous'}, + }, + }, + 'interpretation_status': 'UNKNOWN_STATUS', + 'subject_or_biosample_id': 'I_3_0', }], }, - 'proband': { - 'id': 'I_4_0', - 'interpretations': [{ - 'diagnosis': { - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, - }, - }, - 'interpretation_status': 'UNKNOWN_STATUS', - 'subject_or_biosample_id': 'I_4_0', - }], + 'id': 'I_3_0', + 'progress_status': 'UNKNOWN_PROGRESS', + }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_3_0', + 'sex': 'MALE', + }, + 'meta_data': { + 'submitted_by': 'seqr-test@gmail.com,test@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }], + }, + }, + 'relatives': [], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }, { + 'id': 'I_4_0', + 'pedigree': { + 'persons': [{ + 'affected_status': 'AFFECTED', + 'family_id': 'F_4', + 'individual_id': 'I_4_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'MALE', + }], + }, + 'proband': { + 'id': 'I_4_0', + 'interpretations': [{ + 'diagnosis': { + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000135', 'label': 'heterozygous'}, + }, }, - 'id': 'I_4_0', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'UNKNOWN_STATUS', + 'subject_or_biosample_id': 'I_4_0', }], - 'phenotypic_features': [], - 'subject': { - 'id': 'I_4_0', - 'sex': 'MALE', - }, - 'meta_data': { - 'submitted_by': 'seqr-test@gmail.com,test@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }], - }, }, - 'relatives': [], - 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + 'id': 'I_4_0', + 'progress_status': 'UNKNOWN_PROGRESS', }], + 'phenotypic_features': [], + 'subject': { + 'id': 'I_4_0', + 'sex': 'MALE', + }, + 'meta_data': { + 'submitted_by': 'seqr-test@gmail.com,test@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }], + }, }, - ], - } - } - only_37_response = { - 'beaconHandovers': [ - { - 'handoverType': { - 'id': 'TestVLM', - 'label': 'TestVLM browser', - }, - 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=37&variantId=7-143270172-A-G', - 'email': None, - } - ], - 'meta': { - 'apiVersion': 'v1.0', - 'beaconId': 'com.gnx.beacon.v2', - 'returnedSchemas': [ - { - 'entityType': 'Family', - 'schema': 'phenopacket-2.0', - } - ] + 'relatives': [], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }], }, - 'responseSummary': { + ] + only_37_results = [ + { 'exists': True, - 'total': 1, - }, - 'response': { - 'resultSets': [ - { - 'exists': True, - 'id': 'TestVLM', - 'setType': 'Family', - 'resultsCount': 1, - 'results': [{ - 'id': 'F_0', - 'pedigree': { - 'persons': [{ - 'affected_status': 'AFFECTED', - 'family_id': 'F_0', - 'individual_id': 'I_0_0', - 'maternal_id': '0', - 'paternal_id': '0', - 'sex': 'OTHER_SEX', - }], - }, - 'proband': { - 'id': 'I_0_0', - 'interpretations': [{ - 'diagnosis': { - 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, - 'genomic_interpretations': [{ - 'call': { - 'variation_descriptor': { - 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, - }, - }, - 'interpretation_status': 'UNKNOWN_STATUS', - 'subject_or_biosample_id': 'I_0_0', - }], + 'id': 'TestVLM', + 'setType': 'Family', + 'resultsCount': 1, + 'results': [{ + 'id': 'I_0_0', + 'pedigree': { + 'persons': [{ + 'affected_status': 'AFFECTED', + 'family_id': 'F_0', + 'individual_id': 'I_0_0', + 'maternal_id': '0', + 'paternal_id': '0', + 'sex': 'OTHER_SEX', + }], + }, + 'proband': { + 'id': 'I_0_0', + 'interpretations': [{ + 'diagnosis': { + 'disease': {'id': 'OMIM:615123', 'label': 'Immunodeficiency 38'}, + 'genomic_interpretations': [{ + 'call': { + 'variation_descriptor': { + 'allelic_state': {'id': 'GENO:0000136', 'label': 'homozygous'}, + }, }, - 'id': 'I_0_0', - 'progress_status': 'UNKNOWN_PROGRESS', + 'interpretation_status': 'UNKNOWN_STATUS', + 'subject_or_biosample_id': 'I_0_0', }], - 'phenotypic_features': [ - {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, - {'id': 'HP:0011675', 'label': 'Arrhythmia'}, - ], - 'subject': { - 'id': 'I_0_0', - 'sex': 'OTHER_SEX', - }, - 'meta_data': { - 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', - 'phenopacket_schema_version': '2.0', - 'resources': [{ - 'id': 'geno', - 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', - 'name': 'GENO ontology', - 'namespacePrefix': 'GENO', - 'url': 'http://purl.obolibrary.org/obo/geno.owl', - 'version': '2026-02-02', - }, { - 'id': 'hp', - 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', - 'name': 'Human Phenotype Ontology', - 'namespacePrefix': 'HP', - 'url': 'http://purl.obolibrary.org/obo/hp.owl', - 'version': datetime.now().strftime('%Y-%m-%d'), - }, { - 'id': 'omim', - 'iriPrefix': 'https://www.omim.org/entry/', - 'name': 'Online Mendelian Inheritance in Man', - 'namespacePrefix': 'OMIM', - 'url': 'https://www.omim.org', - 'version': datetime.now().strftime('%Y-%m-%d'), - }], - }, }, - 'relatives': [], - 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + 'id': 'I_0_0', + 'progress_status': 'UNKNOWN_PROGRESS', }], + 'phenotypic_features': [ + {'id': 'HP:0002011', 'label': 'Morphological central nervous system abnormality'}, + {'id': 'HP:0011675', 'label': 'Arrhythmia'}, + ], + 'subject': { + 'id': 'I_0_0', + 'sex': 'OTHER_SEX', + }, + 'meta_data': { + 'submitted_by': 'test@broadinstitute.org,vlm@broadinstitute.org', + 'phenopacket_schema_version': '2.0', + 'resources': [{ + 'id': 'geno', + 'iriPrefix': 'http://purl.obolibrary.org/obo/GENO_', + 'name': 'GENO ontology', + 'namespacePrefix': 'GENO', + 'url': 'http://purl.obolibrary.org/obo/geno.owl', + 'version': '2026-02-02', + }, { + 'id': 'hp', + 'iriPrefix': 'http://purl.obolibrary.org/obo/HP_', + 'name': 'Human Phenotype Ontology', + 'namespacePrefix': 'HP', + 'url': 'http://purl.obolibrary.org/obo/hp.owl', + 'version': datetime.now().strftime('%Y-%m-%d'), + }, { + 'id': 'omim', + 'iriPrefix': 'https://www.omim.org/entry/', + 'name': 'Online Mendelian Inheritance in Man', + 'namespacePrefix': 'OMIM', + 'url': 'https://www.omim.org', + 'version': datetime.now().strftime('%Y-%m-%d'), + }], + }, }, - ], - } - } - empty_response = { - 'beaconHandovers': [ - { - 'handoverType': { - 'id': 'TestVLM', - 'label': 'TestVLM browser', - }, - 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=38&variantId=7-143270172-A-G', - 'email': None, - } - ], - 'meta': { - 'apiVersion': 'v1.0', - 'beaconId': 'com.gnx.beacon.v2', - 'returnedSchemas': [ - { - 'entityType': 'Family', - 'schema': 'phenopacket-2.0', - } - ] + 'relatives': [], + 'meta_data': {'phenopacket_schema_version': '2.0', 'resources': []}, + }], }, - 'responseSummary': { + ] + empty_results = [ + { 'exists': False, - 'total': 0, + 'id': 'TestVLM', + 'results': [], + 'resultsCount': 0, + 'setType': 'Family' }, - 'response': { - 'resultSets': [ - { - 'exists': False, - 'id': 'TestVLM', - 'results': [], - 'resultsCount': 0, - 'setType': 'Family' - }, - ], - } - } - await self._test_match_endpoint('match_details', mocked_responses, response, only_37_response, empty_response) + ] + await self._test_match_endpoint('match_details', mocked_responses, meta, results, only_37_results, empty_results) - async def _test_match_endpoint(self, path, mocked_responses, response, only_37_response, empty_response): + async def _test_match_endpoint(self, path, mocked_responses, meta, results, only_37_results, empty_results): mocked_responses.post( 'https://vlm-auth.us.auth0.com/oauth/token', payload={'access_token': 'test_token'}, repeat=True, # nosec @@ -801,6 +867,26 @@ async def _test_match_endpoint(self, path, mocked_responses, response, only_37_r async with self.client.request('GET', f'/vlm/{path}?assemblyId=GRCh38&referenceName=1&start=38724419&referenceBases=T&alternateBases=G', headers=headers) as resp: self.assertEqual(resp.status, 200) resp_json = await resp.json() + response = { + 'beaconHandovers': [ + { + 'handoverType': { + 'id': 'TestVLM', + 'label': 'TestVLM browser', + }, + 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=38&variantId=1-38724419-T-G', + 'email': None, + } + ], + 'meta': meta, + 'responseSummary': { + 'exists': True, + 'total': 7, + }, + 'response': { + 'resultSets': results, + }, + } self.assertDictEqual(resp_json, response) mocked_responses.assert_called_with( 'https://vlm-auth.us.auth0.com/oauth/token', @@ -821,6 +907,26 @@ async def _test_match_endpoint(self, path, mocked_responses, response, only_37_r resp_json = await resp.json() self.assertDictEqual(resp_json, response) + only_37_response = { + 'beaconHandovers': [ + { + 'handoverType': { + 'id': 'TestVLM', + 'label': 'TestVLM browser', + }, + 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=37&variantId=7-143270172-A-G', + 'email': None, + } + ], + 'meta': meta, + 'responseSummary': { + 'exists': True, + 'total': 1, + }, + 'response': { + 'resultSets': only_37_results, + }, + } async with self.client.request('GET', f'/vlm/{path}?assemblyId=hg19&referenceName=chr7&start=143270172&referenceBases=A&alternateBases=G', headers=headers) as resp: self.assertEqual(resp.status, 200) resp_json = await resp.json() @@ -834,7 +940,26 @@ async def _test_match_endpoint(self, path, mocked_responses, response, only_37_r async with self.client.request('GET', f'/vlm/{path}?assemblyId=hg38&referenceName=chr7&start=143270172&referenceBases=A&alternateBases=G', headers=headers) as resp: self.assertEqual(resp.status, 200) resp_json = await resp.json() - self.assertDictEqual(resp_json, empty_response) + self.assertDictEqual(resp_json, { + 'beaconHandovers': [ + { + 'handoverType': { + 'id': 'TestVLM', + 'label': 'TestVLM browser', + }, + 'url': 'https://test-seqr.org/variant_lookup?genomeVersion=38&variantId=7-143270172-A-G', + 'email': None, + } + ], + 'meta': meta, + 'responseSummary': { + 'exists': False, + 'total': 0, + }, + 'response': { + 'resultSets': empty_results, + }, + }) @aioresponses(passthrough=['http://127.0.0.1']) async def test_match_error(self, mocked_responses):