grz-cli 0.1.3__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.3/src/grz_cli.egg-info → grz_cli-0.1.4}/PKG-INFO +3 -4
  2. {grz_cli-0.1.3 → grz_cli-0.1.4}/pyproject.toml +11 -3
  3. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/download.py +1 -1
  4. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/metadata.py +124 -45
  5. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/parser.py +7 -1
  6. grz_cli-0.1.4/src/grz_cli/resources/grz-schema.json +595 -0
  7. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/upload.py +1 -1
  8. {grz_cli-0.1.3 → grz_cli-0.1.4/src/grz_cli.egg-info}/PKG-INFO +3 -4
  9. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/requires.txt +1 -1
  10. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_cli.py +16 -16
  11. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_download.py +3 -3
  12. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_parser.py +6 -4
  13. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_upload.py +20 -20
  14. grz_cli-0.1.3/src/grz_cli/resources/grz-schema.json +0 -574
  15. {grz_cli-0.1.3 → grz_cli-0.1.4}/LICENSE +0 -0
  16. {grz_cli-0.1.3 → grz_cli-0.1.4}/MANIFEST.in +0 -0
  17. {grz_cli-0.1.3 → grz_cli-0.1.4}/README.md +0 -0
  18. {grz_cli-0.1.3 → grz_cli-0.1.4}/setup.cfg +0 -0
  19. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/__init__.py +0 -0
  20. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/__init__.py +0 -0
  21. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/cli.py +0 -0
  22. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/constants.py +0 -0
  23. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/fastq_validation.py +0 -0
  24. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/file_operations.py +0 -0
  25. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/logging_setup.py +0 -0
  26. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/__init__.py +0 -0
  27. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/config.py +0 -0
  28. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/__init__.py +0 -0
  29. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/progress_logging.py +0 -0
  30. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/resources/thresholds.json +0 -0
  31. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/states.py +0 -0
  32. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/SOURCES.txt +0 -0
  33. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/dependency_links.txt +0 -0
  34. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/entry_points.txt +0 -0
  35. {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/top_level.txt +0 -0
  36. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_fastq_validation.py +0 -0
  37. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_file_operations.py +0 -0
  38. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_file_progress_logging.py +0 -0
  39. {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_read_multiple_json.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: grz-cli
3
- Version: 0.1.3
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.3"
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
 
@@ -154,21 +154,25 @@ 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
  ]
@@ -192,3 +197,6 @@ packages = "src"
192
197
  addopts = [
193
198
  "--import-mode=importlib",
194
199
  ]
200
+
201
+ [tool.setuptools]
202
+ license-files = []
@@ -146,7 +146,7 @@ class S3BotoDownloadWorker:
146
146
  if filesize / MULTIPART_CHUNKSIZE > MULTIPART_MAX_CHUNKS
147
147
  else MULTIPART_CHUNKSIZE
148
148
  )
149
- self.__log.debug(f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize/chunksize} chunks")
149
+ self.__log.debug(f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize / chunksize} chunks")
150
150
 
