scout-browser 4.82.2__py3-none-any.whl → 4.84__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 (87) hide show
  1. scout/__version__.py +1 -1
  2. scout/adapter/client.py +1 -0
  3. scout/adapter/mongo/base.py +0 -1
  4. scout/adapter/mongo/case.py +19 -37
  5. scout/adapter/mongo/case_events.py +98 -2
  6. scout/adapter/mongo/hgnc.py +39 -22
  7. scout/adapter/mongo/institute.py +3 -9
  8. scout/adapter/mongo/panel.py +2 -1
  9. scout/adapter/mongo/variant.py +12 -2
  10. scout/adapter/mongo/variant_loader.py +156 -141
  11. scout/build/genes/hgnc_gene.py +5 -134
  12. scout/commands/base.py +1 -0
  13. scout/commands/download/ensembl.py +1 -0
  14. scout/commands/download/everything.py +1 -0
  15. scout/commands/download/exac.py +1 -0
  16. scout/commands/download/hgnc.py +1 -0
  17. scout/commands/download/hpo.py +1 -0
  18. scout/commands/download/omim.py +1 -0
  19. scout/commands/export/database.py +1 -0
  20. scout/commands/load/panel.py +1 -0
  21. scout/commands/load/report.py +1 -0
  22. scout/commands/update/case.py +10 -10
  23. scout/commands/update/individual.py +6 -1
  24. scout/commands/update/omim.py +1 -0
  25. scout/commands/update/panelapp.py +1 -0
  26. scout/constants/file_types.py +86 -13
  27. scout/export/exon.py +1 -0
  28. scout/load/__init__.py +0 -1
  29. scout/load/all.py +8 -5
  30. scout/load/hgnc_gene.py +1 -1
  31. scout/load/panel.py +8 -4
  32. scout/load/setup.py +1 -0
  33. scout/models/case/case_loading_models.py +6 -16
  34. scout/models/hgnc_map.py +50 -87
  35. scout/models/phenotype_term.py +3 -3
  36. scout/parse/case.py +0 -1
  37. scout/parse/disease_terms.py +1 -0
  38. scout/parse/omim.py +1 -0
  39. scout/parse/orpha.py +1 -0
  40. scout/parse/panel.py +40 -15
  41. scout/parse/variant/conservation.py +1 -0
  42. scout/resources/__init__.py +3 -0
  43. scout/server/app.py +4 -50
  44. scout/server/blueprints/alignviewers/controllers.py +15 -17
  45. scout/server/blueprints/alignviewers/templates/alignviewers/igv_viewer.html +13 -3
  46. scout/server/blueprints/alignviewers/views.py +10 -15
  47. scout/server/blueprints/cases/controllers.py +70 -73
  48. scout/server/blueprints/cases/templates/cases/case.html +94 -71
  49. scout/server/blueprints/cases/templates/cases/collapsible_actionbar.html +1 -1
  50. scout/server/blueprints/cases/templates/cases/phenotype.html +8 -6
  51. scout/server/blueprints/cases/templates/cases/utils.html +3 -3
  52. scout/server/blueprints/cases/views.py +8 -6
  53. scout/server/blueprints/panels/forms.py +1 -0
  54. scout/server/blueprints/variant/controllers.py +14 -19
  55. scout/server/blueprints/variant/templates/variant/acmg.html +25 -16
  56. scout/server/blueprints/variant/templates/variant/components.html +11 -6
  57. scout/server/blueprints/variant/views.py +5 -2
  58. scout/server/blueprints/variants/controllers.py +12 -28
  59. scout/server/blueprints/variants/views.py +1 -1
  60. scout/server/config.py +16 -4
  61. scout/server/extensions/__init__.py +4 -2
  62. scout/server/extensions/beacon_extension.py +1 -0
  63. scout/server/extensions/bionano_extension.py +1 -0
  64. scout/server/extensions/chanjo_extension.py +59 -0
  65. scout/server/extensions/gens_extension.py +1 -0
  66. scout/server/extensions/ldap_extension.py +5 -3
  67. scout/server/extensions/loqus_extension.py +16 -14
  68. scout/server/extensions/matchmaker_extension.py +1 -0
  69. scout/server/extensions/mongo_extension.py +1 -0
  70. scout/server/extensions/phenopacket_extension.py +1 -0
  71. scout/server/extensions/rerunner_extension.py +1 -0
  72. scout/server/links.py +4 -4
  73. scout/server/static/bs_styles.css +20 -2
  74. scout/server/utils.py +16 -2
  75. scout/utils/acmg.py +33 -20
  76. scout/utils/ensembl_rest_clients.py +1 -0
  77. scout/utils/scout_requests.py +1 -0
  78. scout/utils/sort.py +21 -0
  79. scout/utils/track_resources.py +70 -0
  80. {scout_browser-4.82.2.dist-info → scout_browser-4.84.dist-info}/METADATA +2 -5
  81. {scout_browser-4.82.2.dist-info → scout_browser-4.84.dist-info}/RECORD +85 -84
  82. {scout_browser-4.82.2.dist-info → scout_browser-4.84.dist-info}/WHEEL +1 -1
  83. {scout_browser-4.82.2.dist-info → scout_browser-4.84.dist-info}/entry_points.txt +0 -1
  84. scout/load/case.py +0 -36
  85. scout/utils/cloud_resources.py +0 -61
  86. {scout_browser-4.82.2.dist-info → scout_browser-4.84.dist-info}/LICENSE +0 -0
  87. {scout_browser-4.82.2.dist-info → scout_browser-4.84.dist-info}/top_level.txt +0 -0
