grzctl 2.1.2__tar.gz → 2.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 (40) hide show
  1. {grzctl-2.1.2 → grzctl-2.1.4}/CHANGELOG.md +14 -0
  2. {grzctl-2.1.2 → grzctl-2.1.4}/PKG-INFO +2 -2
  3. {grzctl-2.1.2 → grzctl-2.1.4}/pyproject.toml +1 -1
  4. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/__init__.py +1 -1
  5. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/cli.py +34 -4
  6. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db.py +94 -2
  7. {grzctl-2.1.2 → grzctl-2.1.4}/.gitignore +0 -0
  8. {grzctl-2.1.2 → grzctl-2.1.4}/README.md +0 -0
  9. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/cli.py +0 -0
  10. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/__init__.py +0 -0
  11. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/archive.py +0 -0
  12. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/clean.py +0 -0
  13. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/consent.py +0 -0
  14. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/__init__.py +0 -0
  15. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/sync.py +0 -0
  16. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/tui.css +0 -0
  17. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/tui.py +0 -0
  18. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/decrypt.py +0 -0
  19. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/download.py +0 -0
  20. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/encrypt.py +0 -0
  21. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/list_submissions.py +0 -0
  22. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/pruefbericht.py +0 -0
  23. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/report.py +0 -0
  24. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/validate.py +0 -0
  25. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/dbcontext.py +0 -0
  26. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/__init__.py +0 -0
  27. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/config.py +0 -0
  28. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/db.py +0 -0
  29. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/pruefbericht.py +0 -0
  30. {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/py.typed +0 -0
  31. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/conftest.py +0 -0
  32. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_backfill.py +0 -0
  33. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db_populate.py +0 -0
  34. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db_sync.py +0 -0
  35. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db_tui.py +0 -0
  36. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_report.py +0 -0
  37. {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_validate.py +0 -0
  38. {grzctl-2.1.2 → grzctl-2.1.4}/tests/conftest.py +0 -0
  39. {grzctl-2.1.2 → grzctl-2.1.4}/tests/test_cli.py +0 -0
  40. {grzctl-2.1.2 → grzctl-2.1.4}/tests/test_dbcontext.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.4](https://github.com/BfArM-MVH/grz-tools/compare/grzctl-v2.1.3...grzctl-v2.1.4) (2026-06-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **grzctl:** take QC workflow version from the report in populate-qc ([#607](https://github.com/BfArM-MVH/grz-tools/issues/607)) ([f1d60f8](https://github.com/BfArM-MVH/grz-tools/commit/f1d60f846895abf7c7c511f565ef7c6e3e4e2e7e))
9
+
10
+ ## [2.1.3](https://github.com/BfArM-MVH/grz-tools/compare/grzctl-v2.1.2...grzctl-v2.1.3) (2026-06-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **grzctl:** bump minimum grz-db version to 2.1.2 ([#596](https://github.com/BfArM-MVH/grz-tools/issues/596)) ([9740878](https://github.com/BfArM-MVH/grz-tools/commit/97408785db8a28dcb779653f1a2d97ded488f894))
16
+
3
17
  ## [2.1.2](https://github.com/BfArM-MVH/grz-tools/compare/grzctl-v2.1.1...grzctl-v2.1.2) (2026-06-02)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grzctl
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: Control CLI for GRZ administrators.
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 @@ Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Requires-Python: >=3.12
15
15
  Requires-Dist: grz-cli<2,>=1.7
16
- Requires-Dist: grz-db<3,>=2.1
16
+ Requires-Dist: grz-db<3,>=2.1.2
17
17
  Requires-Dist: requests<3,>=2.32.3
18
18
  Requires-Dist: textual~=5.3
19
19
  Description-Content-Type: text/markdown
@@ -14,7 +14,7 @@ authors = [
14
14
  dynamic = ["version"]
15
15
  dependencies = [
16
16
  "requests >=2.32.3,<3",
17
- "grz-db >=2.1,<3",
17
+ "grz-db >=2.1.2,<3",
18
18
  "grz-cli >=1.7,<2",
19
19
  "textual~=5.3"
20
20
  ]
@@ -4,7 +4,7 @@ GRZ Control CLI for GRZ administrators.
4
4
 
5
5
  from importlib.metadata import PackageNotFoundError, version
6
6
 
7
- __version__ = "2.1.2" # This version is managed by release-please
7
+ __version__ = "2.1.4" # This version is managed by release-please
8
8
 
9
9
 
10
10
  def get_versions() -> dict[str, str | None]:
@@ -768,6 +768,10 @@ class QCReportRow(StrictBaseModel):
768
768
  targeted_regions_above_min_coverage_required: float
769
769
  targeted_regions_above_min_coverage_deviation: float
770
770
  targeted_regions_above_min_coverage_qc_status: QCStatus = Field(alias="targetedRegionsAboveMinCoverageQCStatus")
771
+ # Written by GRZ_QC_Workflow >= v2.1.0. Optional so reports from older workflow
772
+ # versions (without the column) still parse. Auto-aliased to "grzQcWorkflowVersion"
773
+ # via StrictBaseModel's to_camel alias generator.
774
+ grz_qc_workflow_version: str | None = None
771
775
 
772
776
 
773
777
  @submission.command()
@@ -776,9 +780,11 @@ class QCReportRow(StrictBaseModel):
776
780
  @click.option(
777
781
  "--qc-workflow-version",
778
782
  type=str,
779
- required=True,
783
+ required=False,
784
+ default=None,
780
785
  envvar="GRZCTL_QC_WORKFLOW_VERSION",
781
- help="QC workflow version to store with detailed QC results. Can also be provided via GRZCTL_QC_WORKFLOW_VERSION.",
786
+ help="QC workflow version to record when the report has no 'grzQcWorkflowVersion' column. "
787
+ "If the report carries one, it takes precedence. Env: GRZCTL_QC_WORKFLOW_VERSION.",
782
788
  )
783
789
  @click.option(
784
790
  "--confirm/--no-confirm",
@@ -786,7 +792,9 @@ class QCReportRow(StrictBaseModel):
786
792
  help="Whether to confirm changes before committing to database. (Default: confirm)",
787
793
  )
788
794
  @click.pass_context
789
- def populate_qc(ctx: click.Context, submission_id: str, report_csv_path: str, qc_workflow_version: str, confirm: bool):
795
+ def populate_qc(
796
+ ctx: click.Context, submission_id: str, report_csv_path: str, qc_workflow_version: str | None, confirm: bool
797
+ ):
790
798
  """Populate the submission database from a detailed QC pipeline report."""
791
799
  db = ctx.obj["db_url"]
792
800
  db_service = get_submission_db_instance(db, author=ctx.obj["author"])
@@ -798,6 +806,28 @@ def populate_qc(ctx: click.Context, submission_id: str, report_csv_path: str, qc
798
806
  for row in reader:
799
807
  reports.append(QCReportRow(**dict(zip(header, row, strict=True))))
800
808
 
809
+ # The report (GRZ_QC_Workflow >= v2.1.0) carries its own version in the
810
+ # 'grzQcWorkflowVersion' column; treat that as the source of truth and only fall back to
811
+ # --qc-workflow-version for older reports that lack the column.
812
+ report_versions = {report.grz_qc_workflow_version for report in reports if report.grz_qc_workflow_version}
813
+ if len(report_versions) > 1:
814
+ raise click.ClickException(f"Inconsistent grzQcWorkflowVersion values in report: {sorted(report_versions)}")
815
+ report_version = report_versions.pop() if report_versions else None
816
+
817
+ if report_version and qc_workflow_version and report_version != qc_workflow_version:
818
+ raise click.ClickException(
819
+ f"--qc-workflow-version ({qc_workflow_version}) disagrees with the report's "
820
+ f"grzQcWorkflowVersion ({report_version}). Omit --qc-workflow-version to use the report "
821
+ "value, which is authoritative."
822
+ )
823
+
824
+ effective_qc_workflow_version = report_version or qc_workflow_version
825
+ if not effective_qc_workflow_version:
826
+ raise click.ClickException(
827
+ "No QC workflow version found: the report has no 'grzQcWorkflowVersion' column and "
828
+ "--qc-workflow-version was not provided."
829
+ )
830
+
801
831
  report_mtime = datetime.fromtimestamp(Path(report_csv_path).stat().st_mtime, tz=UTC)
802
832
  results = []
803
833
  for report in reports:
@@ -823,7 +853,7 @@ def populate_qc(ctx: click.Context, submission_id: str, report_csv_path: str, qc
823
853
  targeted_regions_above_min_coverage_passed_qc=report.targeted_regions_above_min_coverage_qc_status
824
854
  == QCStatus.PASS,
825
855
  targeted_regions_above_min_coverage_percent_deviation=report.targeted_regions_above_min_coverage_deviation,
826
- qc_workflow_version=qc_workflow_version,
856
+ qc_workflow_version=effective_qc_workflow_version,
827
857
  )
828
858
  )
829
859
  table = rich.table.Table(
@@ -512,7 +512,7 @@ def test_populate_qc_missing_qc_workflow_version(
512
512
  """)
513
513
  )
514
514
 
515
- # Test without --qc-workflow-version flag and without env var (should fail)
515
+ # No --qc-workflow-version flag/env var and no grzQcWorkflowVersion column in the report → fail
516
516
  result_populate_qc = runner.invoke(
517
517
  cli,
518
518
  [
@@ -525,7 +525,99 @@ def test_populate_qc_missing_qc_workflow_version(
525
525
  ],
526
526
  )
527
527
  assert result_populate_qc.exit_code != 0
528
- assert "Missing option '--qc-workflow-version'" in result_populate_qc.output
528
+ assert "No QC workflow version found" in result_populate_qc.output
529
+
530
+
531
+ def test_populate_qc_version_from_report(blank_database_config_path: Path, tmp_path: Path, test_metadata_path: Path):
532
+ """populate-qc takes the version from the report's grzQcWorkflowVersion column when no flag is given."""
533
+ args_common = ["db", "--config-file", blank_database_config_path]
534
+ metadata = GrzSubmissionMetadata.model_validate_json(test_metadata_path.read_text())
535
+
536
+ runner = click.testing.CliRunner(catch_exceptions=False)
537
+ cli = grzctl.cli.build_cli()
538
+ result_add = runner.invoke(cli, [*args_common, "submission", "add", metadata.submission_id])
539
+ assert result_add.exit_code == 0, result_add.stderr
540
+
541
+ metadata_raw = json.loads(test_metadata_path.read_text())
542
+ metadata_dump_path = tmp_path / "metadata.json"
543
+ with open(metadata_dump_path, "w") as metadata_file:
544
+ json.dump(metadata_raw, metadata_file)
545
+
546
+ result_populate = runner.invoke(
547
+ cli,
548
+ [*args_common, "submission", "populate", metadata.submission_id, str(metadata_dump_path), "--no-confirm"],
549
+ )
550
+ assert result_populate.exit_code == 0, result_populate.stderr
551
+
552
+ report_csv_path = tmp_path / "report.csv"
553
+ with open(report_csv_path, "w") as report_csv_file:
554
+ report_csv_file.write(
555
+ dedent("""\
556
+ sampleId,donorPseudonym,labDataName,libraryType,sequenceSubtype,genomicStudySubtype,qualityControlStatus,meanDepthOfCoverage,meanDepthOfCoverageProvided,meanDepthOfCoverageRequired,meanDepthOfCoverageDeviation,meanDepthOfCoverageQCStatus,percentBasesAboveQualityThreshold,qualityThreshold,percentBasesAboveQualityThresholdProvided,percentBasesAboveQualityThresholdRequired,percentBasesAboveQualityThresholdDeviation,percentBasesAboveQualityThresholdQCStatus,targetedRegionsAboveMinCoverage,minCoverage,targetedRegionsAboveMinCoverageProvided,targetedRegionsAboveMinCoverageRequired,targetedRegionsAboveMinCoverageDeviation,targetedRegionsAboveMinCoverageQCStatus,grzQcWorkflowVersion
557
+ index0_germline0,index,Blut DNA normal,wes,germline,tumor+germline,PASS,49.84,50.0,30.0,-0.3199999999999932,PASS,90.65953529937444,30,88.0,85,3.022199203834591,PASS,1.0,20,1.0,0.8,0.0,PASS,2.1.0+nonredundant
558
+ """)
559
+ )
560
+
561
+ result_populate_qc = runner.invoke(
562
+ cli,
563
+ [*args_common, "submission", "populate-qc", metadata.submission_id, str(report_csv_path), "--no-confirm"],
564
+ )
565
+ assert result_populate_qc.exit_code == 0, result_populate_qc.stderr
566
+
567
+ with open(blank_database_config_path, encoding="utf-8") as blank_database_config_file:
568
+ config = yaml.load(blank_database_config_file, Loader=yaml.Loader)
569
+ db = SubmissionDb(db_url=config["db"]["database_url"], author=None)
570
+
571
+ results = db.get_detailed_qc_results(metadata.submission_id)
572
+ assert len(results) == 1
573
+ assert results[0].qc_workflow_version == "2.1.0+nonredundant"
574
+
575
+
576
+ def test_populate_qc_flag_report_mismatch(blank_database_config_path: Path, tmp_path: Path, test_metadata_path: Path):
577
+ """populate-qc errors when --qc-workflow-version disagrees with the report's grzQcWorkflowVersion."""
578
+ args_common = ["db", "--config-file", blank_database_config_path]
579
+ metadata = GrzSubmissionMetadata.model_validate_json(test_metadata_path.read_text())
580
+
581
+ runner = click.testing.CliRunner(catch_exceptions=False)
582
+ cli = grzctl.cli.build_cli()
583
+ result_add = runner.invoke(cli, [*args_common, "submission", "add", metadata.submission_id])
584
+ assert result_add.exit_code == 0, result_add.stderr
585
+
586
+ metadata_raw = json.loads(test_metadata_path.read_text())
587
+ metadata_dump_path = tmp_path / "metadata.json"
588
+ with open(metadata_dump_path, "w") as metadata_file:
589
+ json.dump(metadata_raw, metadata_file)
590
+
591
+ result_populate = runner.invoke(
592
+ cli,
593
+ [*args_common, "submission", "populate", metadata.submission_id, str(metadata_dump_path), "--no-confirm"],
594
+ )
595
+ assert result_populate.exit_code == 0, result_populate.stderr
596
+
597
+ report_csv_path = tmp_path / "report.csv"
598
+ with open(report_csv_path, "w") as report_csv_file:
599
+ report_csv_file.write(
600
+ dedent("""\
601
+ sampleId,donorPseudonym,labDataName,libraryType,sequenceSubtype,genomicStudySubtype,qualityControlStatus,meanDepthOfCoverage,meanDepthOfCoverageProvided,meanDepthOfCoverageRequired,meanDepthOfCoverageDeviation,meanDepthOfCoverageQCStatus,percentBasesAboveQualityThreshold,qualityThreshold,percentBasesAboveQualityThresholdProvided,percentBasesAboveQualityThresholdRequired,percentBasesAboveQualityThresholdDeviation,percentBasesAboveQualityThresholdQCStatus,targetedRegionsAboveMinCoverage,minCoverage,targetedRegionsAboveMinCoverageProvided,targetedRegionsAboveMinCoverageRequired,targetedRegionsAboveMinCoverageDeviation,targetedRegionsAboveMinCoverageQCStatus,grzQcWorkflowVersion
602
+ index0_germline0,index,Blut DNA normal,wes,germline,tumor+germline,PASS,49.84,50.0,30.0,-0.3199999999999932,PASS,90.65953529937444,30,88.0,85,3.022199203834591,PASS,1.0,20,1.0,0.8,0.0,PASS,2.1.0+nonredundant
603
+ """)
604
+ )
605
+
606
+ result_populate_qc = runner.invoke(
607
+ cli,
608
+ [
609
+ *args_common,
610
+ "submission",
611
+ "populate-qc",
612
+ metadata.submission_id,
613
+ str(report_csv_path),
614
+ "--no-confirm",
615
+ "--qc-workflow-version",
616
+ "v9.9.9",
617
+ ],
618
+ )
619
+ assert result_populate_qc.exit_code != 0
620
+ assert "disagrees" in result_populate_qc.output
529
621
 
530
622
 
531
623
  def test_update_error_confirm(blank_database_config_path: Path, test_metadata_path: Path):
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