grz-cli 0.1.0__tar.gz → 0.1.2__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.
- {grz_cli-0.1.0/src/grz_cli.egg-info → grz_cli-0.1.2}/PKG-INFO +63 -32
- {grz_cli-0.1.0 → grz_cli-0.1.2}/README.md +47 -16
- {grz_cli-0.1.0 → grz_cli-0.1.2}/pyproject.toml +17 -16
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/download.py +6 -3
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/fastq_validation.py +38 -9
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/v1_0_0/metadata.py +7 -3
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/parser.py +154 -80
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/progress_logging.py +28 -8
- grz_cli-0.1.2/src/grz_cli/states.py +53 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/upload.py +6 -3
- {grz_cli-0.1.0 → grz_cli-0.1.2/src/grz_cli.egg-info}/PKG-INFO +63 -32
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/SOURCES.txt +1 -0
- grz_cli-0.1.2/src/grz_cli.egg-info/requires.txt +17 -0
- grz_cli-0.1.0/src/grz_cli.egg-info/requires.txt +0 -17
- {grz_cli-0.1.0 → grz_cli-0.1.2}/LICENSE +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/MANIFEST.in +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/setup.cfg +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/__init__.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/__init__.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/cli.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/constants.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/file_operations.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/logging_setup.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/config.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/v1_0_0/__init__.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/resources/grz-schema.json +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/dependency_links.txt +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/entry_points.txt +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/top_level.txt +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_cli.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_download.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_fastq_validation.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_file_operations.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_file_progress_logging.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_parser.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_read_multiple_json.py +0 -0
- {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_upload.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -12,22 +12,22 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: boto3
|
|
16
|
-
Requires-Dist: click
|
|
17
|
-
Requires-Dist: crypt4gh
|
|
18
|
-
Requires-Dist: jsonschema
|
|
19
|
-
Requires-Dist: pyyaml
|
|
20
|
-
Requires-Dist: tqdm
|
|
21
|
-
Requires-Dist: pydantic
|
|
22
|
-
Requires-Dist: platformdirs
|
|
15
|
+
Requires-Dist: boto3<2,>=1.35.39
|
|
16
|
+
Requires-Dist: click<9,>=8.1.7
|
|
17
|
+
Requires-Dist: crypt4gh<2,>=1.7
|
|
18
|
+
Requires-Dist: jsonschema<5,>=4.23.0
|
|
19
|
+
Requires-Dist: pyyaml<7,>=6.0.2
|
|
20
|
+
Requires-Dist: tqdm<5,>=4.66.5
|
|
21
|
+
Requires-Dist: pydantic<2.10,>=2.9.2
|
|
22
|
+
Requires-Dist: platformdirs<5,>=4.3.6
|
|
23
23
|
Provides-Extra: test
|
|
24
|
-
Requires-Dist: pytest
|
|
25
|
-
Requires-Dist: pytest-cov
|
|
26
|
-
Requires-Dist: pytest-mock
|
|
27
|
-
Requires-Dist: moto[s3]
|
|
28
|
-
Requires-Dist: pytest-sugar
|
|
29
|
-
Requires-Dist: ruff
|
|
30
|
-
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: pytest<9,>=8.3.3; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-mock<4,>=3.14.0; extra == "test"
|
|
27
|
+
Requires-Dist: moto[s3]<6,>=5.0.16; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-sugar<2,>=1.0.0; extra == "test"
|
|
29
|
+
Requires-Dist: ruff<1,>=0.8.1; extra == "test"
|
|
30
|
+
Requires-Dist: numpy<3,>=2.1.2; extra == "test"
|
|
31
31
|
|
|
32
32
|
# GRZ CLI
|
|
33
33
|
|
|
@@ -38,6 +38,11 @@ A command-line tool for validating, encrypting, uploading and downloading submis
|
|
|
38
38
|
- [Introduction](#introduction)
|
|
39
39
|
- [Features](#features)
|
|
40
40
|
- [Installation](#installation)
|
|
41
|
+
- [Requirements](#requirements)
|
|
42
|
+
- [End-user setup](#end-user)
|
|
43
|
+
- [Installation via `conda` (recommended)](#installation-via-conda-recommended)
|
|
44
|
+
- [Installation via `pip` (not recommended)](#installation-via-pip-not-recommended)
|
|
45
|
+
- [Development setup](#development-setup)
|
|
41
46
|
- [Usage](#usage)
|
|
42
47
|
- [Configuration](#configuration)
|
|
43
48
|
- [Exemplary submission procedure](#exemplary-submission-procedure)
|
|
@@ -77,35 +82,61 @@ The current version of the tool requires the `working_dir` to have at least as m
|
|
|
77
82
|
- **Download**: Download encrypted files from a GRZ (via built-in `boto3`).
|
|
78
83
|
- **Logging**: Log progress and results of operations
|
|
79
84
|
|
|
85
|
+
|
|
80
86
|
## Installation
|
|
81
87
|
|
|
82
|
-
###
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
### Requirements
|
|
89
|
+
Beside of the disk space requirements for the submission data, this tool also requires a linux environment, e.g.:
|
|
90
|
+
- Linux server
|
|
91
|
+
- Virtual machine running linux
|
|
92
|
+
- Docker container
|
|
93
|
+
- Windows subsystem for linux
|
|
94
|
+
- ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### End-user setup
|
|
98
|
+
The recommended method to install this tool is using the conda package manager.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#### Installation via `conda` (recommended)
|
|
102
|
+
|
|
103
|
+
If `conda` is not yet available on your system, we recommend to install the [Miniforge conda distribution](https://github.com/conda-forge/miniforge) by running the following commands:
|
|
85
104
|
```bash
|
|
86
105
|
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
|
|
87
106
|
bash Miniforge3-$(uname)-$(uname -m).sh
|
|
88
107
|
```
|
|
108
|
+
There are also alternative ways to install conda:
|
|
109
|
+
- [Micromamba, a single executable that does not require a base environment](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html)
|
|
110
|
+
- [Official installation instructions](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
|
111
|
+
|
|
89
112
|
Next, install the `grz-cli` tool:
|
|
90
113
|
```bash
|
|
91
|
-
# TODO update: This requires membership in the repository
|
|
92
|
-
# download environment.yaml from the git repository:
|
|
93
|
-
git archive --remote=ssh://git@codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli.git dev environment.yaml | tar -xv
|
|
94
|
-
# TODO update: This will work once the repository is publicly accessible:
|
|
95
|
-
# download environment.yaml from the git repository:
|
|
96
|
-
curl -L "https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli/-/raw/v0.1.0/environment.yaml?ref_type=heads" > environment.yaml
|
|
97
|
-
|
|
98
114
|
# create conda environment and activate it
|
|
99
|
-
conda
|
|
115
|
+
conda create -n grz-tools -c conda-forge -c bioconda "grz-cli"
|
|
100
116
|
conda activate grz-tools
|
|
117
|
+
```
|
|
101
118
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
pip install "git+https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli@v0.1.0"
|
|
119
|
+
##### Update instructions:
|
|
120
|
+
Use the following command to update the tool:
|
|
121
|
+
```bash
|
|
122
|
+
conda update -n grz-tools "grz-cli"
|
|
107
123
|
```
|
|
108
124
|
|
|
125
|
+
|
|
126
|
+
#### Installation via `pip` (not recommended)
|
|
127
|
+
While installation via `pip` is possible, it is not recommended because users must ensure
|
|
128
|
+
that the correct Python version is already installed and that they are using a virtual python environment.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install grz-cli
|
|
132
|
+
```
|
|
133
|
+
##### Update instructions:
|
|
134
|
+
Use the following command to update the tool:
|
|
135
|
+
```bash
|
|
136
|
+
pip upgrade grz-cli
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
|
|
109
140
|
### Development setup
|
|
110
141
|
For development purposes, you can clone the repository and install the package in editable mode:
|
|
111
142
|
|
|
@@ -7,6 +7,11 @@ A command-line tool for validating, encrypting, uploading and downloading submis
|
|
|
7
7
|
- [Introduction](#introduction)
|
|
8
8
|
- [Features](#features)
|
|
9
9
|
- [Installation](#installation)
|
|
10
|
+
- [Requirements](#requirements)
|
|
11
|
+
- [End-user setup](#end-user)
|
|
12
|
+
- [Installation via `conda` (recommended)](#installation-via-conda-recommended)
|
|
13
|
+
- [Installation via `pip` (not recommended)](#installation-via-pip-not-recommended)
|
|
14
|
+
- [Development setup](#development-setup)
|
|
10
15
|
- [Usage](#usage)
|
|
11
16
|
- [Configuration](#configuration)
|
|
12
17
|
- [Exemplary submission procedure](#exemplary-submission-procedure)
|
|
@@ -46,35 +51,61 @@ The current version of the tool requires the `working_dir` to have at least as m
|
|
|
46
51
|
- **Download**: Download encrypted files from a GRZ (via built-in `boto3`).
|
|
47
52
|
- **Logging**: Log progress and results of operations
|
|
48
53
|
|
|
54
|
+
|
|
49
55
|
## Installation
|
|
50
56
|
|
|
51
|
-
###
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
### Requirements
|
|
58
|
+
Beside of the disk space requirements for the submission data, this tool also requires a linux environment, e.g.:
|
|
59
|
+
- Linux server
|
|
60
|
+
- Virtual machine running linux
|
|
61
|
+
- Docker container
|
|
62
|
+
- Windows subsystem for linux
|
|
63
|
+
- ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
### End-user setup
|
|
67
|
+
The recommended method to install this tool is using the conda package manager.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#### Installation via `conda` (recommended)
|
|
71
|
+
|
|
72
|
+
If `conda` is not yet available on your system, we recommend to install the [Miniforge conda distribution](https://github.com/conda-forge/miniforge) by running the following commands:
|
|
54
73
|
```bash
|
|
55
74
|
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
|
|
56
75
|
bash Miniforge3-$(uname)-$(uname -m).sh
|
|
57
76
|
```
|
|
77
|
+
There are also alternative ways to install conda:
|
|
78
|
+
- [Micromamba, a single executable that does not require a base environment](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html)
|
|
79
|
+
- [Official installation instructions](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
|
80
|
+
|
|
58
81
|
Next, install the `grz-cli` tool:
|
|
59
82
|
```bash
|
|
60
|
-
# TODO update: This requires membership in the repository
|
|
61
|
-
# download environment.yaml from the git repository:
|
|
62
|
-
git archive --remote=ssh://git@codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli.git dev environment.yaml | tar -xv
|
|
63
|
-
# TODO update: This will work once the repository is publicly accessible:
|
|
64
|
-
# download environment.yaml from the git repository:
|
|
65
|
-
curl -L "https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli/-/raw/v0.1.0/environment.yaml?ref_type=heads" > environment.yaml
|
|
66
|
-
|
|
67
83
|
# create conda environment and activate it
|
|
68
|
-
conda
|
|
84
|
+
conda create -n grz-tools -c conda-forge -c bioconda "grz-cli"
|
|
69
85
|
conda activate grz-tools
|
|
86
|
+
```
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
pip install "git+https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli@v0.1.0"
|
|
88
|
+
##### Update instructions:
|
|
89
|
+
Use the following command to update the tool:
|
|
90
|
+
```bash
|
|
91
|
+
conda update -n grz-tools "grz-cli"
|
|
76
92
|
```
|
|
77
93
|
|
|
94
|
+
|
|
95
|
+
#### Installation via `pip` (not recommended)
|
|
96
|
+
While installation via `pip` is possible, it is not recommended because users must ensure
|
|
97
|
+
that the correct Python version is already installed and that they are using a virtual python environment.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install grz-cli
|
|
101
|
+
```
|
|
102
|
+
##### Update instructions:
|
|
103
|
+
Use the following command to update the tool:
|
|
104
|
+
```bash
|
|
105
|
+
pip upgrade grz-cli
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
|
|
78
109
|
### Development setup
|
|
79
110
|
For development purposes, you can clone the repository and install the package in editable mode:
|
|
80
111
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "grz-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Tool for validation, encryption and upload of MV submissions to GDCs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -16,14 +16,14 @@ authors = [
|
|
|
16
16
|
{ name = "Thomas Sell", email = "thomas.sell@bih-charite.de" },
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
-
"boto3
|
|
20
|
-
"click
|
|
21
|
-
"crypt4gh
|
|
22
|
-
"jsonschema
|
|
23
|
-
"pyyaml
|
|
24
|
-
"tqdm
|
|
25
|
-
"pydantic
|
|
26
|
-
"platformdirs
|
|
19
|
+
"boto3 >=1.35.39,<2",
|
|
20
|
+
"click >=8.1.7,<9",
|
|
21
|
+
"crypt4gh >=1.7,<2",
|
|
22
|
+
"jsonschema >=4.23.0,<5",
|
|
23
|
+
"pyyaml >=6.0.2,<7",
|
|
24
|
+
"tqdm >=4.66.5,<5",
|
|
25
|
+
"pydantic >=2.9.2,<2.10",
|
|
26
|
+
"platformdirs >=4.3.6,<5",
|
|
27
27
|
]
|
|
28
28
|
classifiers = [
|
|
29
29
|
"Programming Language :: Python :: 3",
|
|
@@ -41,13 +41,13 @@ grz-cli = "grz_cli.cli:cli"
|
|
|
41
41
|
|
|
42
42
|
[project.optional-dependencies]
|
|
43
43
|
test = [
|
|
44
|
-
"pytest
|
|
45
|
-
"pytest-cov
|
|
46
|
-
"pytest-mock
|
|
47
|
-
"moto[s3]
|
|
48
|
-
"pytest-sugar
|
|
49
|
-
"ruff
|
|
50
|
-
"numpy
|
|
44
|
+
"pytest >=8.3.3,<9",
|
|
45
|
+
"pytest-cov >=5.0.0,<6",
|
|
46
|
+
"pytest-mock >=3.14.0,<4",
|
|
47
|
+
"moto[s3] >=5.0.16,<6",
|
|
48
|
+
"pytest-sugar >=1.0.0,<2",
|
|
49
|
+
"ruff >=0.8.1,<1",
|
|
50
|
+
"numpy >=2.1.2,<3"
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
[dependency-groups]
|
|
@@ -177,6 +177,7 @@ commands = [["mypy"]]
|
|
|
177
177
|
dev-dependencies = [
|
|
178
178
|
"datamodel-code-generator>=0.26.2",
|
|
179
179
|
"mypy>=1.13.0",
|
|
180
|
+
"ruff>=0.8.1",
|
|
180
181
|
"tox>=4.23.2",
|
|
181
182
|
"types-pyyaml>=6.0.12.20240917",
|
|
182
183
|
"types-tqdm>=4.66.0.20240417",
|
|
@@ -18,6 +18,7 @@ from tqdm.auto import tqdm
|
|
|
18
18
|
|
|
19
19
|
from .models.config import ConfigModel
|
|
20
20
|
from .progress_logging import FileProgressLogger
|
|
21
|
+
from .states import DownloadState
|
|
21
22
|
|
|
22
23
|
MULTIPART_THRESHOLD = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
23
24
|
MULTIPART_CHUNKSIZE = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
@@ -174,7 +175,7 @@ class S3BotoDownloadWorker:
|
|
|
174
175
|
Upload an encrypted submission
|
|
175
176
|
:param encrypted_submission: The encrypted submission to upload
|
|
176
177
|
"""
|
|
177
|
-
progress_logger = FileProgressLogger(self._status_file_path)
|
|
178
|
+
progress_logger = FileProgressLogger[DownloadState](self._status_file_path)
|
|
178
179
|
|
|
179
180
|
encrypted_files_key_prefix = f"{submission_id}/files/"
|
|
180
181
|
|
|
@@ -216,7 +217,7 @@ class S3BotoDownloadWorker:
|
|
|
216
217
|
progress_logger.set_state(
|
|
217
218
|
full_path,
|
|
218
219
|
file_metadata,
|
|
219
|
-
state=
|
|
220
|
+
state=DownloadState(download_successful=True),
|
|
220
221
|
)
|
|
221
222
|
except Exception as e:
|
|
222
223
|
self.__log.error("Download failed for '%s'", str(full_path))
|
|
@@ -224,7 +225,9 @@ class S3BotoDownloadWorker:
|
|
|
224
225
|
progress_logger.set_state(
|
|
225
226
|
full_path,
|
|
226
227
|
file_metadata,
|
|
227
|
-
state=
|
|
228
|
+
state=DownloadState(
|
|
229
|
+
download_successful=False, errors=[str(e)]
|
|
230
|
+
),
|
|
228
231
|
)
|
|
229
232
|
|
|
230
233
|
raise e
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import gzip
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
6
|
+
import typing
|
|
5
7
|
from collections.abc import Generator
|
|
6
8
|
from contextlib import contextmanager
|
|
9
|
+
from gzip import GzipFile
|
|
10
|
+
from io import RawIOBase
|
|
7
11
|
from os import PathLike
|
|
12
|
+
from typing import TextIO
|
|
13
|
+
|
|
14
|
+
from tqdm.auto import tqdm
|
|
15
|
+
|
|
16
|
+
from .file_operations import TqdmIOWrapper
|
|
8
17
|
|
|
9
18
|
log = logging.getLogger(__name__)
|
|
10
19
|
|
|
@@ -20,21 +29,41 @@ def is_gzipped(file_path: str | PathLike) -> bool:
|
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
@contextmanager
|
|
23
|
-
def open_fastq(
|
|
32
|
+
def open_fastq(
|
|
33
|
+
file_path: str | PathLike, progress=True
|
|
34
|
+
) -> Generator[TextIO, None, None]:
|
|
24
35
|
"""
|
|
25
36
|
Open a FASTQ file, handling both regular and gzipped formats.
|
|
26
37
|
|
|
27
38
|
:param file_path: Path to the FASTQ file
|
|
39
|
+
:param progress: Whether to show a progress bar
|
|
28
40
|
:return: A file object opened in the appropriate mode (gzipped or plain text)
|
|
29
41
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
handle: TqdmIOWrapper | GzipFile | typing.BinaryIO | None = None
|
|
43
|
+
with open(file_path, "rb") as fd:
|
|
44
|
+
# Open FASTQ
|
|
45
|
+
total_size = os.stat(file_path).st_size
|
|
46
|
+
if progress:
|
|
47
|
+
handle = TqdmIOWrapper(
|
|
48
|
+
typing.cast(RawIOBase, fd),
|
|
49
|
+
tqdm(
|
|
50
|
+
total=total_size,
|
|
51
|
+
desc="Reading FASTQ: ",
|
|
52
|
+
unit="B",
|
|
53
|
+
unit_scale=True,
|
|
54
|
+
# unit_divisor=1024, # make use of standard units e.g. KB, MB, etc.
|
|
55
|
+
miniters=1,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
handle = fd
|
|
60
|
+
|
|
61
|
+
if is_gzipped(file_path):
|
|
62
|
+
# decompress
|
|
63
|
+
with gzip.open(typing.cast(RawIOBase, handle), "rb") as decompressed_fd:
|
|
64
|
+
yield typing.cast(TextIO, decompressed_fd)
|
|
65
|
+
else:
|
|
66
|
+
yield typing.cast(TextIO, handle)
|
|
38
67
|
|
|
39
68
|
|
|
40
69
|
def calculate_fastq_stats(file_path) -> tuple[int, set[int]]:
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import logging
|
|
19
20
|
from collections.abc import Generator
|
|
20
21
|
from datetime import date
|
|
21
22
|
from enum import StrEnum
|
|
@@ -33,6 +34,8 @@ from pydantic.alias_generators import to_camel
|
|
|
33
34
|
|
|
34
35
|
from grz_cli.file_operations import calculate_sha256 # type: ignore
|
|
35
36
|
|
|
37
|
+
log = logging.getLogger(__name__)
|
|
38
|
+
|
|
36
39
|
|
|
37
40
|
class StrictBaseModel(BaseModel):
|
|
38
41
|
model_config = ConfigDict(
|
|
@@ -831,10 +834,11 @@ class GrzSubmissionMetadata(StrictBaseModel):
|
|
|
831
834
|
threshold = thresholds.get(
|
|
832
835
|
(lab_datum.library_type, lab_datum.sequence_subtype)
|
|
833
836
|
)
|
|
834
|
-
if
|
|
835
|
-
|
|
836
|
-
f"Threshold for {lab_datum.library_type} {lab_datum.sequence_subtype} not found
|
|
837
|
+
if threshold is None:
|
|
838
|
+
log.warning(
|
|
839
|
+
f"Threshold for {lab_datum.library_type} - {lab_datum.sequence_subtype} not found!"
|
|
837
840
|
)
|
|
841
|
+
|
|
838
842
|
sequence_data = lab_datum.sequence_data
|
|
839
843
|
if sequence_data.mean_depth_of_coverage < threshold:
|
|
840
844
|
raise ValueError(
|
|
@@ -13,15 +13,17 @@ from .download import S3BotoDownloadWorker
|
|
|
13
13
|
from .fastq_validation import validate_paired_end_reads, validate_single_end_reads
|
|
14
14
|
from .file_operations import Crypt4GH, calculate_sha256
|
|
15
15
|
from .models.config import ConfigModel
|
|
16
|
-
from .models.v1_0_0.metadata import File as SubmissionFileMetadata
|
|
17
16
|
from .models.v1_0_0.metadata import (
|
|
17
|
+
File,
|
|
18
18
|
FileType,
|
|
19
19
|
GrzSubmissionMetadata,
|
|
20
20
|
ReadOrder,
|
|
21
21
|
SequenceData,
|
|
22
|
-
SequenceType,
|
|
23
22
|
SequencingLayout,
|
|
24
23
|
)
|
|
24
|
+
from .models.v1_0_0.metadata import File as SubmissionFileMetadata
|
|
25
|
+
from .progress_logging import FileProgressLogger
|
|
26
|
+
from .states import DecryptionState, EncryptionState, ValidationState
|
|
25
27
|
from .upload import S3BotoUploadWorker
|
|
26
28
|
|
|
27
29
|
log = logging.getLogger(__name__)
|
|
@@ -168,9 +170,9 @@ class Submission:
|
|
|
168
170
|
|
|
169
171
|
:return: Generator of errors
|
|
170
172
|
"""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
progress_logger = FileProgressLogger[ValidationState](
|
|
174
|
+
log_file_path=progress_log_file
|
|
175
|
+
)
|
|
174
176
|
# cleanup log file and keep only files listed here
|
|
175
177
|
progress_logger.cleanup(
|
|
176
178
|
keep=[
|
|
@@ -182,43 +184,25 @@ class Submission:
|
|
|
182
184
|
# - "errors": List[str]
|
|
183
185
|
# - "validation_passed": bool
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
logged_state = progress_logger.get_state(local_file_path, file_metadata)
|
|
187
|
-
|
|
188
|
-
# determine if we can skip the verification
|
|
189
|
-
if logged_state is None:
|
|
190
|
-
self.__log.debug("State for %s not calculated yet", local_file_path)
|
|
191
|
-
elif not logged_state.get("validation_passed", False):
|
|
192
|
-
errors = logged_state.get("errors", [])
|
|
193
|
-
yield from errors
|
|
194
|
-
|
|
195
|
-
# skip re-verification
|
|
196
|
-
continue
|
|
197
|
-
else:
|
|
198
|
-
self.__log.debug(
|
|
199
|
-
"Validation for %s already passed, skipping...",
|
|
200
|
-
str(local_file_path),
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
# skip re-verification
|
|
204
|
-
continue
|
|
205
|
-
|
|
187
|
+
def validate_file(local_file_path, file_metadata):
|
|
206
188
|
self.__log.debug("Validating '%s'...", str(local_file_path))
|
|
189
|
+
|
|
207
190
|
# validate the file
|
|
208
191
|
errors = list(file_metadata.validate_data(local_file_path))
|
|
209
192
|
validation_passed = len(errors) == 0
|
|
210
193
|
|
|
211
|
-
# log state
|
|
212
|
-
|
|
194
|
+
# return log state
|
|
195
|
+
return ValidationState(errors=errors, validation_passed=validation_passed)
|
|
196
|
+
|
|
197
|
+
for local_file_path, file_metadata in self.files.items():
|
|
198
|
+
logged_state = progress_logger.get_state(
|
|
213
199
|
local_file_path,
|
|
214
200
|
file_metadata,
|
|
215
|
-
state
|
|
216
|
-
"errors": errors,
|
|
217
|
-
"validation_passed": validation_passed,
|
|
218
|
-
},
|
|
201
|
+
default=validate_file, # validate the file if the state was not calculated yet
|
|
219
202
|
)
|
|
220
203
|
|
|
221
|
-
|
|
204
|
+
if logged_state:
|
|
205
|
+
yield from logged_state["errors"]
|
|
222
206
|
|
|
223
207
|
def validate_sequencing_data(
|
|
224
208
|
self, progress_log_file: str | PathLike
|
|
@@ -228,51 +212,131 @@ class Submission:
|
|
|
228
212
|
|
|
229
213
|
:return: Generator of errors
|
|
230
214
|
"""
|
|
215
|
+
from .progress_logging import FileProgressLogger
|
|
231
216
|
|
|
232
|
-
|
|
217
|
+
progress_logger = FileProgressLogger[ValidationState](
|
|
218
|
+
log_file_path=progress_log_file
|
|
219
|
+
)
|
|
220
|
+
# cleanup log file and keep only files listed here
|
|
221
|
+
progress_logger.cleanup(
|
|
222
|
+
keep=[
|
|
223
|
+
(file_path, file_metadata)
|
|
224
|
+
for file_path, file_metadata in self.files.items()
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
# fields:
|
|
228
|
+
# - "errors": List[str]
|
|
229
|
+
# - "validation_passed": bool
|
|
230
|
+
|
|
231
|
+
def find_fastq_files(sequence_data: SequenceData) -> list[File]:
|
|
233
232
|
return [f for f in sequence_data.files if f.file_type == FileType.fastq]
|
|
234
233
|
|
|
235
234
|
for donor in self.metadata.content.donors:
|
|
236
235
|
for lab_data in donor.lab_data:
|
|
237
|
-
sequence_type = lab_data.sequence_type
|
|
238
236
|
sequencing_layout = lab_data.sequencing_layout
|
|
239
237
|
sequence_data = lab_data.sequence_data
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
# find all FASTQ files
|
|
239
|
+
fastq_files = find_fastq_files(sequence_data)
|
|
240
|
+
|
|
241
|
+
match sequencing_layout:
|
|
242
|
+
case (
|
|
243
|
+
SequencingLayout.single_end
|
|
244
|
+
| SequencingLayout.reverse
|
|
245
|
+
| SequencingLayout.other
|
|
246
|
+
):
|
|
247
|
+
yield from self._validate_single_end(
|
|
248
|
+
fastq_files, progress_logger
|
|
249
|
+
)
|
|
243
250
|
|
|
244
|
-
|
|
245
|
-
yield from
|
|
246
|
-
|
|
251
|
+
case SequencingLayout.paired_end:
|
|
252
|
+
yield from self._validate_paired_end(
|
|
253
|
+
fastq_files, progress_logger
|
|
247
254
|
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
255
|
+
|
|
256
|
+
def _validate_single_end(
|
|
257
|
+
self,
|
|
258
|
+
fastq_files: list[File],
|
|
259
|
+
progress_logger: FileProgressLogger[ValidationState],
|
|
260
|
+
) -> Generator[str, None, None]:
|
|
261
|
+
def validate_file(local_file_path, _file_metadata) -> ValidationState:
|
|
262
|
+
self.__log.debug("Validating '%s'...", str(local_file_path))
|
|
263
|
+
|
|
264
|
+
# validate the file
|
|
265
|
+
errors = list(validate_single_end_reads(local_file_path))
|
|
266
|
+
validation_passed = len(errors) == 0
|
|
267
|
+
|
|
268
|
+
# return log state
|
|
269
|
+
return ValidationState(
|
|
270
|
+
errors=errors,
|
|
271
|
+
validation_passed=validation_passed,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
for fastq_file in fastq_files:
|
|
275
|
+
logged_state = progress_logger.get_state(
|
|
276
|
+
self.files_dir / fastq_file.file_path,
|
|
277
|
+
fastq_file,
|
|
278
|
+
default=validate_file, # validate the file if the state was not calculated yet
|
|
279
|
+
)
|
|
280
|
+
if logged_state:
|
|
281
|
+
yield from logged_state["errors"]
|
|
282
|
+
|
|
283
|
+
def _validate_paired_end(
|
|
284
|
+
self,
|
|
285
|
+
fastq_files: list[File],
|
|
286
|
+
progress_logger: FileProgressLogger[ValidationState],
|
|
287
|
+
) -> Generator[str, None, None]:
|
|
288
|
+
key = lambda f: (f.flowcell_id, f.lane_id)
|
|
289
|
+
fastq_files.sort(key=key)
|
|
290
|
+
for _key, group in groupby(fastq_files, key):
|
|
291
|
+
files = list(group)
|
|
292
|
+
|
|
293
|
+
# separate R1 and R2 files
|
|
294
|
+
fastq_r1_files = [f for f in files if f.read_order == ReadOrder.r1]
|
|
295
|
+
fastq_r2_files = [f for f in files if f.read_order == ReadOrder.r2]
|
|
296
|
+
|
|
297
|
+
for fastq_r1, fastq_r2 in zip(fastq_r1_files, fastq_r2_files, strict=True):
|
|
298
|
+
local_fastq_r1_path = self.files_dir / fastq_r1.file_path
|
|
299
|
+
local_fastq_r2_path = self.files_dir / fastq_r2.file_path
|
|
300
|
+
|
|
301
|
+
# get saved state
|
|
302
|
+
logged_state_r1 = progress_logger.get_state(
|
|
303
|
+
local_fastq_r1_path,
|
|
304
|
+
fastq_r1,
|
|
305
|
+
)
|
|
306
|
+
logged_state_r2 = progress_logger.get_state(
|
|
307
|
+
local_fastq_r2_path,
|
|
308
|
+
fastq_r2,
|
|
309
|
+
)
|
|
310
|
+
if (
|
|
311
|
+
logged_state_r1 is None
|
|
312
|
+
or logged_state_r2 is None
|
|
313
|
+
or logged_state_r1 != logged_state_r2
|
|
314
|
+
):
|
|
315
|
+
# calculate state
|
|
316
|
+
errors = list(
|
|
317
|
+
validate_paired_end_reads(
|
|
318
|
+
local_fastq_r1_path, # fastq R1
|
|
319
|
+
local_fastq_r2_path, # fastq R2
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
validation_passed = len(errors) == 0
|
|
323
|
+
|
|
324
|
+
state = ValidationState(
|
|
325
|
+
errors=errors,
|
|
326
|
+
validation_passed=validation_passed,
|
|
327
|
+
)
|
|
328
|
+
# update state for both files
|
|
329
|
+
progress_logger.set_state( # fastq R1
|
|
330
|
+
local_fastq_r1_path, fastq_r1, state
|
|
331
|
+
)
|
|
332
|
+
progress_logger.set_state( # fastq R2
|
|
333
|
+
local_fastq_r2_path, fastq_r2, state
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
yield from state["errors"]
|
|
337
|
+
else:
|
|
338
|
+
# both fastq states are equal, so simply yield one of them
|
|
339
|
+
yield from logged_state_r1["errors"]
|
|
276
340
|
|
|
277
341
|
def encrypt(
|
|
278
342
|
self,
|
|
@@ -312,7 +376,9 @@ class Submission:
|
|
|
312
376
|
|
|
313
377
|
from .progress_logging import FileProgressLogger
|
|
314
378
|
|
|
315
|
-
progress_logger = FileProgressLogger(
|
|
379
|
+
progress_logger = FileProgressLogger[EncryptionState](
|
|
380
|
+
log_file_path=progress_log_file
|
|
381
|
+
)
|
|
316
382
|
|
|
317
383
|
try:
|
|
318
384
|
public_keys = Crypt4GH.prepare_c4gh_keys(recipient_public_key_path)
|
|
@@ -347,7 +413,9 @@ class Submission:
|
|
|
347
413
|
|
|
348
414
|
self.__log.info(f"Encryption complete for {str(file_path)}. ")
|
|
349
415
|
progress_logger.set_state(
|
|
350
|
-
file_path,
|
|
416
|
+
file_path,
|
|
417
|
+
file_metadata,
|
|
418
|
+
state=EncryptionState(encryption_successful=True),
|
|
351
419
|
)
|
|
352
420
|
except Exception as e:
|
|
353
421
|
self.__log.error("Encryption failed for '%s'", str(file_path))
|
|
@@ -355,7 +423,9 @@ class Submission:
|
|
|
355
423
|
progress_logger.set_state(
|
|
356
424
|
file_path,
|
|
357
425
|
file_metadata,
|
|
358
|
-
state=
|
|
426
|
+
state=EncryptionState(
|
|
427
|
+
encryption_successful=False, errors=[str(e)]
|
|
428
|
+
),
|
|
359
429
|
)
|
|
360
430
|
|
|
361
431
|
raise e
|
|
@@ -476,7 +546,9 @@ class EncryptedSubmission:
|
|
|
476
546
|
|
|
477
547
|
from .progress_logging import FileProgressLogger
|
|
478
548
|
|
|
479
|
-
progress_logger = FileProgressLogger(
|
|
549
|
+
progress_logger = FileProgressLogger[DecryptionState](
|
|
550
|
+
log_file_path=progress_log_file
|
|
551
|
+
)
|
|
480
552
|
|
|
481
553
|
try:
|
|
482
554
|
private_key = Crypt4GH.retrieve_private_key(recipient_private_key_path)
|
|
@@ -516,7 +588,7 @@ class EncryptedSubmission:
|
|
|
516
588
|
progress_logger.set_state(
|
|
517
589
|
encrypted_file_path,
|
|
518
590
|
file_metadata,
|
|
519
|
-
state=
|
|
591
|
+
state=DecryptionState(decryption_successful=True),
|
|
520
592
|
)
|
|
521
593
|
except Exception as e:
|
|
522
594
|
self.__log.error(
|
|
@@ -526,7 +598,9 @@ class EncryptedSubmission:
|
|
|
526
598
|
progress_logger.set_state(
|
|
527
599
|
encrypted_file_path,
|
|
528
600
|
file_metadata,
|
|
529
|
-
state=
|
|
601
|
+
state=DecryptionState(
|
|
602
|
+
decryption_successful=False, errors=[str(e)]
|
|
603
|
+
),
|
|
530
604
|
)
|
|
531
605
|
|
|
532
606
|
raise e
|
|
@@ -665,21 +739,21 @@ class Worker:
|
|
|
665
739
|
raise SubmissionValidationError(error_msg)
|
|
666
740
|
else:
|
|
667
741
|
self.__log.info("Checksum validation successful!")
|
|
668
|
-
self.__log.info("Starting checksum validation...")
|
|
669
742
|
|
|
743
|
+
self.__log.info("Starting sequencing data validation...")
|
|
670
744
|
if errors := list(
|
|
671
745
|
submission.validate_sequencing_data(
|
|
672
746
|
progress_log_file=self.progress_file_sequencing_data_validation
|
|
673
747
|
)
|
|
674
748
|
):
|
|
675
|
-
error_msg = "\n".join(
|
|
749
|
+
error_msg = "\n".join(
|
|
750
|
+
["Sequencing data validation failed! Errors:", *errors]
|
|
751
|
+
)
|
|
676
752
|
self.__log.error(error_msg)
|
|
677
753
|
|
|
678
754
|
raise SubmissionValidationError(error_msg)
|
|
679
755
|
else:
|
|
680
|
-
self.__log.info("
|
|
681
|
-
|
|
682
|
-
# TODO: validate FASTQ
|
|
756
|
+
self.__log.info("Sequencing data validation successful!")
|
|
683
757
|
|
|
684
758
|
def encrypt(
|
|
685
759
|
self,
|
|
@@ -5,17 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import copy
|
|
6
6
|
import json
|
|
7
7
|
import typing
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
from os import PathLike
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
from .file_operations import read_multiple_json
|
|
12
13
|
from .models.v1_0_0.metadata import File as SubmissionFileMetadata
|
|
14
|
+
from .states import State
|
|
13
15
|
|
|
14
16
|
Index = tuple[str, float, int]
|
|
15
|
-
State = dict
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class FileProgressLogger:
|
|
19
|
+
class FileProgressLogger[T: State]:
|
|
19
20
|
"""
|
|
20
21
|
A class to log and track the state of files over time. It stores file states in a log file of JSON entries
|
|
21
22
|
and allows querying the state based on the file path and modification time.
|
|
@@ -23,7 +24,7 @@ class FileProgressLogger:
|
|
|
23
24
|
|
|
24
25
|
_index = {"file_path": str, "modification_time": float, "size": int}
|
|
25
26
|
# mapping of index -> (metadata, data)
|
|
26
|
-
_file_states: dict[Index, tuple[SubmissionFileMetadata,
|
|
27
|
+
_file_states: dict[Index, tuple[SubmissionFileMetadata, T]]
|
|
27
28
|
|
|
28
29
|
def __init__(self, log_file_path: str | PathLike):
|
|
29
30
|
"""
|
|
@@ -95,12 +96,17 @@ class FileProgressLogger:
|
|
|
95
96
|
self,
|
|
96
97
|
file_path: str | PathLike,
|
|
97
98
|
file_metadata: dict | SubmissionFileMetadata,
|
|
98
|
-
|
|
99
|
+
default: T | Callable[[Path, SubmissionFileMetadata], T] | None = None,
|
|
100
|
+
) -> T | None:
|
|
99
101
|
"""
|
|
100
102
|
Retrieves the stored state of a file if it exists in the log.
|
|
101
103
|
|
|
102
104
|
:param file_path: The file path to query for the state.
|
|
103
105
|
:param file_metadata: The metadata of the file to query for the state.
|
|
106
|
+
:param default: Default state to use if the file does not exist.
|
|
107
|
+
Can be a Callable that takes the file path and the file metadata as input and returns some state.
|
|
108
|
+
|
|
109
|
+
The default state gets automatically saved as the state for this file in case there is no stored state.
|
|
104
110
|
:return: A dictionary representing the file's state, or None if the file's state isn't logged.
|
|
105
111
|
"""
|
|
106
112
|
file_path = Path(file_path)
|
|
@@ -110,7 +116,7 @@ class FileProgressLogger:
|
|
|
110
116
|
stored_metadata, stored_data = self._file_states.get(index, (None, None))
|
|
111
117
|
|
|
112
118
|
# check if metadata matches
|
|
113
|
-
if
|
|
119
|
+
if not isinstance(file_metadata, SubmissionFileMetadata):
|
|
114
120
|
file_metadata = SubmissionFileMetadata(**file_metadata)
|
|
115
121
|
|
|
116
122
|
if stored_metadata and not isinstance(stored_metadata, SubmissionFileMetadata):
|
|
@@ -118,14 +124,28 @@ class FileProgressLogger:
|
|
|
118
124
|
|
|
119
125
|
if stored_metadata and file_metadata == stored_metadata:
|
|
120
126
|
return copy.deepcopy(stored_data)
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
|
|
128
|
+
# metadata mismatch -> no valid stored state -
|
|
129
|
+
if file_metadata and default:
|
|
130
|
+
if callable(default):
|
|
131
|
+
state = default(file_path, file_metadata)
|
|
132
|
+
self.set_state(file_path, file_metadata, state)
|
|
133
|
+
return state
|
|
134
|
+
else:
|
|
135
|
+
if not default:
|
|
136
|
+
raise ValueError("Default state must be provided if not callable")
|
|
137
|
+
# return None
|
|
138
|
+
default = typing.cast(T, default)
|
|
139
|
+
self.set_state(file_path, file_metadata, default)
|
|
140
|
+
return default
|
|
141
|
+
else:
|
|
142
|
+
return None
|
|
123
143
|
|
|
124
144
|
def set_state(
|
|
125
145
|
self,
|
|
126
146
|
file_path: str | PathLike,
|
|
127
147
|
file_metadata: dict | SubmissionFileMetadata,
|
|
128
|
-
state:
|
|
148
|
+
state: T,
|
|
129
149
|
):
|
|
130
150
|
"""
|
|
131
151
|
Log the state of a file:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the type definitions for the progress logging states.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class State(TypedDict, total=False):
|
|
9
|
+
"""
|
|
10
|
+
Base state class for progress logging.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
errors: list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationState(State):
|
|
17
|
+
"""
|
|
18
|
+
State for validation progress.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
validation_passed: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UploadState(State):
|
|
25
|
+
"""
|
|
26
|
+
State for upload progress.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
upload_successful: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EncryptionState(State):
|
|
33
|
+
"""
|
|
34
|
+
State for encryption progress.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
encryption_successful: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DecryptionState(State):
|
|
41
|
+
"""
|
|
42
|
+
State for decryption progress.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
decryption_successful: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DownloadState(State):
|
|
49
|
+
"""
|
|
50
|
+
State for download progress.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
download_successful: bool
|
|
@@ -20,6 +20,7 @@ from tqdm.auto import tqdm
|
|
|
20
20
|
|
|
21
21
|
from .models.config import ConfigModel
|
|
22
22
|
from .progress_logging import FileProgressLogger
|
|
23
|
+
from .states import UploadState
|
|
23
24
|
|
|
24
25
|
MULTIPART_THRESHOLD = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
25
26
|
MULTIPART_CHUNKSIZE = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
@@ -159,7 +160,7 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
159
160
|
Upload an encrypted submission
|
|
160
161
|
:param encrypted_submission: The encrypted submission to upload
|
|
161
162
|
"""
|
|
162
|
-
progress_logger = FileProgressLogger(self._status_file_path)
|
|
163
|
+
progress_logger = FileProgressLogger[UploadState](self._status_file_path)
|
|
163
164
|
metadata_file_path, metadata_s3_object_id = (
|
|
164
165
|
encrypted_submission.get_metadata_file_path_and_object_id()
|
|
165
166
|
)
|
|
@@ -190,7 +191,9 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
190
191
|
|
|
191
192
|
self.__log.info(f"Upload complete for {str(file_path)}. ")
|
|
192
193
|
progress_logger.set_state(
|
|
193
|
-
file_path,
|
|
194
|
+
file_path,
|
|
195
|
+
file_metadata,
|
|
196
|
+
state=UploadState(upload_successful=True),
|
|
194
197
|
)
|
|
195
198
|
except Exception as e:
|
|
196
199
|
self.__log.error("Upload failed for '%s'", str(file_path))
|
|
@@ -198,7 +201,7 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
198
201
|
progress_logger.set_state(
|
|
199
202
|
file_path,
|
|
200
203
|
file_metadata,
|
|
201
|
-
state=
|
|
204
|
+
state=UploadState(upload_successful=False, errors=[str(e)]),
|
|
202
205
|
)
|
|
203
206
|
|
|
204
207
|
raise e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -12,22 +12,22 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: boto3
|
|
16
|
-
Requires-Dist: click
|
|
17
|
-
Requires-Dist: crypt4gh
|
|
18
|
-
Requires-Dist: jsonschema
|
|
19
|
-
Requires-Dist: pyyaml
|
|
20
|
-
Requires-Dist: tqdm
|
|
21
|
-
Requires-Dist: pydantic
|
|
22
|
-
Requires-Dist: platformdirs
|
|
15
|
+
Requires-Dist: boto3<2,>=1.35.39
|
|
16
|
+
Requires-Dist: click<9,>=8.1.7
|
|
17
|
+
Requires-Dist: crypt4gh<2,>=1.7
|
|
18
|
+
Requires-Dist: jsonschema<5,>=4.23.0
|
|
19
|
+
Requires-Dist: pyyaml<7,>=6.0.2
|
|
20
|
+
Requires-Dist: tqdm<5,>=4.66.5
|
|
21
|
+
Requires-Dist: pydantic<2.10,>=2.9.2
|
|
22
|
+
Requires-Dist: platformdirs<5,>=4.3.6
|
|
23
23
|
Provides-Extra: test
|
|
24
|
-
Requires-Dist: pytest
|
|
25
|
-
Requires-Dist: pytest-cov
|
|
26
|
-
Requires-Dist: pytest-mock
|
|
27
|
-
Requires-Dist: moto[s3]
|
|
28
|
-
Requires-Dist: pytest-sugar
|
|
29
|
-
Requires-Dist: ruff
|
|
30
|
-
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: pytest<9,>=8.3.3; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-mock<4,>=3.14.0; extra == "test"
|
|
27
|
+
Requires-Dist: moto[s3]<6,>=5.0.16; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-sugar<2,>=1.0.0; extra == "test"
|
|
29
|
+
Requires-Dist: ruff<1,>=0.8.1; extra == "test"
|
|
30
|
+
Requires-Dist: numpy<3,>=2.1.2; extra == "test"
|
|
31
31
|
|
|
32
32
|
# GRZ CLI
|
|
33
33
|
|
|
@@ -38,6 +38,11 @@ A command-line tool for validating, encrypting, uploading and downloading submis
|
|
|
38
38
|
- [Introduction](#introduction)
|
|
39
39
|
- [Features](#features)
|
|
40
40
|
- [Installation](#installation)
|
|
41
|
+
- [Requirements](#requirements)
|
|
42
|
+
- [End-user setup](#end-user)
|
|
43
|
+
- [Installation via `conda` (recommended)](#installation-via-conda-recommended)
|
|
44
|
+
- [Installation via `pip` (not recommended)](#installation-via-pip-not-recommended)
|
|
45
|
+
- [Development setup](#development-setup)
|
|
41
46
|
- [Usage](#usage)
|
|
42
47
|
- [Configuration](#configuration)
|
|
43
48
|
- [Exemplary submission procedure](#exemplary-submission-procedure)
|
|
@@ -77,35 +82,61 @@ The current version of the tool requires the `working_dir` to have at least as m
|
|
|
77
82
|
- **Download**: Download encrypted files from a GRZ (via built-in `boto3`).
|
|
78
83
|
- **Logging**: Log progress and results of operations
|
|
79
84
|
|
|
85
|
+
|
|
80
86
|
## Installation
|
|
81
87
|
|
|
82
|
-
###
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
### Requirements
|
|
89
|
+
Beside of the disk space requirements for the submission data, this tool also requires a linux environment, e.g.:
|
|
90
|
+
- Linux server
|
|
91
|
+
- Virtual machine running linux
|
|
92
|
+
- Docker container
|
|
93
|
+
- Windows subsystem for linux
|
|
94
|
+
- ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### End-user setup
|
|
98
|
+
The recommended method to install this tool is using the conda package manager.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#### Installation via `conda` (recommended)
|
|
102
|
+
|
|
103
|
+
If `conda` is not yet available on your system, we recommend to install the [Miniforge conda distribution](https://github.com/conda-forge/miniforge) by running the following commands:
|
|
85
104
|
```bash
|
|
86
105
|
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
|
|
87
106
|
bash Miniforge3-$(uname)-$(uname -m).sh
|
|
88
107
|
```
|
|
108
|
+
There are also alternative ways to install conda:
|
|
109
|
+
- [Micromamba, a single executable that does not require a base environment](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html)
|
|
110
|
+
- [Official installation instructions](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
|
111
|
+
|
|
89
112
|
Next, install the `grz-cli` tool:
|
|
90
113
|
```bash
|
|
91
|
-
# TODO update: This requires membership in the repository
|
|
92
|
-
# download environment.yaml from the git repository:
|
|
93
|
-
git archive --remote=ssh://git@codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli.git dev environment.yaml | tar -xv
|
|
94
|
-
# TODO update: This will work once the repository is publicly accessible:
|
|
95
|
-
# download environment.yaml from the git repository:
|
|
96
|
-
curl -L "https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli/-/raw/v0.1.0/environment.yaml?ref_type=heads" > environment.yaml
|
|
97
|
-
|
|
98
114
|
# create conda environment and activate it
|
|
99
|
-
conda
|
|
115
|
+
conda create -n grz-tools -c conda-forge -c bioconda "grz-cli"
|
|
100
116
|
conda activate grz-tools
|
|
117
|
+
```
|
|
101
118
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
pip install "git+https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli@v0.1.0"
|
|
119
|
+
##### Update instructions:
|
|
120
|
+
Use the following command to update the tool:
|
|
121
|
+
```bash
|
|
122
|
+
conda update -n grz-tools "grz-cli"
|
|
107
123
|
```
|
|
108
124
|
|
|
125
|
+
|
|
126
|
+
#### Installation via `pip` (not recommended)
|
|
127
|
+
While installation via `pip` is possible, it is not recommended because users must ensure
|
|
128
|
+
that the correct Python version is already installed and that they are using a virtual python environment.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install grz-cli
|
|
132
|
+
```
|
|
133
|
+
##### Update instructions:
|
|
134
|
+
Use the following command to update the tool:
|
|
135
|
+
```bash
|
|
136
|
+
pip upgrade grz-cli
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
|
|
109
140
|
### Development setup
|
|
110
141
|
For development purposes, you can clone the repository and install the package in editable mode:
|
|
111
142
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
boto3<2,>=1.35.39
|
|
2
|
+
click<9,>=8.1.7
|
|
3
|
+
crypt4gh<2,>=1.7
|
|
4
|
+
jsonschema<5,>=4.23.0
|
|
5
|
+
pyyaml<7,>=6.0.2
|
|
6
|
+
tqdm<5,>=4.66.5
|
|
7
|
+
pydantic<2.10,>=2.9.2
|
|
8
|
+
platformdirs<5,>=4.3.6
|
|
9
|
+
|
|
10
|
+
[test]
|
|
11
|
+
pytest<9,>=8.3.3
|
|
12
|
+
pytest-cov<6,>=5.0.0
|
|
13
|
+
pytest-mock<4,>=3.14.0
|
|
14
|
+
moto[s3]<6,>=5.0.16
|
|
15
|
+
pytest-sugar<2,>=1.0.0
|
|
16
|
+
ruff<1,>=0.8.1
|
|
17
|
+
numpy<3,>=2.1.2
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
boto3~=1.35.39
|
|
2
|
-
click~=8.1.7
|
|
3
|
-
crypt4gh~=1.7
|
|
4
|
-
jsonschema~=4.23.0
|
|
5
|
-
pyyaml~=6.0.2
|
|
6
|
-
tqdm~=4.66.5
|
|
7
|
-
pydantic~=2.9.2
|
|
8
|
-
platformdirs~=4.3.6
|
|
9
|
-
|
|
10
|
-
[test]
|
|
11
|
-
pytest~=8.3.3
|
|
12
|
-
pytest-cov~=5.0.0
|
|
13
|
-
pytest-mock~=3.14.0
|
|
14
|
-
moto[s3]~=5.0.16
|
|
15
|
-
pytest-sugar~=1.0.0
|
|
16
|
-
ruff~=0.7.0
|
|
17
|
-
numpy~=2.1.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|