scout/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.82.2"
1
+ __version__ = "4.84"
scout/adapter/client.py CHANGED
@@ -4,6 +4,7 @@ client.py
4
4
  Establish a connection to the database
5
5
 
6
6
  """
7
+
7
8
  import logging
8
9
 
9
10
  from pymongo import MongoClient
@@ -79,7 +79,6 @@ class MongoAdapter(
79
79
  PhenoModelHandler,
80
80
  RankModelHandler,
81
81
  ):
82
-
83
82
  """Adapter for communication with a Mongo database."""
84
83
 
85
84
  def __init__(self, database=None):
@@ -10,10 +10,11 @@ import pymongo
10
10
  from bson import ObjectId
11
11
 
12
12
  from scout.build.case import build_case
13
- from scout.constants import ACMG_MAP, ID_PROJECTION
13
+ from scout.constants import ACMG_MAP, FILE_TYPE_MAP, ID_PROJECTION
14
14
  from scout.exceptions import ConfigError, IntegrityError
15
15
  from scout.parse.variant.ids import parse_document_id
16
16
  from scout.utils.algorithms import ui_score
17
+ from scout.utils.sort import get_load_priority
17
18
 
18
19
  LOG = logging.getLogger(__name__)
19
20
 
@@ -883,44 +884,37 @@ class CaseHandler(object):
883
884
  self.evaluated_variants(case_obj["_id"], case_obj["owner"])
884
885
  )
885
886
 
887
+ # load from files
886
888
  files = [
887
- {"file_name": "vcf_snv", "variant_type": "clinical", "category": "snv"},
888
- {"file_name": "vcf_sv", "variant_type": "clinical", "category": "sv"},
889
889
  {
890
- "file_name": "vcf_cancer",
891
- "variant_type": "clinical",
892
- "category": "cancer",
893
- },
894
- {
895
- "file_name": "vcf_cancer_sv",
896
- "variant_type": "clinical",
897
- "category": "cancer_sv",
898
- },
899
- {"file_name": "vcf_str", "variant_type": "clinical", "category": "str"},
900
- {"file_name": "vcf_mei", "variant_type": "clinical", "category": "mei"},
901
- {
902
- "file_name": "vcf_fusion",
903
- "variant_type": "clinical",
904
- "category": "fusion",
905
- },
890
+ "file_name": file_type,
891
+ "variant_type": FILE_TYPE_MAP[file_type]["variant_type"],
892
+ "category": FILE_TYPE_MAP[file_type]["category"],
893
+ }
894
+ for file_type in FILE_TYPE_MAP.keys()
895
+ if FILE_TYPE_MAP[file_type]["variant_type"] != "research"
906
896
  ]
907
897
 
898
+ # (type, category) tuples are not unique - eg SNV, SNV_MT
899
+ load_variants = set()
908
900
  try:
909
901
  for vcf_file in files:
910
- # Check if file exists
902
+ # Check if any file of this kind is configured for case
911
903
  if not case_obj["vcf_files"].get(vcf_file["file_name"]):
912
904
  LOG.debug("didn't find {}, skipping".format(vcf_file["file_name"]))
913
905
  continue
906
+ load_variants.add((vcf_file["variant_type"], vcf_file["category"]))
914
907
 
915
- variant_type = vcf_file["variant_type"]
916
- category = vcf_file["category"]
908
+ for variant_type, category in sorted(
909
+ load_variants,
910
+ key=lambda tup: get_load_priority(variant_type=tup[0], category=tup[1]),
911
+ ):
917
912
  if update:
918
913
  self.delete_variants(
919
914
  case_id=case_obj["_id"],
920
915
  variant_type=variant_type,
921
916
  category=category,
922
917
  )
923
-
924
918
  # add variants
925
919
  self.load_variants(
926
920
  case_obj=case_obj,
@@ -948,7 +942,7 @@ class CaseHandler(object):
948
942
  force_update_case=True,
949
943
  )
950
944
 
951
- self.update_case(case_obj)
945
+ self.update_case_cli(case_obj, institute_obj)
952
946
  # update Sanger status for the new inserted variants
953
947
  self.update_case_sanger_variants(institute_obj, case_obj, old_sanger_variants)
954
948
 
@@ -957,7 +951,7 @@ class CaseHandler(object):
957
951
 
958
952
  else:
959
953
  LOG.info("Loading case %s into database", case_obj["display_name"])
960
- self._add_case(case_obj)
954
+ self.add_case(case_obj, institute_obj)
961
955
 
962
956
  return case_obj
963
957
 
@@ -972,18 +966,6 @@ class CaseHandler(object):
972
966
  "custom_images"
973
967
  ].get(variant_category)
974
968
 
975
- def _add_case(self, case_obj):
976
- """Add a case to the database
977
- If the case already exists exception is raised
978
-
979
- Args:
980
- case_obj(Case)
981
- """
982
- if self.case(case_obj["_id"], projection=ID_PROJECTION):
983
- raise IntegrityError("Case %s already exists in database" % case_obj["_id"])
984
-
985
- return self.case_collection.insert_one(case_obj)
986
-
987
969
  def update_case(self, case_obj, keep_date=False):
988
970
  """Update a case in the database.
