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.
Files changed (39) hide show
  1. {grz_cli-0.1.2 → grz_cli-0.1.4}/MANIFEST.in +1 -0
  2. {grz_cli-0.1.2/src/grz_cli.egg-info → grz_cli-0.1.4}/PKG-INFO +3 -4
  3. {grz_cli-0.1.2 → grz_cli-0.1.4}/pyproject.toml +17 -5
  4. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/cli.py +1 -3
  5. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/download.py +7 -22
  6. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/fastq_validation.py +3 -9
  7. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/file_operations.py +4 -12
  8. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/config.py +3 -11
  9. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/metadata.py +225 -95
  10. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/parser.py +43 -116
  11. grz_cli-0.1.4/src/grz_cli/resources/grz-schema.json +595 -0
  12. grz_cli-0.1.4/src/grz_cli/resources/thresholds.json +299 -0
  13. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/upload.py +7 -22
  14. {grz_cli-0.1.2 → grz_cli-0.1.4/src/grz_cli.egg-info}/PKG-INFO +3 -4
  15. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/SOURCES.txt +1 -0
  16. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/requires.txt +1 -1
  17. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_cli.py +24 -42
  18. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_download.py +8 -19
  19. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_fastq_validation.py +1 -5
  20. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_file_operations.py +2 -6
  21. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_file_progress_logging.py +1 -3
  22. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_parser.py +8 -10
  23. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_upload.py +25 -37
  24. grz_cli-0.1.2/src/grz_cli/resources/grz-schema.json +0 -560
  25. {grz_cli-0.1.2 → grz_cli-0.1.4}/LICENSE +0 -0
  26. {grz_cli-0.1.2 → grz_cli-0.1.4}/README.md +0 -0
  27. {grz_cli-0.1.2 → grz_cli-0.1.4}/setup.cfg +0 -0
  28. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/__init__.py +0 -0
  29. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/__init__.py +0 -0
  30. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/constants.py +0 -0
  31. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/logging_setup.py +0 -0
  32. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/__init__.py +0 -0
  33. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/__init__.py +0 -0
  34. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/progress_logging.py +0 -0
  35. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli/states.py +0 -0
  36. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/dependency_links.txt +0 -0
  37. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/entry_points.txt +0 -0
  38. {grz_cli-0.1.2 → grz_cli-0.1.4}/src/grz_cli.egg-info/top_level.txt +0 -0
  39. {grz_cli-0.1.2 → grz_cli-0.1.4}/tests/test_read_multiple_json.py +0 -0
@@ -1 +1,2 @@
1
1
  include src/grz_cli/resources/grz-schema.json
2
+ include src/grz_cli/resources/thresholds.json
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: grz-cli
3
- Version: 0.1.2
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.8.1; extra == "test"
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.2"
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.8.1,<1",
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 = 88
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.8.1",
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