scout-browser 4.98.0__py3-none-any.whl → 4.99.0__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 (54) hide show
  1. scout/adapter/mongo/institute.py +42 -55
  2. scout/adapter/mongo/variant.py +19 -15
  3. scout/adapter/mongo/variant_loader.py +11 -11
  4. scout/build/individual.py +2 -0
  5. scout/build/variant/variant.py +8 -0
  6. scout/commands/download/ensembl.py +18 -2
  7. scout/commands/update/individual.py +2 -0
  8. scout/commands/update/panelapp.py +15 -2
  9. scout/constants/__init__.py +6 -7
  10. scout/constants/clnsig.py +2 -0
  11. scout/constants/file_types.py +12 -0
  12. scout/constants/igv_tracks.py +8 -6
  13. scout/constants/panels.py +3 -0
  14. scout/constants/variant_tags.py +6 -6
  15. scout/demo/643594.config.yaml +1 -0
  16. scout/load/panelapp.py +11 -5
  17. scout/models/case/case_loading_models.py +4 -0
  18. scout/parse/variant/clnsig.py +38 -0
  19. scout/parse/variant/genotype.py +4 -10
  20. scout/parse/variant/models.py +5 -11
  21. scout/parse/variant/rank_score.py +5 -13
  22. scout/parse/variant/variant.py +90 -111
  23. scout/server/app.py +33 -22
  24. scout/server/blueprints/alignviewers/controllers.py +29 -10
  25. scout/server/blueprints/alignviewers/templates/alignviewers/igv_viewer.html +41 -11
  26. scout/server/blueprints/cases/templates/cases/utils.html +6 -6
  27. scout/server/blueprints/clinvar/controllers.py +29 -14
  28. scout/server/blueprints/clinvar/templates/clinvar/multistep_add_variant.html +13 -4
  29. scout/server/blueprints/clinvar/views.py +14 -2
  30. scout/server/blueprints/institutes/controllers.py +10 -2
  31. scout/server/blueprints/login/controllers.py +112 -12
  32. scout/server/blueprints/login/views.py +38 -60
  33. scout/server/blueprints/public/templates/public/index.html +5 -1
  34. scout/server/blueprints/variant/controllers.py +1 -1
  35. scout/server/blueprints/variant/templates/variant/acmg.html +6 -2
  36. scout/server/blueprints/variant/templates/variant/components.html +19 -0
  37. scout/server/blueprints/variant/templates/variant/utils.html +3 -3
  38. scout/server/blueprints/variants/controllers.py +10 -1
  39. scout/server/blueprints/variants/templates/variants/components.html +28 -0
  40. scout/server/blueprints/variants/templates/variants/mei-variants.html +8 -6
  41. scout/server/blueprints/variants/templates/variants/sv-variants.html +9 -7
  42. scout/server/blueprints/variants/templates/variants/utils.html +8 -12
  43. scout/server/blueprints/variants/templates/variants/variants.html +4 -25
  44. scout/server/config.py +8 -0
  45. scout/server/utils.py +22 -5
  46. scout/utils/acmg.py +25 -26
  47. scout/utils/ensembl_biomart_clients.py +1 -1
  48. scout/utils/ensembl_rest_clients.py +25 -32
  49. scout/utils/hgvs.py +1 -1
  50. {scout_browser-4.98.0.dist-info → scout_browser-4.99.0.dist-info}/METADATA +10 -14
  51. {scout_browser-4.98.0.dist-info → scout_browser-4.99.0.dist-info}/RECORD +54 -54
  52. {scout_browser-4.98.0.dist-info → scout_browser-4.99.0.dist-info}/WHEEL +0 -0
  53. {scout_browser-4.98.0.dist-info → scout_browser-4.99.0.dist-info}/entry_points.txt +0 -0
  54. {scout_browser-4.98.0.dist-info → scout_browser-4.99.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  {% extends "layout.html" %}
2
2
  {% from "variants/utils.html" import sv_filters, cell_rank, pagination_footer, pagination_hidden_div, dismiss_variants_block, filter_form_footer, filter_script_main, update_stash_filter_button_status, callers_cell %}
3
- {% from "variants/components.html" import external_scripts, external_stylesheets, frequency_cell_general, observed_cell_general, variant_gene_symbols_cell, variant_funct_anno_cell %}
3
+ {% from "variants/components.html" import external_scripts, external_stylesheets, frequency_cell_general, observed_cell_general, overlapping_cell, variant_gene_symbols_cell, variant_funct_anno_cell %}
4
4
 
5
5
  {% block title %}
6
6
  {{ super() }} - {{ institute.display_name }} - {{ case.display_name }} - SV variants
@@ -57,18 +57,19 @@ onsubmit="return validateForm()">
57
57
  <thead class="table-light thead">
58
58
  <tr>
59
59
  <th style="width:3%"></th>
60
- <th>Rank</th>
61
- <th>Score</th>
60
+ <th title="Rank position">Rank</th>
61
+ <th title="Rank score">Score</th>
62
62
  <th>Callers</th>
63
63
  <th>Type</th>
64
- <th>Chr</th>
64
+ <th title="Chromosome">Chr</th>
65
65
  <th>Start</th>
66
66
  <th>End</th>
67
67
  <th>Length</th>
68
- <th>Pop Freq</th>
69
- <th>Observed</th>
68
+ <th title="Population Frequency">Pop Freq</th>
69
+ <th title="Observed database matches">Observed</th>
70
70
  <th>Gene(s)</th>
71
- <th>Function</th>
71
+ <th title="Functional annotation">Function</th>
72
+ <th title="Gene overlapping variants">Overlap</th>
72
73
  </tr>
73
74
  </thead>
74
75
  <tbody>
@@ -121,6 +122,7 @@ onsubmit="return validateForm()">
121
122
  <td style="word-wrap:break-word;">
122
123
  {{ variant_funct_anno_cell(variant) }}
123
124
  </td>
125
+ <td>{{ overlapping_cell(variant, institute, case) }}</td>
124
126
  </tr>
125
127
  {% endmacro %}
126
128
 
@@ -1,5 +1,6 @@
1
1
  {% import "bootstrap/wtf.html" as wtf %}
2
2
  {% from "utils.html" import comments_table %}
3
+ {% from "cases/utils.html" import pretty_link_variant %}
3
4
  {% from "variants/indicators.html" import pin_indicator, causative_badge, clinical_assessments_badge, comments_badge, dismissals_badge, evaluations_badge, group_assessments_badge, matching_manual_rank, research_assessments_badge %}
4
5
 
5
6
  {% macro filter_script_main(cytobands) %}
@@ -225,30 +226,25 @@
225
226
  </table>
226
227
  {% endmacro %}
227
228
 
228
- {% macro svs_table(institute, case, overlapping) %}
229
+ {% macro overlapping_tooltip_table(institute, case, overlapping) %}
229
230
  <table class='table table-bordered table-hover table-condensed'>
230
231
  <thead>
231
232
  <tr>
232
- <th>Region</th>
233
+ <th>Pos</th>
233
234
  <th>Type</th>
234
235
  <th>Length</th>
235
236
  <th>Rank score</th>
236
237
  </tr>
237
238
  </thead>
238
239
  <tbody>
239
- {% for sv in overlapping %}
240
+ {% for variant in overlapping %}
240
241
  <tr>
241
242
  <td>
242
- <a href='{{ url_for("variant.sv_variant",
243
- institute_id=institute._id,
244
- case_name=case.display_name,
245
- variant_id=sv._id) }}'>
246
- {{ sv.chromosome }}{{ sv.cytoband_start }}
247
- </a>
243
+ {{ pretty_link_variant(variant, case) }}
248
244
  </td>