989
971
  While updating the case, it compares the date of the latest analysis (case_obj["analysis_date"]) against
@@ -1,10 +1,12 @@
1
1
  import logging
2
2
  from collections import Counter
3
+ from os import getlogin
3
4
  from typing import Dict, List, Optional
4
5
 
5
6
  import pymongo
6
7
 
7
- from scout.constants import CASE_STATUSES, CASE_TAGS
8
+ from scout.constants import CASE_STATUSES, CASE_TAGS, ID_PROJECTION
9
+ from scout.exceptions import IntegrityError
8
10
 
9
11
  LOG = logging.getLogger(__name__)
10
12
 
@@ -12,6 +14,100 @@ LOG = logging.getLogger(__name__)
12
14
  class CaseEventHandler(object):
13
15
  """Class to handle case events for the mongo adapter"""
14
16
 
17
+ def get_cli_user(self) -> dict:
18
+ """
19
+ Return a faux CLI user with a login username from OS CLI if it is available.
20
+ """
21
+ try:
22
+ cli_user_name = getlogin()
23
+ except OSError:
24
+ # no controlling terminal
25
+ cli_user_name = "CLI user"
26
+
27
+ return {"_id": "CLI", "name": cli_user_name}
28
+
29
+ def add_case(self, case_obj: dict, institute_obj: dict):
30
+ """Add a case to the database
31
+ If the case already exists exception is raised.
32
+ Add case will only be called from CLI, or tests, so the user will be the faux CLI user with
33
+ a login username from OS CLI if available.
34
+ """
35
+ if self.case(case_obj["_id"], projection=ID_PROJECTION):
36
+ raise IntegrityError("Case %s already exists in database" % case_obj["_id"])
37
+ link = f"/{case_obj['owner']}/{case_obj['display_name']}"
38
+
39
+ self.create_event(
40
+ institute=institute_obj,
41
+ case=case_obj,
42
+ user=self.get_cli_user(),
43
+ link=link,
44
+ category="case",
45
+ verb="add_case",
46
+ subject=case_obj["display_name"],
47
+ )
48
+
49
+ return self.case_collection.insert_one(case_obj)
50
+
51
+ def update_case_individual(
52
+ self, case_obj: dict, user_obj: dict, institute_obj: dict, link: str, keep_date: bool = True
53
+ ):
54
+ """Update case with new individual data (age and/or Tissue type) for a case
55
+ and create an associated event"""
56
+ self._update_case_component(
57
+ case_obj, user_obj, institute_obj, link, verb="update_individual", keep_date=keep_date
58
+ )
59
+
60
+ def update_case_sample(
61
+ self, case_obj: dict, user_obj: dict, institute_obj: dict, link: str, keep_date=True
62
+ ):
63
+ """Handle update of sample data data (tissue, tumor_type, tumor_purity) for a cancer case
64
+ and create an associated event"""
65
+ self._update_case_component(
66
+ case_obj, user_obj, institute_obj, link, verb="update_sample", keep_date=keep_date
67
+ )
68
+
69
+ def _update_case_component(
70
+ self,
71
+ case_obj: dict,
72
+ user_obj: Optional[dict],
73
+ institute_obj: dict,
74
+ link: str,
75
+ verb: str,
76
+ keep_date: bool = True,
77
+ ):
78
+ """Update case with new sample data, and create an associated event"""
79
+ self.update_case(case_obj, keep_date)
80
+
81
+ if not user_obj:
82
+ user_obj = self.get_cli_user()
83
+
84
+ self.create_event(
85
+ institute=institute_obj,
86
+ case=case_obj,
87
+ user=user_obj,
88
+ link=link,
89
+ category="case",
90
+ verb=verb,
91
+ subject=case_obj["display_name"],
92
+ )
93
+
94
+ def update_case_cli(self, case_obj: dict, institute_obj: dict):
95
+ """Update case with new case obj, and create an associated CLI user event."""
96
+
97
+ link = f"/{case_obj['owner']}/{case_obj['display_name']}"
98
+
99
+ self.create_event(
100
+ institute=institute_obj,
101
+ case=case_obj,
102
+ user=self.get_cli_user(),
103
+ link=link,
104
+ category="case",
105
+ verb="update_case",
106
+ subject=case_obj["display_name"],
107
+ )
108
+
109
+ self.update_case(case_obj)
110
+
15
111
  def assign(self, institute, case, user, link):
