grz-cli 0.3.0__tar.gz → 0.4.0__tar.gz

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 (39) hide show
  1. grz_cli-0.4.0/MANIFEST.in +2 -0
  2. {grz_cli-0.3.0/src/grz_cli.egg-info → grz_cli-0.4.0}/PKG-INFO +6 -3
  3. {grz_cli-0.3.0 → grz_cli-0.4.0}/README.md +2 -0
  4. {grz_cli-0.3.0 → grz_cli-0.4.0}/pyproject.toml +3 -2
  5. grz_cli-0.4.0/src/grz_cli/__init__.py +1 -0
  6. grz_cli-0.4.0/src/grz_cli/bam_validation.py +39 -0
  7. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/cli.py +5 -4
  8. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/constants.py +0 -1
  9. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/download.py +5 -2
  10. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/fastq_validation.py +41 -24
  11. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/models/config.py +18 -0
  12. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/parser.py +61 -11
  13. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/progress_logging.py +3 -2
  14. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/upload.py +8 -3
  15. {grz_cli-0.3.0 → grz_cli-0.4.0/src/grz_cli.egg-info}/PKG-INFO +6 -3
  16. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli.egg-info/SOURCES.txt +2 -1
  17. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli.egg-info/requires.txt +3 -2
  18. grz_cli-0.4.0/tests/test_bam_validation.py +15 -0
  19. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_cli.py +5 -3
  20. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_fastq_validation.py +1 -1
  21. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_file_progress_logging.py +1 -1
  22. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_upload.py +12 -11
  23. grz_cli-0.3.0/MANIFEST.in +0 -3
  24. grz_cli-0.3.0/src/grz_cli/__init__.py +0 -1
  25. grz_cli-0.3.0/src/grz_cli/resources/thresholds.json +0 -299
  26. {grz_cli-0.3.0 → grz_cli-0.4.0}/LICENSE +0 -0
  27. {grz_cli-0.3.0 → grz_cli-0.4.0}/setup.cfg +0 -0
  28. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/__init__.py +0 -0
  29. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/file_operations.py +0 -0
  30. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/logging_setup.py +0 -0
  31. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/models/__init__.py +0 -0
  32. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli/states.py +0 -0
  33. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli.egg-info/dependency_links.txt +0 -0
  34. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli.egg-info/entry_points.txt +0 -0
  35. {grz_cli-0.3.0 → grz_cli-0.4.0}/src/grz_cli.egg-info/top_level.txt +0 -0
  36. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_download.py +0 -0
  37. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_file_operations.py +0 -0
  38. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_parser.py +0 -0
  39. {grz_cli-0.3.0 → grz_cli-0.4.0}/tests/test_read_multiple_json.py +0 -0
@@ -0,0 +1,2 @@
1
+ graft src
2
+ include LICENSE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grz-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
5
5
  Author-email: Koray Kirli <koraykirli@gmail.com>, Mathias Lesche <mathias.lesche@tu-dresden.de>, "Florian R. Hölzlwimmer" <git.ich@frhoelzlwimmer.de>, Till Hartmann <till.hartmann@bih-charite.de>, Thomas Sell <thomas.sell@bih-charite.de>
6
6
  License: MIT
@@ -11,7 +11,7 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
- Requires-Dist: boto3<2,>=1.35.39
14
+ Requires-Dist: boto3<2,>=1.36
15
15
  Requires-Dist: click<9,>=8.1.7
16
16
  Requires-Dist: crypt4gh<2,>=1.7
17
17
  Requires-Dist: jsonschema<5,>=4.23.0
@@ -19,7 +19,8 @@ Requires-Dist: pyyaml<7,>=6.0.2
19
19
  Requires-Dist: tqdm<5,>=4.66.5
20
20
  Requires-Dist: pydantic<2.10,>=2.9.2
21
21
  Requires-Dist: platformdirs<5,>=4.3.6
22
- Requires-Dist: grz-pydantic-models>=1.2.0
22
+ Requires-Dist: grz-pydantic-models>=1.2.1
23
+ Requires-Dist: pysam==0.23.*
23
24
  Provides-Extra: test
24
25
  Requires-Dist: pytest<9,>=8.3.3; extra == "test"
25
26
  Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
@@ -254,6 +255,8 @@ Option is for the usage at a GDC/GRZ.
254
255
 
255
256
  ## Testing
256
257
 