249
- <td class='text-end'>{{ sv.sub_category }}</td>
250
- <td class='text-end'>{{ sv.length if sv.length < 100000000000 else "-" }}</td>
251
- <td class='text-end'>{{ sv.rank_score }}</td>
245
+ <td class='text-end'>{{ variant.sub_category }}</td>
246
+ <td class='text-end'>{{ variant.length if variant.length < 100000000000 else '-' }}</td>
247
+ <td class='text-end'>{{ variant.rank_score }}</td>
252
248
  </tr>
253
249
  {% endfor %}
254
250
  </tbody>
@@ -1,6 +1,6 @@
1
1
  {% extends "layout.html" %}
2
- {% from "variants/utils.html" import compounds_table, svs_table, snv_filters, cell_rank, pagination_footer, pagination_hidden_div, dismiss_variants_block, filter_form_footer, filter_script_main, update_stash_filter_button_status, mark_heteroplasmic_mt %}
3
- {% from "variants/components.html" import external_scripts, external_stylesheets, frequency_cell_general, gene_cell, observed_cell_general %}
2
+ {% from "variants/utils.html" import snv_filters, cell_rank, pagination_footer, pagination_hidden_div, dismiss_variants_block, filter_form_footer, filter_script_main, update_stash_filter_button_status, mark_heteroplasmic_mt %}
3
+ {% from "variants/components.html" import external_scripts, external_stylesheets, frequency_cell_general, gene_cell, observed_cell_general, overlapping_cell %}
4
4
 
5
5
  {% block title %}
6
6
  {{ super() }} - {{ institute.display_name }} - {{ case.display_name }} - {{ form.variant_type.data|capitalize }} variants
@@ -59,7 +59,7 @@
59
59
  <th style="width:5%" title="Rank score">Score</th>
60
60
  <th style="width:4%" title="Chromosome">Chr.</th>
61
61
  <th style="width:12%" title="HGNC symbols">Gene</th>
62
- <th style="width:6%" title="Pop Freq">Pop Freq</th>
62
+ <th style="width:6%" title="Population Frequency">Pop Freq</th>
63
63
  <th style="width:10%" title="Observed database matches">Observed</th>
64
64
  <th style="width:5%" title="CADD score">CADD</th>
65
65
  <th style="width:18%" title="Functional annotation">Function</th>
@@ -93,7 +93,7 @@
93
93
  {{ functional_annotation_cell(variant) }}
94
94
  </td>
95
95
  <td>{{ cell_models(variant) }}</td>
96
- <td>{{ overlapping_cell(variant) }}</td>
96
+ <td>{{ overlapping_cell(variant, institute, case) }}</td>
97
97
  </tr>
98
98
  {% else %}
99
99
  <tr>
@@ -159,27 +159,6 @@
159
159
  {% endif %}
160
160
  {% endmacro %}
161
161
 
162
- {% macro overlapping_cell(variant) %}
163
-
164
- {% if variant.compounds %}
165
- {% set ns=namespace(show_compounds=false) %}
166
- {% for compound in variant.compounds if not compound.is_dismissed %}
167
- {% set ns.show_compounds = true %}
168
- {% endfor %}
169
- {% if ns.show_compounds %}
170
- <a href="#" class="badge bg-primary text-white" data-bs-toggle="popover" data-bs-placement="left"
171
- data-bs-html="true" data-bs-trigger="hover click"
172
- data-bs-content="{{ compounds_table(institute, case, variant.compounds[:20], is_popover=true) }}">Compounds</a>
173
- {% endif %}
174
- {% endif %}
175
-
176
- {% if variant.overlapping %}
177
- <a href="#" class="badge bg-warning" data-bs-toggle="popover" data-bs-placement="left"
178
- data-bs-html="true" data-bs-trigger="hover click"
179
- data-bs-content="{{ svs_table(institute, case, variant.overlapping[:20]) }}">Overlapping SVs</a>
180
- {% endif %}
181
- {% endmacro %}
182
-
183
162
  {% block scripts %}
184
163
  {{ super() }}
185
164
  {{ external_scripts() }}
scout/server/config.py CHANGED
@@ -38,6 +38,14 @@ ACCREDITATION_BADGE = "swedac-1926-iso17025.png"
38
38
  # discovery_url="https://accounts.google.com/.well-known/openid-configuration",
39
39
  # )
40
40
 
41
+ # Parameters required for login with Keycloak
42
+ # KEYCLOAK = dict(
43
+ # client_id="<name_of_client>",
44
+ # client_secret="secret",
45
+ # discovery_url="http://localhost:8080/realms/<name_of_realm>/.well-known/openid-configuration",
46
+ # logout_url="http://localhost:8080/realms/<name_of_realm>/protocol/openid-connect/logout",
47
+ # )
48
+
41
49
  # Chanjo database connection string - used by chanjo report to create coverage reports
42
50
  # SQLALCHEMY_DATABASE_URI = "mysql+pymysql://test_user:test_passwordw@127.0.0.1:3306/chanjo"
43
51
 
scout/server/utils.py CHANGED
@@ -244,15 +244,23 @@ def case_has_chanjo2_coverage(case_obj: dict):
244
244
  def case_has_alignments(case_obj: dict):