16
112
  """Assign a user to a case.
17
113
 
@@ -550,7 +646,7 @@ class CaseEventHandler(object):
550
646
  updated_diagnoses = []
551
647
  case_diagnoses = case.get("diagnosis_phenotypes") or []
552
648
 
553
- if remove is True: # Remove term from case diagnoses list
649
+ if remove: # Remove term from case diagnoses list
554
650
  for case_dia in case_diagnoses:
555
651
  if case_dia.get("disease_id") == disease_id:
556
652
  continue
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from typing import Dict
2
3
 
3
4
  import intervaltree
4
5
  from pymongo.errors import BulkWriteError, DuplicateKeyError
@@ -186,11 +187,11 @@ class GeneHandler(object):
186
187
  if build == "GRCh38":
187
188
  build = "38"
188
189
 
189
- LOG.info("Fetching all genes")
190
+ LOG.debug("Fetching all genes")
190
191
 
191
192
  hgnc_tx = {}
192
193
  if add_transcripts:
193
- LOG.info("Adding transcripts")
194
+ LOG.debug("Adding transcripts")
194
195
  for tx in self.transcripts(build=str(build)):
195
196
  hgnc_id = tx["hgnc_id"]
196
197
  if not hgnc_id in hgnc_tx:
@@ -318,7 +319,7 @@ class GeneHandler(object):
318
319
  res = self.hgnc_collection.find({"aliases": symbol, "build": str(build)})
319
320
  return res
320
321
 
321
- def genes_by_alias(self, build="37", genes=None):
322
+ def genes_by_alias(self, build=None, genes=None):
322
323
  """Return a dictionary with hgnc symbols as keys and a list of hgnc ids
