scout-browser 4.102.0__py3-none-any.whl → 4.103.1__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.
Files changed (59) hide show
  1. scout/adapter/mongo/case.py +26 -122
  2. scout/adapter/mongo/clinvar.py +91 -25
  3. scout/adapter/mongo/event.py +0 -47
  4. scout/adapter/mongo/variant_loader.py +7 -3
  5. scout/build/variant/variant.py +1 -0
  6. scout/commands/load/variants.py +1 -1
  7. scout/commands/update/user.py +87 -49
  8. scout/constants/__init__.py +3 -0
  9. scout/constants/clinvar.py +10 -0
  10. scout/constants/variant_tags.py +18 -0
  11. scout/demo/NIST.trgt.stranger.vcf.gz +0 -0
  12. scout/demo/NIST.trgt.stranger.vcf.gz.tbi +0 -0
  13. scout/demo/__init__.py +1 -0
  14. scout/models/clinvar.py +86 -0
  15. scout/parse/variant/coordinates.py +5 -1
  16. scout/parse/variant/gene.py +5 -9
  17. scout/parse/variant/genotype.py +66 -42
  18. scout/parse/variant/variant.py +2 -0
  19. scout/server/app.py +71 -2
  20. scout/server/blueprints/alignviewers/templates/alignviewers/igv_viewer.html +2 -0
  21. scout/server/blueprints/cases/controllers.py +1 -1
  22. scout/server/blueprints/cases/templates/cases/case_report.html +19 -2
  23. scout/server/blueprints/cases/templates/cases/utils.html +8 -29
  24. scout/server/blueprints/clinvar/controllers.py +233 -53
  25. scout/server/blueprints/clinvar/form.py +38 -1
  26. scout/server/blueprints/clinvar/static/form_style.css +8 -1
  27. scout/server/blueprints/clinvar/templates/clinvar/clinvar_onc_submissions.html +200 -0
  28. scout/server/blueprints/clinvar/templates/clinvar/clinvar_submissions.html +3 -2
  29. scout/server/blueprints/clinvar/templates/clinvar/components.html +198 -0
  30. scout/server/blueprints/clinvar/templates/clinvar/multistep_add_onc_variant.html +187 -0
  31. scout/server/blueprints/clinvar/templates/clinvar/multistep_add_variant.html +9 -348
  32. scout/server/blueprints/clinvar/templates/clinvar/scripts.html +193 -0
  33. scout/server/blueprints/clinvar/views.py +90 -13
  34. scout/server/blueprints/institutes/controllers.py +44 -5
  35. scout/server/blueprints/institutes/forms.py +1 -0
  36. scout/server/blueprints/institutes/templates/overview/gene_variants.html +15 -6
  37. scout/server/blueprints/institutes/templates/overview/institute_sidebar.html +28 -2
  38. scout/server/blueprints/institutes/templates/overview/utils.html +1 -1
  39. scout/server/blueprints/institutes/views.py +17 -4
  40. scout/server/blueprints/mme/templates/mme/mme_submissions.html +2 -2
  41. scout/server/blueprints/omics_variants/templates/omics_variants/outliers.html +2 -2
  42. scout/server/blueprints/variant/controllers.py +1 -1
  43. scout/server/blueprints/variant/templates/variant/cancer-variant.html +2 -1
  44. scout/server/blueprints/variant/templates/variant/components.html +0 -1
  45. scout/server/blueprints/variant/templates/variant/sv-variant.html +2 -1
  46. scout/server/blueprints/variant/templates/variant/variant.html +2 -2
  47. scout/server/blueprints/variant/templates/variant/variant_details.html +33 -25
  48. scout/server/blueprints/variants/templates/variants/cancer-variants.html +5 -3
  49. scout/server/blueprints/variants/templates/variants/str-variants.html +4 -1
  50. scout/server/blueprints/variants/templates/variants/sv-variants.html +3 -3
  51. scout/server/blueprints/variants/templates/variants/utils.html +4 -0
  52. scout/server/blueprints/variants/templates/variants/variants.html +4 -4
  53. scout/server/extensions/clinvar_extension.py +2 -2
  54. scout/server/templates/layout.html +1 -1
  55. {scout_browser-4.102.0.dist-info → scout_browser-4.103.1.dist-info}/METADATA +1 -1
  56. {scout_browser-4.102.0.dist-info → scout_browser-4.103.1.dist-info}/RECORD +59 -53
  57. {scout_browser-4.102.0.dist-info → scout_browser-4.103.1.dist-info}/WHEEL +0 -0
  58. {scout_browser-4.102.0.dist-info → scout_browser-4.103.1.dist-info}/entry_points.txt +0 -0
  59. {scout_browser-4.102.0.dist-info → scout_browser-4.103.1.dist-info}/licenses/LICENSE +0 -0