245
245
  """Add info on bam/cram files availability to a case dictionary
246
246
 
247
+ This sets the availability of alignments for autosomal chromosomes.
248
+ If paraphase or de novo assembly alignments, this is also sufficient to set
249
+ alignment availability.
247
250
  Args:
248
251
  case_obj(scout.models.Case)
249
252
  """
250
- case_obj["bam_files"] = False # Availability of alignments for autosomal chromosomes
253
+ case_obj["bam_files"] = False
251
254
  for ind in case_obj.get("individuals"):
252
- bam_path = ind.get("bam_file")
253
- if bam_path and os.path.exists(bam_path):
254
- case_obj["bam_files"] = True
255
- return
255
+ for alignment_path_key in [
256
+ "bam_file",
257
+ "assembly_alignment_path",
258
+ "paraphase_alignment_path",
259
+ ]:
260
+ bam_path = ind.get(alignment_path_key)
261
+ if bam_path and os.path.exists(bam_path):
262
+ case_obj["bam_files"] = True
263
+ return
256
264
 
257
265
 
258
266
  def case_has_mt_alignments(case_obj: dict):
@@ -301,10 +309,19 @@ def case_append_alignments(case_obj: dict):
301
309
  """Deconvolute information about files to case_obj.
302
310
 
303
311
  This function prepares bam/cram files and their indexes for easy access in templates.
312
+
313
+ index is set only if it is expected as a separate key on the indiviudal: an
314
+ attempt at discovery is still made for files with index: None.
304
315
  """