258
+ Please note that binary files used for testing are managed with [Git LFS](https://git-lfs.com), which will be needed to clone them locally with the git repository.
259
+
257
260
  To run the tests, navigate to the root directory of your project and invoke `pytest`.
258
261
  Alternatively, install `uv` and `tox` and run `uv run tox`.
259
262
 
@@ -223,6 +223,8 @@ Option is for the usage at a GDC/GRZ.
223
223
 
224
224
  ## Testing
225
225
 
226
+ Please note that binary files used for testing are managed with [Git LFS](https://git-lfs.com), which will be needed to clone them locally with the git repository.
227
+
226
228
  To run the tests, navigate to the root directory of your project and invoke `pytest`.
227
229
  Alternatively, install `uv` and `tox` and run `uv run tox`.
228
230
 
@@ -16,7 +16,7 @@ authors = [
16
16
  ]
17
17
  dynamic = ["version"]
18
18
  dependencies = [
19
- "boto3 >=1.35.39,<2",
19
+ "boto3 >=1.36,<2",
20
20
  "click >=8.1.7,<9",
21
21
  "crypt4gh >=1.7,<2",
22
22
  "jsonschema >=4.23.0,<5",
@@ -24,7 +24,8 @@ dependencies = [
24
24
  "tqdm >=4.66.5,<5",
25
25
  "pydantic >=2.9.2,<2.10",
26
26
  "platformdirs >=4.3.6,<5",
27
- "grz-pydantic-models >=1.2.0"
27
+ "grz-pydantic-models >=1.2.1",
28
+ "pysam ==0.23.*"
28
29
  ]
29
30
  classifiers = [
30
31
  "Programming Language :: Python :: 3",
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -0,0 +1,39 @@
1
+ """
2
+ Validation methods for BAM files
3
+
4
+ e.g. warn on headers present, which can contain personally identifying
5
+ information
6
+ """
7
+
8
+ import logging
9
+ from collections.abc import Generator
10
+ from os import PathLike
11
+
12
+ import pysam
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ def validate_bam(bam_path: str | PathLike) -> Generator[str]:
18
+ """Check if a BAM file is valid for submission"""
19
+ errors: list[str] = []
20
+
21
+ # disable pysam's SQ header presence enforcement (check_sq)
22
+ # these are used to optionally sort alignments by a custom contig order,
23
+ # which is unnecessary in unmapped BAM files
24
+ bam_file = pysam.AlignmentFile(str(bam_path), mode="rb", check_sq=False)
25
+
26
+ header = bam_file.header.to_dict()
27
+ # ignore the HD key as it is almost always present and extremely
28
+ # unlikely to accidentally contain identifying information
29
+ concerning_keys = header.keys() - {"HD"}
30
+ if concerning_keys:
31
+ log.warning(f"Detected a header in BAM file '{bam_path}', ensure it contains no private information:")
32
+ for key in concerning_keys:
33
+ for line in header[key]:
34
+ log.warning(f"@{key}")
35
+ for tag, value in line.items():
36
+ log.warning(f" {tag}: {value}")
37
+ log.warning("--- End of potentially-concerning BAM header information ---")
38
+
39
+ yield from errors
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
  from tempfile import NamedTemporaryFile
12
12
 
13
13
  import click
14
- import grz_pydantic_models
14
+ import grz_pydantic_models.submission.metadata
15
15
  import platformdirs
16
16
  import yaml
17
17
 
@@ -82,7 +82,7 @@ submission_id = click.option(
82
82
  required=True,
83
83
  type=str,
84
84
  metavar="STRING",
85
- help="S3 submission prefix (corresponds to the tanG of a submission)",
85
+ help="S3 submission ID",
86
86
  )
87
87
 
88
88
  output_dir = click.option(
@@ -112,7 +112,7 @@ class OrderedGroup(click.Group):
112
112
  @click.version_option(
113
113
  version=importlib.metadata.version("grz-cli"),
114
114
  prog_name="grz-cli",
115
- message=f"%(prog)s v%(version)s (metadata schema versions: {', '.join(grz_pydantic_models.get_supported_versions())})",
115
+ message=f"%(prog)s v%(version)s (metadata schema versions: {', '.join(grz_pydantic_models.submission.metadata.get_supported_versions())})",
116
116
  )
117
117
  @click.option("--log-file", metavar="FILE", type=str, help="Path to log file")
118
118
  @click.option(
@@ -275,7 +275,8 @@ def upload(
275
275
  encrypted_files_dir=submission_dir / "encrypted_files",
276
276
  threads=threads,
277
277
  )
278
- worker_inst.upload(config)
278
+ # output the generated submission ID
279
+ print(worker_inst.upload(config))
279
280
 
280
281
  log.info("Upload finished!")
281
282
 
@@ -18,7 +18,6 @@ LOGGING_CONFIG = {
18
18
  "level": "INFO",
19
19
  "formatter": "standard",
20
20
  "class": "logging.StreamHandler",
21
- "stream": "ext://sys.stdout", # Default is stderr
22
21
  },
23
22
  },
24
23
  "loggers": {
@@ -73,8 +73,11 @@ class S3BotoDownloadWorker:
73
73
  return string
74
74
 
75
75
  # configure proxies if proxy_url is defined
76
- proxy_url = empty_str_to_none(self._config.s3_options.proxy_url)
77
- config = Boto3Config(proxies={"http": proxy_url, "https": proxy_url}) if proxy_url is not None else None
76
+ proxy_url = self._config.s3_options.proxy_url
77
+ config = Boto3Config(
78
+ proxies={"http": str(proxy_url), "https": str(proxy_url)} if proxy_url is not None else None,
79
+ request_checksum_calculation=self._config.s3_options.request_checksum_calculation,
80
+ )
78
81
 
79
82
  # Initialize S3 client for uploading
80
83
  self._s3_client: boto3.session.Session.client = boto3_client(
@@ -64,40 +64,58 @@ def open_fastq(file_path: str | PathLike, progress=True) -> Generator[TextIO, No
64
64
  yield typing.cast(TextIO, handle)
65
65
 
66
66
 
67
- def calculate_fastq_stats(file_path) -> tuple[int, set[int]]:
67
+ def calculate_fastq_stats(file_path, expected_read_length: int | None = None) -> tuple[int, int | None]:
68
68
  """
69
69
  Calculate line number and read lengths in FASTQ file.
70
70
  :param file_path: Path to the FASTQ file
71
+ :param expected_read_length: Expected read length (None if not known)
71
72
  :return: tuple with the following values:
72
- - number of lines in the file
73
- - Set of observed read lengths
73
+ - Number of lines in the file
74
+ - Observed read length
74
75
  """
75
- read_lengths = set()
76
-
77
76
  with open_fastq(file_path) as f:
78
77
  for line_number, line in enumerate(f):
79
78
  if (line_number % 4) == 1:
80
79
  # Sequence lines are every 4th line starting from the 2nd
81
- read_lengths.add(len(line.strip()))
80
+ # Check if the read length is consistent
81
+ read_length = len(line.strip())
82
+ if expected_read_length is None:
83
+ expected_read_length = read_length
84
+ elif read_length != expected_read_length:
85
+ raise ValueError(
86
+ f"Read length mismatch at line {line_number + 1}: "
87
+ f"expected {expected_read_length}, found {read_length}"
88
+ )
82
89
 
83
90
  return (
84
91
  line_number + 1, # enumerate starts indexing at 0
85
- read_lengths,
92
+ expected_read_length,
86
93
  )
87
94
 
88
95
 
89
- def validate_fastq_file(fastq_file: str | PathLike) -> tuple[int, set[int], list[str]]:
96
+ def validate_fastq_file(
97
+ fastq_file: str | PathLike, expected_read_length: int | None = None
98
+ ) -> tuple[int, int, list[str]] | tuple[int, int | None, list[str]]:
90
99
  """
91
100
  Validates a FASTQ file.
92
101
 
93
102
  :param fastq_file: Path to the fastq file
103
+ :param expected_read_length: Expected read length (None if not known)
94
104
  :return: Tuple with the following fields:
95
105
  - number of lines
96
106
  - set of observed read lengths
97
107
  - list of errors found
98
108
  """
99
- num_lines, read_lengths = calculate_fastq_stats(fastq_file)
100
109
  errors = []
110
+ try:
111
+ if expected_read_length is not None:
112
+ log.debug("%s: expecting read length of %s", fastq_file, expected_read_length)
113
+
114
+ # Calculate the number of lines and read lengths
115
+ num_lines, read_length = calculate_fastq_stats(fastq_file, expected_read_length=expected_read_length)
116
+ except ValueError as e:
117
+ errors.append(f"{fastq_file}: {e}")
118
+ return -1, -1, errors
101
119
 
102
120
  # Check if the number of lines in a FASTQ file is a multiple of 4.
103
121
  if num_lines % 4 != 0:
@@ -105,40 +123,39 @@ def validate_fastq_file(fastq_file: str | PathLike) -> tuple[int, set[int], list
105
123
  else:
106
124
  log.debug("%s: %s lines", fastq_file, num_lines)
107
125
 
108
- # Check if all reads in the FASTQ file have the same length.
109
- if len(read_lengths) != 1:
110
- errors.append(
111
- f"{fastq_file}: Inconsistent read lengths! "
112
- f"Found reads with following lengths: {', '.join([str(c) for c in read_lengths])}"
113
- )
114
- else:
115
- log.debug("%s: read length: %s", fastq_file, next(iter(read_lengths)))
116
-
117
- return num_lines, read_lengths, errors
126
+ return num_lines, read_length, errors
118
127
 
119
128
 
120
- def validate_single_end_reads(fastq_file: str | PathLike) -> Generator[str]:
129
+ def validate_single_end_reads(fastq_file: str | PathLike, expected_read_length: int | None = None) -> Generator[str]:
121
130
  """
122
131
  Validate a single-end FASTQ file.
123
132
 
124
133
  :param fastq_file: Path to the FASTQ file
134
+ :param expected_read_length: Expected read length (None if not known)
125
135
  :return: Generator of errors, if any.
126
136
  """
127
- num_lines, read_lengths, errors = validate_fastq_file(fastq_file)
137
+ num_lines, read_lengths, errors = validate_fastq_file(fastq_file, expected_read_length=expected_read_length)
128
138
  yield from errors
129
139
 
130
140
 
131
- def validate_paired_end_reads(fastq_file1: str | PathLike, fastq_file2: str | PathLike) -> Generator[str]:
141
+ def validate_paired_end_reads(
142
+ fastq_file1: str | PathLike, fastq_file2: str | PathLike, expected_read_length: int | None = None
143
+ ) -> Generator[str]:
132
144
  """
133
145
  Validate two paired-end FASTQ files.
134
146
 
135
147
  :param fastq_file1: Path to the first FASTQ file (Read 1)
136
148
  :param fastq_file2: Path to the second FASTQ file (Read 2)
149
+ :param expected_read_length: Expected read length (None if not known)
137
150
  :return: Generator of errors, if any.
138
151
  """
139
- num_lines_file1, read_lengths_file1, errors_file1 = validate_fastq_file(fastq_file1)
152
+ num_lines_file1, read_lengths_file1, errors_file1 = validate_fastq_file(
153
+ fastq_file1, expected_read_length=expected_read_length
154
+ )
140
155
  yield from errors_file1
141
- num_lines_file2, read_lengths_file2, errors_file2 = validate_fastq_file(fastq_file2)
156
+ num_lines_file2, read_lengths_file2, errors_file2 = validate_fastq_file(
157
+ fastq_file2, expected_read_length=expected_read_length
158
+ )
142
159
  yield from errors_file2
143
160
 
144
161
  if num_lines_file1 != num_lines_file2:
@@ -67,6 +67,24 @@ class S3Options(StrictBaseModel):
67
67
  The proxy URL for S3 operations (optional).
68
68
  """
69
69
 
70
+ request_checksum_calculation: str | None = None
71
+ """
72
+ Whether to calculate checksums for S3 request payloads (optional).
73
+ Valid values are:
74
+
75
+ * ``when_supported`` -- When set, a checksum will be calculated for
76
+ all request payloads of operations modeled with the ``httpChecksum``
77
+ trait where ``requestChecksumRequired`` is ``true`` or a
78
+ ``requestAlgorithmMember`` is modeled.
79
+
80
+ * ``when_required`` -- When set, a checksum will only be calculated
81
+ for request payloads of operations modeled with the ``httpChecksum``
82
+ trait where ``requestChecksumRequired`` is ``true`` or where a
83
+ ``requestAlgorithmMember`` is modeled and supplied.
84
+
85
+ Defaults to None.
86
+ """
87
+
70
88
 
71
89
  FilePath = Annotated[Path, AfterValidator(lambda v: v.expanduser()), PathType("file")]
72
90
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
5
6
  import json
6
7
  import logging
7
8
  from collections.abc import Generator
@@ -9,7 +10,7 @@ from itertools import groupby
9
10
  from os import PathLike
10
11
  from pathlib import Path
11
12
 
12
- from grz_pydantic_models.v1_1_1.metadata import (
13
+ from grz_pydantic_models.submission.metadata.v1 import (
13
14
  ChecksumType,
14
15
  File,
15
16
  FileType,
@@ -18,9 +19,10 @@ from grz_pydantic_models.v1_1_1.metadata import (
18
19
  SequenceData,
19
20
  SequencingLayout,
20
21
  )
21
- from grz_pydantic_models.v1_1_1.metadata import File as SubmissionFileMetadata
22
+ from grz_pydantic_models.submission.metadata.v1 import File as SubmissionFileMetadata
22
23
  from pydantic import ValidationError
23
24
 
25
+ from .bam_validation import validate_bam
24
26
  from .download import S3BotoDownloadWorker
25
27
  from .fastq_validation import validate_paired_end_reads, validate_single_end_reads
26
28
  from .file_operations import Crypt4GH, calculate_sha256
@@ -265,12 +267,16 @@ class Submission:
265
267
  def find_fastq_files(sequence_data: SequenceData) -> list[File]:
266
268
  return [f for f in sequence_data.files if f.file_type == FileType.fastq]
267
269
 
270
+ def find_bam_files(sequence_data: SequenceData) -> list[File]:
271
+ return [f for f in sequence_data.files if f.file_type == FileType.bam]
272
+
268
273
  for donor in self.metadata.content.donors:
269
274
  for lab_data in donor.lab_data:
270
275
  sequencing_layout = lab_data.sequencing_layout
271
276
  sequence_data = lab_data.sequence_data
272
277
  # find all FASTQ files
273
278
  fastq_files = find_fastq_files(sequence_data)
279
+ bam_files = find_bam_files(sequence_data)
274
280
 
275
281
  match sequencing_layout:
276
282
  case SequencingLayout.single_end | SequencingLayout.reverse | SequencingLayout.other:
@@ -279,16 +285,45 @@ class Submission:
279
285
  case SequencingLayout.paired_end:
280
286
  yield from self._validate_paired_end(fastq_files, progress_logger)
281
287
 
288
+ yield from self._validate_bams(bam_files, progress_logger)
289
+
290
+ def _validate_bams(
291
+ self,
292
+ bam_files: list[File],
293
+ progress_logger: FileProgressLogger[ValidationState],
294
+ ) -> Generator[str, None, None]:
295
+ def validate_file(local_file_path, _file_metadata) -> ValidationState:
296
+ self.__log.debug("Validating '%s'...", str(local_file_path))
297
+
298
+ # validate the file
299
+ errors = list(validate_bam(local_file_path))
300
+ validation_passed = len(errors) == 0
301
+
302
+ # return log state
303
+ return ValidationState(
304
+ errors=errors,
305
+ validation_passed=validation_passed,
306
+ )
307
+
308
+ for bam_file in bam_files:
309
+ logged_state = progress_logger.get_state(
310
+ self.files_dir / bam_file.file_path,
311
+ bam_file,
312
+ default=validate_file, # validate the file if the state was not calculated yet
313
+ )
314
+ if logged_state:
315
+ yield from logged_state["errors"]
316
+
282
317
  def _validate_single_end(
283
318
  self,
284
319
  fastq_files: list[File],
285
320
  progress_logger: FileProgressLogger[ValidationState],
286
321
  ) -> Generator[str, None, None]:
287
- def validate_file(local_file_path, _file_metadata) -> ValidationState:
322
+ def validate_file(local_file_path, file_metadata: SubmissionFileMetadata) -> ValidationState:
288
323
  self.__log.debug("Validating '%s'...", str(local_file_path))
289
324
 
290
325
  # validate the file
291
- errors = list(validate_single_end_reads(local_file_path))
326
+ errors = list(validate_single_end_reads(local_file_path, expected_read_length=file_metadata.read_length))
292
327
  validation_passed = len(errors) == 0
293
328
 
294
329
  # return log state
@@ -339,6 +374,7 @@ class Submission:
339
374
  validate_paired_end_reads(
340
375
  local_fastq_r1_path, # fastq R1
341
376
  local_fastq_r2_path, # fastq R2
377
+ expected_read_length=fastq_r1.read_length, # fastq R1 and R2 should have the same read length
342
378
  )
343
379
  )
344
380
  validation_passed = len(errors) == 0
@@ -493,13 +529,26 @@ class EncryptedSubmission:
493
529
 
494
530
  return retval
495
531
 
532
+ _submission_id: str | None = None
533
+
534
+ @property
535
+ def submission_id(self) -> str:
536
+ """ID to refer to an individual submission to a GRZ"""
537
+ if self._submission_id is None:
538
+ # generate a submission ID once
539
+ submitter_id = self.metadata.content.submission.submitter_id
540
+ submission_date = self.metadata.content.submission.submission_date
541
+ # use first 8 characters of SHA256 hash of transaction ID to virtually prevent collisions
542
+ suffix = hashlib.sha256(self.metadata.transaction_id.encode("utf-8")).hexdigest()[:8]
543
+ self._submission_id = f"{submitter_id}_{submission_date}_{suffix}"
544
+
545
+ return self._submission_id
546
+
496
547
  def get_metadata_file_path_and_object_id(self) -> tuple[Path, str]:
497
548
  """
498
549
  :return: tuple with the `local_file_path` and s3_object_id of the metadata file
499
550
  """
500
- return Path(self.metadata.file_path), str(
501
- Path(self.metadata.transaction_id) / "metadata" / self.metadata.file_path.name
502
- )
551
+ return Path(self.metadata.file_path), str(Path(self.submission_id) / "metadata" / self.metadata.file_path.name)
503
552
 
504
553
  def get_encrypted_files_and_object_id(self) -> dict[Path, str]:
505
554
  """
@@ -508,7 +557,7 @@ class EncryptedSubmission:
508
557
  retval = {}
509
558
  for local_file_path, file_metadata in self.encrypted_files.items():
510
559
  retval[local_file_path] = str(
511
- Path(self.metadata.transaction_id) / "files" / self.get_encrypted_file_path(file_metadata.file_path)
560
+ Path(self.submission_id) / "files" / self.get_encrypted_file_path(file_metadata.file_path)
512
561
  )
513
562
  return retval
514
563
 
@@ -788,10 +837,9 @@ class Worker:
788
837
 
789
838
  return submission
790
839
 
791
- def upload(self, config: ConfigModel):
840
+ def upload(self, config: ConfigModel) -> str:
792
841
  """
793
- Upload an encrypted submission
794
-
842
+ Upload an encrypted submission and return the generated submission ID
795
843
  """
796
844
  upload_worker = S3BotoUploadWorker(config, status_file_path=self.progress_file_upload)
797
845
 
@@ -799,6 +847,8 @@ class Worker:
799
847
 
800
848
  upload_worker.upload(encrypted_submission)
801
849
 
850
+ return encrypted_submission.submission_id
851
+
802
852
  def download(self, config: ConfigModel, submission_id: str):
803
853
  """
804
854
  Download an encrypted submission
@@ -9,7 +9,7 @@ from collections.abc import Callable
9
9
  from os import PathLike
10
10
  from pathlib import Path
11
11
 
12
- from grz_pydantic_models.v1_1_1.metadata import File as SubmissionFileMetadata
12
+ from grz_pydantic_models.submission.metadata.v1 import File as SubmissionFileMetadata
13
13
 
14
14
  from .file_operations import read_multiple_json
15
15
  from .states import State
@@ -105,7 +105,8 @@ class FileProgressLogger[T: State]:
105
105
  :param file_path: The file path to query for the state.
106
106
  :param file_metadata: The metadata of the file to query for the state.
107
107
  :param default: Default state to use if the file does not exist.
108
- Can be a Callable that takes the file path and the file metadata as input and returns some state.
108
+ Can be a Callable that takes the file path and the file metadata as input and returns some state:
109
+ `Callable[[Path, SubmissionFileMetadata], T]`.
109
110
 
110
111
  The default state gets automatically saved as the state for this file in case there is no stored state.
111
112
  :return: A dictionary representing the file's state, or None if the file's state isn't logged.
@@ -98,8 +98,11 @@ class S3BotoUploadWorker(UploadWorker):
98
98
  return string
99
99
 
100
100
  # configure proxies if proxy_url is defined
101
- proxy_url = empty_str_to_none(self._config.s3_options.proxy_url)
102
- config = Boto3Config(proxies={"http": proxy_url, "https": proxy_url}) if proxy_url is not None else None
101
+ proxy_url = self._config.s3_options.proxy_url
102
+ config = Boto3Config(
103
+ proxies={"http": str(proxy_url), "https": str(proxy_url)} if proxy_url is not None else None,
104
+ request_checksum_calculation=self._config.s3_options.request_checksum_calculation,
105
+ )
103
106
 
104
107
  # Initialize S3 client for uploading
105
108
  self._s3_client: boto3.session.Session.client = boto3_client(
@@ -130,7 +133,9 @@ class S3BotoUploadWorker(UploadWorker):
130
133
  if filesize / MULTIPART_CHUNKSIZE > MULTIPART_MAX_CHUNKS
131
134
  else MULTIPART_CHUNKSIZE
132
135
  )
133
- self.__log.debug(f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize / chunksize} chunks")
136
+ self.__log.debug(
137
+ f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {math.ceil(filesize / chunksize)} chunk(s)"
138
+ )
134
139
 
135
140
  config = TransferConfig(
136
141
  multipart_threshold=MULTIPART_THRESHOLD,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grz-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
5
5
  Author-email: Koray Kirli <koraykirli@gmail.com>, Mathias Lesche <mathias.lesche@tu-dresden.de>, "Florian R. Hölzlwimmer" <git.ich@frhoelzlwimmer.de>, Till Hartmann <till.hartmann@bih-charite.de>, Thomas Sell <thomas.sell@bih-charite.de>
6
6
  License: MIT
@@ -11,7 +11,7 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
- Requires-Dist: boto3<2,>=1.35.39
14
+ Requires-Dist: boto3<2,>=1.36
15
15
  Requires-Dist: click<9,>=8.1.7
16
16
  Requires-Dist: crypt4gh<2,>=1.7
17
17
  Requires-Dist: jsonschema<5,>=4.23.0
@@ -19,7 +19,8 @@ Requires-Dist: pyyaml<7,>=6.0.2
19
19
  Requires-Dist: tqdm<5,>=4.66.5
20
20
  Requires-Dist: pydantic<2.10,>=2.9.2
21
21
  Requires-Dist: platformdirs<5,>=4.3.6
22
- Requires-Dist: grz-pydantic-models>=1.2.0
22
+ Requires-Dist: grz-pydantic-models>=1.2.1
23
+ Requires-Dist: pysam==0.23.*
23
24
  Provides-Extra: test
24
25
  Requires-Dist: pytest<9,>=8.3.3; extra == "test"
25
26
  Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
@@ -254,6 +255,8 @@ Option is for the usage at a GDC/GRZ.
254
255
 
255
256
  ## Testing
256
257
 
258
+ Please note that binary files used for testing are managed with [Git LFS](https://git-lfs.com), which will be needed to clone them locally with the git repository.
259
+
257
260
  To run the tests, navigate to the root directory of your project and invoke `pytest`.
258
261
  Alternatively, install `uv` and `tox` and run `uv run tox`.
259
262
 
@@ -4,6 +4,7 @@ README.md
4
4
  pyproject.toml
5
5
  src/__init__.py
6
6
  src/grz_cli/__init__.py
7
+ src/grz_cli/bam_validation.py
7
8
  src/grz_cli/cli.py
8
9
  src/grz_cli/constants.py
9
10
  src/grz_cli/download.py
@@ -22,7 +23,7 @@ src/grz_cli.egg-info/requires.txt
22
23
  src/grz_cli.egg-info/top_level.txt
23
24
  src/grz_cli/models/__init__.py
24
25
  src/grz_cli/models/config.py
25
- src/grz_cli/resources/thresholds.json
26
+ tests/test_bam_validation.py
26
27
  tests/test_cli.py
27
28
  tests/test_download.py
28
29
  tests/test_fastq_validation.py
@@ -1,4 +1,4 @@
1
- boto3<2,>=1.35.39
1
+ boto3<2,>=1.36
2
2
  click<9,>=8.1.7
3
3
  crypt4gh<2,>=1.7
4
4
  jsonschema<5,>=4.23.0
@@ -6,7 +6,8 @@ pyyaml<7,>=6.0.2
6
6
  tqdm<5,>=4.66.5
7
7
  pydantic<2.10,>=2.9.2
8
8
  platformdirs<5,>=4.3.6
9
- grz-pydantic-models>=1.2.0
9
+ grz-pydantic-models>=1.2.1
10
+ pysam==0.23.*
10
11
 
11
12
  [test]
12
13
  pytest<9,>=8.3.3
@@ -0,0 +1,15 @@
1
+ """Tests for the bam_validation module."""
2
+
3
+ import importlib.resources
4
+
5
+ from grz_cli.bam_validation import validate_bam
6
+
7
+ from . import resources
8
+
9
+
10
+ def test_valid_hifi_bam():
11
+ """Valid HiFi BAM files should pass validation"""
12
+ bam_ptr = importlib.resources.files(resources).joinpath("reads", "valid_HiFi.bam")
13
+ with importlib.resources.as_file(bam_ptr) as bam_path:
14
+ errors = list(validate_bam(bam_path))
15
+ assert len(errors) == 0
@@ -183,7 +183,6 @@ def test_upload_download_submission(
183
183
  remote_bucket,
184
184
  ):
185
185
  submission_dir = Path("tests/mock_files/submissions/valid_submission")
186
- transaction_id = "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000"
187
186
 
188
187
  shutil.copytree(
189
188
  submission_dir / "encrypted_files",
@@ -200,7 +199,7 @@ def test_upload_download_submission(
200
199
  "--config-file",
201
200
  temp_config_file_path,
202
201
  ]
203
- runner = CliRunner()
202
+ runner = CliRunner(mix_stderr=False)
204
203
  with mock.patch(
205
204
  "grz_cli.models.config.S3Options.__getattr__",
206
205
  lambda self, name: None if name == "endpoint_url" else AttributeError,
@@ -208,6 +207,9 @@ def test_upload_download_submission(
208
207
  result = runner.invoke(grz_cli.cli.cli, testargs, catch_exceptions=False)
209
208
 
210
209
  assert result.exit_code == 0, result.output
210
+ assert len(result.output) != 0, result.stderr
211
+
212
+ submission_id = result.output.strip()
211
213
 
212
214
  # download
213
215
  download_dir = tmpdir_factory.mktemp("submission_download")
@@ -217,7 +219,7 @@ def test_upload_download_submission(
217
219
  testargs = [
218
220
  "download",
219
221
  "--submission-id",
220
- transaction_id,
222
+ submission_id,
221
223
  "--output-dir",
222
224
  str(download_dir_path),
223
225
  "--config-file",
@@ -21,7 +21,7 @@ def test_single_end_inconsistent_read_length():
21
21
  )
22
22
  )
23
23
  assert len(errors) == 1
24
- assert "Inconsistent read lengths" in errors[0]
24
+ assert "Read length mismatch" in errors[0]
25
25
 
26
26
 
27
27
  # Test case 3: Paired end, differing line numbers
@@ -5,7 +5,7 @@ import json
5
5
  from pathlib import Path
6
6
 
7
7
  import pytest
8
- from grz_pydantic_models.v1_1_1.metadata import File as SubmissionFileMetadata
8
+ from grz_pydantic_models.submission.metadata.v1 import File as SubmissionFileMetadata
9
9
 
10
10
  from grz_cli.file_operations import read_multiple_json
11
11
  from grz_cli.progress_logging import FileProgressLogger
@@ -62,6 +62,7 @@ def test_boto_upload(
62
62
 
63
63
 
64
64
  def test__gather_files_to_upload(encrypted_submission):
65
+ submission_id = encrypted_submission.submission_id
65
66
  metadata_file_path, metadata_s3_object_id = encrypted_submission.get_metadata_file_path_and_object_id()
66
67
  gathered_files = encrypted_submission.get_encrypted_files_and_object_id()
67
68
  gathered_files[metadata_file_path] = metadata_s3_object_id
@@ -70,47 +71,47 @@ def test__gather_files_to_upload(encrypted_submission):
70
71
  expected_files = [
71
72
  (
72
73
  "tests/mock_files/submissions/valid_submission/encrypted_files/target_regions.bed.c4gh",
73
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/target_regions.bed.c4gh",
74
+ f"{submission_id}/files/target_regions.bed.c4gh",
74
75
  ),
75
76
  (
76
77
  "tests/mock_files/submissions/valid_submission/encrypted_files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz.c4gh",
77
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz.c4gh",
78
+ f"{submission_id}/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz.c4gh",
78
79
  ),
79
80
  (
80
81
  "tests/mock_files/submissions/valid_submission/encrypted_files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read2.fastq.gz.c4gh",
81
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read2.fastq.gz.c4gh",
82
+ f"{submission_id}/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read2.fastq.gz.c4gh",
82
83
  ),
83
84
  (
84
85
  "tests/mock_files/submissions/valid_submission/encrypted_files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.vcf.c4gh",
85
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.vcf.c4gh",
86
+ f"{submission_id}/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_normal.vcf.c4gh",
86
87
  ),
87
88
  (
88
89
  "tests/mock_files/submissions/valid_submission/encrypted_files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.read1.fastq.gz.c4gh",
89
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.read1.fastq.gz.c4gh",
90
+ f"{submission_id}/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.read1.fastq.gz.c4gh",
90
91
  ),
91
92
  (
92
93
  "tests/mock_files/submissions/valid_submission/encrypted_files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.read2.fastq.gz.c4gh",
93
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.read2.fastq.gz.c4gh",
94
+ f"{submission_id}/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.read2.fastq.gz.c4gh",
94
95
  ),
95
96
  (
96
97
  "tests/mock_files/submissions/valid_submission/encrypted_files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.vcf.c4gh",
97
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.vcf.c4gh",
98
+ f"{submission_id}/files/aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000_blood_tumor.vcf.c4gh",
98
99
  ),
99
100
  (
100
101
  "tests/mock_files/submissions/valid_submission/encrypted_files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.read1.fastq.gz.c4gh",
101
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.read1.fastq.gz.c4gh",
102
+ f"{submission_id}/files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.read1.fastq.gz.c4gh",
102
103
  ),
103
104
  (
104
105
  "tests/mock_files/submissions/valid_submission/encrypted_files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.read2.fastq.gz.c4gh",
105
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.read2.fastq.gz.c4gh",
106
+ f"{submission_id}/files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.read2.fastq.gz.c4gh",
106
107
  ),
107
108
  (
108
109
  "tests/mock_files/submissions/valid_submission/encrypted_files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.vcf.c4gh",
109
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.vcf.c4gh",
110
+ f"{submission_id}/files/bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111bbbbbbbb11111111_blood_normal.vcf.c4gh",
110
111
  ),
111
112
  (
112
113
  "tests/mock_files/submissions/valid_submission/metadata/metadata.json",
113
- "aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000aaaaaaaa00000000/metadata/metadata.json",
114
+ f"{submission_id}/metadata/metadata.json",
114
115
  ),
115
116
  ]
116
117
  expected_files = sorted(expected_files)
grz_cli-0.3.0/MANIFEST.in DELETED
@@ -1,3 +0,0 @@
1
- graft src
2
- include src/grz_cli/resources/thresholds.json
3
- include LICENSE
@@ -1 +0,0 @@
1
- __version__ = "0.3.0"
@@ -1,299 +0,0 @@
1
- [
2
- {
3
- "libraryType": "panel",
4
- "sequenceSubtype": "somatic",
5
- "genomicStudySubtype": "tumor+germline",
6
- "thresholds": {
7
- "meanDepthOfCoverage": 200,
8
- "readLength": 100,
9
- "fractionBasesAboveQualityThreshold": {
10
- "qualityThreshold": 30,
11
- "fractionBasesAbove": 0.85
12
- },
13
- "targetedRegionsAboveMinCoverage": {
14
- "minCoverage": 100,
15
- "fractionAbove": 0.8
16
- }
17
- }
18
- },
19
- {
20
- "libraryType": "panel",
21
- "sequenceSubtype": "somatic",
22
- "genomicStudySubtype": "tumor-only",
23
- "thresholds": {
24
- "meanDepthOfCoverage": 200,
25
- "readLength": 100,
26
- "fractionBasesAboveQualityThreshold": {
27
- "qualityThreshold": 30,
28
- "fractionBasesAbove": 0.85
29
- },
30
- "targetedRegionsAboveMinCoverage": {
31
- "minCoverage": 100,
32
- "fractionAbove": 0.8
33
- }
34
- }
35
- },
36
- {
37
- "libraryType": "wes",
38
- "sequenceSubtype": "somatic",
39
- "genomicStudySubtype": "tumor+germline",
40
- "thresholds": {
41
- "meanDepthOfCoverage": 200,
42
- "readLength": 100,
43
- "fractionBasesAboveQualityThreshold": {
44
- "qualityThreshold": 30,
45
- "fractionBasesAbove": 0.85
46
- },
47
- "targetedRegionsAboveMinCoverage": {
48
- "minCoverage": 100,
49
- "fractionAbove": 0.8
50
- }
51
- }
52
- },
53
- {
54
- "libraryType": "wes",
55
- "sequenceSubtype": "somatic",
56
- "genomicStudySubtype": "tumor-only",
57
- "thresholds": {
58
- "meanDepthOfCoverage": 200,
59
- "readLength": 100,
60
- "fractionBasesAboveQualityThreshold": {
61
- "qualityThreshold": 30,
62
- "fractionBasesAbove": 0.85
63
- },
64
- "targetedRegionsAboveMinCoverage": {
65
- "minCoverage": 100,
66
- "fractionAbove": 0.8
67
- }
68
- }
69
- },
70
- {
71
- "libraryType": "wes",
72
- "sequenceSubtype": "germline",
73
- "genomicStudySubtype": "tumor+germline",
74
- "thresholds": {
75
- "meanDepthOfCoverage": 100,
76
- "readLength": 100,
77
- "fractionBasesAboveQualityThreshold": {
78
- "qualityThreshold": 30,
79
- "fractionBasesAbove": 0.85
80
- },
81
- "targetedRegionsAboveMinCoverage": {
82
- "minCoverage": 30,
83
- "fractionAbove": 0.8
84
- }
85
- }
86
- },
87
- {
88
- "libraryType": "wgs",
89
- "sequenceSubtype": "somatic",
90
- "genomicStudySubtype": "tumor+germline",
91
- "thresholds": {
92
- "meanDepthOfCoverage": 100,
93
- "readLength": 100,
94
- "fractionBasesAboveQualityThreshold": {
95
- "qualityThreshold": 30,
96
- "fractionBasesAbove": 0.85
97
- },
98
- "targetedRegionsAboveMinCoverage": {
99
- "minCoverage": 30,
100
- "fractionAbove": 0.8
101
- }
102
- }
103
- },
104
- {
105
- "libraryType": "wgs",
106
- "sequenceSubtype": "somatic",
107
- "genomicStudySubtype": "tumor-only",
108
- "thresholds": {
109
- "meanDepthOfCoverage": 100,
110
- "readLength": 100,
111
- "fractionBasesAboveQualityThreshold": {
112
- "qualityThreshold": 30,
113
- "fractionBasesAbove": 0.85
114
- },
115
- "targetedRegionsAboveMinCoverage": {
116
- "minCoverage": 30,
117
- "fractionAbove": 0.8
118
- }
119
- }
120
- },
121
- {
122
- "libraryType": "wgs",
123
- "sequenceSubtype": "germline",
124
- "genomicStudySubtype": "tumor+germline",
125
- "thresholds": {
126
- "meanDepthOfCoverage": 30,
127
- "readLength": 100,
128
- "fractionBasesAboveQualityThreshold": {
129
- "qualityThreshold": 30,
130
- "fractionBasesAbove": 0.85
131
- },
132
- "targetedRegionsAboveMinCoverage": {
133
- "minCoverage": 20,
134
- "fractionAbove": 0.8
135
- }
136
- }
137
- },
138
- {
139
- "libraryType": "wgs",
140
- "sequenceSubtype": "germline",
141
- "genomicStudySubtype": "germline-only",
142
- "thresholds": {
143
- "meanDepthOfCoverage": 30,
144
- "readLength": 100,
145
- "fractionBasesAboveQualityThreshold": {
146
- "qualityThreshold": 30,
147
- "fractionBasesAbove": 0.85
148
- },
149
- "targetedRegionsAboveMinCoverage": {
150
- "minCoverage": 20,
151
- "fractionAbove": 0.8
152
- }
153
- }
154
- },
155
- {
156
- "libraryType": "panel_lr",
157
- "sequenceSubtype": "somatic",
158
- "genomicStudySubtype": "tumor+germline",
159
- "thresholds": {
160
- "meanDepthOfCoverage": 200,
161
- "fractionBasesAboveQualityThreshold": {
162
- "qualityThreshold": 20,
163
- "fractionBasesAbove": 0.8
164
- },
165
- "targetedRegionsAboveMinCoverage": {
166
- "minCoverage": 100,
167
- "fractionAbove": 0.8
168
- }
169
- }
170
- },
171
- {
172
- "libraryType": "panel_lr",
173
- "sequenceSubtype": "somatic",
174
- "genomicStudySubtype": "tumor-only",
175
- "thresholds": {
176
- "meanDepthOfCoverage": 200,
177
- "fractionBasesAboveQualityThreshold": {
178
- "qualityThreshold": 20,
179
- "fractionBasesAbove": 0.8
180
- },
181
- "targetedRegionsAboveMinCoverage": {
182
- "minCoverage": 100,
183
- "fractionAbove": 0.8
184
- }
185
- }
186
- },
187
- {
188
- "libraryType": "wes_lr",
189
- "sequenceSubtype": "somatic",
190
- "genomicStudySubtype": "tumor+germline",
191
- "thresholds": {
192
- "meanDepthOfCoverage": 200,
193
- "fractionBasesAboveQualityThreshold": {
194
- "qualityThreshold": 20,
195
- "fractionBasesAbove": 0.8
196
- },
197
- "targetedRegionsAboveMinCoverage": {
198
- "minCoverage": 100,
199
- "fractionAbove": 0.8
200
- }
201
- }
202
- },
203
- {
204
- "libraryType": "wes_lr",
205
- "sequenceSubtype": "somatic",
206
- "genomicStudySubtype": "tumor-only",
207
- "thresholds": {
208
- "meanDepthOfCoverage": 200,
209
- "fractionBasesAboveQualityThreshold": {
210
- "qualityThreshold": 20,
211
- "fractionBasesAbove": 0.8
212
- },
213
- "targetedRegionsAboveMinCoverage": {
214
- "minCoverage": 100,
215
- "fractionAbove": 0.8
216
- }
217
- }
218
- },
219
- {
220
- "libraryType": "wes_lr",
221
- "sequenceSubtype": "germline",
222
- "genomicStudySubtype": "tumor+germline",
223
- "thresholds": {
224
- "meanDepthOfCoverage": 100,
225
- "fractionBasesAboveQualityThreshold": {
226
- "qualityThreshold": 20,
227
- "fractionBasesAbove": 0.8
228
- },
229
- "targetedRegionsAboveMinCoverage": {
230
- "minCoverage": 30,
231
- "fractionAbove": 0.8
232
- }
233
- }
234
- },
235
- {
236
- "libraryType": "wgs_lr",
237
- "sequenceSubtype": "somatic",
238
- "genomicStudySubtype": "tumor+germline",
239
- "thresholds": {
240
- "meanDepthOfCoverage": 100,
241
- "fractionBasesAboveQualityThreshold": {
242
- "qualityThreshold": 20,
243
- "fractionBasesAbove": 0.8
244
- },
245
- "targetedRegionsAboveMinCoverage": {
246
- "minCoverage": 30,
247
- "fractionAbove": 0.8
248
- }
249
- }
250
- },
251
- {
252
- "libraryType": "wgs_lr",
253
- "sequenceSubtype": "somatic",
254
- "genomicStudySubtype": "tumor-only",
255
- "thresholds": {
256
- "meanDepthOfCoverage": 100,
257
- "fractionBasesAboveQualityThreshold": {
258
- "qualityThreshold": 20,
259
- "fractionBasesAbove": 0.8
260
- },
261
- "targetedRegionsAboveMinCoverage": {
262
- "minCoverage": 30,
263
- "fractionAbove": 0.8
264
- }
265
- }
266
- },
267
- {
268
- "libraryType": "wgs_lr",
269
- "sequenceSubtype": "germline",
270
- "genomicStudySubtype": "tumor+germline",
271
- "thresholds": {
272
- "meanDepthOfCoverage": 30,
273
- "fractionBasesAboveQualityThreshold": {
274
- "qualityThreshold": 20,
275
- "fractionBasesAbove": 0.8
276
- },
277
- "targetedRegionsAboveMinCoverage": {
278
- "minCoverage": 20,
279
- "fractionAbove": 0.8
280
- }
281
- }
282
- },
283
- {
284
- "libraryType": "wgs_lr",
285
- "sequenceSubtype": "germline",
286
- "genomicStudySubtype": "germline-only",
287
- "thresholds": {
288
- "meanDepthOfCoverage": 30,
289
- "fractionBasesAboveQualityThreshold": {
290
- "qualityThreshold": 20,
291
- "fractionBasesAbove": 0.8
292
- },
293
- "targetedRegionsAboveMinCoverage": {
294
- "minCoverage": 20,
295
- "fractionAbove": 0.8
296
- }
297
- }
298
- }
299
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes