grz-cli 0.1.2__tar.gz → 0.1.4__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.2 → grz_cli-0.1.4}/MANIFEST.in +1 -0
- {grz_cli-0.1.2/src/grz_cli.egg-info → grz_cli-0.1.4}/PKG-INFO +3 -4
- {grz_cli-0.1.2 → grz_cli-0.1.4}/pyproject.toml +17 -5
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/cli.py +1 -3
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/download.py +7 -22
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/fastq_validation.py +3 -9
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/file_operations.py +4 -12
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/config.py +3 -11
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/metadata.py +225 -95
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/parser.py +43 -116
- grz_cli-0.1.4/src/grz_cli/resources/grz-schema.json +595 -0
- grz_cli-0.1.4/src/grz_cli/resources/thresholds.json +299 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/upload.py +7 -22
- {grz_cli-0.1.2 → grz_cli-0.1.4/src/grz_cli.egg-info}/PKG-INFO +3 -4
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/SOURCES.txt +1 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/requires.txt +1 -1
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_cli.py +24 -42
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_download.py +8 -19
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_fastq_validation.py +1 -5
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_file_operations.py +2 -6
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_file_progress_logging.py +1 -3
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_parser.py +8 -10
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_upload.py +25 -37
- grz_cli-0.1.2/src/grz_cli/resources/grz-schema.json +0 -560
- {grz_cli-0.1.2 → grz_cli-0.1.4}/LICENSE +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/README.md +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/setup.cfg +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/__init__.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/__init__.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/constants.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/logging_setup.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/__init__.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/progress_logging.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/states.py +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/dependency_links.txt +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/entry_points.txt +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/top_level.txt +0 -0
- {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_read_multiple_json.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Author-email: Koray Kirli <koraykirli@gmail.com>, Mathias Lesche <mathias.lesche@tu-dresden.de>, "Florian R. Hölzlwimmer" <git.ich@frhoelzlwimmer.de>, Till Hartmann <till.hartmann@bih-charite.de>, Thomas Sell <thomas.sell@bih-charite.de>
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
14
|
Requires-Dist: boto3<2,>=1.35.39
|
|
16
15
|
Requires-Dist: click<9,>=8.1.7
|
|
17
16
|
Requires-Dist: crypt4gh<2,>=1.7
|
|
@@ -26,7 +25,7 @@ Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
|
|
|
26
25
|
Requires-Dist: pytest-mock<4,>=3.14.0; extra == "test"
|
|
27
26
|
Requires-Dist: moto[s3]<6,>=5.0.16; extra == "test"
|
|
28
27
|
Requires-Dist: pytest-sugar<2,>=1.0.0; extra == "test"
|
|
29
|
-
Requires-Dist: ruff<1,>=0.
|
|
28
|
+
Requires-Dist: ruff<1,>=0.9.6; extra == "test"
|
|
30
29
|
Requires-Dist: numpy<3,>=2.1.2; extra == "test"
|
|
31
30
|
|
|
32
31
|
# GRZ CLI
|
|
@@ -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.4"
|
|
8
8
|
description = "Tool for validation, encryption and upload of MV submissions to GDCs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -46,7 +46,7 @@ test = [
|
|
|
46
46
|
"pytest-mock >=3.14.0,<4",
|
|
47
47
|
"moto[s3] >=5.0.16,<6",
|
|
48
48
|
"pytest-sugar >=1.0.0,<2",
|
|
49
|
-
"ruff >=0.
|
|
49
|
+
"ruff >=0.9.6,<1",
|
|
50
50
|
"numpy >=2.1.2,<3"
|
|
51
51
|
]
|
|
52
52
|
|
|
@@ -76,7 +76,7 @@ exclude = [
|
|
|
76
76
|
"dist",
|
|
77
77
|
"out"
|
|
78
78
|
]
|
|
79
|
-
line-length =
|
|
79
|
+
line-length = 120
|
|
80
80
|
src = [
|
|
81
81
|
"src",
|
|
82
82
|
"tests"
|
|
@@ -149,26 +149,30 @@ convention = "pep257"
|
|
|
149
149
|
|
|
150
150
|
[tool.tox]
|
|
151
151
|
requires = ["tox>=4.23"]
|
|
152
|
-
env_list = ["format-check", "lints", "mypy", "3.12"]
|
|
152
|
+
env_list = ["format-check", "lints", "mypy", "3.12", "3.13"]
|
|
153
153
|
allowlist_externals = ["pytest", "ruff"]
|
|
154
154
|
isolated_build = true
|
|
155
155
|
|
|
156
156
|
[tool.tox.env_run_base]
|
|
157
|
+
runner = "uv-venv-lock-runner"
|
|
157
158
|
description = "run unit tests"
|
|
158
159
|
dependency_groups = ["test"]
|
|
159
160
|
commands = [["pytest", { replace = "posargs", default = ["tests"], extend = true }]]
|
|
160
161
|
|
|
161
162
|
[tool.tox.env."format-check"]
|
|
163
|
+
runner = "uv-venv-lock-runner"
|
|
162
164
|
skip_install = true
|
|
163
165
|
dependency_groups = ["lint"]
|
|
164
166
|
commands = [["ruff", "format", "--check"]]
|
|
165
167
|
|
|
166
168
|
[tool.tox.env."lints"]
|
|
169
|
+
runner = "uv-venv-lock-runner"
|
|
167
170
|
skip_install = true
|
|
168
171
|
dependency_groups = ["lint"]
|
|
169
172
|
commands = [["ruff", "check"]]
|
|
170
173
|
|
|
171
174
|
[tool.tox.env."mypy"]
|
|
175
|
+
runner = "uv-venv-lock-runner"
|
|
172
176
|
skip_install = false
|
|
173
177
|
dependency_groups = ["lint"]
|
|
174
178
|
commands = [["mypy"]]
|
|
@@ -177,8 +181,9 @@ commands = [["mypy"]]
|
|
|
177
181
|
dev-dependencies = [
|
|
178
182
|
"datamodel-code-generator>=0.26.2",
|
|
179
183
|
"mypy>=1.13.0",
|
|
180
|
-
"ruff>=0.
|
|
184
|
+
"ruff>=0.9.6",
|
|
181
185
|
"tox>=4.23.2",
|
|
186
|
+
"tox-uv>=1.20.1",
|
|
182
187
|
"types-pyyaml>=6.0.12.20240917",
|
|
183
188
|
"types-tqdm>=4.66.0.20240417",
|
|
184
189
|
]
|
|
@@ -188,3 +193,10 @@ python_version = "3.12"
|
|
|
188
193
|
plugins = "pydantic.mypy"
|
|
189
194
|
packages = "src"
|
|
190
195
|
|
|
196
|
+
[tool.pytest.ini_options]
|
|
197
|
+
addopts = [
|
|
198
|
+
"--import-mode=importlib",
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
[tool.setuptools]
|
|
202
|
+
license-files = []
|
|
@@ -48,9 +48,7 @@ DIR_RW_C = click.Path(
|
|
|
48
48
|
writable=True,
|
|
49
49
|
resolve_path=True,
|
|
50
50
|
)
|
|
51
|
-
FILE_R_E = click.Path(
|
|
52
|
-
exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True
|
|
53
|
-
)
|
|
51
|
+
FILE_R_E = click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True)
|
|
54
52
|
|
|
55
53
|
submission_dir = click.option(
|
|
56
54
|
"--submission-dir",
|
|
@@ -74,10 +74,7 @@ class S3BotoDownloadWorker:
|
|
|
74
74
|
|
|
75
75
|
# configure proxies if proxy_url is defined
|
|
76
76
|
proxy_url = empty_str_to_none(self._config.s3_options.proxy_url)
|
|
77
|
-
if proxy_url is not None
|
|
78
|
-
config = Boto3Config(proxies={"http": proxy_url, "https": proxy_url})
|
|
79
|
-
else:
|
|
80
|
-
config = None
|
|
77
|
+
config = Boto3Config(proxies={"http": proxy_url, "https": proxy_url}) if proxy_url is not None else None
|
|
81
78
|
|
|
82
79
|
# Initialize S3 client for uploading
|
|
83
80
|
self._s3_client: boto3.session.Session.client = boto3_client(
|
|
@@ -149,9 +146,7 @@ class S3BotoDownloadWorker:
|
|
|
149
146
|
if filesize / MULTIPART_CHUNKSIZE > MULTIPART_MAX_CHUNKS
|
|
150
147
|
else MULTIPART_CHUNKSIZE
|
|
151
148
|
)
|
|
152
|
-
self.__log.debug(
|
|
153
|
-
f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize/chunksize} chunks"
|
|
154
|
-
)
|
|
149
|
+
self.__log.debug(f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize / chunksize} chunks")
|
|
155
150
|
|
|
156
151
|
config = TransferConfig(
|
|
157
152
|
multipart_threshold=MULTIPART_THRESHOLD,
|
|
@@ -160,9 +155,7 @@ class S3BotoDownloadWorker:
|
|
|
160
155
|
)
|
|
161
156
|
|
|
162
157
|
transfer = S3Transfer(self._s3_client, config)
|
|
163
|
-
progress_bar = tqdm(
|
|
164
|
-
total=filesize, unit="B", unit_scale=True, unit_divisor=1024
|
|
165
|
-
)
|
|
158
|
+
progress_bar = tqdm(total=filesize, unit="B", unit_scale=True, unit_divisor=1024)
|
|
166
159
|
transfer.download_file(
|
|
167
160
|
self._config.s3_options.bucket,
|
|
168
161
|
s3_object_id,
|
|
@@ -192,15 +185,11 @@ class S3BotoDownloadWorker:
|
|
|
192
185
|
file_path = Path(file_key).relative_to(encrypted_files_key_prefix)
|
|
193
186
|
full_path = encrypted_submission.encrypted_files_dir / file_path
|
|
194
187
|
if full_path not in encrypted_submission.encrypted_files:
|
|
195
|
-
raise DownloadError(
|
|
196
|
-
f"File {file_path} not listed in metadata.json"
|
|
197
|
-
)
|
|
188
|
+
raise DownloadError(f"File {file_path} not listed in metadata.json")
|
|
198
189
|
file_metadata = encrypted_submission.encrypted_files[full_path]
|
|
199
190
|
logged_state = progress_logger.get_state(full_path, file_metadata)
|
|
200
191
|
|
|
201
|
-
if (logged_state is None) or not logged_state.get(
|
|
202
|
-
"download_successful", False
|
|
203
|
-
):
|
|
192
|
+
if (logged_state is None) or not logged_state.get("download_successful", False):
|
|
204
193
|
self.__log.info(
|
|
205
194
|
"Download file: '%s' -> '%s'",
|
|
206
195
|
file_key,
|
|
@@ -208,9 +197,7 @@ class S3BotoDownloadWorker:
|
|
|
208
197
|
)
|
|
209
198
|
|
|
210
199
|
try:
|
|
211
|
-
full_path.parent.mkdir(
|
|
212
|
-
mode=0o770, parents=True, exist_ok=True
|
|
213
|
-
)
|
|
200
|
+
full_path.parent.mkdir(mode=0o770, parents=True, exist_ok=True)
|
|
214
201
|
self.download_file(str(full_path), file_key, file["Size"])
|
|
215
202
|
|
|
216
203
|
self.__log.info(f"Download complete for {str(full_path)}. ")
|
|
@@ -225,9 +212,7 @@ class S3BotoDownloadWorker:
|
|
|
225
212
|
progress_logger.set_state(
|
|
226
213
|
full_path,
|
|
227
214
|
file_metadata,
|
|
228
|
-
state=DownloadState(
|
|
229
|
-
download_successful=False, errors=[str(e)]
|
|
230
|
-
),
|
|
215
|
+
state=DownloadState(download_successful=False, errors=[str(e)]),
|
|
231
216
|
)
|
|
232
217
|
|
|
233
218
|
raise e
|
|
@@ -29,9 +29,7 @@ def is_gzipped(file_path: str | PathLike) -> bool:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@contextmanager
|
|
32
|
-
def open_fastq(
|
|
33
|
-
file_path: str | PathLike, progress=True
|
|
34
|
-
) -> Generator[TextIO, None, None]:
|
|
32
|
+
def open_fastq(file_path: str | PathLike, progress=True) -> Generator[TextIO, None, None]:
|
|
35
33
|
"""
|
|
36
34
|
Open a FASTQ file, handling both regular and gzipped formats.
|
|
37
35
|
|
|
@@ -103,9 +101,7 @@ def validate_fastq_file(fastq_file: str | PathLike) -> tuple[int, set[int], list
|
|
|
103
101
|
|
|
104
102
|
# Check if the number of lines in a FASTQ file is a multiple of 4.
|
|
105
103
|
if num_lines % 4 != 0:
|
|
106
|
-
errors.append(
|
|
107
|
-
f"{fastq_file}: Number of lines is not a multiple of 4! Found {num_lines} lines."
|
|
108
|
-
)
|
|
104
|
+
errors.append(f"{fastq_file}: Number of lines is not a multiple of 4! Found {num_lines} lines.")
|
|
109
105
|
else:
|
|
110
106
|
log.debug("%s: %s lines", fastq_file, num_lines)
|
|
111
107
|
|
|
@@ -132,9 +128,7 @@ def validate_single_end_reads(fastq_file: str | PathLike) -> Generator[str]:
|
|
|
132
128
|
yield from errors
|
|
133
129
|
|
|
134
130
|
|
|
135
|
-
def validate_paired_end_reads(
|
|
136
|
-
fastq_file1: str | PathLike, fastq_file2: str | PathLike
|
|
137
|
-
) -> Generator[str]:
|
|
131
|
+
def validate_paired_end_reads(fastq_file1: str | PathLike, fastq_file2: str | PathLike) -> Generator[str]:
|
|
138
132
|
"""
|
|
139
133
|
Validate two paired-end FASTQ files.
|
|
140
134
|
|
|
@@ -69,9 +69,7 @@ def calculate_md5(file_path, chunk_size=2**16, progress=True) -> str:
|
|
|
69
69
|
md5_hash = hashlib.md5() # noqa: S324
|
|
70
70
|
with open(file_path, "rb") as f:
|
|
71
71
|
if progress and (total_size > chunk_size):
|
|
72
|
-
with tqdm(
|
|
73
|
-
total=total_size, unit="B", unit_scale=True, desc="Calculating MD5"
|
|
74
|
-
) as pbar:
|
|
72
|
+
with tqdm(total=total_size, unit="B", unit_scale=True, desc="Calculating MD5") as pbar:
|
|
75
73
|
while chunk := f.read(chunk_size):
|
|
76
74
|
md5_hash.update(chunk)
|
|
77
75
|
pbar.update(len(chunk))
|
|
@@ -117,9 +115,7 @@ def read_multiple_json(input: TextIO, buffer_size=65536, max_buffer_size=1342177
|
|
|
117
115
|
raise ValueError("Remaining data is not empty. Is there invalid JSON?")
|
|
118
116
|
|
|
119
117
|
|
|
120
|
-
def is_relative_subdirectory(
|
|
121
|
-
relative_path: str | PathLike, root_directory: str | PathLike
|
|
122
|
-
) -> bool:
|
|
118
|
+
def is_relative_subdirectory(relative_path: str | PathLike, root_directory: str | PathLike) -> bool:
|
|
123
119
|
"""
|
|
124
120
|
Check if the target path is a subdirectory of the root path
|
|
125
121
|
using os.path.commonpath() without checking the file system.
|
|
@@ -223,9 +219,7 @@ class Crypt4GH:
|
|
|
223
219
|
if passphrase:
|
|
224
220
|
passphrase_callback = lambda: passphrase
|
|
225
221
|
else:
|
|
226
|
-
passphrase_callback = partial(
|
|
227
|
-
getpass, prompt=f"Passphrase for {seckey_path}: "
|
|
228
|
-
)
|
|
222
|
+
passphrase_callback = partial(getpass, prompt=f"Passphrase for {seckey_path}: ")
|
|
229
223
|
|
|
230
224
|
return crypt4gh.keys.get_private_key(seckeypath, passphrase_callback)
|
|
231
225
|
|
|
@@ -254,9 +248,7 @@ class Crypt4GH:
|
|
|
254
248
|
) as pbar_in_fd,
|
|
255
249
|
):
|
|
256
250
|
crypt4gh.lib.decrypt(
|
|
257
|
-
keys=[
|
|
258
|
-
(0, private_key, None)
|
|
259
|
-
], # list of (method, privkey, recipient_pubkey=None),
|
|
251
|
+
keys=[(0, private_key, None)], # list of (method, privkey, recipient_pubkey=None),
|
|
260
252
|
infile=pbar_in_fd,
|
|
261
253
|
outfile=out_fd,
|
|
262
254
|
)
|
|
@@ -97,22 +97,14 @@ class ConfigModel(StrictBaseModel):
|
|
|
97
97
|
@field_validator("grz_public_key")
|
|
98
98
|
@classmethod
|
|
99
99
|
def check_grz_public_key(cls, v):
|
|
100
|
-
if
|
|
101
|
-
v is not None
|
|
102
|
-
and "BEGIN CRYPT4GH PUBLIC KEY" not in v
|
|
103
|
-
and "END CRYPT4GH PUBLIC KEY" not in v
|
|
104
|
-
):
|
|
100
|
+
if v is not None and "BEGIN CRYPT4GH PUBLIC KEY" not in v and "END CRYPT4GH PUBLIC KEY" not in v:
|
|
105
101
|
raise ValueError("Invalid public key format")
|
|
106
102
|
return v
|
|
107
103
|
|
|
108
104
|
@model_validator(mode="after")
|
|
109
105
|
def validate_grz_public_key(self) -> Self:
|
|
110
106
|
if self.grz_public_key is None and self.grz_public_key_path is None:
|
|
111
|
-
raise ValueError(
|
|
112
|
-
"Either grz_public_key or grz_public_key_path must be set."
|
|
113
|
-
)
|
|
107
|
+
raise ValueError("Either grz_public_key or grz_public_key_path must be set.")
|
|
114
108
|
if self.grz_public_key is not None and self.grz_public_key_path is not None:
|
|
115
|
-
raise ValueError(
|
|
116
|
-
"Only one of grz_public_key or grz_public_key_path must be set."
|
|
117
|
-
)
|
|
109
|
+
raise ValueError("Only one of grz_public_key or grz_public_key_path must be set.")
|
|
118
110
|
return self
|