scout-browser 4.90.1__py3-none-any.whl → 4.91__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scout/__version__.py +1 -1
- scout/adapter/mongo/case.py +27 -38
- scout/commands/export/variant.py +14 -4
- scout/commands/load/panel.py +2 -1
- scout/commands/update/panelapp.py +11 -3
- scout/commands/view/case.py +2 -2
- scout/constants/__init__.py +1 -2
- scout/constants/acmg.py +15 -15
- scout/constants/case_tags.py +0 -46
- scout/constants/clnsig.py +2 -1
- scout/constants/gene_tags.py +0 -6
- scout/constants/panels.py +16 -0
- scout/constants/variants_export.py +2 -0
- scout/demo/__init__.py +4 -1
- scout/demo/panelapp_panel.json +463 -0
- scout/demo/panelapp_panels_reduced.json +37 -0
- scout/load/panel.py +3 -142
- scout/load/panelapp.py +138 -0
- scout/models/case/case_loading_models.py +5 -4
- scout/parse/panel.py +3 -117
- scout/parse/panelapp.py +112 -0
- scout/parse/variant/clnsig.py +26 -21
- scout/parse/variant/genotype.py +6 -5
- scout/server/blueprints/alignviewers/controllers.py +7 -5
- scout/server/blueprints/alignviewers/templates/alignviewers/utils.html +1 -1
- scout/server/blueprints/cases/templates/cases/case_sma.html +49 -42
- scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +27 -12
- scout/server/blueprints/cases/views.py +18 -7
- scout/server/blueprints/clinvar/templates/clinvar/clinvar_submissions.html +7 -7
- scout/server/blueprints/clinvar/templates/clinvar/multistep_add_variant.html +2 -2
- scout/server/blueprints/dashboard/controllers.py +128 -165
- scout/server/blueprints/dashboard/forms.py +3 -13
- scout/server/blueprints/dashboard/templates/dashboard/dashboard_general.html +17 -22
- scout/server/blueprints/institutes/forms.py +1 -2
- scout/server/blueprints/institutes/templates/overview/cases.html +2 -133
- scout/server/blueprints/institutes/templates/overview/utils.html +135 -0
- scout/server/blueprints/omics_variants/templates/omics_variants/outliers.html +5 -0
- scout/server/blueprints/panels/templates/panels/panel.html +5 -1
- scout/server/blueprints/panels/templates/panels/panel_pdf_simple.html +5 -1
- scout/server/blueprints/variant/controllers.py +6 -1
- scout/server/blueprints/variant/templates/variant/buttons.html +11 -10
- scout/server/blueprints/variant/templates/variant/components.html +63 -44
- scout/server/blueprints/variant/templates/variant/str-variant-reviewer.html +1 -1
- scout/server/blueprints/variant/templates/variant/utils.html +38 -10
- scout/server/blueprints/variant/templates/variant/variant.html +1 -1
- scout/server/blueprints/variants/controllers.py +9 -4
- scout/server/blueprints/variants/templates/variants/cancer-sv-variants.html +9 -5
- scout/server/blueprints/variants/templates/variants/cancer-variants.html +6 -17
- scout/server/blueprints/variants/templates/variants/str-variants.html +2 -2
- scout/server/blueprints/variants/templates/variants/sv-variants.html +8 -1
- scout/server/blueprints/variants/templates/variants/utils.html +14 -0
- scout/server/extensions/__init__.py +2 -0
- scout/server/extensions/panelapp_extension.py +75 -0
- scout/server/links.py +19 -1
- scout/server/utils.py +25 -33
- {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/METADATA +1 -1
- {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/RECORD +61 -57
- {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/WHEEL +1 -1
- scout/demo/panelapp_test_panel.json +0 -79
- {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/LICENSE +0 -0
- {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/entry_points.txt +0 -0
- {scout_browser-4.90.1.dist-info → scout_browser-4.91.dist-info}/top_level.txt +0 -0
scout/load/panelapp.py
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
import logging
|
2
|
+
import math
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Dict, List, Set
|
5
|
+
|
6
|
+
from click import Abort, progressbar
|
7
|
+
|
8
|
+
from scout.adapter import MongoAdapter
|
9
|
+
from scout.constants.panels import PRESELECTED_PANELAPP_PANEL_TYPE_SLUGS
|
10
|
+
from scout.parse.panelapp import parse_panelapp_panel
|
11
|
+
from scout.server.extensions import panelapp
|
12
|
+
|
13
|
+
LOG = logging.getLogger(__name__)
|
14
|
+
PANEL_NAME = "PANELAPP-GREEN"
|
15
|
+
|
16
|
+
|
17
|
+
def load_panelapp_panel(
|
18
|
+
adapter: MongoAdapter,
|
19
|
+
panel_id: str = None,
|
20
|
+
institute: str = "cust000",
|
21
|
+
confidence: str = "green",
|
22
|
+
):
|
23
|
+
"""Load PanelApp panels into scout database."""
|
24
|
+
|
25
|
+
panel_ids = [panel_id]
|
26
|
+
|
27
|
+
if not panel_id:
|
28
|
+
LOG.info("Fetching all panel app panels")
|
29
|
+
panel_ids: List[str] = panelapp.get_panel_ids(signed_off=False)
|
30
|
+
|
31
|
+
ensembl_id_to_hgnc_id_map: Dict[str, int] = adapter.ensembl_to_hgnc_id_mapping()
|
32
|
+
hgnc_symbol_to_ensembl_id_map: Dict[int, str] = adapter.hgnc_symbol_ensembl_id_mapping()
|
33
|
+
|
34
|
+
for _ in panel_ids:
|
35
|
+
panel_info: dict = panelapp.get_panel(panel_id)
|
36
|
+
parsed_panel = parse_panelapp_panel(
|
37
|
+
panel_info=panel_info,
|
38
|
+
ensembl_id_to_hgnc_id_map=ensembl_id_to_hgnc_id_map,
|
39
|
+
hgnc_symbol_to_ensembl_id_map=hgnc_symbol_to_ensembl_id_map,
|
40
|
+
institute=institute,
|
41
|
+
confidence=confidence,
|
42
|
+
)
|
43
|
+
|
44
|
+
if len(parsed_panel["genes"]) == 0:
|
45
|
+
LOG.warning("Panel %s is missing genes. Skipping.", parsed_panel["display_name"])
|
46
|
+
continue
|
47
|
+
|
48
|
+
adapter.load_panel(parsed_panel=parsed_panel, replace=True)
|
49
|
+
|
50
|
+
|
51
|
+
def get_panelapp_genes(
|
52
|
+
adapter: MongoAdapter, institute: str, panel_ids: List[str], types_filter: List[str]
|
53
|
+
) -> Set[tuple]:
|
54
|
+
"""Parse and collect genes from one or more panelApp panels."""
|
55
|
+
|
56
|
+
genes = set()
|
57
|
+
ensembl_id_to_hgnc_id_map: Dict[str, int] = adapter.ensembl_to_hgnc_id_mapping()
|
58
|
+
hgnc_symbol_to_ensembl_id_map: Dict[int, str] = adapter.hgnc_symbol_ensembl_id_mapping()
|
59
|
+
|
60
|
+
with progressbar(panel_ids, label="Parsing panels", length=len(panel_ids)) as panel_ids:
|
61
|
+
for panel_id in panel_ids:
|
62
|
+
panel_dict: dict = panelapp.get_panel(panel_id)
|
63
|
+
panel_type_slugs = [type["slug"] for type in panel_dict.get("types")]
|
64
|
+
# Parse panel only if it's of the expect type(s)
|
65
|
+
if not set(types_filter).intersection(panel_type_slugs):
|
66
|
+
continue
|
67
|
+
|
68
|
+
parsed_panel = parse_panelapp_panel(
|
69
|
+
panel_info=panel_dict,
|
70
|
+
ensembl_id_to_hgnc_id_map=ensembl_id_to_hgnc_id_map,
|
71
|
+
hgnc_symbol_to_ensembl_id_map=hgnc_symbol_to_ensembl_id_map,
|
72
|
+
institute=institute,
|
73
|
+
confidence="green",
|
74
|
+
)
|
75
|
+
genes.update(
|
76
|
+
{(gene["hgnc_id"], gene["hgnc_symbol"]) for gene in parsed_panel.get("genes")}
|
77
|
+
)
|
78
|
+
|
79
|
+
return genes
|
80
|
+
|
81
|
+
|
82
|
+
def load_panelapp_green_panel(adapter: MongoAdapter, institute: str, force: bool, signed_off: bool):
|
83
|
+
"""Load/Update the panel containing all Panelapp Green genes."""
|
84
|
+
|
85
|
+
def parse_types_filter(types_filter: str, available_types: List[str]) -> List[str]:
|
86
|
+
"""Translate panel type input from users to panel type slugs."""
|
87
|
+
if not types_filter:
|
88
|
+
return PRESELECTED_PANELAPP_PANEL_TYPE_SLUGS
|
89
|
+
index_list = [int(typeint) - 1 for typeint in types_filter.replace(" ", "").split(",")]
|
90
|
+
return [available_types[i] for i in index_list]
|
91
|
+
|
92
|
+
# check and set panel version
|
93
|
+
old_panel = adapter.gene_panel(panel_id=PANEL_NAME)
|
94
|
+
green_panel = {
|
95
|
+
"panel_name": PANEL_NAME,
|
96
|
+
"display_name": "PanelApp Green Genes",
|
97
|
+
"institute": institute,
|
98
|
+
"version": float(math.floor(old_panel["version"]) + 1) if old_panel else 1.0,
|
99
|
+
"date": datetime.now(),
|
100
|
+
}
|
101
|
+
|
102
|
+
LOG.info("Fetching all PanelApp panels")
|
103
|
+
|
104
|
+
panel_ids = panelapp.get_panel_ids(signed_off=signed_off)
|
105
|
+
LOG.info(f"\n\nQuery returned {len(panel_ids)} panels\n")
|
106
|
+
LOG.info("Panels have the following associated types:")
|
107
|
+
available_types: List[str] = panelapp.get_panel_types()
|
108
|
+
for number, type in enumerate(available_types, 1):
|
109
|
+
LOG.info(f"{number}: {type}")
|
110
|
+
preselected_options_idx: List[str] = [
|
111
|
+
str(available_types.index(presel) + 1)
|
112
|
+
for presel in PRESELECTED_PANELAPP_PANEL_TYPE_SLUGS
|
113
|
+
if presel in available_types
|
114
|
+
]
|
115
|
+
types_filter: str = input(
|
116
|
+
f"Please provide a comma-separated list of types you'd like to use to build your panel (leave blank to use the following types:{', '.join(preselected_options_idx)}): "
|
117
|
+
)
|
118
|
+
types_filter: List[str] = parse_types_filter(
|
119
|
+
types_filter=types_filter, available_types=available_types
|
120
|
+
)
|
121
|
+
LOG.info(f"Collecting green genes from panels of type: {types_filter}")
|
122
|
+
green_panel["description"] = (
|
123
|
+
f"This panel contains green genes from {'signed off ' if signed_off else ''}panels of the following types: {', '.join(types_filter)}"
|
124
|
+
)
|
125
|
+
genes: Set[tuple] = get_panelapp_genes(
|
126
|
+
adapter=adapter, institute=institute, panel_ids=panel_ids, types_filter=types_filter
|
127
|
+
)
|
128
|
+
green_panel["genes"] = [{"hgnc_id": tup[0], "hgnc_symbol": tup[1]} for tup in genes]
|
129
|
+
|
130
|
+
# Do not update panel if new version contains less genes and force flag is False
|
131
|
+
if old_panel and len(old_panel.get("genes")) > len(green_panel["genes"]):
|
132
|
+
LOG.warning(
|
133
|
+
f"This new version of PANELAPP-GREEN contains less genes (n={len(green_panel['genes'])}) than the previous one (n={len(old_panel['genes'])})"
|
134
|
+
)
|
135
|
+
if force is False:
|
136
|
+
LOG.error("Aborting. Please use the force flag -f to update the panel anyway")
|
137
|
+
return
|
138
|
+
adapter.load_panel(parsed_panel=green_panel, replace=True)
|
@@ -557,10 +557,11 @@ class CaseLoader(BaseModel):
|
|
557
557
|
images=custom_images.str_variants_images
|
558
558
|
)
|
559
559
|
|
560
|
-
|
561
|
-
custom_images.case_images
|
562
|
-
|
563
|
-
|
560
|
+
if custom_images.case_images:
|
561
|
+
for key, images in custom_images.case_images.items():
|
562
|
+
custom_images.case_images[key] = set_custom_images(
|
563
|
+
images=custom_images.case_images[key]
|
564
|
+
)
|
564
565
|
|
565
566
|
return custom_images
|
566
567
|
|
scout/parse/panel.py
CHANGED
@@ -2,15 +2,9 @@
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
from datetime import datetime
|
5
|
-
from typing import
|
6
|
-
|
7
|
-
from scout.constants import
|
8
|
-
INCOMPLETE_PENETRANCE_MAP,
|
9
|
-
MODELS_MAP,
|
10
|
-
PANEL_GENE_INFO_MODELS,
|
11
|
-
PANEL_GENE_INFO_TRANSCRIPTS,
|
12
|
-
PANELAPP_CONFIDENCE_EXCLUDE,
|
13
|
-
)
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
from scout.constants import PANEL_GENE_INFO_MODELS, PANEL_GENE_INFO_TRANSCRIPTS
|
14
8
|
from scout.utils.date import get_date
|
15
9
|
from scout.utils.handle import get_file_handle
|
16
10
|
|
@@ -266,114 +260,6 @@ def parse_gene_panel(
|
|
266
260
|
return gene_panel
|
267
261
|
|
268
262
|
|
269
|
-
def parse_panel_app_gene(
|
270
|
-
app_gene: dict,
|
271
|
-
ensembl_gene_hgnc_id_map: Dict[str, int],
|
272
|
-
hgnc_symbol_ensembl_gene_map: Dict[str, str],
|
273
|
-
confidence: str,
|
274
|
-
) -> dict:
|
275
|
-
"""Parse a panel app-formatted gene."""
|
276
|
-
|
277
|
-
gene_info = {}
|
278
|
-
confidence_level = app_gene["LevelOfConfidence"]
|
279
|
-
# Return empty gene if not confident gene
|
280
|
-
if confidence_level in PANELAPP_CONFIDENCE_EXCLUDE[confidence]:
|
281
|
-
return gene_info
|
282
|
-
|
283
|
-
hgnc_symbol = app_gene["GeneSymbol"]
|
284
|
-
|
285
|
-
ensembl_ids = app_gene["EnsembleGeneIds"]
|
286
|
-
|
287
|
-
if not ensembl_ids: # This gene is probably tagged as ensembl_ids_known_missing on PanelApp
|
288
|
-
if hgnc_symbol in hgnc_symbol_ensembl_gene_map:
|
289
|
-
LOG.warning(
|
290
|
-
f"PanelApp gene {hgnc_symbol} does not contain Ensembl IDs. Using Ensembl IDs from internal gene collection instead."
|
291
|
-
)
|
292
|
-
ensembl_ids = [hgnc_symbol_ensembl_gene_map[hgnc_symbol]]
|
293
|
-
else:
|
294
|
-
LOG.warning(
|
295
|
-
f"PanelApp gene {hgnc_symbol} does not contain Ensembl IDs and gene symbol does not correspond to a gene in scout."
|
296
|
-
)
|
297
|
-
|
298
|
-
hgnc_ids = set(
|
299
|
-
ensembl_gene_hgnc_id_map.get(ensembl_id)
|
300
|
-
for ensembl_id in ensembl_ids
|
301
|
-
if ensembl_gene_hgnc_id_map.get(ensembl_id)
|
302
|
-
)
|
303
|
-
if not hgnc_ids:
|
304
|
-
LOG.warning("Gene %s does not exist in database. Skipping gene...", hgnc_symbol)
|
305
|
-
return gene_info
|
306
|
-
|
307
|
-
if len(hgnc_ids) > 1:
|
308
|
-
LOG.warning("Gene %s has unclear identifier. Choose random id", hgnc_symbol)
|
309
|
-
|
310
|
-
gene_info["hgnc_symbol"] = hgnc_symbol
|
311
|
-
for hgnc_id in hgnc_ids:
|
312
|
-
gene_info["hgnc_id"] = hgnc_id
|
313
|
-
|
314
|
-
gene_info["reduced_penetrance"] = INCOMPLETE_PENETRANCE_MAP.get(app_gene["Penetrance"])
|
315
|
-
|
316
|
-
inheritance_models = []
|
317
|
-
for model in MODELS_MAP.get(app_gene["ModeOfInheritance"], []):
|
318
|
-
inheritance_models.append(model)
|
319
|
-
|
320
|
-
gene_info["inheritance_models"] = inheritance_models
|
321
|
-
|
322
|
-
return gene_info
|
323
|
-
|
324
|
-
|
325
|
-
def parse_panel_app_panel(
|
326
|
-
panel_info: dict,
|
327
|
-
ensembl_gene_hgnc_id_map: Dict[str, int],
|
328
|
-
hgnc_symbol_ensembl_gene_map: Dict[str, str],
|
329
|
-
institute: Optional[str] = "cust000",
|
330
|
-
panel_type: Optional[str] = "clinical",
|
331
|
-
confidence: Optional[str] = "green",
|
332
|
-
) -> dict:
|
333
|
-
"""Parse a PanelApp panel
|
334
|
-
|
335
|
-
Args:
|
336
|
-
panel_info(dict)
|
337
|
-
hgnc_map(dict): Map from symbol to hgnc ids
|
338
|
-
institute(str)
|
339
|
-
panel_type(str)
|
340
|
-
confidence(str): enum green|amber|red
|
341
|
-
|
342
|
-
Returns:
|
343
|
-
gene_panel(dict)
|
344
|
-
"""
|
345
|
-
date_format = "%Y-%m-%dT%H:%M:%S.%f"
|
346
|
-
|
347
|
-
gene_panel = {}
|
348
|
-
gene_panel["version"] = float(panel_info["version"])
|
349
|
-
gene_panel["date"] = get_date(panel_info["Created"][:-1], date_format=date_format)
|
350
|
-
gene_panel["display_name"] = " - ".join(
|
351
|
-
[panel_info["SpecificDiseaseName"], f"[{confidence.upper()}]"]
|
352
|
-
)
|
353
|
-
gene_panel["institute"] = institute
|
354
|
-
gene_panel["panel_type"] = panel_type
|
355
|
-
|
356
|
-
LOG.info("Parsing panel %s", gene_panel["display_name"])
|
357
|
-
|
358
|
-
gene_panel["genes"] = []
|
359
|
-
|
360
|
-
nr_excluded = 0
|
361
|
-
nr_genes = 0
|
362
|
-
for nr_genes, gene in enumerate(panel_info["Genes"], 1):
|
363
|
-
gene_info = parse_panel_app_gene(
|
364
|
-
gene, ensembl_gene_hgnc_id_map, hgnc_symbol_ensembl_gene_map, confidence
|
365
|
-
)
|
366
|
-
if not gene_info:
|
367
|
-
nr_excluded += 1
|
368
|
-
continue
|
369
|
-
gene_panel["genes"].append(gene_info)
|
370
|
-
|
371
|
-
LOG.info("Number of genes in panel %s", nr_genes)
|
372
|
-
LOG.info("Number of genes exluded due to confidence threshold: %s", nr_excluded)
|
373
|
-
|
374
|
-
return gene_panel
|
375
|
-
|
376
|
-
|
377
263
|
def get_omim_panel_genes(genemap2_lines: list, mim2gene_lines: list, alias_genes: dict):
|
378
264
|
"""Return all genes that should be included in the OMIM-AUTO panel
|
379
265
|
Return the hgnc symbols
|
scout/parse/panelapp.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
"""Code to parse panel information"""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Dict, List, Optional
|
5
|
+
|
6
|
+
from scout.constants import INCOMPLETE_PENETRANCE_MAP, MODELS_MAP, PANELAPP_CONFIDENCE_EXCLUDE
|
7
|
+
from scout.utils.date import get_date
|
8
|
+
|
9
|
+
LOG = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
def parse_panel_app_gene(
|
13
|
+
panelapp_gene: dict,
|
14
|
+
ensembl_gene_hgnc_id_map: Dict[str, int],
|
15
|
+
hgnc_symbol_ensembl_gene_map: Dict[str, str],
|
16
|
+
confidence: str,
|
17
|
+
) -> dict:
|
18
|
+
"""Parse a panel app-formatted gene."""
|
19
|
+
gene_info = {}
|
20
|
+
confidence_level = panelapp_gene["confidence_level"]
|
21
|
+
# Return empty gene if not confident gene
|
22
|
+
if confidence_level in PANELAPP_CONFIDENCE_EXCLUDE[confidence]:
|
23
|
+
return gene_info
|
24
|
+
|
25
|
+
hgnc_symbol = panelapp_gene["gene_data"]["gene_symbol"]
|
26
|
+
ensembl_ids = [
|
27
|
+
version["ensembl_id"]
|
28
|
+
for genome in panelapp_gene["gene_data"]["ensembl_genes"].values()
|
29
|
+
for version in genome.values()
|
30
|
+
]
|
31
|
+
|
32
|
+
if not ensembl_ids: # This gene is probably tagged as ensembl_ids_known_missing on PanelApp
|
33
|
+
if hgnc_symbol in hgnc_symbol_ensembl_gene_map:
|
34
|
+
LOG.warning(
|
35
|
+
f"PanelApp gene {hgnc_symbol} does not contain Ensembl IDs. Using Ensembl IDs from internal gene collection instead."
|
36
|
+
)
|
37
|
+
ensembl_ids = [hgnc_symbol_ensembl_gene_map[hgnc_symbol]]
|
38
|
+
else:
|
39
|
+
LOG.warning(
|
40
|
+
f"PanelApp gene {hgnc_symbol} does not contain Ensembl IDs and gene symbol does not correspond to a gene in scout."
|
41
|
+
)
|
42
|
+
|
43
|
+
hgnc_ids = set(
|
44
|
+
ensembl_gene_hgnc_id_map.get(ensembl_id)
|
45
|
+
for ensembl_id in ensembl_ids
|
46
|
+
if ensembl_gene_hgnc_id_map.get(ensembl_id)
|
47
|
+
)
|
48
|
+
if not hgnc_ids:
|
49
|
+
LOG.warning("Gene %s does not exist in database. Skipping gene...", hgnc_symbol)
|
50
|
+
return gene_info
|
51
|
+
|
52
|
+
if len(hgnc_ids) > 1:
|
53
|
+
LOG.warning("Gene %s has unclear identifier. Choose random id", hgnc_symbol)
|
54
|
+
|
55
|
+
gene_info["hgnc_symbol"] = hgnc_symbol
|
56
|
+
for hgnc_id in hgnc_ids:
|
57
|
+
gene_info["hgnc_id"] = hgnc_id
|
58
|
+
|
59
|
+
gene_info["reduced_penetrance"] = INCOMPLETE_PENETRANCE_MAP.get(panelapp_gene["penetrance"])
|
60
|
+
|
61
|
+
inheritance_models = []
|
62
|
+
for model in MODELS_MAP.get(panelapp_gene["mode_of_inheritance"], []):
|
63
|
+
inheritance_models.append(model)
|
64
|
+
|
65
|
+
gene_info["inheritance_models"] = inheritance_models
|
66
|
+
|
67
|
+
return gene_info
|
68
|
+
|
69
|
+
|
70
|
+
def parse_panelapp_panel(
|
71
|
+
panel_info: dict,
|
72
|
+
ensembl_id_to_hgnc_id_map: Dict[str, int],
|
73
|
+
hgnc_symbol_to_ensembl_id_map: Dict[str, str],
|
74
|
+
institute: Optional[str] = "cust000",
|
75
|
+
confidence: Optional[str] = "green",
|
76
|
+
) -> dict:
|
77
|
+
"""Parse a PanelApp panel"""
|
78
|
+
|
79
|
+
date_format = "%Y-%m-%dT%H:%M:%S.%f"
|
80
|
+
|
81
|
+
gene_panel = {}
|
82
|
+
panel_id = str(panel_info["id"])
|
83
|
+
if confidence != "green":
|
84
|
+
gene_panel["panel_id"] = "_".join([panel_id, confidence])
|
85
|
+
else: # This way the old green panels will be overwritten, instead of creating 2 sets of green panels, old and new
|
86
|
+
gene_panel["panel_id"] = panel_id
|
87
|
+
|
88
|
+
gene_panel["version"] = float(panel_info["version"])
|
89
|
+
gene_panel["date"] = get_date(panel_info["version_created"][:-1], date_format=date_format)
|
90
|
+
gene_panel["display_name"] = " - ".join([panel_info["name"], f"[{confidence.upper()}]"])
|
91
|
+
gene_panel["institute"] = institute
|
92
|
+
gene_panel["panel_type"] = ("clinical",)
|
93
|
+
|
94
|
+
LOG.info("Parsing panel %s", gene_panel["display_name"])
|
95
|
+
|
96
|
+
gene_panel["genes"] = []
|
97
|
+
|
98
|
+
nr_excluded = 0
|
99
|
+
nr_genes = 0
|
100
|
+
for nr_genes, gene in enumerate(panel_info["genes"], 1):
|
101
|
+
gene_info = parse_panel_app_gene(
|
102
|
+
gene, ensembl_id_to_hgnc_id_map, hgnc_symbol_to_ensembl_id_map, confidence
|
103
|
+
)
|
104
|
+
if not gene_info:
|
105
|
+
nr_excluded += 1
|
106
|
+
continue
|
107
|
+
gene_panel["genes"].append(gene_info)
|
108
|
+
|
109
|
+
LOG.info("Number of genes in panel %s", nr_genes)
|
110
|
+
LOG.info("Number of genes excluded due to confidence threshold: %s", nr_excluded)
|
111
|
+
|
112
|
+
return gene_panel
|
scout/parse/variant/clnsig.py
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
import logging
|
2
|
+
from typing import Dict, List, Optional, Union
|
3
|
+
|
4
|
+
import cyvcf2
|
2
5
|
|
3
6
|
LOG = logging.getLogger(__name__)
|
4
7
|
|
5
8
|
|
6
|
-
def parse_clnsig(
|
9
|
+
def parse_clnsig(
|
10
|
+
variant: cyvcf2.Variant, transcripts: Optional[dict] = None
|
11
|
+
) -> List[Dict[str, Union[str, int]]]:
|
7
12
|
"""Get the clnsig information
|
8
13
|
|
9
14
|
The clinvar format has changed several times and this function will try to parse all of them.
|
@@ -15,12 +20,7 @@ def parse_clnsig(variant, transcripts=None):
|
|
15
20
|
sometimes with CLNVID.
|
16
21
|
This function parses also Clinvar annotations performed by VEP (CSQ field, parsed transcripts required)
|
17
22
|
|
18
|
-
|
19
|
-
variant(cyvcf2.Variant)
|
20
|
-
transcripts(iterable(dict))
|
21
|
-
|
22
|
-
Returns:
|
23
|
-
clnsig_accsessions(list(dict)): A list with clnsig accessions
|
23
|
+
Returns a list with clnsig accessions
|
24
24
|
"""
|
25
25
|
transcripts = transcripts or []
|
26
26
|
acc = variant.INFO.get("CLNACC", variant.INFO.get("CLNVID", ""))
|
@@ -87,33 +87,42 @@ def parse_clnsig(variant, transcripts=None):
|
|
87
87
|
return clnsig_accessions
|
88
88
|
|
89
89
|
|
90
|
-
def is_pathogenic(variant):
|
90
|
+
def is_pathogenic(variant: cyvcf2.Variant):
|
91
91
|
"""Check if a variant has the clinical significance to be loaded
|
92
92
|
|
93
93
|
We want to load all variants that are in any of the predefined categories regardless of rank
|
94
94
|
scores etc.
|
95
95
|
|
96
|
-
|
97
|
-
|
96
|
+
To avoid parsing much, we first quickly check if we have a string match to substrings in
|
97
|
+
all relevant categories, that are somewhat rare in the CSQ strings in general. If not,
|
98
|
+
we check more carefully.
|
99
|
+
|
100
|
+
We include the old style numerical categories as well for backwards compatibility.
|
98
101
|
|
99
102
|
Returns:
|
100
103
|
bool: If variant should be loaded based on clinvar or not
|
101
104
|
"""
|
102
105
|
|
103
|
-
|
104
|
-
"pathogenic",
|
105
|
-
"likely_pathogenic",
|
106
|
-
"conflicting_interpretations_of_pathogenicity",
|
107
|
-
"conflicting_interpretations",
|
108
|
-
}
|
106
|
+
quick_load_categories = {"pathogenic", "conflicting_"}
|
109
107
|
|
110
108
|
# check if VEP-annotated field contains clinvar pathogenicity info
|
111
109
|
vep_info = variant.INFO.get("CSQ")
|
112
110
|
if vep_info:
|
113
|
-
for category in
|
111
|
+
for category in quick_load_categories:
|
114
112
|
if category in vep_info.lower():
|
115
113
|
return True
|
116
114
|
|
115
|
+
load_categories: set = {
|
116
|
+
"pathogenic",
|
117
|
+
"likely_pathogenic",
|
118
|
+
"conflicting_classifications_of_pathogenicity",
|
119
|
+
"conflicting_interpretations_of_pathogenicity",
|
120
|
+
"conflicting_interpretations",
|
121
|
+
4,
|
122
|
+
5,
|
123
|
+
8,
|
124
|
+
}
|
125
|
+
|
117
126
|
# Otherwise check if clinvar pathogenicity status is in INFO field
|
118
127
|
clnsig_accessions = parse_clnsig(variant)
|
119
128
|
|
@@ -121,8 +130,4 @@ def is_pathogenic(variant):
|
|
121
130
|
clnsig = annotation["value"]
|
122
131
|
if clnsig in load_categories:
|
123
132
|
return True
|
124
|
-
if isinstance(clnsig, int):
|
125
|
-
if clnsig == 4 or clnsig == 5:
|
126
|
-
return True
|
127
|
-
|
128
133
|
return False
|
scout/parse/variant/genotype.py
CHANGED
@@ -291,11 +291,12 @@ def get_split_reads(variant, pos):
|
|
291
291
|
|
292
292
|
|
293
293
|
def get_alt_frequency(variant, pos):
|
294
|
-
""" """
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
294
|
+
"""AF - prioritise caller AF if set in FORMAT (e.g. DeepVariant does an INFO field)"""
|
295
|
+
if "AF" in variant.FORMAT:
|
296
|
+
alt_frequency = float(variant.format("AF")[pos][0])
|
297
|
+
else:
|
298
|
+
alt_frequency = float(variant.gt_alt_freqs[pos])
|
299
|
+
|
299
300
|
return alt_frequency
|
300
301
|
|
301
302
|
|
@@ -297,13 +297,13 @@ def set_common_tracks(display_obj, build):
|
|
297
297
|
display_obj["custom_tracks"].append(track)
|
298
298
|
|
299
299
|
|
300
|
-
def set_sample_tracks(display_obj, case_groups, chromosome):
|
300
|
+
def set_sample_tracks(display_obj: dict, case_groups: list, chromosome: str):
|
301
301
|
"""Set up individual-specific alignment tracks (bam/cram files)
|
302
302
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
303
|
+
Given a dictionary containing all tracks info (display_obj), a list of case group dictionaries
|
304
|
+
A chromosome string argument is used to check if we should look at mt alignment files for MT.
|
305
|
+
|
306
|
+
A missing file is indicated with the string "missing", and no track is made for such entries.
|
307
307
|
"""
|
308
308
|
sample_tracks = []
|
309
309
|
|
@@ -321,6 +321,8 @@ def set_sample_tracks(display_obj, case_groups, chromosome):
|
|
321
321
|
return
|
322
322
|
|
323
323
|
for count, sample in enumerate(case.get("sample_names")):
|
324
|
+
if case[track_items][count] == "missing" or case[track_index_items][count] == "missing":
|
325
|
+
continue
|
324
326
|
sample_tracks.append(
|
325
327
|
{
|
326
328
|
"name": sample,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
{% macro igv_script() %}
|
2
2
|
<link rel="shortcut icon" href="//igv.org/web/img/favicon.ico">
|
3
3
|
<!-- IGV JS-->
|
4
|
-
<script src="https://cdn.jsdelivr.net/npm/igv@3.0.
|
4
|
+
<script src="https://cdn.jsdelivr.net/npm/igv@3.0.9/dist/igv.min.js" integrity="sha512-vcMdgqBb5jsj6fAIlBMkDe3riA5meRJARkXtDFAl+5/jm5TxL6agAFq8Ek8LzrHtLncs40Qa8s98awdkINjy+Q==" crossorigin="anonymous"></script>
|
5
5
|
{% endmacro %}
|
@@ -30,61 +30,68 @@
|
|
30
30
|
<div class="card col-md-12">
|
31
31
|
<h4 class="mt-3">Case: {{case.display_name}}</h4>
|
32
32
|
<div class="card-body">
|
33
|
-
|
34
33
|
<div class="row">
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
</div>
|
34
|
+
<div class="col-xs-12 col-md-12">{{ smn_individuals_table(case, institute, tissue_types) }}</div>
|
35
|
+
</div> <!-- end of div class row -->
|
36
|
+
<div class="row">
|
37
|
+
<div class="col-md-4">
|
38
|
+
{% if case.madeline_info and case.individuals|length > 1 %}
|
39
|
+
{{ pedigree_panel() }}
|
40
|
+
{% else %}
|
41
|
+
<p>No pedigree picture available.</p>
|
42
|
+
{% endif %}
|
43
|
+
</div>
|
44
|
+
<div class="col-md-8">
|
45
|
+
{{ synopsis_panel() }}
|
46
|
+
<div class="panel-default">
|
47
|
+
{{ comments_panel(institute, case, current_user, comments) }}
|
50
48
|
</div>
|
51
|
-
</div>
|
49
|
+
</div>
|
50
|
+
</div> <!-- end of div class row -->
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
52
|
+
<span class="d-flex">
|
53
|
+
{% if case.vcf_files.vcf_snv %}
|
54
|
+
<span class="me-3">
|
55
|
+
<form action="{{url_for('variants.variants', institute_id=institute._id, case_name=case.display_name) }}">
|
56
|
+
<input type="hidden" id="hgnc_symbols" name="hgnc_symbols" value="SMN1, SMN2"></input>
|
57
|
+
<input type="hidden" id="gene_panels" name="gene_panels" value="['']"></input>
|
58
|
+
<span><button type="submit" class="btn btn-secondary btn-sm" target="_blank" rel="noopener" data-bs-toggle="tooltip" title="SNV and INDEL variants view filtered for the genes SMN1 and SMN2">SNVs</button></span>
|
59
|
+
</form>
|
60
|
+
</span>
|
61
|
+
{% endif %}
|
62
|
+
{% if case.vcf_files.vcf_sv %}
|
64
63
|
<span class="me-3">
|
65
64
|
<form action="{{url_for('variants.sv_variants', institute_id=institute._id, case_name=case.display_name) }}">
|
66
65
|
<input type="hidden" id="hgnc_symbols" name="hgnc_symbols" value="SMN1, SMN2"></input>
|
67
66
|
<input type="hidden" id="gene_panels" name="gene_panels" value="['']"></input>
|
68
|
-
<button type="submit" class="btn btn-secondary btn-sm" target="_blank" rel="noopener" data-bs-toggle="tooltip" title="Structural variants view filtered for the genes SMN1 and SMN2">SVs</button
|
67
|
+
<button type="submit" class="btn btn-secondary btn-sm" target="_blank" rel="noopener" data-bs-toggle="tooltip" title="Structural variants view filtered for the genes SMN1 and SMN2">SVs</button>
|
69
68
|
</form>
|
70
69
|
</span>
|
71
|
-
|
72
|
-
{% if case.bam_files %}
|
73
|
-
<span class="me-3"><a class="btn btn-secondary btn-sm text-white" href="{{url_for('alignviewers.igv', institute_id=case['owner'], case_name=case['display_name'], chrom=region['smn1']['chrom'], start=region['smn1']['start'], stop=region['smn1']['end'] )}}" target="_blank">IGV viewer SMN1</a></span>
|
74
|
-
<span class="me-3"><a class="btn btn-secondary btn-sm text-white" href="{{url_for('alignviewers.igv', institute_id=case['owner'], case_name=case['display_name'], chrom=region['smn2']['chrom'], start=region['smn2']['start'], stop=region['smn2']['end'] )}}" target="_blank">IGV viewer SMN2</a></span>
|
75
|
-
{% else %}
|
76
|
-
<span class="me-3 text-muted">BAM file(s) missing</span>
|
77
|
-
{% endif %}
|
78
|
-
</div>
|
70
|
+
{% endif %}
|
79
71
|
|
80
|
-
<
|
81
|
-
|
82
|
-
|
72
|
+
<span class="me-3"
|
73
|
+
{% if not case.bam_files %}title="Alignment file(s) missing" data-bs-toggle="tooltip"{% endif %}>
|
74
|
+
<a href="{{url_for('alignviewers.igv', institute_id=case['owner'], case_name=case['display_name'], chrom=region['smn1']['chrom'], start=region['smn1']['start'], stop=region['smn1']['end'] )}}" target="_blank"
|
75
|
+
class="btn btn-secondary btn-sm text-white{% if not case.bam_files %} disabled{% endif %}">
|
76
|
+
IGV DNA SMN1
|
77
|
+
</a>
|
78
|
+
</span>
|
79
|
+
<span class="me-3"
|
80
|
+
{% if not case.bam_files %}title="Alignment file(s) missing" data-bs-toggle="tooltip"{% endif %}>
|
81
|
+
<a href="{{url_for('alignviewers.igv', institute_id=case['owner'], case_name=case['display_name'], chrom=region['smn2']['chrom'], start=region['smn2']['start'], stop=region['smn2']['end'] )}}" target="_blank"
|
82
|
+
class="btn btn-secondary btn-sm text-white{% if not case.bam_files %} disabled{% endif %}">
|
83
|
+
IGV DNA SMN2
|
84
|
+
</a>
|
85
|
+
</span>
|
86
|
+
</span>
|
83
87
|
|
84
|
-
|
88
|
+
<div class="row">
|
89
|
+
<div class="col-sm-12">{{activity_panel(events)}}</div>
|
90
|
+
</div>
|
91
|
+
|
92
|
+
{{ modal_synopsis() }}
|
85
93
|
</div> <!-- end of card body -->
|
86
94
|
</div> <!-- end of card div-->
|
87
|
-
</div>
|
88
95
|
</div> <!-- end of div class col -->
|
89
96
|
{% endmacro %}
|
90
97
|
|