@@ -36,6 +36,7 @@ from .clinvar import (
36
36
  CONDITION_PREFIX,
37
37
  GERMLINE_CLASSIF_TERMS,
38
38
  MULTIPLE_CONDITION_EXPLANATION,
39
+ ONCOGENIC_CLASSIF_TERMS,
39
40
  )
40
41
  from .clnsig import CLINSIG_MAP, ONC_CLNSIG, REV_CLINSIG_MAP, TRUSTED_REVSTAT_LEVEL
41
42
  from .disease_parsing import (
@@ -100,8 +101,10 @@ from .variant_tags import (
100
101
  MANUAL_RANK_OPTIONS,
101
102
  MOSAICISM_OPTIONS,
102
103
  OUTLIER_TYPES,
104
+ REVEL_SCORE_LABEL_COLOR_MAP,
103
105
  SPIDEX_HUMAN,
104
106
  SPIDEX_LEVELS,
107
+ SPLICEAI_SCORE_LABEL_COLOR_MAP,
105
108
  SV_TYPES,
106
109
  VARIANT_CALL,
107
110
  VARIANT_FILTERS,
@@ -1,8 +1,12 @@
1
+ from scout.constants.clnsig import ONC_CLNSIG
2
+
1
3
  CLINVAR_API_URL_DEFAULT = "https://submit.ncbi.nlm.nih.gov/api/v1/submissions/"
2
4
  PRECLINVAR_URL = "https://preclinvar.scilifelab.se"
3
5
 
4
6
  ASSERTION_METHOD = "ACMG Guidelines, 2015"
5
7
  ASSERTION_METHOD_CIT = "PMID:25741868"
8
+ ASSERTION_ONC_ONC_DB = "PubMed"
9
+ ASSERTION_CRITERIA_ONC_ID = "36063163"
6
10
  NOT_PROVIDED = "not provided"
7
11
 
8
12
  # Header used to create the Variant .CSV file for the manual ClinVar submission
@@ -95,6 +99,8 @@ GERMLINE_CLASSIF_TERMS = [
95
99
  NOT_PROVIDED,
96
100
  ]
97
101
 
102
+ ONCOGENIC_CLASSIF_TERMS = ONC_CLNSIG
103
+
98
104
  REVSTAT_TERMS = {
99
105
  "conflicting_interpretations",
100
106
  "multiple_submitters",
@@ -187,6 +193,10 @@ CONDITION_PREFIX = {
187
193
  "Orphanet": "ORPHA",
188
194
  }
189
195
 
196
+ CONDITION_DBS_API = ["HP", "MedGen", "MeSH", "MONDO", "OMIM", "Orphanet"]
197
+
190
198
  CLINVAR_ASSERTION_METHOD_CIT_DB_OPTIONS = {"DOI", "pmc", "PMID"}
199
+ CITATION_DBS_API = ["PubMed", "BookShelf", "DOI", "pmc"]
191
200
 
192
201
  MULTIPLE_CONDITION_EXPLANATION = ["Novel disease", "Uncertain", "Co-occurring"]
202
+ PRESENCE_IN_NORMAL_TISSUE = ["present", "absent", "not tested"]
@@ -19,6 +19,22 @@ CONSERVATION = {
19
19
  "phylop": {"conserved_min": 2.5, "conserved_max": 100},
20
20
  }
21
21
 
22
+ REVEL_SCORE_LABEL_COLOR_MAP = {
23
+ (0.932, 1.000): {"label": "Strong pathogenic", "color": "danger"},
24
+ (0.773, 0.932): {"label": "Moderate pathogenic", "color": "red"},
25
+ (0.644, 0.773): {"label": "Supporting pathogenic", "color": "orange"},
26
+ (0.290, 0.644): {"label": "Uncertain significance", "color": "secondary"},
27
+ (0.183, 0.290): {"label": "Supporting benign", "color": "info"},
28
+ (0.016, 0.183): {"label": "Moderate benign", "color": "info"},
29
+ (0.0, 0.016): {"label": "Strong to very strong benign", "color": "success"},
30
+ }
31
+
32
+ SPLICEAI_SCORE_LABEL_COLOR_MAP = {
33
+ (0.20, 1.00): {"label": "Predicted impact on splicing", "color": "warning"},
34
+ (0.10, 0.20): {"label": "Not informative", "color": "secondary"},
35
+ (0.00, 0.10): {"label": "No impact on splicing", "color": "success"},
36
+ }
37
+
22
38
  FEATURE_TYPES = (
23
39
  "exonic",
24
40
  "splicing",
@@ -535,6 +551,7 @@ CALLERS = {
535
551
  {"id": "deepvariant", "name": "DeepVariant"},
536
552
  _FREEBAYES,
537
553
  _GATK,
554
+ {"id": "mutect2", "name": "MuTect2"},
538
555
  {"id": "samtools", "name": "SAMtools"},
539
556
  ],
540
557
  "cancer": [
@@ -562,6 +579,7 @@ CALLERS = {
562
579
  {"id": "cnvpytor", "name": "CNVpytor"},
563
580
  {"id": "delly", "name": "Delly"},
564
581
  _GATK,
582
+ {"id": "gcnvcaller", "name": "GATK GermlineCNV"},
565
583
  {"id": "hificnv", "name": "HiFiCNV"},
566
584
  _MANTA,
567
585
  {"id": "severus", "name": "Severus"},
Binary file
Binary file
scout/demo/__init__.py CHANGED
@@ -25,6 +25,7 @@ gene_fusion_report_path = str(files(BASE_PATH).joinpath("draw-fusions-example.pd
25
25
  clinical_snv_path = str(files(BASE_PATH).joinpath("643594.clinical.vcf.gz"))
26
26
  clinical_sv_path = str(files(BASE_PATH).joinpath("643594.clinical.SV.vcf.gz"))
27
27
  clinical_str_path = str(files(BASE_PATH).joinpath("643594.clinical.str.stranger.vcf.gz"))
28
+ str_trgt_path = str(files(BASE_PATH).joinpath("NIST.trgt.stranger.vcf.gz"))
28
29
  clinical_fusion_path = str(files(BASE_PATH).joinpath("fusion_data.vcf"))
29
30
  customannotation_snv_path = str(files(BASE_PATH).joinpath("customannotations_one.vcf.gz"))
30
31
  vep_97_annotated_path = str(
scout/models/clinvar.py CHANGED
@@ -1,6 +1,12 @@
1
1
  from datetime import date, datetime
2
+ from enum import Enum
3
+ from typing import List, Literal, Optional
2
4
 
3
5
  from bson.objectid import ObjectId
6
+ from pydantic import BaseModel
7
+
8
+ from scout.constants import CHROMOSOMES
9
+ from scout.constants.clinvar import CITATION_DBS_API, ONCOGENIC_CLASSIF_TERMS
4
10
 
5
11
  """Model of the document that gets saved/updated in the clinvar_submission collection
6
12
  for each institute that has cases with ClinVar submission objects"""
@@ -76,3 +82,83 @@ clinvar_casedata = {
76
82
  "method_purpose": str, # default: "discovery"
77
83
  "reported_at": date,
78
84
  }
85
+
86
+ ### Models used for oncogenocity submissions via API
87
+
88
+
89
+ CitationDB = Enum("CitationDB", {db.upper(): db for db in CITATION_DBS_API})
90
+ OncogenicityClassificationDescription = Enum(
91
+ "OncogenicityClassificationDescription",
92
+ {term.upper().replace(" ", "_"): term for term in ONCOGENIC_CLASSIF_TERMS},
93
+ )
94
+
95
+
96
+ class Citation(BaseModel):
97
+ db: CitationDB
98
+ id: str
99
+
100
+
101
+ class OncogenicityClassification(BaseModel):
102
+ oncogenicityClassificationDescription: OncogenicityClassificationDescription
103
+ dateLastEvaluated: str
104
+ comment: Optional[str] = None
105
+ citation: Optional[List[Citation]] = None
106
+
107
+
108
+ class ObservedIn(BaseModel):
109
+ alleleOrigin: str
110
+ affectedStatus: str
111
+ collectionMethod: str
112
+ numberOfIndividuals: int
113
+ presenceOfSomaticVariantInNormalTissue: str
114
+ somaticVariantAlleleFraction: Optional[float] = None
115
+
116
+
117
+ class Gene(BaseModel):
118
+ symbol: str
119
+
120
+
121
+ Chromosome = Enum(
122
+ "Chromosome", {c: c for c in CHROMOSOMES}
123
+ ) # ClinVar API accepts only 'MT' chromosome
124
+
125
+
126
+ class Variant(BaseModel):
127
+ """It's defined by either coordinates or hgvs."""
128
+
129
+ alternateAllele: Optional[str] = None
130
+ assembly: Optional[Literal["GRCh37", "GRCh38"]] = None
131
+ chromosome: Optional[Chromosome] = None
132
+ gene: Optional[List[Gene]] = None
133
+ hgvs: Optional[str] = None
134
+ start: Optional[int] = None
135
+ stop: Optional[int] = None
136
+
137
+
138
+ class VariantSet(BaseModel):
139
+ variant: List[Variant]
140
+
141
+
142
+ class Condition(BaseModel):
143
+ db: Optional[str] = None
144
+ id: Optional[str] = None
145
+ name: Optional[str] = None
146
+
147
+
148
+ class ConditionSet(BaseModel):
149
+ condition: List[Condition]
150
+
151
+
152
+ class OncogenicitySubmissionItem(BaseModel):
153
+ # Field necessary for the API submissions:
154
+ recordStatus: str
155
+ oncogenicityClassification: OncogenicityClassification
156
+ observedIn: List[ObservedIn]
157
+ variantSet: VariantSet
158
+ conditionSet: ConditionSet
159
+
160
+ # Fields necessary to map the variant to a variant in Scout:
161
+ institute_id: str
162
+ case_id: str
163
+ case_name: str
164
+ variant_id: str
@@ -1,6 +1,6 @@
1
1
  """Code to parse variant coordinates"""
2
2
 
3
- from scout.constants import BND_ALT_PATTERN, CHR_PATTERN, CYTOBANDS_37, CYTOBANDS_38
3
+ from scout.constants import BND_ALT_PATTERN, CHR_PATTERN, CYTOBANDS_37, CYTOBANDS_38, SV_TYPES
4
4
 
5
5
 
6
6
  def get_cytoband_coordinates(chrom, pos, build):
@@ -144,6 +144,10 @@ def parse_coordinates(variant, category, build="37"):
144
144
  svtype = variant.INFO.get("SVTYPE")
145
145
  if svtype:
146
146
  svtype = svtype.lower()
147
+ else:
148
+ alt_type = alt.lstrip("<").rstrip(">").lower()
149
+ if alt_type in SV_TYPES:
150
+ svtype = alt_type
147
151
  sub_category = svtype
148
152
  if sub_category == "bnd":
149
153
  end_chrom = get_end_chrom(alt, chrom)
@@ -48,10 +48,6 @@ def parse_genes(transcripts):
48
48
  # List with all genes and their transcripts
49
49
  genes = []
50
50
 
51
- hgvs_identifier = None
52
- canonical_transcript = None
53
- exon = None
54
-
55
51
  # Loop over all genes
56
52
  for gene_id in genes_to_transcripts:
57
53
  # Get the transcripts for a gene
@@ -78,8 +74,7 @@ def parse_genes(transcripts):
78
74
  hgnc_symbol = transcript["hgnc_symbol"]
79
75
  if not hgvs_identifier:
80
76
  hgvs_identifier = transcript.get("coding_sequence_name")
81
- if not canonical_transcript:
82
- canonical_transcript = transcript["transcript_id"]
77
+
83
78
  if not exon:
84
79
  exon = transcript["exon"]
85
80
 
@@ -102,10 +97,11 @@ def parse_genes(transcripts):
102
97
  most_severe_spliceai_position = transcript["spliceai_delta_position"]
103
98
  spliceai_prediction = transcript["spliceai_prediction"]
104
99
 
105
- if transcript["is_canonical"] and transcript.get("coding_sequence_name"):
106
- hgvs_identifier = transcript.get("coding_sequence_name")
100
+ if transcript["is_canonical"]:
107
101
  canonical_transcript = transcript["transcript_id"]
108
- exon = transcript["exon"]
102
+ if transcript.get("coding_sequence_name"):
103
+ hgvs_identifier = transcript.get("coding_sequence_name")
104
+ exon = transcript["exon"]
109
105
 
110
106
  gene = {
111
107
  "transcripts": gene_transcripts,
@@ -18,6 +18,7 @@ Uses 'DV' to describe number of paired ends that supports the event and
18
18
 
19
19
  """
20
20
 
21
+ import ast
21
22
  import logging
22
23
  from typing import Dict, List, Optional, Tuple, Union
23
24
 
@@ -101,7 +102,7 @@ def parse_genotype(variant, ind, pos):
101
102
  (_, mc_alt) = _parse_format_entry_trgt_mc(variant, pos)
102
103
  gt_call["alt_mc"] = mc_alt
103
104
 
104
- (sd_ref, sd_alt) = _parse_format_entry(variant, pos, "SD", float)
105
+ (sd_ref, sd_alt) = _parse_format_entry(variant, pos, "SD", int)
105
106
  (ap_ref, ap_alt) = _parse_format_entry(variant, pos, "AP", float)
106
107
  (am_ref, am_alt) = _parse_format_entry(variant, pos, "AM", float)
107
108
 
@@ -122,6 +123,7 @@ def parse_genotype(variant, ind, pos):
122
123
  spanning_alt,
123
124
  flanking_alt,
124
125
  inrepeat_alt,
126
+ sd_alt,
125
127
  clip5_alt,
126
128
  clip3_alt,
127
129
  )
@@ -135,8 +137,10 @@ def parse_genotype(variant, ind, pos):
135
137
  spanning_ref,
136
138
  flanking_ref,
137
139
  inrepeat_ref,
140
+ sd_ref,
138
141
  spanning_mei_ref,
139
142
  )
143
+
140
144
  gt_call["ref_depth"] = ref_depth
141
145
  gt_call["read_depth"] = get_read_depth(variant, pos, alt_depth, ref_depth)
142
146
  gt_call["alt_frequency"] = get_alt_frequency(variant, pos)
@@ -202,7 +206,7 @@ def get_ffpm_info(variant: cyvcf2.Variant, pos: Dict[str, int]) -> Optional[int]
202
206
  pass
203
207
 
204
208
 
205
- def get_paired_ends(variant, pos):
209
+ def get_paired_ends(variant: cyvcf2.Variant, pos: int) -> tuple:
206
210
  """Get paired ends"""
207
211
  # SV specific
208
212
  paired_end_alt = None
@@ -224,9 +228,10 @@ def get_paired_ends(variant, pos):
224
228
  values = variant.format("PR")[pos]
225
229
  try:
226
230
  alt_value = int(values[1])
227
- ref_value = int(values[0])
228
231
  if alt_value >= 0:
229
232
  paired_end_alt = alt_value
233
+
234
+ ref_value = int(values[0])
230
235
  if ref_value >= 0:
231
236
  paired_end_ref = ref_value
232
237
  except ValueError as _ignore_error:
@@ -317,32 +322,30 @@ def get_read_depth(variant, pos, alt_depth, ref_depth):
317
322
 
318
323
 
319
324
  def get_ref_depth(
320
- variant,
321
- pos,
322
- paired_end_ref,
323
- split_read_ref,
324
- spanning_ref,
325
- flanking_ref,
326
- inrepeat_ref,
327
- spanning_mei_ref,
328
- ):
325
+ variant: cyvcf2.Variant,
326
+ pos: int,
327
+ paired_end_ref: int,
328
+ split_read_ref: int,
329
+ spanning_ref: int,
330
+ flanking_ref: int,
331
+ inrepeat_ref: int,
332
+ sd_ref: int,
333
+ spanning_mei_ref: int,
334
+ ) -> int:
329
335
  """Get reference read depth"""
330
336
  ref_depth = int(variant.gt_ref_depths[pos])
331
337
  if ref_depth != -1:
332
338
  return ref_depth
333
339
 
334
340
  REF_ITEMS_LIST: List[tuple] = [
341
+ (sd_ref,),
335
342
  (paired_end_ref, split_read_ref),
336
343
  (spanning_ref, flanking_ref, inrepeat_ref),
337
344
  ]
338
345
 
339
- for list in REF_ITEMS_LIST:
340
- if all(item is None for item in list):
341
- continue
342
- ref_depth = 0
343
- for item in list:
344
- if item:
345
- ref_depth += item
346
+ for items in REF_ITEMS_LIST:
347
+ if any(item is not None for item in items):
348
+ ref_depth = sum(item for item in items if item)
346
349
 
347
350
  if spanning_mei_ref:
348
351
  ref_depth += spanning_mei_ref
@@ -350,16 +353,17 @@ def get_ref_depth(
350
353
 
351
354
 
352
355
  def get_alt_depth(
353
- variant,
354
- pos,
355
- paired_end_alt,
356
- split_read_alt,
357
- spanning_alt,
358
- flanking_alt,
359
- inrepeat_alt,
360
- clip5_alt,
361
- clip3_alt,
362
- ):
356
+ variant: cyvcf2.Variant,
357
+ pos: int,
358
+ paired_end_alt: int,
359
+ split_read_alt: int,
360
+ spanning_alt: int,
361
+ flanking_alt: int,
362
+ inrepeat_alt: int,
363
+ sd_alt: int,
364
+ clip5_alt: int,
365
+ clip3_alt: int,
366
+ ) -> int:
363
367
  """Get alternative read depth"""
364
368
  alt_depth = int(variant.gt_alt_depths[pos])
365
369
  if alt_depth != -1:
@@ -369,19 +373,15 @@ def get_alt_depth(
369
373
  alt_depth = int(variant.format("VD")[pos][0])
370
374
 
371
375
  ALT_ITEMS_LIST: List[tuple] = [
376
+ (sd_alt,),
372
377
  (paired_end_alt, split_read_alt),
373
378
  (clip5_alt, clip3_alt),
374
379
  (spanning_alt, flanking_alt, inrepeat_alt),
375
380
  ]
376
381
 
377
- for list in ALT_ITEMS_LIST:
378
- if all(item is None for item in list):
379
- continue
380
- alt_depth = 0
381
- for item in list:
382
- if item:
383
- alt_depth += item
384
-
382
+ for items in ALT_ITEMS_LIST:
383
+ if any(item is not None for item in items):
384
+ alt_depth = sum(item for item in items if item)
385
385
  return alt_depth
386
386
 
387
387
 
@@ -430,7 +430,13 @@ def _parse_format_entry(
430
430
  alt = None
431
431
  if format_entry_name in variant.FORMAT:
432
432
  try:
433
- values = split_values(variant.format(format_entry_name)[pos])
433
+ requested_format_entry = variant.format(format_entry_name)[pos]
434
+
435
+ values = (
436
+ split_values(requested_format_entry)
437
+ if type(requested_format_entry) is str
438
+ else requested_format_entry
439
+ )
434
440
 
435
441
  ref_value = None
436
442
  alt_value = None
@@ -440,15 +446,31 @@ def _parse_format_entry(
440
446
  alt_value = (number_format)(values[1])
441
447
  if len(values) == 1:
442
448
  alt_value = (number_format)(values[0])
443
- if ref_value >= 0:
449
+ if ref_value and ref_value >= 0:
444
450
  ref = ref_value
445
- if alt_value >= 0:
451
+ if alt_value and alt_value >= 0:
446
452
  alt = alt_value
447
453
  except (ValueError, TypeError) as _ignore_error:
448
454
  pass
449
455
  return (ref, alt)
450
456
 
451
457
 
458
+ def _get_pathologic_struc(variant: cyvcf2.Variant) -> Optional[list]:
459
+ """Check for a PathologicStruc on the variant. If not present, return None.
460
+ If present, and in string format, convert to a list of ints.
461
+ If it is already parsed to a list by a later improvement in the parser,
462
+ simply return it.
463
+ """
464
+
465
+ pathologic_struc_entry = variant.INFO.get("PathologicStruc", None)
466
+ if not pathologic_struc_entry:
467
+ return pathologic_struc_entry
468
+ if type(pathologic_struc_entry) is str:
469
+ return ast.literal_eval(pathologic_struc_entry)
470
+ if type(pathologic_struc_entry) is list:
471
+ return pathologic_struc_entry
472
+
473
+
452
474
  def _parse_format_entry_trgt_mc(variant: cyvcf2.Variant, pos: int):
453
475
  """Parse genotype entry for TRGT FORMAT MC
454
476
 
@@ -477,14 +499,16 @@ def _parse_format_entry_trgt_mc(variant: cyvcf2.Variant, pos: int):
477
499
  if allele == 0:
478
500
  ref_idx = idx
479
501
 
480
- pathologic_struc = variant.INFO.get("PathologicStruc", None)
481
- pathologic_counts = 0
502
+ pathologic_struc = _get_pathologic_struc(variant)
503
+
482
504
  for idx, allele in enumerate(mc.split(",")):
505
+
483
506
  mcs = allele.split("_")
484
507
 
485
508
  if len(mcs) > 1:
486
509
  pathologic_mcs = pathologic_struc or range(len(mcs))
487
510
 
511
+ pathologic_counts = 0
488
512
  for index, count in enumerate(mcs):
489
513
  if index in pathologic_mcs:
490
514
  pathologic_counts += int(count)
@@ -82,6 +82,8 @@ def parse_variant(
82
82
  variant_type=variant_type,
83
83
  ),
84
84
  "rank_score": parse_rank_score(variant.INFO.get("RankScore", ""), genmod_key) or 0,
85
+ "norm_rank_score": parse_rank_score(variant.INFO.get("RankScoreNormalized", ""), genmod_key)
86
+ or 0,
85
87
  "genetic_models": parse_genetic_models(variant.INFO.get("GeneticModels"), genmod_key),
86
88
  "str_swegen_mean": call_safe(float, variant.INFO.get("SweGenMean")),
87
89
  "str_swegen_std": call_safe(float, variant.INFO.get("SweGenStd")),
scout/server/app.py CHANGED
@@ -4,7 +4,7 @@ import logging
4
4
  import os
5
5
  import re
6
6
  from datetime import timedelta
7
- from typing import Dict, Union
7
+ from typing import Dict, List, Optional, Union
8
8
  from urllib.parse import parse_qsl, unquote, urlsplit
9
9
 
10
10
  from flask import Flask, redirect, request, url_for
@@ -14,7 +14,11 @@ from markdown import markdown as python_markdown
14
14
  from markupsafe import Markup
15
15
 
16
16
  from scout import __version__
17
- from scout.constants import SPIDEX_HUMAN
17
+ from scout.constants import (
18
+ REVEL_SCORE_LABEL_COLOR_MAP,
19
+ SPIDEX_HUMAN,
20
+ SPLICEAI_SCORE_LABEL_COLOR_MAP,
21
+ )
18
22
  from scout.log import init_log
19
23
 
20
24
  from . import extensions
@@ -207,6 +211,9 @@ def register_blueprints(app):
207
211
 
208
212
 
209
213
  def register_filters(app):
214
+ """Creates methods that can be invoked from jinja2 or views/controllers, given that they are included app.custom_filters."""
215
+ app.custom_filters = type("CustomFilters", (), {})() # Create an empty object as namespace
216
+
210
217
  @app.template_filter()
211
218
  def human_longint(value: Union[int, str]) -> str:
212
219
  """Convert a long integers int or string representation into a human easily readable number."""
@@ -226,6 +233,27 @@ def register_filters(app):
226
233
  return "medium"
227
234
  return "high"
228
235
 
236
+ @app.template_filter()
237
+ def get_label_or_color_by_score(
238
+ score: float,
239
+ map: str,
240
+ map_key: str,
241
+ ) -> str:
242
+ """Return a label or color for a given score based on predefined score ranges from the provided items_map."""
243
+ SCORE_ITEM_MAPS = {
244
+ "revel": REVEL_SCORE_LABEL_COLOR_MAP,
245
+ "spliceai": SPLICEAI_SCORE_LABEL_COLOR_MAP,
246
+ }
247
+ for (low, high), info in SCORE_ITEM_MAPS[map].items():
248
+ if low <= score <= high:
249
+ return info[map_key]
250
+
251
+ @app.template_filter()
252
+ def l2fc_2_fc(l2fc: float) -> float:
253
+ """Converts Log2 fold change to fold change."""
254
+ fc = 2 ** abs(l2fc)
255
+ return fc if l2fc >= 0 else -1 / fc
256
+
229
257
  @app.template_filter()
230
258
  def human_decimal(number, ndigits=4):
231
259
  """Return a standard representation of a decimal number.
@@ -281,6 +309,33 @@ def register_filters(app):
281
309
  return "COSM" + str(cosmicId)
282
310
  return cosmicId
283
311
 
312
+ @app.template_filter()
313
+ def format_variant_canonical_transcripts(variant: dict) -> List[str]:
314
+ """Formats canonical transcripts for all genes in a variant."""
315
+
316
+ lines = set()
317
+ genes = variant.get("genes") or []
318
+
319
+ for gene in genes:
320
+ transcripts = gene.get("transcripts") or []
321
+ for tx in transcripts:
322
+ if not tx.get("is_canonical"):
323
+ continue
324
+ canonical_tx = tx.get("transcript_id")
325
+ protein = tx.get("protein_sequence_name")
326
+ line_components = [f"{canonical_tx} ({gene.get('hgnc_symbol', '')})"]
327
+ hgvs = gene.get("hgvs_identifier")
328
+ if hgvs:
329
+ line_components.append(hgvs)
330
+ if protein:
331
+ line_components.append(protein)
332
+
333
+ lines.add(" ".join(line_components))
334
+
335
+ return list(lines)
336
+
337
+ app.custom_filters.format_variant_canonical_transcripts = format_variant_canonical_transcripts
338
+
284
339
  @app.template_filter()
285
340
  def upper_na(string):
286
341
  """
@@ -306,6 +361,20 @@ def register_filters(app):
306
361
  """Returns a list after removing any None values from it."""
307
362
  return [item for item in in_list if item is not None]
308
363
 
364
+ @app.template_filter()
365
+ def spliceai_max(values) -> Optional[float]:
366
+ """Returns a list of SpliceAI values, extracting floats only from values like 'score:0.23'."""
367
+ float_values = []
368
+ for value in values:
369
+ if isinstance(value, str):
370
+ if ":" in value: # Variant hits multiple genes
371
+ value = value.split(":")[1].strip()
372
+ if value in [None, "-", "None"]:
373
+ continue
374
+ float_values.append(float(value))
375
+ if float_values:
376
+ return max(float_values)
377
+
309
378
 
310
379
  def register_tests(app):
311
380
  @app.template_test("existing")
@@ -119,6 +119,8 @@
119
119
  indexURL: "{{ url_for('alignviewers.remote_static', file=track.indexURL) }}",
120
120
  sourceType: "file",
121
121
  groupBy: "tag:HP",
122
+ showInsertionText: true,
123
+ showDeletionText: true,
122
124
  showSoftClips: {{track.show_soft_clips | lower }},
123
125
  format: "{{ track.format }}",
124
126
  height: "{{track.height}}"
@@ -371,7 +371,7 @@ def case(
371
371
  partial_causatives = _get_partial_causatives(store, institute_obj, case_obj)
372
372
  _populate_assessments(partial_causatives)
373
373
 
374
- case_obj["clinvar_variants"] = store.case_to_clinVars(case_obj["_id"])
374
+ case_obj["clinvar_variants"] = store.case_to_clinvars(case_obj["_id"])
375
375
 
376
376
  # check for variants submitted to clinVar but not present in suspects for the case
377
377
  clinvar_variants_not_in_suspects = [
@@ -700,11 +700,28 @@
700
700
  <tbody>
701
701
  <tr>
702
702
  <td>{{ variant.sift_predictions|join(', ') or '-'}}</td>
703
- <td>{{ variant.revel or '-' }}</td>
703
+ <td>
704
+ {% if variant.revel %}
705
+ <span class="badge bg-{{variant.revel|get_label_or_color_by_score('revel', 'color')}}">
706
+ {{ variant.revel }}
707
+ </span>
708
+ {% else %}
709
+ <span class="badge bg-secondary">–</span>
710
+ {% endif %}
711
+ </td>
704
712
  <td>{{ variant.revel_score or '-' }}</td>
705
713
  <td>{{ variant.polyphen_predictions|join(', ') or '-' }}</td>
706
714
  <td>{{ variant.spidex|spidex_human if variant.spidex else none|spidex_human }}</td>
707
- <td>{{ variant.spliceai_scores|join(', ') or '-' }}</td>
715
+ <td>
716
+ {% set spliceai_highest = variant.spliceai_scores | spliceai_max %}
717
+ {% if spliceai_highest %}
718
+ <span class="badge bg-{{ spliceai_highest | get_label_or_color_by_score('spliceai', 'color') }}">
719
+ {{ variant.spliceai_scores|join(', ') }}
720
+ </span>
721
+ {% else %}
722
+ <span class="badge bg-secondary">–</span>
723
+ {% endif %}
724
+ </td>
708
725
  </tr>
709
726
  </tbody>
710
727
  </table>