151
151
  config = TransferConfig(
152
152
  multipart_threshold=MULTIPART_THRESHOLD,
@@ -18,6 +18,7 @@ from __future__ import annotations
18
18
 
19
19
  import json
20
20
  import logging
21
+ import typing
21
22
  from collections.abc import Generator
22
23
  from datetime import date
23
24
  from enum import StrEnum
@@ -90,9 +91,9 @@ class Submission(StrictBaseModel):
90
91
  The options are: 'initial' for first submission, 'followup' is for followup submissions, 'addition' for additional submission, 'correction' for correction
91
92
  """
92
93
 
93
- tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{32}$")]
94
+ tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{64}$")]
94
95
  """
95
- The VNg of the genomic data of the patient that will be reimbursed --> a unique 32-byte hex ID.
96
+ The VNg of the genomic data of the patient that will be reimbursed --> a unique 32-length byte code represented in a hex string of length 64.
96
97
  """
97
98
 
98
99
  local_case_id: str | None = None
@@ -134,9 +135,9 @@ class Submission(StrictBaseModel):
134
135
  """
135
136
 
136
137
 
137
- class Sex(StrEnum):
138
+ class Gender(StrEnum):
138
139
  """
139
- Sex of the donor.
140
+ Gender of the donor.
140
141
  """
141
142
 
142
143
  male = "male"
@@ -156,6 +157,7 @@ class Relation(StrEnum):
156
157
  sister = "sister"
157
158
  child = "child"
158
159
  self = "self"
160
+ other = "other"
159
161
 
160
162
 
161
163
  class MvConsentScope(StrictBaseModel):
@@ -284,6 +286,7 @@ class EnrichmentKitManufacturer(StrEnum):
284
286
  neb = "NEB"
285
287
  other = "other"
286
288
  unknown = "unknown"
289
+ none = "none"
287
290
 
288
291
 
289
292
  class SequencingLayout(StrEnum):
@@ -308,6 +311,22 @@ class TumorCellCountMethod(StrEnum):
308
311
  unknown = "unknown"
309
312
 
310
313
 
314
+ class TumorCellCount(StrictBaseModel):
315
+ """
316
+ Tuple of tumor cell counts and how they were determined.
317
+ """
318
+
319
+ count: Annotated[float, Field(ge=0.0, le=100.0)]
320
+ """
321
+ Tumor cell count in %
322
+ """
323
+
324
+ method: TumorCellCountMethod
325
+ """
326
+ Method used to determine cell count.
327
+ """
328
+
329
+
311
330
  class CallerUsedItem(StrictBaseModel):
312
331
  name: str
313
332
  """
@@ -443,7 +462,9 @@ class PercentBasesAboveQualityThreshold(StrictBaseModel):
443
462
  """The minimum quality score threshold"""
444
463
 
445
464
  percent: Annotated[float, Field(strict=True, ge=0.0, le=100.0)]
446
- """Percentage of bases that meet or exceed the minimum quality score"""
465
+ """
466
+ Percentage of bases with a specified minimum quality threshold, according to https://www.bfarm.de/SharedDocs/Downloads/DE/Forschung/modellvorhaben-genomsequenzierung/Qs-durch-GRZ.pdf?__blob=publicationFile
467
+ """
447
468
 
448
469
 
449
470
  class SequenceData(StrictBaseModel):
@@ -502,6 +523,12 @@ class SequenceData(StrictBaseModel):
502
523
  List of files generated and required in this analysis.
503
524
  """
504
525
 
526
+ def contains_files(self, file_type: FileType) -> bool:
527
+ return any(f.file_type == file_type for f in self.files)
528
+
529
+ def list_files(self, file_type: FileType) -> list[File]:
530
+ return [f for f in self.files if f.file_type == file_type]
531
+
505
532
 
506
533
  class LabDatum(StrictBaseModel):
507
534
  lab_data_name: str
@@ -601,21 +628,19 @@ class LabDatum(StrictBaseModel):
601
628
  The sequencing layout, aka the end type of sequencing.
602
629
  """
603
630
 
604
- tumor_cell_count: Annotated[float | None, Field(alias="tumorCellCount", ge=0.0, le=100.0)] = None
605
- """
606
- Tumor cell count in %
607
- """
608
-
609
- tumor_cell_count_method: TumorCellCountMethod | None = None
631
+ tumor_cell_count: list[TumorCellCount] | None = None
610
632
  """
611
- Method used to determine cell count.
633
+ Tuple of tumor cell counts and how they were determined.
612
634
  """
613
635
 
614
- sequence_data: SequenceData
636
+ sequence_data: SequenceData | None = None
615
637
  """
616
638
  Sequence data generated from the wet lab experiment.
617
639
  """
618
640
 
641
+ def has_sequence_data(self) -> bool:
642
+ return self.sequence_data is not None
643
+
619
644
  @model_validator(mode="after")
620
645
  def validate_sequencing_setup(self) -> Self:
621
646
  if self.library_type in {LibraryType.wxs, LibraryType.wxs_lr} and self.sequence_type != SequenceType.rna:
@@ -627,14 +652,19 @@ class LabDatum(StrictBaseModel):
627
652
 
628
653
 
629
654
  class Donor(StrictBaseModel):
630
- case_id: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{32}$")]
655
+ tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{64}$")]
631
656
  """
632
- The VNg of the genomic data of the donor --> a unique 32-byte hex ID.
657
+ The VNg of the genomic data of the donor --> a unique 32-length byte code represented in a hex string of length 64.
633
658
  """
634
659
 
635
- sex: Sex
660
+ local_case_id: str | None = None
636
661
  """
637
- Sex of the donor.
662
+ A local case identifier for synchronizing locally
663
+ """
664
+
665
+ gender: Gender
666
+ """
667
+ Gender of the donor.
638
668
  """
639
669
 
640
670
  relation: Relation
@@ -707,15 +737,21 @@ class Donor(StrictBaseModel):
707
737
  Lab data related to the donor.
708
738
  """
709
739
 
740
+ @model_validator(mode="after")
741
+ def warn_empty_sequence_data(self):
742
+ for lab_datum in self.lab_data:
743
+ if not lab_datum.has_sequence_data():
744
+ log.warning(
745
+ f"No sequence data found for lab datum '{lab_datum.lab_data_name}' in donor '{self.tan_g}'. "
746
+ "Is this a submission without sequence data?"
747
+ )
748
+ return self
749
+
710
750
  @model_validator(mode="after")
711
751
  def validate_target_bed_files_exist(self):
712
752
  """
713
753
  Check if the submission has the required bed files for panel sequencing.
714
754
  """
715
-
716
- def contains_bed_files(sequence_data: SequenceData) -> bool:
717
- return any(f.file_type == FileType.bed for f in sequence_data.files)
718
-
719
755
  lib_types = {
720
756
  LibraryType.panel,
721
757
  LibraryType.wes,
@@ -726,41 +762,36 @@ class Donor(StrictBaseModel):
726
762
  }
727
763
 
728
764
  for lab_datum in self.lab_data:
729
- if lab_datum.library_type in lib_types and not contains_bed_files(lab_datum.sequence_data):
730
- raise ValueError(
731
- f"BED file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.case_id}'."
732
- )
765
+ if (
766
+ lab_datum.has_sequence_data()
767
+ and lab_datum.library_type in lib_types
768
+ and not lab_datum.sequence_data.contains_files(FileType.bed)
769
+ ):
770
+ raise ValueError(f"BED file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.tan_g}'.")
733
771
 
734
772
  return self
735
773
 
736
774
  @model_validator(mode="after")
737
775
  def validate_vcf_file_exists(self):
738
776
  """
739
- check if there is a VCF file
777
+ Check if there is a VCF file
740
778
  """
741
-
742
- def contains_vcf_files(sequence_data: SequenceData) -> bool:
743
- return any(f.file_type == FileType.vcf for f in sequence_data.files)
744
-
745
779
  for lab_datum in self.lab_data:
746
- if not contains_vcf_files(lab_datum.sequence_data):
747
- raise ValueError(
748
- f"VCF file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.case_id}'."
749
- )
780
+ if lab_datum.has_sequence_data() and not lab_datum.sequence_data.contains_files(FileType.vcf):
781
+ raise ValueError(f"VCF file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.tan_g}'.")
750
782
 
751
783
  return self
752
784
 
753
785
  @model_validator(mode="after")
754
786
  def validate_fastq_file_exists(self):
755
787
  """
756
- check if there is a FASTQ file
788
+ Check if there is a FASTQ file
757
789
  """
758
-
759
- def get_fastq_files(sequence_data: SequenceData):
760
- return [f for f in sequence_data.files if f.file_type == FileType.fastq]
761
-
762
790
  for lab_datum in self.lab_data:
763
- fastq_files = get_fastq_files(lab_datum.sequence_data)
791
+ if not lab_datum.has_sequence_data():
792
+ # Skip if no sequence data is present
793
+ continue
794
+ fastq_files = lab_datum.sequence_data.list_files(FileType.fastq)
764
795
 
765
796
  if len(fastq_files) == 0:
766
797
  raise ValueError("No FASTQ file found!")
@@ -769,7 +800,7 @@ class Donor(StrictBaseModel):
769
800
  for i in fastq_files:
770
801
  if i.read_order is None:
771
802
  raise ValueError(
772
- f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.case_id}': "
803
+ f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.tan_g}': "
773
804
  f"No read order specified for FASTQ file '{i.file_path}'!"
