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.
Files changed (38) hide show
  1. {grz_cli-0.1.0/src/grz_cli.egg-info → grz_cli-0.1.2}/PKG-INFO +63 -32
  2. {grz_cli-0.1.0 → grz_cli-0.1.2}/README.md +47 -16
  3. {grz_cli-0.1.0 → grz_cli-0.1.2}/pyproject.toml +17 -16
  4. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/download.py +6 -3
  5. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/fastq_validation.py +38 -9
  6. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/v1_0_0/metadata.py +7 -3
  7. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/parser.py +154 -80
  8. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/progress_logging.py +28 -8
  9. grz_cli-0.1.2/src/grz_cli/states.py +53 -0
  10. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/upload.py +6 -3
  11. {grz_cli-0.1.0 → grz_cli-0.1.2/src/grz_cli.egg-info}/PKG-INFO +63 -32
  12. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/SOURCES.txt +1 -0
  13. grz_cli-0.1.2/src/grz_cli.egg-info/requires.txt +17 -0
  14. grz_cli-0.1.0/src/grz_cli.egg-info/requires.txt +0 -17
  15. {grz_cli-0.1.0 → grz_cli-0.1.2}/LICENSE +0 -0
  16. {grz_cli-0.1.0 → grz_cli-0.1.2}/MANIFEST.in +0 -0
  17. {grz_cli-0.1.0 → grz_cli-0.1.2}/setup.cfg +0 -0
  18. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/__init__.py +0 -0
  19. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/__init__.py +0 -0
  20. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/cli.py +0 -0
  21. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/constants.py +0 -0
  22. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/file_operations.py +0 -0
  23. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/logging_setup.py +0 -0
  24. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/__init__.py +0 -0
  25. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/config.py +0 -0
  26. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/models/v1_0_0/__init__.py +0 -0
  27. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli/resources/grz-schema.json +0 -0
  28. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/dependency_links.txt +0 -0
  29. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/entry_points.txt +0 -0
  30. {grz_cli-0.1.0 → grz_cli-0.1.2}/src/grz_cli.egg-info/top_level.txt +0 -0
  31. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_cli.py +0 -0
  32. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_download.py +0 -0
  33. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_fastq_validation.py +0 -0
  34. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_file_operations.py +0 -0
  35. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_file_progress_logging.py +0 -0
  36. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_parser.py +0 -0
  37. {grz_cli-0.1.0 → grz_cli-0.1.2}/tests/test_read_multiple_json.py +0 -0
  38. {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.0
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~=1.35.39
16
- Requires-Dist: click~=8.1.7
17
- Requires-Dist: crypt4gh~=1.7
18
- Requires-Dist: jsonschema~=4.23.0
19
- Requires-Dist: pyyaml~=6.0.2
20
- Requires-Dist: tqdm~=4.66.5
21
- Requires-Dist: pydantic~=2.9.2
22
- Requires-Dist: platformdirs~=4.3.6
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~=8.3.3; extra == "test"
25
- Requires-Dist: pytest-cov~=5.0.0; extra == "test"
26
- Requires-Dist: pytest-mock~=3.14.0; extra == "test"
27
- Requires-Dist: moto[s3]~=5.0.16; extra == "test"
28
- Requires-Dist: pytest-sugar~=1.0.0; extra == "test"
29
- Requires-Dist: ruff~=0.7.0; extra == "test"
30
- Requires-Dist: numpy~=2.1.2; extra == "test"
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
- ### End-user
83
- This tool uses the `conda` package manager.
84
- If `conda` is not yet available on your system, you can install the [Miniforge conda distribution](https://github.com/conda-forge/miniforge) by running the following commands:
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 env create -f environment.yaml -n grz-tools
115
+ conda create -n grz-tools -c conda-forge -c bioconda "grz-cli"
100
116
  conda activate grz-tools
117
+ ```
101
118
 
102
- # install the grz-cli tool
103
- # TODO update: This requires membership in the repository
104
- pip install "git+ssh://git@codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli.git@dev"
105
- # TODO update: This will work once the repository is publicly accessible:
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
- ### End-user
52
- This tool uses the `conda` package manager.
53
- If `conda` is not yet available on your system, you can install the [Miniforge conda distribution](https://github.com/conda-forge/miniforge) by running the following commands:
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 env create -f environment.yaml -n grz-tools
84
+ conda create -n grz-tools -c conda-forge -c bioconda "grz-cli"
69
85
  conda activate grz-tools
86
+ ```
70
87
 
71
- # install the grz-cli tool
72
- # TODO update: This requires membership in the repository
73
- pip install "git+ssh://git@codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli.git@dev"
74
- # TODO update: This will work once the repository is publicly accessible:
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.0"
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 ~=1.35.39",
20
- "click ~=8.1.7",
21
- "crypt4gh ~=1.7",
22
- "jsonschema ~=4.23.0",
23
- "pyyaml ~=6.0.2",
24
- "tqdm ~=4.66.5",
25
- "pydantic ~=2.9.2",
26
- "platformdirs ~=4.3.6",
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 ~=8.3.3",
45
- "pytest-cov ~=5.0.0",
46
- "pytest-mock ~=3.14.0",
47
- "moto[s3] ~=5.0.16",
48
- "pytest-sugar ~=1.0.0",
49
- "ruff ~=0.7.0",
50
- "numpy ~=2.1.2"
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={"download_successful": True},
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={"download_successful": False, "error": str(e)},
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(file_path: str | PathLike):
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
- if is_gzipped(file_path):
31
- with gzip.open(file_path, "rb") as f:
32
- # Open gzipped FASTQ
33
- yield f
34
- else:
35
- with open(file_path) as f:
36
- # Open regular FASTQ
37
- yield f
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 not threshold:
835
- raise ValueError(
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
- from .progress_logging import FileProgressLogger
172
-
173
- progress_logger = FileProgressLogger(log_file_path=progress_log_file)
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
- for local_file_path, file_metadata in self.files.items():
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
- progress_logger.set_state(
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
- yield from errors
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
- def find_fastq_files(sequence_data: SequenceData):
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
- if sequence_type == SequenceType.dna:
241
- # find all FASTQ files
242
- fastq_files = find_fastq_files(sequence_data)
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
- if sequencing_layout == SequencingLayout.single_end:
245
- yield from validate_single_end_reads(
246
- self.files_dir / fastq_files[0].file_path
251
+ case SequencingLayout.paired_end:
252
+ yield from self._validate_paired_end(
253
+ fastq_files, progress_logger
247
254
  )
248
- else:
249
- # group reads by flowcell id and lane id
250
- key = lambda f: (f.flowcell_id, f.lane_id)
251
- fastq_files.sort(key=key)
252
- for _key, group in groupby(fastq_files, key):
253
- files = list(group)
254
-
255
- # separate R1 and R2 files
256
- fastq_r1_files = [
257
- f for f in files if f.read_order == ReadOrder.r1
258
- ]
259
- fastq_r2_files = [
260
- f for f in files if f.read_order == ReadOrder.r2
261
- ]
262
-
263
- for fastq_r1, fastq_r2 in zip(
264
- fastq_r1_files, fastq_r2_files, strict=True
265
- ):
266
- yield from validate_paired_end_reads(
267
- # fastq R1
268
- self.files_dir / fastq_r1.file_path,
269
- # fastq R2
270
- self.files_dir / fastq_r2.file_path,
271
- )
272
-
273
- elif sequence_type == SequenceType.rna:
274
- # TODO: What to check here?
275
- pass
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(log_file_path=progress_log_file)
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, file_metadata, state={"encryption_successful": True}
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={"encryption_successful": False, "error": str(e)},
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(log_file_path=progress_log_file)
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={"decryption_successful": True},
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={"decryption_successful": False, "error": str(e)},
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(["Checksum validation failed! Errors:", *errors])
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("Checksum validation successful!")
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, State]]
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
- ) -> State | None:
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 file_metadata and not isinstance(file_metadata, SubmissionFileMetadata):
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
- # metadata mismatch -> no valid stored state -> return None
122
- return None
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: 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, file_metadata, state={"upload_successful": True}
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={"upload_successful": False, "error": str(e)},
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.0
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~=1.35.39
16
- Requires-Dist: click~=8.1.7
17
- Requires-Dist: crypt4gh~=1.7
18
- Requires-Dist: jsonschema~=4.23.0
19
- Requires-Dist: pyyaml~=6.0.2
20
- Requires-Dist: tqdm~=4.66.5
21
- Requires-Dist: pydantic~=2.9.2
22
- Requires-Dist: platformdirs~=4.3.6
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~=8.3.3; extra == "test"
25
- Requires-Dist: pytest-cov~=5.0.0; extra == "test"
26
- Requires-Dist: pytest-mock~=3.14.0; extra == "test"
27
- Requires-Dist: moto[s3]~=5.0.16; extra == "test"
28
- Requires-Dist: pytest-sugar~=1.0.0; extra == "test"
29
- Requires-Dist: ruff~=0.7.0; extra == "test"
30
- Requires-Dist: numpy~=2.1.2; extra == "test"
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
- ### End-user
83
- This tool uses the `conda` package manager.
84
- If `conda` is not yet available on your system, you can install the [Miniforge conda distribution](https://github.com/conda-forge/miniforge) by running the following commands:
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 env create -f environment.yaml -n grz-tools
115
+ conda create -n grz-tools -c conda-forge -c bioconda "grz-cli"
100
116
  conda activate grz-tools
117
+ ```
101
118
 
102
- # install the grz-cli tool
103
- # TODO update: This requires membership in the repository
104
- pip install "git+ssh://git@codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli.git@dev"
105
- # TODO update: This will work once the repository is publicly accessible:
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
 
@@ -12,6 +12,7 @@ src/grz_cli/file_operations.py
12
12
  src/grz_cli/logging_setup.py
13
13
  src/grz_cli/parser.py
14
14
  src/grz_cli/progress_logging.py
15
+ src/grz_cli/states.py
15
16
  src/grz_cli/upload.py
16
17
  src/grz_cli.egg-info/PKG-INFO
17
18
  src/grz_cli.egg-info/SOURCES.txt
@@ -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