cg 76.0.0__py3-none-any.whl → 83.14.0__py3-none-any.whl

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 (152) hide show
  1. cg/__init__.py +1 -1
  2. cg/apps/housekeeper/hk.py +18 -1
  3. cg/apps/tb/api.py +42 -5
  4. cg/cli/transfer.py +13 -2
  5. cg/cli/upload/mutacc.py +16 -3
  6. cg/cli/upload/scout.py +2 -2
  7. cg/cli/upload/utils.py +10 -1
  8. cg/cli/workflow/balsamic/base.py +86 -172
  9. cg/cli/workflow/balsamic/options.py +3 -48
  10. cg/cli/workflow/balsamic/umi.py +210 -15
  11. cg/cli/workflow/microsalt/base.py +4 -2
  12. cg/cli/workflow/mip_dna/base.py +1 -1
  13. cg/cli/workflow/nallo/base.py +73 -23
  14. cg/cli/workflow/nf_analysis.py +5 -207
  15. cg/cli/workflow/raredisease/base.py +41 -54
  16. cg/cli/workflow/rnafusion/base.py +38 -8
  17. cg/cli/workflow/taxprofiler/base.py +31 -18
  18. cg/cli/workflow/tomte/base.py +83 -10
  19. cg/constants/constants.py +25 -30
  20. cg/constants/devices.py +6 -1
  21. cg/constants/gene_panel.py +3 -1
  22. cg/constants/housekeeper_tags.py +28 -28
  23. cg/constants/lims.py +4 -0
  24. cg/constants/nf_analysis.py +0 -1
  25. cg/constants/observations.py +21 -5
  26. cg/constants/orderforms.py +3 -3
  27. cg/constants/pacbio.py +1 -0
  28. cg/constants/priority.py +1 -1
  29. cg/constants/report.py +1 -0
  30. cg/constants/scout.py +12 -9
  31. cg/constants/sequencing.py +2 -2
  32. cg/constants/tb.py +5 -5
  33. cg/exc.py +27 -5
  34. cg/meta/compress/compress.py +7 -2
  35. cg/meta/delivery_report/balsamic.py +3 -1
  36. cg/meta/delivery_report/delivery_report_api.py +4 -3
  37. cg/meta/delivery_report/nallo.py +11 -11
  38. cg/meta/delivery_report/raredisease.py +7 -3
  39. cg/meta/delivery_report/templates/macros/data_analysis/qc_metrics/balsamic_qc_metrics.html +1 -0
  40. cg/meta/delivery_report/templates/macros/ticket_system.html +1 -1
  41. cg/meta/observations/balsamic_observations_api.py +110 -14
  42. cg/meta/observations/mip_dna_observations_api.py +1 -1
  43. cg/meta/observations/nallo_observations_api.py +1 -1
  44. cg/meta/observations/observations_api.py +23 -32
  45. cg/meta/observations/raredisease_observations_api.py +1 -1
  46. cg/meta/tar/tar.py +5 -2
  47. cg/meta/transfer/lims.py +32 -3
  48. cg/meta/upload/balsamic/balsamic.py +1 -8
  49. cg/meta/upload/coverage.py +5 -5
  50. cg/meta/upload/raredisease/raredisease.py +3 -0
  51. cg/meta/upload/scout/hk_tags.py +1 -0
  52. cg/meta/upload/scout/nallo_config_builder.py +31 -7
  53. cg/meta/workflow/balsamic.py +70 -36
  54. cg/meta/workflow/fastq.py +8 -0
  55. cg/meta/workflow/microsalt/quality_controller/models.py +0 -2
  56. cg/meta/workflow/microsalt/quality_controller/quality_controller.py +8 -16
  57. cg/meta/workflow/microsalt/quality_controller/result_logger.py +3 -6
  58. cg/meta/workflow/microsalt/quality_controller/utils.py +2 -45
  59. cg/meta/workflow/nallo.py +21 -99
  60. cg/meta/workflow/nf_analysis.py +12 -263
  61. cg/meta/workflow/raredisease.py +3 -112
  62. cg/meta/workflow/rnafusion.py +2 -34
  63. cg/meta/workflow/taxprofiler.py +2 -38
  64. cg/meta/workflow/tomte.py +2 -42
  65. cg/models/balsamic/config.py +0 -24
  66. cg/models/balsamic/metrics.py +5 -3
  67. cg/models/cg_config.py +39 -16
  68. cg/models/deliverables/metric_deliverables.py +1 -1
  69. cg/models/delivery_report/metadata.py +2 -1
  70. cg/models/nallo/nallo.py +14 -64
  71. cg/models/nf_analysis.py +1 -41
  72. cg/models/raredisease/raredisease.py +1 -63
  73. cg/models/rnafusion/rnafusion.py +0 -26
  74. cg/models/scout/scout_load_config.py +5 -2
  75. cg/models/taxprofiler/taxprofiler.py +0 -42
  76. cg/models/tomte/tomte.py +0 -69
  77. cg/resources/nallo_bundle_filenames.yaml +292 -22
  78. cg/resources/raredisease_bundle_filenames.yaml +11 -1
  79. cg/resources/taxprofiler_bundle_filenames.yaml +20 -0
  80. cg/server/admin.py +106 -25
  81. cg/server/app.py +15 -4
  82. cg/server/endpoints/sequencing_run/dtos.py +21 -3
  83. cg/server/endpoints/sequencing_run/pacbio_sequencing_run.py +29 -10
  84. cg/server/endpoints/sequencing_run/pacbio_smrt_cell_metrics.py +20 -0
  85. cg/services/analysis_starter/{service.py → analysis_starter.py} +11 -9
  86. cg/services/analysis_starter/configurator/abstract_model.py +8 -0
  87. cg/services/analysis_starter/configurator/configurator.py +1 -1
  88. cg/services/analysis_starter/configurator/extensions/nallo.py +27 -0
  89. cg/services/analysis_starter/configurator/extensions/{abstract.py → pipeline_extension.py} +1 -1
  90. cg/services/analysis_starter/configurator/extensions/raredisease.py +3 -1
  91. cg/services/analysis_starter/configurator/extensions/tomte_extension.py +28 -0
  92. cg/services/analysis_starter/configurator/file_creators/balsamic_config.py +240 -0
  93. cg/services/analysis_starter/configurator/file_creators/gene_panel.py +10 -5
  94. cg/services/analysis_starter/configurator/file_creators/nextflow/params_file/abstract.py +2 -1
  95. cg/services/analysis_starter/configurator/file_creators/nextflow/params_file/models.py +40 -1
  96. cg/services/analysis_starter/configurator/file_creators/nextflow/params_file/nallo.py +37 -0
  97. cg/services/analysis_starter/configurator/file_creators/nextflow/params_file/raredisease.py +8 -5
  98. cg/services/analysis_starter/configurator/file_creators/nextflow/params_file/tomte_params_file_creator.py +64 -0
  99. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/creator.py +1 -1
  100. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/nallo_sample_sheet_creator.py +65 -0
  101. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/protocol.py +12 -0
  102. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/{raredisease.py → raredisease_sample_sheet_creator.py} +2 -2
  103. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/{rnafusion.py → rnafusion_sample_sheet_creator.py} +2 -2
  104. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/{taxprofiler.py → taxprofiler_sample_sheet_creator.py} +2 -2
  105. cg/services/analysis_starter/configurator/file_creators/nextflow/sample_sheet/tomte_sample_sheet_creator.py +36 -0
  106. cg/services/analysis_starter/configurator/implementations/balsamic.py +68 -0
  107. cg/services/analysis_starter/configurator/implementations/nextflow.py +22 -5
  108. cg/services/analysis_starter/configurator/models/balsamic.py +152 -0
  109. cg/services/analysis_starter/configurator/models/mip_dna.py +6 -8
  110. cg/services/analysis_starter/configurator/models/nextflow.py +9 -0
  111. cg/services/analysis_starter/constants.py +2 -0
  112. cg/services/analysis_starter/factories/configurator_factory.py +131 -51
  113. cg/services/analysis_starter/factories/starter_factory.py +36 -7
  114. cg/services/analysis_starter/input_fetcher/implementations/bam_fetcher.py +57 -0
  115. cg/services/analysis_starter/input_fetcher/implementations/fastq_fetcher.py +3 -3
  116. cg/services/analysis_starter/submitters/seqera_platform/{client.py → seqera_platform_client.py} +19 -3
  117. cg/services/analysis_starter/submitters/seqera_platform/seqera_platform_submitter.py +73 -0
  118. cg/services/analysis_starter/submitters/submitter.py +1 -1
  119. cg/services/analysis_starter/submitters/subprocess/submitter.py +2 -1
  120. cg/services/analysis_starter/tracker/implementations/balsamic.py +22 -0
  121. cg/services/analysis_starter/tracker/implementations/microsalt.py +4 -4
  122. cg/services/analysis_starter/tracker/implementations/mip_dna.py +4 -1
  123. cg/services/analysis_starter/tracker/implementations/{nextflow.py → nextflow_tracker.py} +6 -4
  124. cg/services/analysis_starter/tracker/tracker.py +19 -15
  125. cg/services/deliver_files/factory.py +1 -1
  126. cg/services/delivery_message/messages/__init__.py +24 -14
  127. cg/services/delivery_message/messages/{microsalt_mwr_message.py → microsalt_message.py} +1 -1
  128. cg/services/delivery_message/utils.py +4 -40
  129. cg/services/illumina/backup/backup_service.py +29 -7
  130. cg/services/orders/validation/constants.py +3 -0
  131. cg/services/orders/validation/index_sequences.py +558 -0
  132. cg/services/orders/validation/order_types/microsalt/models/sample.py +2 -3
  133. cg/services/run_devices/pacbio/data_storage_service/pacbio_store_service.py +39 -18
  134. cg/services/run_devices/pacbio/data_transfer_service/data_transfer_service.py +8 -2
  135. cg/services/run_devices/pacbio/data_transfer_service/dto.py +9 -3
  136. cg/services/run_devices/pacbio/data_transfer_service/utils.py +14 -7
  137. cg/services/run_devices/pacbio/metrics_parser/models.py +1 -0
  138. cg/services/run_devices/pacbio/sequencing_runs_service.py +35 -7
  139. cg/services/sequencing_qc_service/quality_checks/checks.py +18 -16
  140. cg/services/sequencing_qc_service/quality_checks/utils.py +82 -18
  141. cg/services/sequencing_qc_service/sequencing_qc_service.py +12 -10
  142. cg/store/crud/create.py +73 -42
  143. cg/store/crud/read.py +73 -7
  144. cg/store/crud/update.py +14 -3
  145. cg/store/models.py +98 -35
  146. cg/store/store.py +8 -1
  147. {cg-76.0.0.dist-info → cg-83.14.0.dist-info}/METADATA +1 -1
  148. {cg-76.0.0.dist-info → cg-83.14.0.dist-info}/RECORD +150 -138
  149. cg/services/analysis_starter/submitters/seqera_platform/submitter.py +0 -39
  150. cg/services/delivery_message/messages/microsalt_mwx_message.py +0 -18
  151. {cg-76.0.0.dist-info → cg-83.14.0.dist-info}/WHEEL +0 -0
  152. {cg-76.0.0.dist-info → cg-83.14.0.dist-info}/entry_points.txt +0 -0