774
805
  )
775
806
 
@@ -779,7 +810,7 @@ class Donor(StrictBaseModel):
779
810
 
780
811
  if len(r1_fastq_files) != len(r2_fastq_files):
781
812
  raise ValueError(
782
- f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.case_id}': "
813
+ f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.tan_g}': "
783
814
  f"Paired end sequencing layout but number of R1 FASTQ files ({len(r1_fastq_files)})"
784
815
  f" differs from number of R2 FASTQ files ({len(r2_fastq_files)})!"
785
816
  )
@@ -821,6 +852,37 @@ class GrzSubmissionMetadata(StrictBaseModel):
821
852
 
822
853
  return self
823
854
 
855
+ @model_validator(mode="after")
856
+ def check_for_tumor_cell_count(self):
857
+ """
858
+ Check if oncology samples have tumor cell counts.
859
+ """
860
+ for donor in self.donors:
861
+ case_id = donor.tan_g
862
+ for lab_datum in donor.lab_data:
863
+ if lab_datum.sequence_subtype == SequenceSubtype.somatic and lab_datum.tumor_cell_count is None:
864
+ raise ValueError(
865
+ f"Missing tumor cell count for donor '{case_id}', lab datum '{lab_datum.lab_data_name}'!"
866
+ )
867
+
868
+ return self
869
+
870
+ @model_validator(mode="after")
871
+ def check_duplicate_lab_data_names(self):
872
+ """
873
+ Check if the submission contains lab data with the same name within one donor.
874
+ """
875
+ for donor in self.donors:
876
+ case_id = donor.tan_g
877
+ lab_data_names = set()
878
+ for lab_datum in donor.lab_data:
879
+ if lab_datum.lab_data_name in lab_data_names:
880
+ raise ValueError(f"Duplicate lab datum '{lab_datum.lab_data_name}' in donor '{case_id}'")
881
+ else:
882
+ lab_data_names.add(lab_datum.lab_data_name)
883
+
884
+ return self
885
+
824
886
  @model_validator(mode="after")