323
324
  as value.
324
325
 
@@ -333,45 +334,61 @@ class GeneHandler(object):
333
334
  Returns:
334
335
  alias_genes(dict): {<hgnc_alias>: {'true': <hgnc_id>, 'ids': {<hgnc_id_1>, <hgnc_id_2>, ...}}}
335
336
  """
336
- LOG.info("Fetching all genes by alias")
337
+ LOG.debug("Fetching all genes by alias")
337
338
  # Collect one entry for each alias symbol that exists
338
339
  alias_genes = {}
339
- # Loop over all genes
340
- if not genes:
341
- genes = self.hgnc_collection.find({"build": str(build)})
340
+
341
+ if genes is None:
342
+ genes_query = {"build": str(build)} if build else {}
343
+ genes = self.hgnc_collection.find(
344
+ genes_query, projection={"hgnc_id": 1, "hgnc_symbol": 1, "aliases": 1}
345
+ )
342
346
 
343
347
  for gene in genes:
344
- # Collect the hgnc_id
345
348
  hgnc_id = gene["hgnc_id"]
346
- # Collect the true symbol given by hgnc
347
349
  hgnc_symbol = gene["hgnc_symbol"]
348
- # Loop aver all aliases
350
+
349
351
  for alias in gene["aliases"]:
350
- true_id = None
351
- # If the alias is the same as hgnc symbol we know the true id
352
+ if alias not in alias_genes:
353
+ alias_genes[alias] = {"true": None, "ids": set()}
354
+
355
+ alias_genes[alias]["ids"].add(hgnc_id)
352
356
  if alias == hgnc_symbol:
353
- true_id = hgnc_id
354
- # If the alias is already in the list we add the id
355
- if alias in alias_genes:
356
- alias_genes[alias]["ids"].add(hgnc_id)
357
- if true_id:
358
- alias_genes[alias]["true"] = hgnc_id
359
- else:
360
- alias_genes[alias] = {"true": hgnc_id, "ids": set([hgnc_id])}
357
+ alias_genes[alias]["true"] = hgnc_id
361
358
 
362
359
  return alias_genes
363
360
 
364
- def ensembl_to_hgnc_mapping(self):
361
+ def ensembl_to_hgnc_id_mapping(self) -> Dict[str, int]:
365
362
  """Return a dictionary with Ensembl ids as keys and hgnc_ids as values
366
363
 
367
364
  Returns:
368
- mapping(dict): {"ENSG00000121410":"A1BG", ...}
365
+ mapping(dict): {"ENSG00000121410": 5, ...}
369
366
  """
370
367
  pipeline = [{"$group": {"_id": {"ensembl_id": "$ensembl_id", "hgnc_id": "$hgnc_id"}}}]
371
368
  result = self.hgnc_collection.aggregate(pipeline)
372
369
  mapping = {res["_id"]["ensembl_id"]: res["_id"]["hgnc_id"] for res in result}
373
370
  return mapping
374
371
 
372
+ def hgnc_symbol_ensembl_id_mapping(self) -> Dict[str, str]:
373
+ """Return a dictionary with HGNC symbols as keys and Ensembl ids as values.
374
+
375
+ Returns:
376
+ mapping(dict): {"A1BG": "ENSG00000121410".}
377
+ """
378
+ pipeline = [
379
+ {
380
+ "$group": {
381
+ "_id": {
382
+ "hgnc_symbol": "$hgnc_symbol",
383
+ "ensembl_id": "$ensembl_id",
384
+ }
385
+ }
386
+ }
387
+ ]
388
+ result = self.hgnc_collection.aggregate(pipeline)
389
+ mapping = {res["_id"]["hgnc_symbol"]: res["_id"]["ensembl_id"] for res in result}
390
+ return mapping
391
+
375
392
  def ensembl_genes(self, build=None, add_transcripts=False, id_transcripts=False):
376
393
  """Return a dictionary with ensembl ids as keys and gene objects as value.
