grz-cli 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grz_cli-0.4.0/src/grz_cli.egg-info → grz_cli-0.5.0}/PKG-INFO +5 -3
- {grz_cli-0.4.0 → grz_cli-0.5.0}/pyproject.toml +4 -7
- grz_cli-0.5.0/src/grz_cli/__init__.py +1 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/cli.py +174 -85
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/constants.py +2 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/download.py +48 -34
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/fastq_validation.py +2 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/file_operations.py +6 -1
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/models/config.py +9 -2
- grz_cli-0.5.0/src/grz_cli/transfer.py +42 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/upload.py +32 -37
- {grz_cli-0.4.0 → grz_cli-0.5.0/src/grz_cli.egg-info}/PKG-INFO +5 -3
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli.egg-info/SOURCES.txt +1 -0
- grz_cli-0.5.0/src/grz_cli.egg-info/entry_points.txt +2 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli.egg-info/requires.txt +1 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_cli.py +12 -8
- grz_cli-0.4.0/src/grz_cli/__init__.py +0 -1
- grz_cli-0.4.0/src/grz_cli.egg-info/entry_points.txt +0 -2
- {grz_cli-0.4.0 → grz_cli-0.5.0}/LICENSE +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/MANIFEST.in +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/README.md +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/setup.cfg +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/__init__.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/bam_validation.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/logging_setup.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/parser.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/progress_logging.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli/states.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli.egg-info/dependency_links.txt +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/src/grz_cli.egg-info/top_level.txt +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_bam_validation.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_download.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_fastq_validation.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_file_operations.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_file_progress_logging.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_parser.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_read_multiple_json.py +0 -0
- {grz_cli-0.4.0 → grz_cli-0.5.0}/tests/test_upload.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Author-email: Koray Kirli <koraykirli@gmail.com>, Mathias Lesche <mathias.lesche@tu-dresden.de>, "Florian R. Hölzlwimmer" <git.ich@frhoelzlwimmer.de>, Till Hartmann <till.hartmann@bih-charite.de>, Thomas Sell <thomas.sell@bih-charite.de>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli
|
|
8
8
|
Keywords: GRZ,GDC,S3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Requires-Python: >=3.12
|
|
13
12
|
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
14
|
Requires-Dist: boto3<2,>=1.36
|
|
15
15
|
Requires-Dist: click<9,>=8.1.7
|
|
16
16
|
Requires-Dist: crypt4gh<2,>=1.7
|
|
@@ -21,6 +21,7 @@ Requires-Dist: pydantic<2.10,>=2.9.2
|
|
|
21
21
|
Requires-Dist: platformdirs<5,>=4.3.6
|
|
22
22
|
Requires-Dist: grz-pydantic-models>=1.2.1
|
|
23
23
|
Requires-Dist: pysam==0.23.*
|
|
24
|
+
Requires-Dist: rich==13.*
|
|
24
25
|
Provides-Extra: test
|
|
25
26
|
Requires-Dist: pytest<9,>=8.3.3; extra == "test"
|
|
26
27
|
Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
|
|
@@ -29,6 +30,7 @@ Requires-Dist: moto[s3]<6,>=5.0.16; extra == "test"
|
|
|
29
30
|
Requires-Dist: pytest-sugar<2,>=1.0.0; extra == "test"
|
|
30
31
|
Requires-Dist: ruff<1,>=0.9.6; extra == "test"
|
|
31
32
|
Requires-Dist: numpy<3,>=2.1.2; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
32
34
|
|
|
33
35
|
# GRZ CLI
|
|
34
36
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
name = "grz-cli"
|
|
7
7
|
description = "Tool for validation, encryption and upload of MV submissions to GDCs."
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
license =
|
|
9
|
+
license = "MIT"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "Koray Kirli", email = "koraykirli@gmail.com" },
|
|
12
12
|
{ name = "Mathias Lesche", email = "mathias.lesche@tu-dresden.de" },
|
|
@@ -25,11 +25,11 @@ dependencies = [
|
|
|
25
25
|
"pydantic >=2.9.2,<2.10",
|
|
26
26
|
"platformdirs >=4.3.6,<5",
|
|
27
27
|
"grz-pydantic-models >=1.2.1",
|
|
28
|
-
"pysam ==0.23.*"
|
|
28
|
+
"pysam ==0.23.*",
|
|
29
|
+
"rich ==13.*"
|
|
29
30
|
]
|
|
30
31
|
classifiers = [
|
|
31
32
|
"Programming Language :: Python :: 3",
|
|
32
|
-
"License :: OSI Approved :: MIT License",
|
|
33
33
|
"Operating System :: OS Independent"
|
|
34
34
|
]
|
|
35
35
|
requires-python = ">=3.12"
|
|
@@ -43,7 +43,7 @@ version = { attr = "grz_cli.__version__" }
|
|
|
43
43
|
"Homepage" = "https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli"
|
|
44
44
|
|
|
45
45
|
[project.scripts]
|
|
46
|
-
grz-cli = "grz_cli.cli:
|
|
46
|
+
grz-cli = "grz_cli.cli:main"
|
|
47
47
|
|
|
48
48
|
[project.optional-dependencies]
|
|
49
49
|
test = [
|
|
@@ -207,6 +207,3 @@ follow_untyped_imports = true
|
|
|
207
207
|
addopts = [
|
|
208
208
|
"--import-mode=importlib",
|
|
209
209
|
]
|
|
210
|
-
|
|
211
|
-
[tool.setuptools]
|
|
212
|
-
license-files = []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -3,8 +3,10 @@ CLI module for handling command-line interface operations.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import importlib
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
8
|
import logging.config
|
|
9
|
+
import os
|
|
8
10
|
import sys
|
|
9
11
|
from os import PathLike, sched_getaffinity
|
|
10
12
|
from pathlib import Path
|
|
@@ -13,9 +15,13 @@ from tempfile import NamedTemporaryFile
|
|
|
13
15
|
import click
|
|
14
16
|
import grz_pydantic_models.submission.metadata
|
|
15
17
|
import platformdirs
|
|
18
|
+
import rich.console
|
|
19
|
+
import rich.table
|
|
16
20
|
import yaml
|
|
21
|
+
from pydantic_core import to_jsonable_python
|
|
17
22
|
|
|
18
23
|
from .constants import PACKAGE_ROOT
|
|
24
|
+
from .download import query_submissions
|
|
19
25
|
from .logging_setup import add_filelogger
|
|
20
26
|
from .models.config import ConfigModel
|
|
21
27
|
from .parser import Worker
|
|
@@ -94,6 +100,16 @@ output_dir = click.option(
|
|
|
94
100
|
help="Path to the target submission output directory",
|
|
95
101
|
)
|
|
96
102
|
|
|
103
|
+
output_json = click.option("--json", "output_json", is_flag=True, help="Output JSON for machine-readability.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def read_config(config_path: str | PathLike) -> ConfigModel:
|
|
107
|
+
"""Reads the configuration file and validates it against the schema."""
|
|
108
|
+
with open(config_path, encoding="utf-8") as f:
|
|
109
|
+
config = ConfigModel(**yaml.safe_load(f))
|
|
110
|
+
|
|
111
|
+
return config
|
|
112
|
+
|
|
97
113
|
|
|
98
114
|
class OrderedGroup(click.Group):
|
|
99
115
|
"""
|
|
@@ -105,48 +121,7 @@ class OrderedGroup(click.Group):
|
|
|
105
121
|
return list(self.commands.keys())
|
|
106
122
|
|
|
107
123
|
|
|
108
|
-
@click.
|
|
109
|
-
cls=OrderedGroup,
|
|
110
|
-
help="Validate, encrypt, decrypt and upload submissions to a GRZ/GDC.",
|
|
111
|
-
)
|
|
112
|
-
@click.version_option(
|
|
113
|
-
version=importlib.metadata.version("grz-cli"),
|
|
114
|
-
prog_name="grz-cli",
|
|
115
|
-
message=f"%(prog)s v%(version)s (metadata schema versions: {', '.join(grz_pydantic_models.submission.metadata.get_supported_versions())})",
|
|
116
|
-
)
|
|
117
|
-
@click.option("--log-file", metavar="FILE", type=str, help="Path to log file")
|
|
118
|
-
@click.option(
|
|
119
|
-
"--log-level",
|
|
120
|
-
default="INFO",
|
|
121
|
-
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
|
|
122
|
-
help="Set the log level (default: INFO)",
|
|
123
|
-
)
|
|
124
|
-
def cli(log_file: str | None = None, log_level: str = "INFO"):
|
|
125
|
-
"""
|
|
126
|
-
Command-line interface function for setting up logging.
|
|
127
|
-
|
|
128
|
-
:param log_file: Path to the log file. If provided, a file logger will be added.
|
|
129
|
-
:param log_level: Log level for the logger. It should be one of the following:
|
|
130
|
-
DEBUG, INFO, WARNING, ERROR, CRITICAL.
|
|
131
|
-
"""
|
|
132
|
-
if log_file:
|
|
133
|
-
add_filelogger(
|
|
134
|
-
log_file,
|
|
135
|
-
log_level.upper(),
|
|
136
|
-
) # Add file logger
|
|
137
|
-
|
|
138
|
-
# show only time and log level in STDOUT
|
|
139
|
-
logging.basicConfig(
|
|
140
|
-
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
# set the log level for this package
|
|
144
|
-
logging.getLogger(PACKAGE_ROOT).setLevel(log_level.upper())
|
|
145
|
-
|
|
146
|
-
log.debug("Logging setup complete.")
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@cli.command()
|
|
124
|
+
@click.command()
|
|
150
125
|
@submission_dir
|
|
151
126
|
def validate(submission_dir):
|
|
152
127
|
"""
|
|
@@ -170,7 +145,7 @@ def validate(submission_dir):
|
|
|
170
145
|
log.info("Validation finished!")
|
|
171
146
|
|
|
172
147
|
|
|
173
|
-
@
|
|
148
|
+
@click.command()
|
|
174
149
|
@submission_dir
|
|
175
150
|
@config_file
|
|
176
151
|
def encrypt(
|
|
@@ -216,41 +191,7 @@ def encrypt(
|
|
|
216
191
|
log.info("Encryption successful!")
|
|
217
192
|
|
|
218
193
|
|
|
219
|
-
@
|
|
220
|
-
@submission_dir
|
|
221
|
-
@config_file
|
|
222
|
-
def decrypt(
|
|
223
|
-
submission_dir,
|
|
224
|
-
config_file,
|
|
225
|
-
):
|
|
226
|
-
"""
|
|
227
|
-
Decrypt a submission.
|
|
228
|
-
|
|
229
|
-
Decrypting a submission requires the _private_ key of the original recipient.
|
|
230
|
-
"""
|
|
231
|
-
config = read_config(config_file)
|
|
232
|
-
|
|
233
|
-
grz_privkey_path = config.grz_private_key_path
|
|
234
|
-
if not grz_privkey_path:
|
|
235
|
-
log.error("GRZ private key path is required for decryption.")
|
|
236
|
-
sys.exit(1)
|
|
237
|
-
|
|
238
|
-
log.info("Starting decryption...")
|
|
239
|
-
|
|
240
|
-
submission_dir = Path(submission_dir)
|
|
241
|
-
|
|
242
|
-
worker_inst = Worker(
|
|
243
|
-
metadata_dir=submission_dir / "metadata",
|
|
244
|
-
files_dir=submission_dir / "files",
|
|
245
|
-
log_dir=submission_dir / "logs",
|
|
246
|
-
encrypted_files_dir=submission_dir / "encrypted_files",
|
|
247
|
-
)
|
|
248
|
-
worker_inst.decrypt(grz_privkey_path)
|
|
249
|
-
|
|
250
|
-
log.info("Encryption successful!")
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@cli.command()
|
|
194
|
+
@click.command()
|
|
254
195
|
@submission_dir
|
|
255
196
|
@config_file
|
|
256
197
|
@threads
|
|
@@ -281,7 +222,28 @@ def upload(
|
|
|
281
222
|
log.info("Upload finished!")
|
|
282
223
|
|
|
283
224
|
|
|
284
|
-
@
|
|
225
|
+
@click.command("submit")
|
|
226
|
+
@submission_dir
|
|
227
|
+
@config_file
|
|
228
|
+
@threads
|
|
229
|
+
@click.pass_context
|
|
230
|
+
def submit(ctx, submission_dir, config_file, threads):
|
|
231
|
+
"""
|
|
232
|
+
Validate, encrypt, and then upload.
|
|
233
|
+
|
|
234
|
+
This is a convenience command that performs the following steps in order:
|
|
235
|
+
1. Validate the submission
|
|
236
|
+
2. Encrypt the submission
|
|
237
|
+
3. Upload the encrypted submission
|
|
238
|
+
"""
|
|
239
|
+
click.echo("Starting submission process...")
|
|
240
|
+
ctx.invoke(validate, submission_dir=submission_dir)
|
|
241
|
+
ctx.invoke(encrypt, submission_dir=submission_dir, config_file=config_file)
|
|
242
|
+
ctx.invoke(upload, submission_dir=submission_dir, config_file=config_file, threads=threads)
|
|
243
|
+
click.echo("Submission finished!")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@click.command()
|
|
285
247
|
@submission_id
|
|
286
248
|
@output_dir
|
|
287
249
|
@config_file
|
|
@@ -319,13 +281,140 @@ def download(
|
|
|
319
281
|
log.info("Download finished!")
|
|
320
282
|
|
|
321
283
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
284
|
+
@click.command()
|
|
285
|
+
@config_file
|
|
286
|
+
@output_json
|
|
287
|
+
def list_submissions(config_file, output_json):
|
|
288
|
+
"""
|
|
289
|
+
List submissions within an inbox from oldest to newest.
|
|
290
|
+
"""
|
|
291
|
+
config = read_config(config_file)
|
|
292
|
+
submissions = query_submissions(config)
|
|
326
293
|
|
|
327
|
-
|
|
294
|
+
if output_json:
|
|
295
|
+
json.dump(to_jsonable_python(submissions), sys.stdout)
|
|
296
|
+
else:
|
|
297
|
+
console = rich.console.Console()
|
|
298
|
+
table = rich.table.Table()
|
|
299
|
+
table.add_column("ID", no_wrap=True)
|
|
300
|
+
table.add_column("Status", no_wrap=True)
|
|
301
|
+
table.add_column("Oldest Upload", overflow="fold")
|
|
302
|
+
table.add_column("Newest Upload", overflow="fold")
|
|
303
|
+
for submission in submissions:
|
|
304
|
+
status_text = rich.text.Text(
|
|
305
|
+
"Complete" if submission.complete else "Incomplete",
|
|
306
|
+
style="green" if submission.complete else "yellow",
|
|
307
|
+
)
|
|
308
|
+
table.add_row(
|
|
309
|
+
submission.submission_id,
|
|
310
|
+
status_text,
|
|
311
|
+
submission.oldest_upload.astimezone().strftime("%Y-%m-%d %H:%M:%S"),
|
|
312
|
+
submission.newest_upload.astimezone().strftime("%Y-%m-%d %H:%M:%S"),
|
|
313
|
+
)
|
|
314
|
+
console.print(table)
|
|
328
315
|
|
|
329
316
|
|
|
330
|
-
|
|
317
|
+
@click.command()
|
|
318
|
+
@submission_dir
|
|
319
|
+
@config_file
|
|
320
|
+
def decrypt(
|
|
321
|
+
submission_dir,
|
|
322
|
+
config_file,
|
|
323
|
+
):
|
|
324
|
+
"""
|
|
325
|
+
Decrypt a submission.
|
|
326
|
+
|
|
327
|
+
Decrypting a submission requires the _private_ key of the original recipient.
|
|
328
|
+
"""
|
|
329
|
+
config = read_config(config_file)
|
|
330
|
+
|
|
331
|
+
grz_privkey_path = config.grz_private_key_path
|
|
332
|
+
if not grz_privkey_path:
|
|
333
|
+
log.error("GRZ private key path is required for decryption.")
|
|
334
|
+
sys.exit(1)
|
|
335
|
+
|
|
336
|
+
log.info("Starting decryption...")
|
|
337
|
+
|
|
338
|
+
submission_dir = Path(submission_dir)
|
|
339
|
+
|
|
340
|
+
worker_inst = Worker(
|
|
341
|
+
metadata_dir=submission_dir / "metadata",
|
|
342
|
+
files_dir=submission_dir / "files",
|
|
343
|
+
log_dir=submission_dir / "logs",
|
|
344
|
+
encrypted_files_dir=submission_dir / "encrypted_files",
|
|
345
|
+
)
|
|
346
|
+
worker_inst.decrypt(grz_privkey_path)
|
|
347
|
+
|
|
348
|
+
log.info("Encryption successful!")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_cli(grz_mode=False):
|
|
352
|
+
"""
|
|
353
|
+
Factory for building the CLI application.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
@click.group(
|
|
357
|
+
cls=OrderedGroup,
|
|
358
|
+
help="Validate, encrypt, decrypt and upload submissions to a GRZ/GDC.",
|
|
359
|
+
)
|
|
360
|
+
@click.version_option(
|
|
361
|
+
version=importlib.metadata.version("grz-cli"),
|
|
362
|
+
prog_name="grz-cli",
|
|
363
|
+
message=f"%(prog)s v%(version)s (metadata schema versions: {', '.join(grz_pydantic_models.submission.metadata.get_supported_versions())})",
|
|
364
|
+
)
|
|
365
|
+
@click.option("--log-file", metavar="FILE", type=str, help="Path to log file")
|
|
366
|
+
@click.option(
|
|
367
|
+
"--log-level",
|
|
368
|
+
default="INFO",
|
|
369
|
+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
|
|
370
|
+
help="Set the log level (default: INFO)",
|
|
371
|
+
)
|
|
372
|
+
def cli(log_file: str | None = None, log_level: str = "INFO"):
|
|
373
|
+
"""
|
|
374
|
+
Command-line interface function for setting up logging.
|
|
375
|
+
|
|
376
|
+
:param log_file: Path to the log file. If provided, a file logger will be added.
|
|
377
|
+
:param log_level: Log level for the logger. It should be one of the following:
|
|
378
|
+
DEBUG, INFO, WARNING, ERROR, CRITICAL.
|
|
379
|
+
"""
|
|
380
|
+
if log_file:
|
|
381
|
+
add_filelogger(
|
|
382
|
+
log_file,
|
|
383
|
+
log_level.upper(),
|
|
384
|
+
) # Add file logger
|
|
385
|
+
|
|
386
|
+
# show only time and log level in STDOUT
|
|
387
|
+
logging.basicConfig(
|
|
388
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# set the log level for this package
|
|
392
|
+
logging.getLogger(PACKAGE_ROOT).setLevel(log_level.upper())
|
|
393
|
+
|
|
394
|
+
log.debug("Logging setup complete.")
|
|
395
|
+
|
|
396
|
+
cli.add_command(validate)
|
|
397
|
+
cli.add_command(encrypt)
|
|
398
|
+
cli.add_command(upload)
|
|
399
|
+
cli.add_command(submit)
|
|
400
|
+
|
|
401
|
+
if grz_mode:
|
|
402
|
+
cli.add_command(list_submissions, name="list")
|
|
403
|
+
cli.add_command(download)
|
|
404
|
+
cli.add_command(decrypt)
|
|
405
|
+
|
|
406
|
+
return cli
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def main():
|
|
410
|
+
"""
|
|
411
|
+
Main entry point for the CLI application.
|
|
412
|
+
"""
|
|
413
|
+
grz_mode = os.environ.get("GRZ_MODE", "false").lower() in {"true", "yes", "1"}
|
|
414
|
+
|
|
415
|
+
cli = build_cli(grz_mode=grz_mode)
|
|
331
416
|
cli()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
main()
|
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
"""Module for
|
|
1
|
+
"""Module for downloading encrypted submissions to local storage"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import datetime
|
|
6
|
+
import itertools
|
|
5
7
|
import logging
|
|
6
8
|
import math
|
|
7
9
|
import re
|
|
10
|
+
from operator import attrgetter, itemgetter
|
|
8
11
|
from os import PathLike
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
from typing import TYPE_CHECKING
|
|
11
14
|
|
|
12
|
-
import boto3 # type: ignore[import-untyped]
|
|
13
15
|
import botocore.handlers # type: ignore[import-untyped]
|
|
14
|
-
from boto3 import client as boto3_client # type: ignore[import-untyped]
|
|
15
16
|
from boto3.s3.transfer import S3Transfer, TransferConfig # type: ignore[import-untyped]
|
|
16
|
-
from
|
|
17
|
+
from pydantic import BaseModel
|
|
17
18
|
from tqdm.auto import tqdm
|
|
18
19
|
|
|
20
|
+
from .constants import TQDM_SMOOTHING
|
|
19
21
|
from .models.config import ConfigModel
|
|
20
22
|
from .progress_logging import FileProgressLogger
|
|
21
23
|
from .states import DownloadState
|
|
24
|
+
from .transfer import init_s3_client
|
|
22
25
|
|
|
23
26
|
MULTIPART_THRESHOLD = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
24
27
|
MULTIPART_CHUNKSIZE = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
@@ -62,35 +65,7 @@ class S3BotoDownloadWorker:
|
|
|
62
65
|
self._config = config
|
|
63
66
|
self._threads = threads
|
|
64
67
|
|
|
65
|
-
self.
|
|
66
|
-
|
|
67
|
-
def _init_s3_client(self):
|
|
68
|
-
# if user specifies empty strings, this might be an issue
|
|
69
|
-
def empty_str_to_none(string: str | None) -> str | None:
|
|
70
|
-
if string == "" or string is None:
|
|
71
|
-
return None
|
|
72
|
-
else:
|
|
73
|
-
return string
|
|
74
|
-
|
|
75
|
-
# configure proxies if proxy_url is defined
|
|
76
|
-
proxy_url = self._config.s3_options.proxy_url
|
|
77
|
-
config = Boto3Config(
|
|
78
|
-
proxies={"http": str(proxy_url), "https": str(proxy_url)} if proxy_url is not None else None,
|
|
79
|
-
request_checksum_calculation=self._config.s3_options.request_checksum_calculation,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Initialize S3 client for uploading
|
|
83
|
-
self._s3_client: boto3.session.Session.client = boto3_client(
|
|
84
|
-
service_name="s3",
|
|
85
|
-
region_name=empty_str_to_none(self._config.s3_options.region_name),
|
|
86
|
-
api_version=empty_str_to_none(self._config.s3_options.api_version),
|
|
87
|
-
use_ssl=self._config.s3_options.use_ssl,
|
|
88
|
-
endpoint_url=empty_str_to_none(str(self._config.s3_options.endpoint_url)),
|
|
89
|
-
aws_access_key_id=empty_str_to_none(self._config.s3_options.access_key),
|
|
90
|
-
aws_secret_access_key=empty_str_to_none(self._config.s3_options.secret),
|
|
91
|
-
aws_session_token=empty_str_to_none(self._config.s3_options.session_token),
|
|
92
|
-
config=config,
|
|
93
|
-
)
|
|
68
|
+
self._s3_client = init_s3_client(config)
|
|
94
69
|
|
|
95
70
|
def prepare_download(
|
|
96
71
|
self,
|
|
@@ -158,7 +133,7 @@ class S3BotoDownloadWorker:
|
|
|
158
133
|
)
|
|
159
134
|
|
|
160
135
|
transfer = S3Transfer(self._s3_client, config)
|
|
161
|
-
progress_bar = tqdm(total=filesize, unit="B", unit_scale=True, unit_divisor=1024)
|
|
136
|
+
progress_bar = tqdm(total=filesize, unit="B", unit_scale=True, unit_divisor=1024, smoothing=TQDM_SMOOTHING)
|
|
162
137
|
transfer.download_file(
|
|
163
138
|
self._config.s3_options.bucket,
|
|
164
139
|
s3_object_id,
|
|
@@ -225,3 +200,42 @@ class S3BotoDownloadWorker:
|
|
|
225
200
|
file_key,
|
|
226
201
|
str(full_path),
|
|
227
202
|
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class SubmissionInboxState(BaseModel):
|
|
206
|
+
"""A summary of the state of a submission in an inbox"""
|
|
207
|
+
|
|
208
|
+
submission_id: str
|
|
209
|
+
complete: bool
|
|
210
|
+
oldest_upload: datetime.datetime
|
|
211
|
+
newest_upload: datetime.datetime
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def query_submissions(config: ConfigModel) -> list[SubmissionInboxState]:
|
|
215
|
+
"""Queries the state of all submissions in the configured bucket."""
|
|
216
|
+
s3_client = init_s3_client(config)
|
|
217
|
+
paginator = s3_client.get_paginator("list_objects_v2")
|
|
218
|
+
|
|
219
|
+
objects = itertools.chain.from_iterable(
|
|
220
|
+
page["Contents"] for page in paginator.paginate(Bucket=config.s3_options.bucket)
|
|
221
|
+
)
|
|
222
|
+
objects_sorted = sorted(objects, key=itemgetter("Key"))
|
|
223
|
+
submission2objects = {
|
|
224
|
+
key: tuple(group) for key, group in itertools.groupby(objects_sorted, key=lambda o: o["Key"].split("/")[0])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
submissions = []
|
|
228
|
+
for submission_id, submission_objects in submission2objects.items():
|
|
229
|
+
submission_objects_sorted = sorted(submission_objects, key=itemgetter("LastModified"))
|
|
230
|
+
oldest_object = submission_objects_sorted[0]
|
|
231
|
+
newest_object = submission_objects_sorted[-1]
|
|
232
|
+
is_complete = len(list(filter(lambda o: o["Key"].endswith("/metadata.json"), submission_objects))) == 1
|
|
233
|
+
submission = SubmissionInboxState(
|
|
234
|
+
submission_id=submission_id,
|
|
235
|
+
complete=is_complete,
|
|
236
|
+
oldest_upload=oldest_object["LastModified"],
|
|
237
|
+
newest_upload=newest_object["LastModified"],
|
|
238
|
+
)
|
|
239
|
+
submissions.append(submission)
|
|
240
|
+
|
|
241
|
+
return sorted(submissions, key=attrgetter("oldest_upload"))
|
|
@@ -13,6 +13,7 @@ from typing import TextIO
|
|
|
13
13
|
|
|
14
14
|
from tqdm.auto import tqdm
|
|
15
15
|
|
|
16
|
+
from .constants import TQDM_SMOOTHING
|
|
16
17
|
from .file_operations import TqdmIOWrapper
|
|
17
18
|
|
|
18
19
|
log = logging.getLogger(__name__)
|
|
@@ -51,6 +52,7 @@ def open_fastq(file_path: str | PathLike, progress=True) -> Generator[TextIO, No
|
|
|
51
52
|
unit_scale=True,
|
|
52
53
|
# unit_divisor=1024, # make use of standard units e.g. KB, MB, etc.
|
|
53
54
|
miniters=1,
|
|
55
|
+
smoothing=TQDM_SMOOTHING,
|
|
54
56
|
),
|
|
55
57
|
)
|
|
56
58
|
else:
|
|
@@ -24,6 +24,8 @@ import crypt4gh.lib # type: ignore[import-untyped]
|
|
|
24
24
|
from nacl.public import PrivateKey
|
|
25
25
|
from tqdm.auto import tqdm
|
|
26
26
|
|
|
27
|
+
from .constants import TQDM_SMOOTHING
|
|
28
|
+
|
|
27
29
|
log = logging.getLogger(__name__)
|
|
28
30
|
|
|
29
31
|
|
|
@@ -69,7 +71,9 @@ def calculate_md5(file_path, chunk_size=2**16, progress=True) -> str:
|
|
|
69
71
|
md5_hash = hashlib.md5() # noqa: S324
|
|
70
72
|
with open(file_path, "rb") as f:
|
|
71
73
|
if progress and (total_size > chunk_size):
|
|
72
|
-
with tqdm(
|
|
74
|
+
with tqdm(
|
|
75
|
+
total=total_size, unit="B", unit_scale=True, desc="Calculating MD5", smoothing=TQDM_SMOOTHING
|
|
76
|
+
) as pbar:
|
|
73
77
|
while chunk := f.read(chunk_size):
|
|
74
78
|
md5_hash.update(chunk)
|
|
75
79
|
pbar.update(len(chunk))
|
|
@@ -195,6 +199,7 @@ class Crypt4GH:
|
|
|
195
199
|
unit_scale=True,
|
|
196
200
|
# unit_divisor=1024, # make use of standard units e.g. KB, MB, etc.
|
|
197
201
|
miniters=1,
|
|
202
|
+
smoothing=TQDM_SMOOTHING,
|
|
198
203
|
),
|
|
199
204
|
) as pbar_in_fd,
|
|
200
205
|
):
|
|
@@ -32,14 +32,16 @@ class S3Options(StrictBaseModel):
|
|
|
32
32
|
The name of the S3 bucket.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
access_key: str
|
|
35
|
+
access_key: str | None = None
|
|
36
36
|
"""
|
|
37
37
|
The access key for the S3 bucket.
|
|
38
|
+
If undefined, it is read from the AWS_ACCESS_KEY_ID environment variable.
|
|
38
39
|
"""
|
|
39
40
|
|
|
40
|
-
secret: str
|
|
41
|
+
secret: str | None = None
|
|
41
42
|
"""
|
|
42
43
|
The secret key for the S3 bucket.
|
|
44
|
+
If undefined, it is read from the AWS_SECRET_ACCESS_KEY environment variable.
|
|
43
45
|
"""
|
|
44
46
|
|
|
45
47
|
session_token: str | None = None
|
|
@@ -85,6 +87,11 @@ class S3Options(StrictBaseModel):
|
|
|
85
87
|
Defaults to None.
|
|
86
88
|
"""
|
|
87
89
|
|
|
90
|
+
multipart_chunksize: int = 256 * 1024**2
|
|
91
|
+
"""
|
|
92
|
+
The size of the chunks to use for multipart uploads in bytes.
|
|
93
|
+
"""
|
|
94
|
+
|
|
88
95
|
|
|
89
96
|
FilePath = Annotated[Path, AfterValidator(lambda v: v.expanduser()), PathType("file")]
|
|
90
97
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common methods for transfering data to and from GRZ buckets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import boto3 # type: ignore[import-untyped]
|
|
6
|
+
from boto3 import client as boto3_client # type: ignore[import-untyped]
|
|
7
|
+
from botocore.config import Config as Boto3Config # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
from .models.config import ConfigModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _empty_str_to_none(string: str | None) -> str | None:
|
|
13
|
+
# if user specifies empty strings, this might be an issue
|
|
14
|
+
if string == "" or string is None:
|
|
15
|
+
return None
|
|
16
|
+
else:
|
|
17
|
+
return string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init_s3_client(config: ConfigModel) -> boto3.session.Session.client:
|
|
21
|
+
"""Create a boto3 Client from a grz-cli configuration."""
|
|
22
|
+
# configure proxies if proxy_url is defined
|
|
23
|
+
proxy_url = config.s3_options.proxy_url
|
|
24
|
+
s3_config = Boto3Config(
|
|
25
|
+
proxies={"http": str(proxy_url), "https": str(proxy_url)} if proxy_url is not None else None,
|
|
26
|
+
request_checksum_calculation=config.s3_options.request_checksum_calculation,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Initialize S3 client for uploading
|
|
30
|
+
s3_client: boto3.session.Session.client = boto3_client(
|
|
31
|
+
service_name="s3",
|
|
32
|
+
region_name=_empty_str_to_none(config.s3_options.region_name),
|
|
33
|
+
api_version=_empty_str_to_none(config.s3_options.api_version),
|
|
34
|
+
use_ssl=config.s3_options.use_ssl,
|
|
35
|
+
endpoint_url=_empty_str_to_none(str(config.s3_options.endpoint_url)),
|
|
36
|
+
aws_access_key_id=_empty_str_to_none(config.s3_options.access_key),
|
|
37
|
+
aws_secret_access_key=_empty_str_to_none(config.s3_options.secret),
|
|
38
|
+
aws_session_token=_empty_str_to_none(config.s3_options.session_token),
|
|
39
|
+
config=s3_config,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return s3_client
|
|
@@ -11,19 +11,17 @@ from os.path import getsize
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import TYPE_CHECKING, override
|
|
13
13
|
|
|
14
|
-
import boto3 # type: ignore[import-untyped]
|
|
15
14
|
import botocore.handlers # type: ignore[import-untyped]
|
|
16
|
-
from boto3 import client as boto3_client # type: ignore[import-untyped]
|
|
17
15
|
from boto3.s3.transfer import S3Transfer, TransferConfig # type: ignore[import-untyped]
|
|
18
|
-
from botocore.config import Config as Boto3Config # type: ignore[import-untyped]
|
|
19
16
|
from tqdm.auto import tqdm
|
|
20
17
|
|
|
18
|
+
from .constants import TQDM_SMOOTHING
|
|
21
19
|
from .models.config import ConfigModel
|
|
22
20
|
from .progress_logging import FileProgressLogger
|
|
23
21
|
from .states import UploadState
|
|
22
|
+
from .transfer import init_s3_client
|
|
24
23
|
|
|
25
|
-
MULTIPART_THRESHOLD = 8 * 1024
|
|
26
|
-
MULTIPART_CHUNKSIZE = 8 * 1024 * 1024 # 8MiB, boto3 default
|
|
24
|
+
MULTIPART_THRESHOLD = 8 * 1024**2 # 8MiB, boto3 default, largely irrelevant
|
|
27
25
|
MULTIPART_MAX_CHUNKS = 1000 # CEPH S3 limit, AWS limit is 10000
|
|
28
26
|
|
|
29
27
|
if TYPE_CHECKING:
|
|
@@ -87,35 +85,7 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
87
85
|
self._config = config
|
|
88
86
|
self._threads = threads
|
|
89
87
|
|
|
90
|
-
self.
|
|
91
|
-
|
|
92
|
-
def _init_s3_client(self):
|
|
93
|
-
# if user specifies empty strings, this might be an issue
|
|
94
|
-
def empty_str_to_none(string: str | None) -> str | None:
|
|
95
|
-
if string == "" or string is None:
|
|
96
|
-
return None
|
|
97
|
-
else:
|
|
98
|
-
return string
|
|
99
|
-
|
|
100
|
-
# configure proxies if proxy_url is defined
|
|
101
|
-
proxy_url = self._config.s3_options.proxy_url
|
|
102
|
-
config = Boto3Config(
|
|
103
|
-
proxies={"http": str(proxy_url), "https": str(proxy_url)} if proxy_url is not None else None,
|
|
104
|
-
request_checksum_calculation=self._config.s3_options.request_checksum_calculation,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Initialize S3 client for uploading
|
|
108
|
-
self._s3_client: boto3.session.Session.client = boto3_client(
|
|
109
|
-
service_name="s3",
|
|
110
|
-
region_name=empty_str_to_none(self._config.s3_options.region_name),
|
|
111
|
-
api_version=empty_str_to_none(self._config.s3_options.api_version),
|
|
112
|
-
use_ssl=self._config.s3_options.use_ssl,
|
|
113
|
-
endpoint_url=empty_str_to_none(str(self._config.s3_options.endpoint_url)),
|
|
114
|
-
aws_access_key_id=empty_str_to_none(self._config.s3_options.access_key),
|
|
115
|
-
aws_secret_access_key=empty_str_to_none(self._config.s3_options.secret),
|
|
116
|
-
aws_session_token=empty_str_to_none(self._config.s3_options.session_token),
|
|
117
|
-
config=config,
|
|
118
|
-
)
|
|
88
|
+
self._s3_client = init_s3_client(config)
|
|
119
89
|
|
|
120
90
|
@override
|
|
121
91
|
def upload_file(self, local_file_path, s3_object_id):
|
|
@@ -127,11 +97,12 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
127
97
|
self.__log.info(f"Uploading {local_file_path} to {s3_object_id}...")
|
|
128
98
|
|
|
129
99
|
filesize = getsize(local_file_path)
|
|
100
|
+
multipart_chunksize = self._config.s3_options.multipart_chunksize
|
|
130
101
|
|
|
131
102
|
chunksize = (
|
|
132
103
|
math.ceil(filesize / MULTIPART_MAX_CHUNKS)
|
|
133
|
-
if filesize /
|
|
134
|
-
else
|
|
104
|
+
if filesize / multipart_chunksize > MULTIPART_MAX_CHUNKS
|
|
105
|
+
else multipart_chunksize
|
|
135
106
|
)
|
|
136
107
|
self.__log.debug(
|
|
137
108
|
f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {math.ceil(filesize / chunksize)} chunk(s)"
|
|
@@ -141,10 +112,11 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
141
112
|
multipart_threshold=MULTIPART_THRESHOLD,
|
|
142
113
|
multipart_chunksize=chunksize,
|
|
143
114
|
max_concurrency=self._threads,
|
|
115
|
+
use_threads=self._threads > 1,
|
|
144
116
|
)
|
|
145
117
|
|
|
146
118
|
transfer = S3Transfer(self._s3_client, config)
|
|
147
|
-
progress_bar = tqdm(total=filesize, unit="B", unit_scale=True, unit_divisor=1024)
|
|
119
|
+
progress_bar = tqdm(total=filesize, unit="B", unit_scale=True, unit_divisor=1024, smoothing=TQDM_SMOOTHING)
|
|
148
120
|
transfer.upload_file(
|
|
149
121
|
local_file_path,
|
|
150
122
|
self._config.s3_options.bucket,
|
|
@@ -152,6 +124,25 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
152
124
|
callback=lambda bytes_transferred: progress_bar.update(bytes_transferred),
|
|
153
125
|
)
|
|
154
126
|
|
|
127
|
+
def _remote_id_exists(self, s3_object_id: str) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Determine if a remote ID already exists
|
|
130
|
+
:param s3_object_id: Remote S3 object ID under which the file should be stored
|
|
131
|
+
"""
|
|
132
|
+
exists = True
|
|
133
|
+
try:
|
|
134
|
+
self._s3_client.head_object(Bucket=self._config.s3_options.bucket, Key=s3_object_id)
|
|
135
|
+
except self._s3_client.exceptions.NoSuchKey:
|
|
136
|
+
exists = False
|
|
137
|
+
except botocore.exceptions.ClientError as error:
|
|
138
|
+
if error.response["Error"]["Code"] in {"403", "404"}:
|
|
139
|
+
# backend can return forbidden instead if user has no ListBucket permission
|
|
140
|
+
exists = False
|
|
141
|
+
else:
|
|
142
|
+
raise error
|
|
143
|
+
|
|
144
|
+
return exists
|
|
145
|
+
|
|
155
146
|
@override
|
|
156
147
|
def upload(self, encrypted_submission: EncryptedSubmission):
|
|
157
148
|
"""
|
|
@@ -160,6 +151,10 @@ class S3BotoUploadWorker(UploadWorker):
|
|
|
160
151
|
"""
|
|
161
152
|
progress_logger = FileProgressLogger[UploadState](self._status_file_path)
|
|
162
153
|
metadata_file_path, metadata_s3_object_id = encrypted_submission.get_metadata_file_path_and_object_id()
|
|
154
|
+
|
|
155
|
+
if self._remote_id_exists(metadata_s3_object_id):
|
|
156
|
+
raise UploadError("Submission already uploaded. Corrections, additions, and followups require a new tanG.")
|
|
157
|
+
|
|
163
158
|
files_to_upload = encrypted_submission.get_encrypted_files_and_object_id()
|
|
164
159
|
files_to_upload[metadata_file_path] = metadata_s3_object_id
|
|
165
160
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Author-email: Koray Kirli <koraykirli@gmail.com>, Mathias Lesche <mathias.lesche@tu-dresden.de>, "Florian R. Hölzlwimmer" <git.ich@frhoelzlwimmer.de>, Till Hartmann <till.hartmann@bih-charite.de>, Thomas Sell <thomas.sell@bih-charite.de>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://codebase.helmholtz.cloud/grz-mv-genomseq/grz-cli
|
|
8
8
|
Keywords: GRZ,GDC,S3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Requires-Python: >=3.12
|
|
13
12
|
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
14
|
Requires-Dist: boto3<2,>=1.36
|
|
15
15
|
Requires-Dist: click<9,>=8.1.7
|
|
16
16
|
Requires-Dist: crypt4gh<2,>=1.7
|
|
@@ -21,6 +21,7 @@ Requires-Dist: pydantic<2.10,>=2.9.2
|
|
|
21
21
|
Requires-Dist: platformdirs<5,>=4.3.6
|
|
22
22
|
Requires-Dist: grz-pydantic-models>=1.2.1
|
|
23
23
|
Requires-Dist: pysam==0.23.*
|
|
24
|
+
Requires-Dist: rich==13.*
|
|
24
25
|
Provides-Extra: test
|
|
25
26
|
Requires-Dist: pytest<9,>=8.3.3; extra == "test"
|
|
26
27
|
Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
|
|
@@ -29,6 +30,7 @@ Requires-Dist: moto[s3]<6,>=5.0.16; extra == "test"
|
|
|
29
30
|
Requires-Dist: pytest-sugar<2,>=1.0.0; extra == "test"
|
|
30
31
|
Requires-Dist: ruff<1,>=0.9.6; extra == "test"
|
|
31
32
|
Requires-Dist: numpy<3,>=2.1.2; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
32
34
|
|
|
33
35
|
# GRZ CLI
|
|
34
36
|
|
|
@@ -42,10 +42,11 @@ def test_validate_submission(
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
runner = CliRunner()
|
|
45
|
-
|
|
45
|
+
cli = grz_cli.cli.build_cli()
|
|
46
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
46
47
|
|
|
47
48
|
# check if re-validation is skipped
|
|
48
|
-
result = runner.invoke(
|
|
49
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
49
50
|
|
|
50
51
|
# test if command has correctly checked for:
|
|
51
52
|
# - mismatched md5sums
|
|
@@ -75,7 +76,8 @@ def test_encrypt_decrypt_submission(
|
|
|
75
76
|
]
|
|
76
77
|
|
|
77
78
|
runner = CliRunner()
|
|
78
|
-
|
|
79
|
+
cli = grz_cli.cli.build_cli(grz_mode=True)
|
|
80
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
79
81
|
|
|
80
82
|
assert result.exit_code == 0, result.output
|
|
81
83
|
|
|
@@ -89,7 +91,7 @@ def test_encrypt_decrypt_submission(
|
|
|
89
91
|
]
|
|
90
92
|
|
|
91
93
|
runner = CliRunner()
|
|
92
|
-
result = runner.invoke(
|
|
94
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
93
95
|
|
|
94
96
|
assert result.exit_code == 0, result.output
|
|
95
97
|
|
|
@@ -126,7 +128,8 @@ def test_decrypt_submission(working_dir_path, temp_config_file_path):
|
|
|
126
128
|
temp_config_file_path,
|
|
127
129
|
]
|
|
128
130
|
runner = CliRunner()
|
|
129
|
-
|
|
131
|
+
cli = grz_cli.cli.build_cli(grz_mode=True)
|
|
132
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
130
133
|
|
|
131
134
|
assert result.exit_code == 0, result.output
|
|
132
135
|
|
|
@@ -204,7 +207,8 @@ def test_upload_download_submission(
|
|
|
204
207
|
"grz_cli.models.config.S3Options.__getattr__",
|
|
205
208
|
lambda self, name: None if name == "endpoint_url" else AttributeError,
|
|
206
209
|
):
|
|
207
|
-
|
|
210
|
+
cli = grz_cli.cli.build_cli(grz_mode=True)
|
|
211
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
208
212
|
|
|
209
213
|
assert result.exit_code == 0, result.output
|
|
210
214
|
assert len(result.output) != 0, result.stderr
|
|
@@ -226,8 +230,8 @@ def test_upload_download_submission(
|
|
|
226
230
|
temp_config_file_path,
|
|
227
231
|
]
|
|
228
232
|
runner = CliRunner()
|
|
229
|
-
|
|
230
|
-
result = runner.invoke(
|
|
233
|
+
cli = grz_cli.cli.build_cli(grz_mode=True)
|
|
234
|
+
result = runner.invoke(cli, testargs, catch_exceptions=False)
|
|
231
235
|
|
|
232
236
|
assert result.exit_code == 0, result.output
|
|
233
237
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0"
|
|
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
|