grz-cli 1.7.2__tar.gz → 2.0.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-1.7.2 → grz_cli-2.0.0}/CHANGELOG.md +21 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/PKG-INFO +3 -3
- {grz_cli-1.7.2 → grz_cli-2.0.0}/pyproject.toml +1 -1
- grz_cli-2.0.0/src/grz_cli/__init__.py +1 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/validate.py +1 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/utils/version_check.py +29 -7
- grz_cli-2.0.0/tests/test_submit.py +105 -0
- grz_cli-1.7.2/src/grz_cli/__init__.py +0 -1
- grz_cli-1.7.2/tests/test_submit.py +0 -71
- {grz_cli-1.7.2 → grz_cli-2.0.0}/.gitignore +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/LICENSE +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/MANIFEST.in +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/README.md +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/docs/version-compatibility-check.md +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/__init__.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/cli.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/__init__.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/encrypt.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/get_id.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/submit.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/upload.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/models/config.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/py.typed +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/tests/conftest.py +0 -0
- {grz_cli-1.7.2 → grz_cli-2.0.0}/tests/test_smoke.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v1.8.0...grz-cli-v2.0.0) (2026-08-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **grzctl:** grzctl requires grz-db 3 and grz-cli 2, and so applies the same consent rules when it reads or backfills submission metadata.
|
|
9
|
+
* **grz-cli:** grz-cli requires grz-common 3. Metadata naming a consent dateTime outside the FHIR grammar, or leaving a 2025 researchConsent provision period open-ended, is now rejected rather than accepted.
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **grz-cli:** require grz-common 3 ([#649](https://github.com/BfArM-MVH/grz-tools/issues/649)) ([69b9c17](https://github.com/BfArM-MVH/grz-tools/commit/69b9c171dd1e248bb38bbc6822dc4e7f3857accc))
|
|
14
|
+
* **grzctl:** require grz-db 3 and grz-cli 2 ([69b9c17](https://github.com/BfArM-MVH/grz-tools/commit/69b9c171dd1e248bb38bbc6822dc4e7f3857accc))
|
|
15
|
+
|
|
16
|
+
## [1.8.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v1.7.2...grz-cli-v1.8.0) (2026-08-13)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **grz-db:** ship reusable database fixtures for downstream tests ([#647](https://github.com/BfArM-MVH/grz-tools/issues/647)) ([a72caba](https://github.com/BfArM-MVH/grz-tools/commit/a72caba963512876be92ac4a8469b931fd1c3767))
|
|
22
|
+
* **grzctl, grz-db, grz-common:** Metadata schema version check via version.json ([#624](https://github.com/BfArM-MVH/grz-tools/issues/624)) ([c6f1e7c](https://github.com/BfArM-MVH/grz-tools/commit/c6f1e7ce07cd5772beee5551e0b95b02a9f83914))
|
|
23
|
+
|
|
3
24
|
## [1.7.2](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v1.7.1...grz-cli-v1.7.2) (2026-06-02)
|
|
4
25
|
|
|
5
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/BfArM-MVH/grz-tools
|
|
6
6
|
Project-URL: Repository, https://github.com/BfArM-MVH/grz-tools
|
|
@@ -13,7 +13,7 @@ Keywords: GDC,GRZ,S3
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Requires-Python: >=3.12
|
|
16
|
-
Requires-Dist: grz-common<3
|
|
16
|
+
Requires-Dist: grz-common<4,>=3
|
|
17
17
|
Requires-Dist: packaging<27,>=23
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.0"
|
|
@@ -43,10 +43,30 @@ def check_version_and_exit_if_needed(
|
|
|
43
43
|
version_file = VersionFile.from_s3(s3_options, version_file_key)
|
|
44
44
|
|
|
45
45
|
current_version = pkg_version.Version(version("grz-cli"))
|
|
46
|
-
|
|
46
|
+
_check_policy_and_exit_if_needed("grz-cli", current_version, version_file.grzcli_version)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_metadata_version_and_exit_if_needed(
|
|
50
|
+
s3_options: S3Options,
|
|
51
|
+
metadata_schema_version: str,
|
|
52
|
+
version_file_key: str = "version.json",
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Validate the metadata schema version against the policy defined in version.json."""
|
|
55
|
+
version_file = VersionFile.from_s3(s3_options, version_file_key)
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
current_version = pkg_version.Version(metadata_schema_version)
|
|
58
|
+
_check_policy_and_exit_if_needed("metadata", current_version, version_file.metadata_version)
|
|
49
59
|
|
|
60
|
+
|
|
61
|
+
def _check_policy_and_exit_if_needed(
|
|
62
|
+
subject: str,
|
|
63
|
+
current_version: pkg_version.Version,
|
|
64
|
+
policies: list[VersionInfo],
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Validate a version against the active policy for a subject."""
|
|
67
|
+
now = datetime.now(UTC)
|
|
68
|
+
|
|
69
|
+
policy = _select_active_policy(policies, now)
|
|
50
70
|
if policy is None:
|
|
51
71
|
logger.debug("No active version policy found — skipping version check.")
|
|
52
72
|
return
|
|
@@ -56,7 +76,7 @@ def check_version_and_exit_if_needed(
|
|
|
56
76
|
max_version = policy.max_version
|
|
57
77
|
enforced_from = policy.enforced_from
|
|
58
78
|
|
|
59
|
-
logger.debug(f"Current
|
|
79
|
+
logger.debug(f"Current {subject} version: {current_version}")
|
|
60
80
|
logger.debug(
|
|
61
81
|
f"Active policy: "
|
|
62
82
|
f"minimal={minimal_version}, "
|
|
@@ -68,22 +88,24 @@ def check_version_and_exit_if_needed(
|
|
|
68
88
|
# old version
|
|
69
89
|
if current_version < minimal_version:
|
|
70
90
|
logger.error(
|
|
71
|
-
f"Your
|
|
91
|
+
f"Your {subject} version ({current_version}) is not supported. Minimum required version is {minimal_version}."
|
|
72
92
|
)
|
|
73
93
|
sys.exit(1)
|
|
74
94
|
|
|
75
95
|
# supported but behind recommended — skip if recommended_version not set
|
|
76
96
|
if recommended_version is not None and minimal_version <= current_version < recommended_version:
|
|
77
97
|
logger.warning(
|
|
78
|
-
f"You are using
|
|
98
|
+
f"You are using {subject} {current_version}, but the recommended version is "
|
|
79
99
|
f"{recommended_version}. Upgrading is strongly recommended."
|
|
80
100
|
)
|
|
81
101
|
return
|
|
82
102
|
|
|
83
103
|
# too new — skip if max_version not set
|
|
84
104
|
if max_version is not None and current_version > max_version:
|
|
85
|
-
logger.error(
|
|
105
|
+
logger.error(
|
|
106
|
+
f"{subject} version {current_version} is newer than the maximum supported version ({max_version})."
|
|
107
|
+
)
|
|
86
108
|
sys.exit(1)
|
|
87
109
|
|
|
88
110
|
# best case
|
|
89
|
-
logger.info(f"
|
|
111
|
+
logger.info(f"{subject} {current_version} is within the supported and tested range.")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Integration tests for the ``grz-cli submit`` command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click.testing
|
|
6
|
+
import pytest
|
|
7
|
+
import yaml
|
|
8
|
+
from grz_cli.cli import build_cli
|
|
9
|
+
from pytest_mock import MockerFixture
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def stub_subcommands(mocker: MockerFixture):
|
|
14
|
+
"""Replace the subcommands ``submit`` invokes with no-op mocks.
|
|
15
|
+
|
|
16
|
+
``ctx.invoke`` accepts any callable when the target is not a click
|
|
17
|
+
``Command``, so swapping the module-level imports lets the real submit
|
|
18
|
+
callback run end-to-end without executing real validation, encryption,
|
|
19
|
+
or upload.
|
|
20
|
+
|
|
21
|
+
:returns: A parent mock recording every subcommand call, so their relative order is
|
|
22
|
+
observable. ``mock_calls`` names them, for example ``call.validate(...)``.
|
|
23
|
+
"""
|
|
24
|
+
mocker.patch("grz_cli.commands.submit.check_version_and_exit_if_needed")
|
|
25
|
+
recorder = mocker.MagicMock()
|
|
26
|
+
for name in ("validate", "encrypt", "upload"):
|
|
27
|
+
recorder.attach_mock(mocker.patch(f"grz_cli.commands.submit.{name}"), name)
|
|
28
|
+
return recorder
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _called_subcommands(recorder) -> list[str]:
|
|
32
|
+
"""Names of the subcommands ``recorder`` saw, in call order."""
|
|
33
|
+
return [call[0] for call in recorder.mock_calls]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _subcommand_kwargs(recorder) -> dict[str, dict]:
|
|
37
|
+
"""The keyword arguments each subcommand was called with, keyed by subcommand name."""
|
|
38
|
+
return {call[0]: call[2] for call in recorder.mock_calls}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _invoke_submit(*extra_args: str) -> click.testing.Result:
|
|
42
|
+
runner = click.testing.CliRunner()
|
|
43
|
+
return runner.invoke(build_cli(), ["submit", *extra_args], catch_exceptions=False)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_submit_runs_subcommands_in_order(
|
|
47
|
+
s3_config_path: Path,
|
|
48
|
+
submission_dir: Path,
|
|
49
|
+
stub_subcommands,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Encrypting before validating, or uploading before encrypting, would ship bad data."""
|
|
52
|
+
result = _invoke_submit(
|
|
53
|
+
"--submission-dir",
|
|
54
|
+
str(submission_dir),
|
|
55
|
+
"--config-file",
|
|
56
|
+
str(s3_config_path),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert result.exit_code == 0, result.output
|
|
60
|
+
assert _called_subcommands(stub_subcommands) == ["validate", "encrypt", "upload"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_submit_hands_each_subcommand_the_submission_it_was_given(
|
|
64
|
+
s3_config_path: Path,
|
|
65
|
+
submission_dir: Path,
|
|
66
|
+
stub_subcommands,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""All three must act on the same directory, and encrypt must refuse to skip the validation logs."""
|
|
69
|
+
result = _invoke_submit(
|
|
70
|
+
"--submission-dir",
|
|
71
|
+
str(submission_dir),
|
|
72
|
+
"--config-file",
|
|
73
|
+
str(s3_config_path),
|
|
74
|
+
)
|
|
75
|
+
assert result.exit_code == 0, result.output
|
|
76
|
+
|
|
77
|
+
kwargs = _subcommand_kwargs(stub_subcommands)
|
|
78
|
+
assert {name: call["submission_dir"] for name, call in kwargs.items()} == {
|
|
79
|
+
"validate": submission_dir,
|
|
80
|
+
"encrypt": submission_dir,
|
|
81
|
+
"upload": submission_dir,
|
|
82
|
+
}
|
|
83
|
+
assert kwargs["encrypt"]["check_validation_logs"] is True, "submit must not encrypt unvalidated data"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_submit_accepts_multiple_config_files(
|
|
87
|
+
tmp_path: Path,
|
|
88
|
+
s3_config_path: Path,
|
|
89
|
+
submission_dir: Path,
|
|
90
|
+
stub_subcommands,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Regression: repeated ``--config-file`` once produced a tuple that crashed config parsing."""
|
|
93
|
+
override_path = tmp_path / "override.yaml"
|
|
94
|
+
override_path.write_text(yaml.dump({"s3": {"access_key": "override-key"}}))
|
|
95
|
+
|
|
96
|
+
result = _invoke_submit(
|
|
97
|
+
"--submission-dir",
|
|
98
|
+
str(submission_dir),
|
|
99
|
+
"--config-file",
|
|
100
|
+
str(s3_config_path),
|
|
101
|
+
"--config-file",
|
|
102
|
+
str(override_path),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert result.exit_code == 0, result.output
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.7.2"
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Integration tests for the ``grz-cli submit`` command."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import click.testing
|
|
6
|
-
import pytest
|
|
7
|
-
import yaml
|
|
8
|
-
from grz_cli.cli import build_cli
|
|
9
|
-
from pytest_mock import MockerFixture
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def stub_subcommands(mocker: MockerFixture) -> dict[str, object]:
|
|
14
|
-
"""Replace the subcommands ``submit`` invokes with no-op mocks.
|
|
15
|
-
|
|
16
|
-
``ctx.invoke`` accepts any callable when the target is not a click
|
|
17
|
-
``Command``, so swapping the module-level imports lets the real submit
|
|
18
|
-
callback run end-to-end without executing real validation, encryption,
|
|
19
|
-
or upload.
|
|
20
|
-
"""
|
|
21
|
-
mocker.patch("grz_cli.commands.submit.check_version_and_exit_if_needed")
|
|
22
|
-
return {
|
|
23
|
-
"validate": mocker.patch("grz_cli.commands.submit.validate"),
|
|
24
|
-
"encrypt": mocker.patch("grz_cli.commands.submit.encrypt"),
|
|
25
|
-
"upload": mocker.patch("grz_cli.commands.submit.upload"),
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _invoke_submit(*extra_args: str) -> click.testing.Result:
|
|
30
|
-
runner = click.testing.CliRunner()
|
|
31
|
-
return runner.invoke(build_cli(), ["submit", *extra_args], catch_exceptions=False)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_submit_runs_subcommands_in_order(
|
|
35
|
-
s3_config_path: Path,
|
|
36
|
-
submission_dir: Path,
|
|
37
|
-
stub_subcommands: dict[str, object],
|
|
38
|
-
) -> None:
|
|
39
|
-
result = _invoke_submit(
|
|
40
|
-
"--submission-dir",
|
|
41
|
-
str(submission_dir),
|
|
42
|
-
"--config-file",
|
|
43
|
-
str(s3_config_path),
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
assert result.exit_code == 0, result.output
|
|
47
|
-
stub_subcommands["validate"].assert_called_once()
|
|
48
|
-
stub_subcommands["encrypt"].assert_called_once()
|
|
49
|
-
stub_subcommands["upload"].assert_called_once()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_submit_accepts_multiple_config_files(
|
|
53
|
-
tmp_path: Path,
|
|
54
|
-
s3_config_path: Path,
|
|
55
|
-
submission_dir: Path,
|
|
56
|
-
stub_subcommands: dict[str, object],
|
|
57
|
-
) -> None:
|
|
58
|
-
"""Regression: repeated ``--config-file`` once produced a tuple that crashed config parsing."""
|
|
59
|
-
override_path = tmp_path / "override.yaml"
|
|
60
|
-
override_path.write_text(yaml.dump({"s3": {"access_key": "override-key"}}))
|
|
61
|
-
|
|
62
|
-
result = _invoke_submit(
|
|
63
|
-
"--submission-dir",
|
|
64
|
-
str(submission_dir),
|
|
65
|
-
"--config-file",
|
|
66
|
-
str(s3_config_path),
|
|
67
|
-
"--config-file",
|
|
68
|
-
str(override_path),
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
assert result.exit_code == 0, result.output
|
|
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
|