825
887
  def validate_thresholds(self):
826
888
  """
@@ -837,7 +899,20 @@ class GrzSubmissionMetadata(StrictBaseModel):
837
899
  )
838
900
  thresholds = threshold_definitions.get(key)
839
901
  if thresholds is None:
840
- log.warning(f"Thresholds for {key} not found! Skipping.")
902
+ allowed_combinations = sorted(list(threshold_definitions.keys()))
903
+ allowed_combinations = "\n".join([f" - {combination}" for combination in allowed_combinations])
904
+ names = (
905
+ "submission.genomicStudySubtype",
906
+ "labData.libraryType",
907
+ "labData.sequenceSubtype",
908
+ )
909
+ info = dict(zip(names, key, strict=True))
910
+ log.warning(
911
+ f"No thresholds for the specified combination {info} found (donor {donor.tan_g})!\n"
912
+ f"Valid combinations:\n{allowed_combinations}.\n"
913
+ f"See https://www.bfarm.de/SharedDocs/Downloads/DE/Forschung/modellvorhaben-genomsequenzierung/Qs-durch-GRZ.pdf?__blob=publicationFile for more details.\n"
914
+ f"Skipping threshold validation."
915
+ )
841
916
  continue
842
917
 
843
918
  _check_thresholds(donor, lab_datum, thresholds)
@@ -846,9 +921,13 @@ class GrzSubmissionMetadata(StrictBaseModel):
846
921
 
847
922
 
848
923
  def _check_thresholds(donor: Donor, lab_datum: LabDatum, thresholds: dict[str, Any]):
849
- sequence_data = lab_datum.sequence_data
850
- case_id = donor.case_id
924
+ if not lab_datum.has_sequence_data():
925
+ # Skip if no sequence data is present; warning issues in the validator `warn_empty_sequence_data` of `Donor`.
926
+ return
927
+ case_id = donor.tan_g
851
928
  lab_data_name = lab_datum.lab_data_name
929
+ # mypy cannot reason about the `has_sequence_data` check
930
+ sequence_data = typing.cast(SequenceData, lab_datum.sequence_data)
852
931
 
853
932
  mean_depth_of_coverage_t = thresholds.get("meanDepthOfCoverage")
854
933
  mean_depth_of_coverage_v = sequence_data.mean_depth_of_coverage
@@ -9,6 +9,8 @@ from itertools import groupby
9
9
  from os import PathLike
10
10
  from pathlib import Path
11
11
 
12
+ from pydantic import ValidationError
13
+
12
14
  from .download import S3BotoDownloadWorker
13
15
  from .fastq_validation import validate_paired_end_reads, validate_single_end_reads
14
16
  from .file_operations import Crypt4GH, calculate_sha256
@@ -60,7 +62,11 @@ class SubmissionMetadata:
60
62
  try:
61
63
  with open(file_path, encoding="utf-8") as jsonfile:
62
64
  metadata = json.load(jsonfile)
63
- metadata_model = GrzSubmissionMetadata(**metadata)
65
+ try:
66
+ metadata_model = GrzSubmissionMetadata(**metadata)
67
+ except ValidationError as ve:
68
+ cls.__log.error("Invalid metadata format in metadata file: %s", file_path)
69
+ raise SystemExit(ve) from ve
64
70
  return metadata_model
65
71
  except json.JSONDecodeError as e:
66
72
  cls.__log.error("Invalid JSON format in metadata file: %s", file_path)