@@ -17,7 +17,6 @@ class NfTowerStatus(StrEnum):
17
17
 
18
18
  NALLO_GENERAL_METRIC_CONDITIONS: dict[str, dict[str, Any]] = {
19
19
  "median_coverage": {"norm": "gt", "threshold": 20},
20
- "predicted_sex_sex_check": {"norm": "eq", "threshold": None},
21
20
  }
22
21
 
23
22
  NALLO_PARENT_PEDDY_METRIC_CONDITION: dict[str, dict[str, Any]] = {
@@ -14,10 +14,14 @@ LOQUSDB_SUPPORTED_WORKFLOWS = [
14
14
  ]
15
15
  LOQUSDB_RARE_DISEASE_CUSTOMERS = [CustomerId.CUST002, CustomerId.CUST003, CustomerId.CUST004]
16
16
  LOQUSDB_CANCER_CUSTOMERS = [
17
+ CustomerId.CUST002,
18
+ CustomerId.CUST087,
17
19
  CustomerId.CUST110,
18
20
  CustomerId.CUST127,
19
21
  CustomerId.CUST143,
20
22
  CustomerId.CUST147,
23
+ CustomerId.CUST175,
24
+ CustomerId.CUST185,
21
25
  ]
22
26
  LOQUSDB_LONG_READ_CUSTOMERS = [
23
27
  CustomerId.CUST002,
@@ -35,9 +39,18 @@ LOQUSDB_RARE_DISEASE_SEQUENCING_METHODS = [
35
39
  LOQUSDB_CANCER_SEQUENCING_METHODS = [
36
40
  CancerAnalysisType.TUMOR_WGS,
37
41
  CancerAnalysisType.TUMOR_NORMAL_WGS,
42
+ CancerAnalysisType.TUMOR_PANEL,
38
43
  ]
39
44
 
40
45
 
46
+ class BalsamicObservationPanel(StrEnum):
47
+ """Group of panels which have associated LoqusDB instances."""
48
+
49
+ EXOME = "Twist Exome Comprehensive"
50
+ LYMPHOID = "GMSlymphoid"
51
+ MYELOID = "GMSmyeloid"
52
+
53
+
41
54
  class BalsamicLoadParameters(Enum):
42
55
  """Cancer Loqusdb load command parameters."""
43
56
 
@@ -57,11 +70,14 @@ class BalsamicObservationsAnalysisTag(StrEnum):
57
70
  class LoqusdbInstance(StrEnum):
58
71
  """Observations instances."""
59
72
 
60
- LWP: str = "loqusdb-lwp"
61
- WGS: str = "loqusdb"
62
- WES: str = "loqusdb-wes"
63
- SOMATIC: str = "loqusdb-somatic"
64
- TUMOR: str = "loqusdb-tumor"
73
+ LWP = "loqusdb-lwp"
74
+ WGS = "loqusdb"
75
+ WES = "loqusdb-wes"
76
+ SOMATIC = "loqusdb-somatic"
77
+ TUMOR = "loqusdb-tumor"
78
+ SOMATIC_LYMPHOID = "loqusdb-somatic-lymphoid"
79
+ SOMATIC_MYELOID = "loqusdb-somatic-myeloid"
80
+ SOMATIC_EXOME = "loqusdb-somatic-exome"
65
81
 
66
82
 
67
83
  class MipDNALoadParameters(Enum):
@@ -31,11 +31,11 @@ class Orderform(StrEnum):
31
31
  """Returns the current version of the given order form."""
32
32
  current_order_form_versions = {
33
33
  Orderform.MIP_DNA: "35",
34
- Orderform.RML: "20",
35
- Orderform.MICROSALT: "11",
34
+ Orderform.RML: "21",
35
+ Orderform.MICROSALT: "12",
36
36
  Orderform.NALLO: "1",
37
37
  Orderform.SARS_COV_2: "10",
38
- Orderform.MICROBIAL_FASTQ: "2",
38
+ Orderform.MICROBIAL_FASTQ: "3",
39
39
  Orderform.PACBIO_LONG_READ: "2",
40
40
  }
41
41
  return current_order_form_versions[order_form]
cg/constants/pacbio.py CHANGED
@@ -64,6 +64,7 @@ class SmrtLinkDatabasesAliases:
64
64
  BIO_SAMPLE_NAME: str = "bioSampleName"
65
65
  CELL_ID: str = "cellId"
66
66
  CELL_INDEX: str = "cellIndex"
67
+ INSTRUMENT_NAME: str = "instrumentName"
67
68
  MOVIE_NAME: str = "metadataContextId"
68
69
  PATH: str = "path"
69
70
  RUN_COMPLETED_AT = "createdAt"
cg/constants/priority.py CHANGED
@@ -34,7 +34,7 @@ class Priority(IntEnum):
34
34
  express = 4
35
35
 
36
36
  @classmethod
37
- def priority_to_slurm_qos(cls) -> dict[int, str]:
37
+ def priority_to_slurm_qos(cls) -> dict["Priority", SlurmQos]:
38
38
  return {
39
39
  Priority.research: SlurmQos.LOW,
40
40
  Priority.standard: SlurmQos.NORMAL,
cg/constants/report.py CHANGED
@@ -209,6 +209,7 @@ REQUIRED_SAMPLE_METADATA_NALLO_FIELDS: list[str] = _REQUIRED_SAMPLE_METADATA_FIE
209
209
  _REQUIRED_SAMPLE_METADATA_BALSAMIC_FIELDS: list[str] = _REQUIRED_SAMPLE_METADATA_FIELDS + [
210
210
  "mean_insert_size",
211
211
  "fold_80",
212
+ "predicted_sex",
212
213
  ]
213
214
 
214
215
  REQUIRED_SAMPLE_METADATA_BALSAMIC_TARGETED_FIELDS: list[str] = (
cg/constants/scout.py CHANGED
@@ -12,14 +12,14 @@ class GenomeBuild(StrEnum):
12
12
 
13
13
 
14
14
  class ScoutExportFileName(StrEnum):
15
- MANAGED_VARIANTS: str = f"managed_variants{FileExtensions.VCF}"
16
- PANELS: str = f"gene_panels{FileExtensions.BED}"
17
- PANELS_TSV: str = f"gene_panels{FileExtensions.TSV}"
15
+ MANAGED_VARIANTS = f"managed_variants{FileExtensions.VCF}"
16
+ PANELS = f"gene_panels{FileExtensions.BED}"
17
+ PANELS_TSV = f"gene_panels{FileExtensions.TSV}"
18
18
 
19
19
 
20
20
  class UploadTrack(StrEnum):
21
- RARE_DISEASE: str = "rare"
22
- CANCER: str = "cancer"
21
+ RARE_DISEASE = "rare"
22
+ CANCER = "cancer"
23
23
 
24
24
 
25
25
  class ScoutCustomCaseReportTags(StrEnum):
@@ -66,12 +66,12 @@ NALLO_CASE_TAGS = dict(
66
66
  multiqc={"multiqc-html"},
67
67
  peddy_check={"ped-check", "peddy"},
68
68
  peddy_ped={"ped", "peddy"},
69
- peddy_sex={"sex-check", "peddy"},
70
- vcf_snv_research={"vcf-snv-research"},
69
+ somalier_samples={"somalier", "relate-samples"},
71
70
  vcf_snv={"vcf-snv-clinical"},
72
- vcf_sv_research={"vcf-sv-research"},
73
- vcf_sv={"vcf-sv-clinical"},
71
+ vcf_snv_research={"vcf-snv-research"},
74
72
  vcf_str={"vcf-str"},
73
+ vcf_sv={"vcf-sv-clinical"},
74
+ vcf_sv_research={"vcf-sv-research"},
75
75
  )
76
76
 
77
77
  MIP_CASE_TAGS: dict[str, set[str]] = dict(
@@ -138,9 +138,12 @@ RAREDISEASE_SAMPLE_TAGS: dict[str, set[str]] = dict(
138
138
  NALLO_SAMPLE_TAGS: dict[str, set[str]] = dict(
139
139
  alignment_path={AlignmentFileTag.BAM, "haplotags"},
140
140
  assembly_alignment_path={AlignmentFileTag.BAM, "assembly"},
141
+ chromograph_autozyg={"chromograph", "autozyg"},
142
+ chromograph_coverage={"chromograph", "tcov"},
141
143
  d4_file={"coverage", "d4"},
142
144
  hificnv_coverage={"hificnv", "bigwig"},
143
145
  paraphase_alignment_path={AlignmentFileTag.BAM, NalloAnalysisTag.PARAPHASE},
146
+ phase_blocks={"whatshap", "gtf"},
144
147
  reviewer_alignment={"repeats", "spanning", "bam"},
145
148
  reviewer_alignment_index={"repeats", "spanning", "bam-index"},
146
149
  reviewer_vcf={"repeats", "sorted", "vcf"},
@@ -60,8 +60,8 @@ class RecordType(StrEnum):
60
60
 
61
61
 
62
62
  class SequencingPlatform(StrEnum):
63
- ILLUMINA: str = "ILLUMINA"
64
- OXFORD_NANOPORE: str = "OXFORD_NANOPORE"
63
+ ILLUMINA = "ILLUMINA"
64
+ OXFORD_NANOPORE = "OXFORD_NANOPORE"
65
65
 
66
66
 
67
67
  class SeqLibraryPrepCategory(StrEnum):
cg/constants/tb.py CHANGED
@@ -15,8 +15,8 @@ class AnalysisStatus:
15
15
 
16
16
 
17
17
  class AnalysisType(StrEnum):
18
- OTHER: str = "other"
19
- TGS: str = SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING
20
- WES: str = SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING
21
- WGS: str = SeqLibraryPrepCategory.WHOLE_GENOME_SEQUENCING
22
- WTS: str = SeqLibraryPrepCategory.WHOLE_TRANSCRIPTOME_SEQUENCING
18
+ OTHER = "other"
19
+ TGS = SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING
20
+ WES = SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING
21
+ WGS = SeqLibraryPrepCategory.WHOLE_GENOME_SEQUENCING
22
+ WTS = SeqLibraryPrepCategory.WHOLE_TRANSCRIPTOME_SEQUENCING
cg/exc.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- Cg Exceptions.
2
+ Cg Exceptions.
3
3
  """
4
4
 
5
5
 
@@ -36,6 +36,12 @@ class AnalysisNotReadyError(CgError):
36
36
  """
37
37
 
38
38
 
39
+ class AnalysisAlreadyCompletedError(CgError):
40
+ """
41
+ Exception raised when the latest analysis is already completed.
42
+ """
43
+
44
+
39
45
  class AnalysisNotCompletedError(CgError):
40
46
  """
41
47
  Exception raised when an analysis has not completed.
@@ -144,6 +150,14 @@ class IlluminaRunAlreadyBackedUpError(CgError):
144
150
  """Raised when a flow cell is already backed-up."""
145
151
 
146
152
 
153
+ class PacbioSequencingRunAlreadyExistsError(CgError):
154
+ """Raised when a PacBio sequencing run already exists."""
155
+
156
+
157
+ class PacbioSequencingRunNotFoundError(CgError):
158
+ """Raised when a PacBio sequencing run is not found."""
159
+
160
+
147
161
  class HousekeeperFileMissingError(CgError):
148
162
  """
149
163
  Exception raised when a file is missing in Housekeeper.
@@ -210,10 +224,6 @@ class RunParametersError(CgError):
210
224
  """Raised when something is wrong with the run parameters file."""
211
225
 
212
226
 
213
- class NfSampleSheetError(CgError):
214
- """Raised when something is wrong with the sample sheet."""
215
-
216
-
217
227
  class SampleSheetContentError(CgError):
218
228
  """Raised when something is wrong with the sample sheet content."""
219
229
 
@@ -334,5 +344,17 @@ class Chanjo2ResponseError(Chanjo2APIClientError):
334
344
  """Exception raised when the response from Chanjo2 API client fails validation."""
335
345
 
336
346
 
347
+ class CaseNotConfiguredError(CgError):
348
+ """Exception raised when a case is being run without a configuration."""
349
+
350
+
337
351
  class MissingConfigFilesError(CgError):
338
352
  """Exception raised when a case is being run with missing configuration files."""
353
+
354
+
355
+ class SeqeraError(CgError):
356
+ """Exception raised when receiving an unexpected response from Seqera platform"""
357
+
358
+
359
+ class ApplicationDoesNotHaveHiFiYieldError(CgError):
360
+ """Exception raised when application does not have HiFi yield set."""
@@ -96,8 +96,13 @@ class CompressAPI:
96
96
  def decompress_case(self, case: Case) -> None:
97
97
  """Decompresses all Spring files tied to the case.
98
98
  Raises:
99
- DecompressionFailedToStartError if no sample could start decompressing."""
100
- if not any(self.decompress_spring(sample.internal_id) for sample in case.samples):
99
+ DecompressionCouldNotStartError if no sample could start decompressing.
100
+ """
101
+ success = False
102
+ for sample in case.samples:
103
+ if self.decompress_spring(sample.internal_id):
104
+ success = True
105
+ if not success:
101
106
  raise DecompressionCouldNotStartError(
102
107
  f"No sample could be decompressed for {case.internal_id}"
103
108
  )
@@ -89,6 +89,7 @@ class BalsamicDeliveryReportAPI(DeliveryReportAPI):
89
89
  million_read_pairs=million_read_pairs,
90
90
  pct_250x=sample_metrics.pct_target_bases_250x if sample_metrics else None,
91
91
  pct_500x=sample_metrics.pct_target_bases_500x if sample_metrics else None,
92
+ predicted_sex=sample_metrics.compare_predicted_to_given_sex if sample_metrics else None,
92
93
  )
93
94
 
94
95
  @staticmethod
@@ -103,13 +104,14 @@ class BalsamicDeliveryReportAPI(DeliveryReportAPI):
103
104
  fold_80=sample_metrics.fold_80_base_penalty if sample_metrics else None,
104
105
  initial_qc=passed_initial_qc,
105
106
  mean_insert_size=sample_metrics.mean_insert_size if sample_metrics else None,
106
- median_coverage=sample_metrics.median_coverage if sample_metrics else None,
107
+ median_coverage=sample_metrics.median_target_coverage if sample_metrics else None,
107
108
  million_read_pairs=million_read_pairs,
108
109
  pct_15x=sample_metrics.pct_15x if sample_metrics else None,
109
110
  pct_60x=sample_metrics.pct_60x if sample_metrics else None,
110
111
  pct_reads_improper_pairs=(
111
112
  sample_metrics.pct_pf_reads_improper_pairs if sample_metrics else None
112
113
  ),
114
+ predicted_sex=sample_metrics.compare_predicted_to_given_sex if sample_metrics else None,
113
115
  )
114
116
 
115
117
  def is_report_accredited(
@@ -23,6 +23,7 @@ from cg.meta.delivery.delivery import DeliveryAPI
23
23
  from cg.meta.delivery_report.data_validators import get_empty_report_data, get_missing_report_data
24
24
  from cg.meta.workflow.analysis import AnalysisAPI
25
25
  from cg.models.analysis import AnalysisModel
26
+ from cg.models.delivery.delivery import DeliveryFile
26
27
  from cg.models.delivery_report.metadata import SampleMetadataModel
27
28
  from cg.models.delivery_report.report import (
28
29
  CaseModel,
@@ -250,14 +251,14 @@ class DeliveryReportAPI:
250
251
  for case_sample in case_samples:
251
252
  sample: Sample = case_sample.sample
252
253
  lims_sample: dict[str, Any] = self.lims_api.sample(sample.internal_id)
253
- delivered_files: list[File] | None = (
254
+ delivered_files: list[DeliveryFile] | None = (
254
255
  self.delivery_api.get_analysis_sample_delivery_files_by_sample(
255
256
  case=case, sample=sample
256
257
  )
257
258
  if self.delivery_api.is_analysis_delivery(case.data_delivery)
258
259
  else None
259
260
  )
260
- delivered_fastq_files: list[File] | None = (
261
+ delivered_fastq_files: list[DeliveryFile] | None = (
261
262
  self.delivery_api.get_fastq_delivery_files_by_sample(case=case, sample=sample)
262
263
  if self.delivery_api.is_fastq_delivery(case.data_delivery)
263
264
  else None
@@ -332,7 +333,7 @@ class DeliveryReportAPI:
332
333
 
333
334
  def get_case_analysis_data(self, case: Case, analysis: Analysis) -> DataAnalysisModel:
334
335
  """Return workflow attributes used for data analysis."""
335
- delivered_files: list[File] | None = (
336
+ delivered_files: list[DeliveryFile] | None = (
336
337
  self.delivery_api.get_analysis_case_delivery_files(case)
337
338
  if self.delivery_api.is_analysis_delivery(case.data_delivery)
338
339
  else None
@@ -1,7 +1,9 @@
1
1
  """Nallo Delivery Report API."""
2
2
 
3
+ from typing import cast
4
+
3
5
  from cg.clients.chanjo2.models import CoverageMetrics
4
- from cg.constants.housekeeper_tags import AnalysisTag, HermesFileTag
6
+ from cg.constants.housekeeper_tags import AnalysisTag
5
7
  from cg.constants.report import (
6
8
  REQUIRED_APPLICATION_FIELDS,
7
9
  REQUIRED_CASE_FIELDS,
@@ -13,8 +15,6 @@ from cg.constants.report import (
13
15
  REQUIRED_SAMPLE_NALLO_FIELDS,
14
16
  REQUIRED_SAMPLE_TIMESTAMP_FIELDS,
15
17
  )
16
- from cg.constants.scout import ScoutUploadKey
17
- from cg.constants.subject import PlinkSex
18
18
  from cg.meta.delivery_report.data_validators import get_million_read_pairs
19
19
  from cg.meta.delivery_report.delivery_report_api import DeliveryReportAPI
20
20
  from cg.meta.workflow.nallo import NalloAnalysisAPI
@@ -36,7 +36,9 @@ class NalloDeliveryReportAPI(DeliveryReportAPI):
36
36
  self, case: Case, sample: Sample, analysis_metadata: NextflowAnalysis
37
37
  ) -> NalloSampleMetadataModel:
38
38
  """Return Nallo sample metadata to include in the report."""
39
- sample_metrics: NalloQCMetrics = analysis_metadata.sample_metrics[sample.internal_id]
39
+ sample_metrics: NalloQCMetrics = cast(
40
+ NalloQCMetrics, analysis_metadata.sample_metrics[sample.internal_id]
41
+ )
40
42
  gene_ids: list[int] = self.analysis_api.get_gene_ids_from_scout(case.panels)
41
43
  coverage_metrics: CoverageMetrics | None = self.analysis_api.get_sample_coverage(
42
44
  case_id=case.internal_id, sample_id=sample.internal_id, gene_ids=gene_ids
@@ -50,7 +52,7 @@ class NalloDeliveryReportAPI(DeliveryReportAPI):
50
52
  median_coverage=sample_metrics.median_coverage,
51
53
  million_read_pairs=get_million_read_pairs(sample.reads),
52
54
  pct_10x=coverage_metrics.coverage_completeness_percent if coverage_metrics else None,
53
- sex=sample_metrics.predicted_sex_sex_check,
55
+ sex=sample_metrics.predicted_sex,
54
56
  )
55
57
 
56
58
  def is_report_accredited(
@@ -65,23 +67,21 @@ class NalloDeliveryReportAPI(DeliveryReportAPI):
65
67
  def get_scout_variants_files(self, case_id: str) -> ScoutVariantsFiles:
66
68
  """Return Nallo files that will be uploaded to Scout."""
67
69
  return ScoutVariantsFiles(
68
- snv_vcf=self.housekeeper_api.get_latest_file(
70
+ snv_vcf=self.housekeeper_api.get_latest_file_strict(
69
71
  bundle=case_id,
70
72
  tags=[
71
73
  AnalysisTag.VCF_SNV_CLINICAL,
72
74
  ],
73
75
  ).full_path,
74
- sv_vcf=self.housekeeper_api.get_latest_file(
76
+ sv_vcf=self.housekeeper_api.get_latest_file_strict(
75
77
  bundle=case_id,
76
78
  tags=[
77
79
  AnalysisTag.VCF_SV_CLINICAL,
78
80
  ],
79
81
  ).full_path,
80
- vcf_str=self.housekeeper_api.get_latest_file(
82
+ vcf_str=self.housekeeper_api.get_latest_file_strict(
81
83
  bundle=case_id,
82
- tags=[
83
- AnalysisTag.VCF_STR,
84
- ],
84
+ tags=[AnalysisTag.VCF_STR],
85
85
  ).full_path,
86
86
  )
87
87
 
@@ -1,6 +1,9 @@
1
1
  """Raredisease Delivery Report API."""
2
2
 
3
+ from housekeeper.store.models import File
4
+
3
5
  from cg.clients.chanjo2.models import CoverageMetrics
6
+ from cg.constants.housekeeper_tags import AnalysisTag
4
7
  from cg.constants.report import (
5
8
  REQUIRED_APPLICATION_FIELDS,
6
9
  REQUIRED_CASE_FIELDS,
@@ -43,7 +46,7 @@ class RarediseaseDeliveryReportAPI(DeliveryReportAPI):
43
46
  )
44
47
  return RarediseaseSampleMetadataModel(
45
48
  bait_set=self.lims_api.capture_kit(sample.internal_id),
46
- duplicates=sample_metrics.percent_duplicates,
49
+ duplicates=round(sample_metrics.percent_duplication * 100, 2),
47
50
  initial_qc=self.lims_api.has_sample_passed_initial_qc(sample.internal_id),
48
51
  mapped_reads=round((sample_metrics.mapped_reads / sample_metrics.total_reads) * 100, 2),
49
52
  mean_target_coverage=coverage_metrics.mean_coverage if coverage_metrics else None,
@@ -69,9 +72,10 @@ class RarediseaseDeliveryReportAPI(DeliveryReportAPI):
69
72
  sv_vcf: str | None = self.get_scout_uploaded_file_from_hk(
70
73
  case_id=case_id, scout_key=ScoutUploadKey.VCF_SV
71
74
  )
72
- vcf_str: str | None = self.get_scout_uploaded_file_from_hk(
73
- case_id=case_id, scout_key=ScoutUploadKey.VCF_STR
75
+ vcf_str_file: File | None = self.housekeeper_api.get_latest_file(
76
+ bundle=case_id, tags=[AnalysisTag.VCF_STR, case_id]
74
77
  )
78
+ vcf_str: str | None = vcf_str_file.full_path if vcf_str_file else None
75
79
  smn_tsv: str | None = self.get_scout_uploaded_file_from_hk(
76
80
  case_id=case_id, scout_key=ScoutUploadKey.SMN_TSV
77
81
  )
@@ -3,6 +3,7 @@
3
3
  {% macro balsamic_qc_metrics(samples, analysis_type) %}
4
4
  {% set
5
5
  metrics = [
6
+ {"name": "Kön", "key": "predicted_sex", "description": "Kön beräknat genom bioinformatisk analys."},
6
7
  {"name": "Läspar [M]", "key": "million_read_pairs", "description": "Antal sekvenseringsläsningar i miljoner läspar."},
7
8
  {"name": "Duplikat [%]", "key": "duplicates", "description": "Sekvenseringsläsningar som är i duplikat och därmed ej unika sekvenser. Hög mängd duplikat kan tyda på dålig komplexitet av sekvenserat bibliotek eller djup sekvensering."},
8
9
  {"name": "Medelfragmentlängd [baspar]", "key": "mean_insert_size", "description": "Medelstorlek av provbiblioteken som laddats på sekvenseringsinstrument."},
@@ -2,7 +2,7 @@
2
2
  <div class="container">
3
3
  <div class="alert alert-info" role="alert">
4
4
  All kommunikation gällande ordern, såsom tillägg, avvikelser eller ev undantag i metoden från
5
- Clinical Genomics, finns tillgänglig i SupportSystem och är länkad till ärendets ticket:
5
+ Clinical Genomics, finns tillgänglig i Freshdesk och är länkad till ärendets ticket:
6
6
  <a class="alert-link">#{{ ticket_id }}</a>. En stängd ticket kan närsomhelst öppnas upp igen för frågor.
7
7
  </div>
8
8
  </div>
@@ -12,19 +12,27 @@ from cg.constants.observations import (
12
12
  LOQUSDB_CANCER_SEQUENCING_METHODS,
13
13
  LOQUSDB_ID,
14
14
  BalsamicLoadParameters,
15
+ BalsamicObservationPanel,
15
16
  BalsamicObservationsAnalysisTag,
16
17
  LoqusdbInstance,
17
18
  )
18
- from cg.exc import CaseNotFoundError, LoqusdbDuplicateRecordError
19
+ from cg.constants.sequencing import SeqLibraryPrepCategory
20
+ from cg.exc import CaseNotFoundError, LoqusdbDeleteCaseError, LoqusdbDuplicateRecordError
19
21
  from cg.meta.observations.observations_api import ObservationsAPI
20
22
  from cg.meta.workflow.balsamic import BalsamicAnalysisAPI
21
23
  from cg.models.cg_config import CGConfig
22
24
  from cg.models.observations.input_files import BalsamicObservationsInputFiles
23
- from cg.store.models import Case
25
+ from cg.store.models import BedVersion, Case, Sample
24
26
  from cg.utils.dict import get_full_path_dictionary
25
27
 
26
28
  LOG = logging.getLogger(__name__)
27
29
 
30
+ PANEL_TO_LOQUSDB_INSTANCE_MAP: dict = {
31
+ BalsamicObservationPanel.LYMPHOID: LoqusdbInstance.SOMATIC_LYMPHOID,
32
+ BalsamicObservationPanel.MYELOID: LoqusdbInstance.SOMATIC_MYELOID,
33
+ BalsamicObservationPanel.EXOME: LoqusdbInstance.SOMATIC_EXOME,
34
+ }
35
+
28
36
 
29
37
  class BalsamicObservationsAPI(ObservationsAPI):
30
38
  """API to manage Balsamic observations."""
@@ -32,6 +40,7 @@ class BalsamicObservationsAPI(ObservationsAPI):
32
40
  def __init__(self, config: CGConfig):
33
41
  self.analysis_api = BalsamicAnalysisAPI(config)
34
42
  super().__init__(config=config, analysis_api=self.analysis_api)
43
+ self.lims_api = config.lims_api
35
44
  self.loqusdb_somatic_api: LoqusdbAPI = self.get_loqusdb_api(LoqusdbInstance.SOMATIC)
36
45
  self.loqusdb_tumor_api: LoqusdbAPI = self.get_loqusdb_api(LoqusdbInstance.TUMOR)
37
46
 
@@ -45,43 +54,122 @@ class BalsamicObservationsAPI(ObservationsAPI):
45
54
  """Return sequencing methods that are eligible for cancer Loqusdb uploads."""
46
55
  return LOQUSDB_CANCER_SEQUENCING_METHODS
47
56
 
48
- def is_analysis_type_eligible_for_observations_upload(self, case_id) -> bool:
57
+ def is_analysis_type_eligible_for_observations_upload(self, case: Case) -> bool:
49
58
  """Return whether the cancer analysis type is eligible for cancer Loqusdb uploads."""
50
- if self.analysis_api.is_analysis_normal_only(case_id):
51
- LOG.error(f"Normal only analysis {case_id} is not supported for Loqusdb uploads")
59
+ if self.analysis_api.is_analysis_normal_only(case.internal_id):
60
+ LOG.error(
61
+ f"Normal only analysis {case.internal_id} is not supported for Loqusdb uploads"
62
+ )
63
+ return False
64
+ prep_category: str = case.samples[0].prep_category
65
+ if prep_category in [
66
+ SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING,
67
+ SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING,
68
+ ] and self._is_paired_analysis(case):
69
+ LOG.error(
70
+ f"Paired analysis {case.internal_id} is not supported for TGS Loqusdb uploads"
71
+ )
52
72
  return False
53
73
  return True
54
74
 
75
+ @staticmethod
76
+ def _is_paired_analysis(case: Case) -> bool:
77
+ return len(case.samples) > 1
78
+
55
79
  def is_case_eligible_for_observations_upload(self, case: Case) -> bool:
56
80
  """Return whether a cancer case is eligible for observations upload."""
57
81
  return all(
58
82
  [
59
83
  self.is_customer_eligible_for_observations_upload(case.customer.internal_id),
60
- self.is_sequencing_method_eligible_for_observations_upload(case.internal_id),
61
- self.is_analysis_type_eligible_for_observations_upload(case.internal_id),
62
- self.is_sample_source_eligible_for_observations_upload(case.internal_id),
84
+ self.is_analysis_type_eligible_for_observations_upload(case),
85
+ self.is_sample_source_type_not_ffpe(case.internal_id),
86
+ self.is_panel_allowed_for_observations_upload(case),
63
87
  ]
64
88
  )
65
89
 
90
+ def is_panel_allowed_for_observations_upload(self, case: Case) -> bool:
91
+ """
92
+ True if WGS.
93
+ If TGS/WES, True for panels with LoqusDB instances.
94
+ This assumes that all samples in the case have the same prep-category.
95
+ """
96
+ if self._is_panel_upload(case):
97
+ sample: Sample = case.samples[0]
98
+ panel_short_name: str | None = self.lims_api.capture_kit(sample.internal_id)
99
+ bed_version: BedVersion | None = self.store.get_bed_version_by_short_name(
100
+ panel_short_name
101
+ )
102
+ if not bed_version:
103
+ LOG.warning(
104
+ f"No bed version found for LIMS panel {panel_short_name} for sample "
105
+ f"{sample.internal_id} in case {case.internal_id}"
106
+ )
107
+ return False
108
+ if bed_version.bed.name not in list(BalsamicObservationPanel):
109
+ return False
110
+ return True
111
+
66
112
  def load_observations(self, case: Case) -> None:
67
113
  """
68
- Load observation counts to Loqusdb for a Balsamic case.
114
+ Upload observation counts to Loqusdb for a Balsamic case.
69
115
 
70
116
  Raises:
71
117
  LoqusdbDuplicateRecordError: If case has already been uploaded.
72
118
  """
73
- loqusdb_upload_apis: list[LoqusdbAPI] = [self.loqusdb_somatic_api, self.loqusdb_tumor_api]
119
+ if self._is_panel_upload(case):
120
+ self._upload_panel_case(case)
121
+ else:
122
+ self._upload_wgs_case(case)
123
+
124
+ @staticmethod
125
+ def _is_panel_upload(case: Case) -> bool:
126
+ sample: Sample = case.samples[0]
127
+ return sample.prep_category in [
128
+ SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING,
129
+ SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING,
130
+ ]
131
+
132
+ def _upload_panel_case(self, case: Case) -> None:
133
+ """
134
+ Uploads the case to one of the somatic panel LoqusDB instances. The case is known to have
135
+ a panel with a known LoqusDB instance.
136
+ """
137
+ loqusdb_api: LoqusdbAPI = self._get_panel_loqusdb_api(case)
138
+ if self.is_duplicate(case=case, loqusdb_api=loqusdb_api):
139
+ LOG.error(f"Case {case.internal_id} has already been uploaded to Loqusdb")
140
+ raise LoqusdbDuplicateRecordError
141
+ input_files: BalsamicObservationsInputFiles = self.get_observations_input_files(case)
142
+ loqusdb_api.load(
143
+ case_id=case.internal_id,
144
+ snv_vcf_path=input_files.snv_vcf_path,
145
+ )
146
+ loqusdb_id: str = str(loqusdb_api.get_case(case_id=case.internal_id)[LOQUSDB_ID])
147
+ self.update_statusdb_loqusdb_id(samples=case.samples, loqusdb_id=loqusdb_id)
148
+
149
+ def _get_panel_loqusdb_api(self, case: Case) -> LoqusdbAPI:
150
+ sample: Sample = case.samples[0]
151
+ bed_short_name: str = self.lims_api.capture_kit(sample.internal_id)
152
+ bed_version: BedVersion = self.store.get_bed_version_by_short_name_strict(bed_short_name)
153
+ panel: str = bed_version.bed.name
154
+ loqusdb_instance: LoqusdbInstance = PANEL_TO_LOQUSDB_INSTANCE_MAP[
155
+ BalsamicObservationPanel(panel)
156
+ ]
157
+ return self.get_loqusdb_api(loqusdb_instance)
158
+
159
+ def _upload_wgs_case(self, case: Case) -> None:
160
+ loqusdb_upload_apis: list[LoqusdbAPI] = [
161
+ self.loqusdb_somatic_api,
162
+ self.loqusdb_tumor_api,
163
+ ]
74
164
  for loqusdb_api in loqusdb_upload_apis:
75
165
  if self.is_duplicate(case=case, loqusdb_api=loqusdb_api):
76
166
  LOG.error(f"Case {case.internal_id} has already been uploaded to Loqusdb")
77
167
  raise LoqusdbDuplicateRecordError
78
-
79
168
  input_files: BalsamicObservationsInputFiles = self.get_observations_input_files(case)
80
169
  for loqusdb_api in loqusdb_upload_apis:
81
170
  self.load_cancer_observations(
82
171
  case=case, input_files=input_files, loqusdb_api=loqusdb_api
83
172
  )
84
-
85
173
  # Update Statusdb with a germline Loqusdb ID
86
174
  loqusdb_id: str = str(self.loqusdb_tumor_api.get_case(case_id=case.internal_id)[LOQUSDB_ID])
87
175
  self.update_statusdb_loqusdb_id(samples=case.samples, loqusdb_id=loqusdb_id)
@@ -137,12 +225,12 @@ class BalsamicObservationsAPI(ObservationsAPI):
137
225
  version=hk_version.id, tags=[BalsamicObservationsAnalysisTag.SV_VCF]
138
226
  ).first(),
139
227
  }
140
- return BalsamicObservationsInputFiles(**get_full_path_dictionary(input_files))
228
+ return BalsamicObservationsInputFiles.model_validate(get_full_path_dictionary(input_files))
141
229
 
142
230
  def delete_case(self, case_id: str) -> None:
143
231
  """Delete cancer case observations from Loqusdb."""
144
232
  case: Case = self.store.get_case_by_internal_id(internal_id=case_id)
145
- loqusdb_apis: list[LoqusdbAPI] = [self.loqusdb_somatic_api, self.loqusdb_tumor_api]
233
+ loqusdb_apis: list[LoqusdbAPI] = self._get_relevant_loqusdb_apis(case)
146
234
  for loqusdb_api in loqusdb_apis:
147
235
  if not loqusdb_api.get_case(case_id):
148
236
  LOG.error(f"Case {case_id} could not be found in Loqusdb. Skipping case deletion.")
@@ -151,3 +239,11 @@ class BalsamicObservationsAPI(ObservationsAPI):
151
239
  loqusdb_api.delete_case(case_id)
152
240
  self.update_statusdb_loqusdb_id(samples=case.samples, loqusdb_id=None)
153
241
  LOG.info(f"Removed observations for case {case_id} from Loqusdb")
242
+
243
+ def _get_relevant_loqusdb_apis(self, case: Case) -> list[LoqusdbAPI]:
244
+ if not self._is_panel_upload(case):
245
+ return [self.loqusdb_somatic_api, self.loqusdb_tumor_api]
246
+ try:
247
+ return [self._get_panel_loqusdb_api(case)]
248
+ except ValueError as error:
249
+ raise LoqusdbDeleteCaseError from error
@@ -58,7 +58,7 @@ class MipDNAObservationsAPI(ObservationsAPI):
58
58
  self.is_customer_eligible_for_observations_upload(case.customer.internal_id),
59
59
  self.is_sequencing_method_eligible_for_observations_upload(case.internal_id),
60
60
  self.is_sample_type_eligible_for_observations_upload(case),
61
- self.is_sample_source_eligible_for_observations_upload(case.internal_id),
61
+ self.is_sample_source_type_not_ffpe(case.internal_id),
62
62
  ]
63
63
  )
64
64
 
@@ -59,7 +59,7 @@ class NalloObservationsAPI(ObservationsAPI):
59
59
  self.is_customer_eligible_for_observations_upload(case.customer.internal_id),
60
60
  self.is_sequencing_method_eligible_for_observations_upload(case.internal_id),
61
61
  self.is_sample_type_eligible_for_observations_upload(case),
62
- self.is_sample_source_eligible_for_observations_upload(case.internal_id),
62
+ self.is_sample_source_type_not_ffpe(case.internal_id),
63
63
  ]
64
64
  )
65
65