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.
- {grzctl-2.1.2 → grzctl-2.1.4}/CHANGELOG.md +14 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/PKG-INFO +2 -2
- {grzctl-2.1.2 → grzctl-2.1.4}/pyproject.toml +1 -1
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/__init__.py +1 -1
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/cli.py +34 -4
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db.py +94 -2
- {grzctl-2.1.2 → grzctl-2.1.4}/.gitignore +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/README.md +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/cli.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/__init__.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/archive.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/clean.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/consent.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/__init__.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/sync.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/tui.css +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/db/tui.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/decrypt.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/download.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/encrypt.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/list_submissions.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/pruefbericht.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/report.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/commands/validate.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/dbcontext.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/__init__.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/config.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/db.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/models/pruefbericht.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/src/grzctl/py.typed +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/conftest.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_backfill.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db_populate.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db_sync.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_db_tui.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_report.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/cli/test_validate.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/conftest.py +0 -0
- {grzctl-2.1.2 → grzctl-2.1.4}/tests/test_cli.py +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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=
|
|
783
|
+
required=False,
|
|
784
|
+
default=None,
|
|
780
785
|
envvar="GRZCTL_QC_WORKFLOW_VERSION",
|
|
781
|
-
help="QC workflow version to
|
|
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(
|
|
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=
|
|
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
|
-
#
|
|
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 "
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|