305
316
  unwrap_settings = [
306
317
  {"path": "bam_file", "append_to": "bam_files", "index": "bai_files"},
318
+ {"path": "assembly_alignment_path", "append_to": "assembly_alignments", "index": None},
307
319
  {"path": "mt_bam", "append_to": "mt_bams", "index": "mt_bais"},
320
+ {
321
+ "path": "paraphase_alignment_path",
322
+ "append_to": "paraphase_alignments",
323
+ "index": None,
324
+ },
308
325
  {"path": "rhocall_bed", "append_to": "rhocall_beds", "index": None},
309
326
  {"path": "rhocall_wig", "append_to": "rhocall_wigs", "index": None},
310
327
  {"path": "upd_regions_bed", "append_to": "upd_regions_beds", "index": None},
scout/utils/acmg.py CHANGED
@@ -173,7 +173,7 @@ def get_acmg_criteria(acmg_terms: set) -> tuple:
173
173
  finally, check remaining prefixes if no suffix match or stand-alone criteria match
174
174
 
175
175
  Return a tuple with
176
- pvs: This variable indicates if Pathogenicity Very Strong exists
176
+ pvs_terms: This variable indicates if Pathogenicity Very Strong exists
177
177
  ps_terms: Collection of terms with Pathogenicity Strong
178
178
  pm_terms: Collection of terms with Pathogenicity moderate
179
179
  pp_terms: Collection of terms with Pathogenicity supporting
@@ -182,25 +182,29 @@ def get_acmg_criteria(acmg_terms: set) -> tuple:
182
182
  bp_terms: Collection of terms with supporting Benign evidence
183
183
  """
184
184
 
185
- pvs = False
185
+ pvs_terms = []
186
186
  ps_terms = []
187
187
  pm_terms = []
188
188
  pp_terms = []
189
189
 
190
- ba = False
190
+ ba_terms = []
191
191
  bs_terms = []
192
192
  bp_terms = []
193
193
 
194
194
  suffix_map = {
195
+ "_Stand-alone": {"B": ba_terms},
196
+ "_Very Strong": {"P": pvs_terms},
195
197
  "_Strong": {"P": ps_terms, "B": bs_terms},
196
198
  "_Moderate": {"P": pm_terms},
197
199
  "_Supporting": {"P": pp_terms, "B": bp_terms},
198
200
  }
199
201
 
200
202
  prefix_map = {
203
+ "PVS": pvs_terms,
201
204
  "PS": ps_terms,
202
205
  "PM": pm_terms,
203
206
  "PP": pp_terms,
207
+ "BA": ba_terms,
204
208
  "BS": bs_terms,
205
209
  "BP": bp_terms,
206
210
  }
@@ -216,17 +220,12 @@ def get_acmg_criteria(acmg_terms: set) -> tuple:
216
220
  continue
217
221
  break
218
222
  else:
219
- if term.startswith("PVS"):
220
- pvs = True
221
- elif term.startswith("BA"):
222
- ba = True
223
- else:
224
- for prefix, term_list in prefix_map.items():
225
- if term.startswith(prefix):
226
- term_list.append(term)
227
- break
223
+ for prefix, term_list in prefix_map.items():
224
+ if term.startswith(prefix):
225
+ term_list.append(term)
226
+ break
228
227
 
229
- return (pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms)
228
+ return (pvs_terms, ps_terms, pm_terms, pp_terms, ba_terms, bs_terms, bp_terms)
230
229
 
231
230
 
232
231
  def get_acmg(acmg_terms: set) -> Optional[str]:
@@ -316,19 +315,19 @@ def get_acmg_temperature(acmg_terms: set) -> Optional[dict]:
316
315
  if not acmg_terms:
317
316
  return {}
318
317
 
319
- (pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms) = get_acmg_criteria(acmg_terms)
320
-
321
- if ba:
322
- points = -8
323
- else:
324
- points = (
325
- 8 * pvs
326
- + 4 * len(ps_terms)
327
- + 2 * len(pm_terms)
328
- + len(pp_terms)
329
- - 4 * len(bs_terms)
330
- - len(bp_terms)
331
- )
318
+ (pvs_terms, ps_terms, pm_terms, pp_terms, ba_terms, bs_terms, bp_terms) = get_acmg_criteria(
319
+ acmg_terms
320
+ )
321
+
322
+ points = (
323
+ 8 * len(pvs_terms)
324
+ + 4 * len(ps_terms)
325
+ + 2 * len(pm_terms)
326
+ + len(pp_terms)
327
+ - 8 * len(ba_terms)
328
+ - 4 * len(bs_terms)
329
+ - len(bp_terms)
330
+ )
332
331
 
333
332
  if points <= -7:
334
333
  point_classification = "benign"
@@ -4,7 +4,7 @@ from typing import Dict, Iterator
4
4
  import requests
5
5
 
6
6
  LOG = logging.getLogger(__name__)
7
- SCHUG_BASE = "https://schug-stage.scilifelab.se"
7
+ SCHUG_BASE = "https://schug.scilifelab.se"
8
8
 
9
9
  BUILDS: Dict[str, str] = {"37": "GRCh37", "38": "GRCh38"}
10
10
 
@@ -1,30 +1,27 @@
1
1
  """Code for talking to ensembl rest API"""
2
2
 
3
3
  import logging
4
+ from typing import Optional
4
5
  from urllib.parse import urlencode
5
6
 
6
7
  import requests
8
+ from flask import flash
7
9
 
8
10
  LOG = logging.getLogger(__name__)
9
11
 
10
12
  HEADERS = {"Content-type": "application/json"}
11
- RESTAPI_37 = "https://grch37.rest.ensembl.org"
12
- RESTAPI_38 = "https://rest.ensembl.org"
13
+ RESTAPI_URL = "https://rest.ensembl.org"
13
14
 
14
15
 
15
16
  class EnsemblRestApiClient:
16
17
  """A class handling requests and responses to and from the Ensembl REST APIs.
17
- Endpoints for human build 37: https://grch37.rest.ensembl.org
18
- Endpoints for human build 38: http://rest.ensembl.org/
18
+ Endpoint: http://rest.ensembl.org/
19
19
  Documentation: https://github.com/Ensembl/ensembl-rest/wiki
20
20
  doi:10.1093/bioinformatics/btu613
21
21
  """
22
22
 
23
- def __init__(self, build="37"):
24
- if build == "38":
25
- self.server = RESTAPI_38
26
- else:
27
- self.server = RESTAPI_37
23
+ def __init__(self):
24
+ self.server = RESTAPI_URL
28
25
 
29
26
  def build_url(self, endpoint, params=None):
30
27
  """Build an url to query ensembml"""
@@ -34,7 +31,7 @@ class EnsemblRestApiClient:
34
31
  return "".join([self.server, endpoint])
35
32
 
36
33
  @staticmethod
37
- def send_request(url):
34
+ def send_request(url) -> Optional[dict]:
38
35
  """Sends the actual request to the server and returns the response
39
36
 
40
37
  Accepts:
@@ -43,33 +40,29 @@ class EnsemblRestApiClient:
43
40
  Returns:
44
41
  data(dict): dictionary from json response
45
42
  """
46
- data = {}
43
+ error = None
44
+ data = None
47
45
  try:
48
46
  response = requests.get(url, headers=HEADERS)
49
- if response.status_code == 404:
50
- LOG.info("Request failed for url %s\n", url)
51
- response.raise_for_status()
52
- data = response.json()
53
- except requests.exceptions.MissingSchema as err:
54
- LOG.info("Request failed for url %s: Error: %s\n", url, err)
55
- data = err
56
- except requests.exceptions.HTTPError as err:
57
- LOG.info("Request failed for url %s: Error: %s\n", url, err)
58
- data = err
47
+ if response.status_code not in [404, 500]:
48
+
49
+ data = response.json()
50
+ else:
51
+ error = f"Ensembl request failed with code:{response.status_code} for url {url}"
52
+ except requests.exceptions.MissingSchema:
53
+ error = f"Ensembl request failed with MissingSchema error for url {url}"
54
+ except requests.exceptions.HTTPError:
55
+ error = f"Ensembl request failed with HTTPError error for url {url}"
56
+
57
+ if error:
58
+ flash(error)
59
59
  return data
60
60
 
61
- def liftover(self, build, chrom, start, end=None):
61
+ def liftover(
62
+ self, build: str, chrom: str, start: int, end: Optional[int] = None
63
+ ) -> Optional[dict]:
62
64
  """Perform variant liftover using Ensembl REST API
63
-
64
- Args:
65
- build(str): genome build: "37" or "38"
66
- chrom(str): 1-22,X,Y,MT,M
67
- start(int): start coordinate
68
- stop(int): stop coordinate or None
69
-
70
- Returns:
71
- mappings(list of dict): example:
72
- example: https://rest.ensembl.org/map/human/GRCh37/X:1000000..1000100:1/GRCh38?content-type=application/json
65
+ example: https://rest.ensembl.org/map/human/GRCh37/X:1000000..1000100:1/GRCh38?content-type=application/json
73
66
  """
74
67
 
75
68
  build = "GRCh38" if "38" in str(build) else "GRCh37"
scout/utils/hgvs.py CHANGED
@@ -6,7 +6,7 @@ LOG = logging.getLogger(__name__)
6
6
  VALIDATOR_URL = "https://rest.variantvalidator.org/VariantValidator/variantvalidator/{}/{}/select?content-type=application%2Fjson"
7
7
 
8
8
 
9
- def validate_hgvs(build, desc):
9
+ def validate_hgvs(build: str, desc: str) -> bool:
10
10
  """Validates a simple hgvs descriptor using the VariantValidator API (https://rest.variantvalidator.org/)
11
11
 
12
12
  Args:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scout-browser
3
- Version: 4.98.0
3
+ Version: 4.99.0
4
4
  Summary: Clinical DNA variant visualizer and browser
5
5
  Project-URL: Repository, https://github.com/Clinical-Genomics/scout
6
6
  Project-URL: Changelog, https://github.com/Clinical-Genomics/scout/blob/main/CHANGELOG.md
@@ -124,29 +124,25 @@ Instructions on how to run a Scout image connected to your local database or a c
124
124
 
125
125
  ## Installation
126
126
 
127
- <!-- You can install the latest release of Scout using `pip`:
128
-
129
- ```bash
130
- pip install scout-browser
131
-
132
- # ... to include optional coverage tools you would use:
133
- pip install scout-browser[coverage]
134
- ```
135
-
136
- If you would like to install Scout for local development: -->
127
+ Here is a quick start. Please see e.g. the [Installation instructions](docs/install.md) for more details.
137
128
 
138
129
  ```bash
139
130
  git clone https://github.com/Clinical-Genomics/scout
140
131
  cd scout
141
- pip install --editable .
142
132
  ```
143
133
 
144
- Scout is configured to use `uv` if you like; either run, install, install as a tool or
145
- ```
134
+ Scout is configured to use `uv`; either run, install, or install as a tool.
135
+
136
+ ```bash
146
137
  uv sync --frozen
147
138
  uv run scout
148
139
  ```
149
140
 
141
+ You can also install using pip:
142
+
143
+ ```
144
+ pip install --editable .
145
+ ```
150
146
 
151
147
  Scout PDF reports are created using [Flask-WeasyPrint](https://pythonhosted.org/Flask-WeasyPrint/). This library requires external dependencies which need be installed separately (namely Cairo and Pango). See platform-specific instructions for Linux, macOS and Windows available on the WeasyPrint installation [pages](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation).
152
148