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.
Files changed (26) hide show
  1. {grz_cli-1.7.2 → grz_cli-2.0.0}/CHANGELOG.md +21 -0
  2. {grz_cli-1.7.2 → grz_cli-2.0.0}/PKG-INFO +3 -3
  3. {grz_cli-1.7.2 → grz_cli-2.0.0}/pyproject.toml +1 -1
  4. grz_cli-2.0.0/src/grz_cli/__init__.py +1 -0
  5. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/validate.py +1 -0
  6. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/utils/version_check.py +29 -7
  7. grz_cli-2.0.0/tests/test_submit.py +105 -0
  8. grz_cli-1.7.2/src/grz_cli/__init__.py +0 -1
  9. grz_cli-1.7.2/tests/test_submit.py +0 -71
  10. {grz_cli-1.7.2 → grz_cli-2.0.0}/.gitignore +0 -0
  11. {grz_cli-1.7.2 → grz_cli-2.0.0}/LICENSE +0 -0
  12. {grz_cli-1.7.2 → grz_cli-2.0.0}/MANIFEST.in +0 -0
  13. {grz_cli-1.7.2 → grz_cli-2.0.0}/README.md +0 -0
  14. {grz_cli-1.7.2 → grz_cli-2.0.0}/docs/version-compatibility-check.md +0 -0
  15. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/__init__.py +0 -0
  16. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/cli.py +0 -0
  17. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/__init__.py +0 -0
  18. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/encrypt.py +0 -0
  19. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/get_id.py +0 -0
  20. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/submit.py +0 -0
  21. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/commands/upload.py +0 -0
  22. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/models/__init__.py +0 -0
  23. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/models/config.py +0 -0
  24. {grz_cli-1.7.2 → grz_cli-2.0.0}/src/grz_cli/py.typed +0 -0
  25. {grz_cli-1.7.2 → grz_cli-2.0.0}/tests/conftest.py +0 -0
  26. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: grz-cli
3
- Version: 1.7.2
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,>=2.1
16
+ Requires-Dist: grz-common<4,>=3
17
17
  Requires-Dist: packaging<27,>=23
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -14,7 +14,7 @@ authors = [
14
14
  dynamic = ["version"]
15
15
  dependencies = [
16
16
  "packaging >=23,<27",
17
- "grz-common >=2.1,<3",
17
+ "grz-common >=3,<4",
18
18
  ]
19
19
  classifiers = [
20
20
  "Programming Language :: Python :: 3",
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
@@ -51,6 +51,7 @@ def validate(
51
51
  encrypted_files_dir=submission_dir / "encrypted_files",
52
52
  threads=threads,
53
53
  )
54
+
54
55
  worker_inst.validate(identifiers=config.identifiers, force=force, no_mmap=not mmap)
55
56
 
56
57
  log.info("Validation finished!")
@@ -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
- now = datetime.now(UTC)
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
- policy = _select_active_policy(version_file.grzcli_version, now)
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 grz-cli version: {current_version}")
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 grz-cli version ({current_version}) is not supported. Minimum required version is {minimal_version}."
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 grz-cli {current_version}, but the recommended version is "
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(f"grz-cli version {current_version} is newer than the maximum supported version ({max_version}).")
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"grz-cli {current_version} is within the supported and tested range.")
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