377
394
 
@@ -158,15 +158,9 @@ class InstituteHandler(object):
158
158
 
159
159
  return institute_obj
160
160
 
161
- def safe_genes_filter(self, institute_id):
161
+ def safe_genes_filter(self, institute_id: str) -> List[int]:
162
162
  """Returns a list of "safe" HGNC IDs to filter variants with. These genes are retrieved from the institute.gene_panels_matching
163
- Can be used to limit secondary findings when retrieving other causatives or matching managed variants
164
-
165
- Args:
166
- institute_id(str): _id of an institute
167
-
168
- Returns:
169
- safe_genes(list of HGNC ids)
163
+ Can be used to limit secondary findings when retrieving other causatives or matching managed variants.
170
164
  """
171
165
  safe_genes = []
172
166
  institute_obj = self.institute(institute_id)
@@ -174,7 +168,7 @@ class InstituteHandler(object):
174
168
  return safe_genes # return an empty list
175
169
  for panel_name in institute_obj.get("gene_panels_matching", {}).keys():
176
170
  safe_genes += self.panel_to_genes(panel_name=panel_name, gene_format="hgnc_id")
177
- return safe_genes
171
+ return list(set(safe_genes))
178
172
 
179
173
  def institutes(self, institute_ids=None):
180
174
  """Fetch all institutes.
@@ -1,4 +1,5 @@
1
1
  """Code to handle panels in the mongo database"""
2
+
2
3
  import datetime as dt
3
4
  import logging
4
5
  import math
@@ -244,7 +245,7 @@ class PanelHandler:
244
245
  for panel in res:
245
246
  return panel
246
247
 
247
- LOG.info("No gene panel found")
248
+ LOG.warning("Gene panel not found")
248
249
 
249
250
  return None
250
251
 
@@ -2,6 +2,7 @@
2
2
  # stdlib modules
3
3
  import logging
4
4
  import re
5
+ from typing import Any
5
6
 
6
7
  # Third party modules
7
8
  import pymongo
@@ -221,6 +222,14 @@ class VariantHandler(VariantLoader):
221
222
  query = self.build_query(case_id, query=query, variant_ids=variant_ids, category=category)
222
223
  return self.variant_collection.count_documents(query)
223
224
 
225
+ def variant_update_field(self, variant_id: str, field_name: str, field_value: Any) -> dict:
226
+ """Updates the value of the given key(field_name) in the variant document in the database."""
227
+ return self.variant_collection.find_one_and_update(
228
+ {"_id": variant_id},
229
+ {"$set": {field_name: field_value}},
230
+ return_document=pymongo.ReturnDocument.AFTER,
231
+ )
232
+
224
233
  def variant(
225
234
  self,
226
235
  document_id=None,
@@ -542,7 +551,8 @@ class VariantHandler(VariantLoader):
542
551
  "institute": case_obj["owner"],
543
552
  "verb": {"$in": ["mark_causative", "mark_partial_causative"]},
544
553
  "category": "variant",
545
- }
554
+ },
555
+ {"case": 1, "link": 1, "subject": 1},
546
556
  )
547
557
 
548
558
  positional_variant_ids = set()
@@ -553,7 +563,7 @@ class VariantHandler(VariantLoader):
553
563
 
554
564
  other_case = self.case(var_event["case"], CASE_CAUSATIVES_PROJECTION)
555
565
  if other_case is None:
556
- # Other variant belongs to a case that doesn't exist any more
566
+ # Other variant belongs to a case that doesn't exist anymore
557
567
  continue
558
568
  other_link = var_event["link"]
559
569
  # link contains other variant ID