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
cg/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __title__ = "cg"
2
- __version__ = "76.0.0"
2
+ __version__ = "83.14.0"
cg/apps/housekeeper/hk.py CHANGED
@@ -120,7 +120,7 @@ class HousekeeperAPI:
120
120
  *,
121
121
  bundle: str = None,
122
122
  tags: set[str] = None,
123
- version: int = None,
123
+ version: int | None = None,
124
124
  path: str = None,
125
125
  ) -> Query:
126
126
  """Fetch files."""
@@ -181,6 +181,23 @@ class HousekeeperAPI:
181
181
  files: Query = self._store.get_files(bundle_name=bundle, tag_names=tags, version_id=version)
182
182
  return files.order_by(File.id.desc()).first()
183
183
 
184
+ def get_latest_file_strict(
185
+ self, bundle: str, tags: list | None = None, version: int | None = None
186
+ ) -> File:
187
+ """
188
+ Return latest file from Housekeeper, filtered by bundle and/or tags and/or version.
189
+ Raises:
190
+ HousekeeperFileMissingError if no file is found.
191
+ """
192
+ files: Query = self._store.get_files(bundle_name=bundle, tag_names=tags, version_id=version)
193
+ latest_file: File | None = files.order_by(File.id.desc()).first()
194
+ if not latest_file:
195
+ raise HousekeeperFileMissingError(
196
+ f"No file matching bundle {bundle}, version {version} with tags {tags}"
197
+ )
198
+ else:
199
+ return latest_file
200
+
184
201
  def check_bundle_files(
185
202
  self,
186
203
  bundle_name: str,
cg/apps/tb/api.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Trailblazer API for cg."""
2
2
 
3
3
  import logging
4
- from datetime import datetime
4
+ from datetime import datetime, timezone
5
5
  from typing import Any
6
6
 
7
7
  from google.auth.transport.requests import Request
@@ -34,15 +34,52 @@ class TrailblazerAPI:
34
34
  self.service_account = config["trailblazer"]["service_account"]
35
35
  self.service_account_auth_file = config["trailblazer"]["service_account_auth_file"]
36
36
  self.host = config["trailblazer"]["host"]
37
+ self._credentials: IDTokenCredentials | None = None
37
38
 
38
- @property
39
- def auth_header(self) -> dict:
39
+ def _are_credentials_expired(self) -> bool:
40
+ """Return True when there are no cached credentials or the token has expired."""
41
+ if not self._credentials:
42
+ return True
43
+
44
+ expiry: datetime | None = getattr(self._credentials, "expiry", None)
45
+ if expiry is None:
46
+ return True
47
+
48
+ if expiry.tzinfo is None:
49
+ expiry = expiry.replace(tzinfo=timezone.utc)
50
+
51
+ return datetime.now(tz=timezone.utc) >= expiry
52
+
53
+ def _refresh_credentials(self) -> IDTokenCredentials:
54
+ """Refresh the Google OAuth token and cache the credential object."""
55
+ LOG.debug("Refreshing Google OAuth token for Trailblazer authentication")
40
56
  credentials: IDTokenCredentials = IDTokenCredentials.from_service_account_file(
41
57
  filename=self.service_account_auth_file,
42
58
  target_audience="trailblazer",
43
59
  )
44
60
  credentials.refresh(Request())
45
- return {"Authorization": f"Bearer {credentials.token}"}
61
+
62
+ self._credentials = credentials
63
+ expiry: datetime | None = getattr(self._credentials, "expiry", None)
64
+ if expiry:
65
+ if expiry.tzinfo is None:
66
+ expiry = expiry.replace(tzinfo=timezone.utc)
67
+ LOG.debug(
68
+ "Token cached until %s (%s local)",
69
+ expiry,
70
+ expiry.astimezone().isoformat(),
71
+ )
72
+ else:
73
+ LOG.debug("Token refreshed without expiry timestamp")
74
+ return self._credentials
75
+
76
+ @property
77
+ def auth_header(self) -> dict:
78
+ """Get authorization header with cached token to prevent Google rate limiting."""
79
+ if not self._credentials or self._are_credentials_expired():
80
+ self._refresh_credentials()
81
+
82
+ return {"Authorization": f"Bearer {self._credentials.token}"}
46
83
 
47
84
  def query_trailblazer(
48
85
  self, command: str, request_body: dict, method: str = APIMethods.POST
@@ -97,7 +134,7 @@ class TrailblazerAPI:
97
134
  self,
98
135
  case_id: str,
99
136
  analysis_type: str,
100
- config_path: str,
137
+ config_path: str | None,
101
138
  out_dir: str,
102
139
  priority: TrailblazerPriority,
103
140
  workflow: Workflow,
cg/cli/transfer.py CHANGED
@@ -23,6 +23,12 @@ def transfer_group(context: CGConfig):
23
23
 
24
24
 
25
25
  @transfer_group.command("lims")
26
+ @click.option(
27
+ "--max-order-age",
28
+ type=click.IntRange(min=1),
29
+ default=None,
30
+ help="Exclude samples with order dates older than N years. Must be greater than or equal to 1.",
31
+ )
26
32
  @click.option(
27
33
  "-s", "--status", type=click.Choice(["received", "prepared", "delivered"]), default="received"
28
34
  )
@@ -31,11 +37,16 @@ def transfer_group(context: CGConfig):
31
37
  )
32
38
  @click.option("--sample-id", help="Lims Submitted Sample id. use together with status.")
33
39
  @click.pass_obj
34
- def check_samples_in_lims(context: CGConfig, status: str, include: str, sample_id: str):
40
+ def check_samples_in_lims(
41
+ context: CGConfig, max_order_age: int | None, status: str, include: str, sample_id: str
42
+ ):
35
43
  """Check if samples have been updated in LIMS."""
36
44
  transfer_api: TransferLims = context.meta_apis["transfer_lims_api"]
37
45
  transfer_api.transfer_samples(
38
- status_type=SampleState[status.upper()], include=include, sample_id=sample_id
46
+ include=include,
47
+ max_order_age=max_order_age,
48
+ sample_id=sample_id,
49
+ status_type=SampleState[status.upper()],
39
50
  )
40
51
 
41
52
 
cg/cli/upload/mutacc.py CHANGED
@@ -7,7 +7,7 @@ import rich_click as click
7
7
  from cg.apps.mutacc_auto import MutaccAutoAPI
8
8
  from cg.apps.scout.scout_export import ScoutExportCase
9
9
  from cg.apps.scout.scoutapi import ScoutAPI
10
- from cg.cli.upload.utils import get_scout_api
10
+ from cg.cli.upload.utils import get_scout_api_by_genome_build
11
11
  from cg.constants.cli_options import DRY_RUN
12
12
  from cg.meta.upload.mutacc import UploadToMutaccAPI
13
13
  from cg.models.cg_config import CGConfig
@@ -19,17 +19,30 @@ LOG = logging.getLogger(__name__)
19
19
  @click.option("-c", "--case-id", help="internal case id, leave empty to process all")
20
20
  @click.option("-d", "--days-ago", type=int, default=1, help="days since solved")
21
21
  @click.option("-C", "--customers", type=str, multiple=True, help="Filter on customers")
22
+ @click.option(
23
+ "--scout-instance",
24
+ type=click.Choice(["hg19", "hg38"]),
25
+ help="Which scout instance to fetch cases from",
26
+ required=True,
27
+ )
22
28
  @DRY_RUN
23
29
  @click.pass_obj
24
30
  def process_solved(
25
- context: CGConfig, case_id: str | None, days_ago: int, customers: tuple[str], dry_run: bool
31
+ context: CGConfig,
32
+ scout_instance: str,
33
+ case_id: str | None,
34
+ days_ago: int,
35
+ customers: tuple[str],
36
+ dry_run: bool,
26
37
  ):
27
38
  """Process cases with mutacc that has been marked as solved in scout.
28
39
  This prepares them to be uploaded to the mutacc database"""
29
40
 
30
41
  LOG.info("----------------- PROCESS-SOLVED ----------------")
31
42
 
32
- scout_api: ScoutAPI = get_scout_api(cg_config=context, case_id=case_id)
43
+ scout_api: ScoutAPI = get_scout_api_by_genome_build(
44
+ cg_config=context, genome_build=scout_instance
45
+ )
33
46
  mutacc_auto_api: MutaccAutoAPI = context.mutacc_auto_api
34
47
  mutacc_upload_api = UploadToMutaccAPI(scout_api=scout_api, mutacc_auto_api=mutacc_auto_api)
35
48
 
cg/cli/upload/scout.py CHANGED
@@ -8,7 +8,7 @@ from housekeeper.store.models import File, Version
8
8
 
9
9
  from cg.apps.housekeeper.hk import HousekeeperAPI
10
10
  from cg.apps.scout.scoutapi import ScoutAPI
11
- from cg.cli.upload.utils import get_scout_api, suggest_cases_to_upload
11
+ from cg.cli.upload.utils import get_scout_api_by_case, suggest_cases_to_upload
12
12
  from cg.constants import Workflow
13
13
  from cg.constants.cli_options import DRY_RUN
14
14
  from cg.constants.constants import FileFormat
@@ -145,7 +145,7 @@ def upload_case_to_scout(context: CGConfig, re_upload: bool, dry_run: bool, case
145
145
  LOG.info("----------------- UPLOAD -----------------------")
146
146
 
147
147
  housekeeper_api: HousekeeperAPI = context.housekeeper_api
148
- scout_api: ScoutAPI = get_scout_api(cg_config=context, case_id=case_id)
148
+ scout_api: ScoutAPI = get_scout_api_by_case(cg_config=context, case_id=case_id)
149
149
 
150
150
  tag_name: str = UploadScoutAPI.get_load_config_tag()
151
151
  version: Version = housekeeper_api.last_version(bundle=case_id)
cg/cli/upload/utils.py CHANGED
@@ -24,6 +24,15 @@ def suggest_cases_to_upload(status_db: Store, workflow: Workflow | None = None)
24
24
  click.echo(case_obj)
25
25
 
26
26
 
27
- def get_scout_api(cg_config: CGConfig, case_id: str) -> ScoutAPI:
27
+ def get_scout_api_by_case(cg_config: CGConfig, case_id: str) -> ScoutAPI:
28
28
  workflow = cg_config.status_db.get_case_by_internal_id(case_id).data_analysis
29
29
  return cg_config.scout_api_38 if workflow == Workflow.NALLO else cg_config.scout_api_37
30
+
31
+
32
+ def get_scout_api_by_genome_build(cg_config: CGConfig, genome_build: str) -> ScoutAPI:
33
+ """Return the appropriate ScoutAPI based on the genome build."""
34
+ if genome_build == "hg38":
35
+ return cg_config.scout_api_38
36
+ if genome_build == "hg19":
37
+ return cg_config.scout_api_37
38
+ raise ValueError(f"Unsupported genome build: {genome_build}")
@@ -1,31 +1,26 @@
1
1
  """CLI support to create config and/or start BALSAMIC."""
2
2
 
3
3
  import logging
4
+ from typing import cast
4
5
 
5
6
  import rich_click as click
6
7
  from pydantic.v1 import ValidationError
7
8
 
8
9
  from cg.apps.housekeeper.hk import HousekeeperAPI
9
10
  from cg.cli.utils import CLICK_CONTEXT_SETTINGS
10
- from cg.cli.workflow.balsamic.options import (
11
- OPTION_CACHE_VERSION,
12
- OPTION_CLUSTER_CONFIG,
13
- OPTION_GENDER,
14
- OPTION_GENOME_VERSION,
15
- OPTION_OBSERVATIONS,
16
- OPTION_PANEL_BED,
17
- OPTION_PON_CNN,
18
- OPTION_QOS,
19
- )
20
- from cg.cli.workflow.commands import ARGUMENT_CASE_ID, link, resolve_compression
11
+ from cg.cli.workflow.balsamic.options import OPTION_PANEL_BED, OPTION_WORKFLOW_PROFILE
12
+ from cg.cli.workflow.commands import ARGUMENT_CASE_ID, resolve_compression
21
13
  from cg.cli.workflow.utils import validate_force_store_option
22
- from cg.constants import EXIT_FAIL, EXIT_SUCCESS
23
- from cg.constants.cli_options import COMMENT, DRY_RUN, FORCE, LIMIT
24
- from cg.exc import AnalysisNotReadyError, CgError
14
+ from cg.constants import Workflow
15
+ from cg.constants.cli_options import COMMENT, DRY_RUN, FORCE
16
+ from cg.exc import CgError
25
17
  from cg.meta.workflow.analysis import AnalysisAPI
26
18
  from cg.meta.workflow.balsamic import BalsamicAnalysisAPI
27
19
  from cg.models.cg_config import CGConfig
28
- from cg.store.models import Case
20
+ from cg.services.analysis_starter.analysis_starter import AnalysisStarter
21
+ from cg.services.analysis_starter.configurator.implementations.balsamic import BalsamicConfigurator
22
+ from cg.services.analysis_starter.factories.configurator_factory import ConfiguratorFactory
23
+ from cg.services.analysis_starter.factories.starter_factory import AnalysisStarterFactory
29
24
  from cg.store.store import Store
30
25
 
31
26
  LOG = logging.getLogger(__name__)
@@ -42,85 +37,6 @@ def balsamic(context: click.Context):
42
37
 
43
38
 
44
39
  balsamic.add_command(resolve_compression)
45
- balsamic.add_command(link)
46
-
47
-
48
- @balsamic.command("config-case")
49
- @ARGUMENT_CASE_ID
50
- @OPTION_GENDER
51
- @OPTION_GENOME_VERSION
52
- @OPTION_PANEL_BED
53
- @OPTION_PON_CNN
54
- @OPTION_OBSERVATIONS
55
- @OPTION_CACHE_VERSION
56
- @DRY_RUN
57
- @click.pass_obj
58
- def config_case(
59
- context: CGConfig,
60
- case_id: str,
61
- gender: str,
62
- genome_version: str,
63
- panel_bed: str,
64
- pon_cnn: click.Path,
65
- observations: list[click.Path],
66
- cache_version: str,
67
- dry_run: bool,
68
- ):
69
- """Create config file for BALSAMIC analysis for a given CASE_ID."""
70
-
71
- analysis_api: AnalysisAPI = context.meta_apis["analysis_api"]
72
- try:
73
- LOG.info(f"Creating config file for {case_id}.")
74
- analysis_api.status_db.verify_case_exists(case_internal_id=case_id)
75
- analysis_api.config_case(
76
- case_id=case_id,
77
- gender=gender,
78
- genome_version=genome_version,
79
- panel_bed=panel_bed,
80
- pon_cnn=pon_cnn,
81
- observations=observations,
82
- cache_version=cache_version,
83
- dry_run=dry_run,
84
- )
85
- except CgError as error:
86
- LOG.error(f"Could not create config: {error}")
87
- raise click.Abort()
88
- except Exception as error:
89
- LOG.error(f"Could not create config: {error}")
90
- raise click.Abort()
91
-
92
-
93
- @balsamic.command("run")
94
- @ARGUMENT_CASE_ID
95
- @OPTION_CLUSTER_CONFIG
96
- @DRY_RUN
97
- @OPTION_QOS
98
- @click.pass_obj
99
- def run(
100
- context: CGConfig,
101
- case_id: str,
102
- cluster_config: click.Path,
103
- slurm_quality_of_service: str,
104
- dry_run: bool,
105
- ):
106
- """Run balsamic analysis for given CASE ID"""
107
- analysis_api: AnalysisAPI = context.meta_apis["analysis_api"]
108
- try:
109
- analysis_api.status_db.verify_case_exists(case_internal_id=case_id)
110
- analysis_api.verify_case_config_file_exists(case_id=case_id, dry_run=dry_run)
111
- analysis_api.check_analysis_ongoing(case_id)
112
- analysis_api.run_analysis(
113
- case_id=case_id,
114
- cluster_config=cluster_config,
115
- slurm_quality_of_service=slurm_quality_of_service,
116
- dry_run=dry_run,
117
- )
118
- if dry_run:
119
- return
120
- analysis_api.on_analysis_started(case_id)
121
- except Exception as error:
122
- LOG.error(f"Could not run analysis: {error}")
123
- raise click.Abort()
124
40
 
125
41
 
126
42
  @balsamic.command("report-deliver")
@@ -185,84 +101,6 @@ def store_housekeeper(
185
101
  raise click.Abort()
186
102
 
187
103
 
188
- @balsamic.command("start")
189
- @ARGUMENT_CASE_ID
190
- @OPTION_GENDER
191
- @OPTION_GENOME_VERSION
192
- @OPTION_QOS
193
- @DRY_RUN
194
- @OPTION_PANEL_BED
195
- @OPTION_PON_CNN
196
- @OPTION_CACHE_VERSION
197
- @OPTION_OBSERVATIONS
198
- @OPTION_CLUSTER_CONFIG
199
- @click.pass_context
200
- def start(
201
- context: click.Context,
202
- case_id: str,
203
- gender: str,
204
- genome_version: str,
205
- cache_version: str,
206
- panel_bed: str,
207
- pon_cnn: str,
208
- observations: list[click.Path],
209
- slurm_quality_of_service: str,
210
- cluster_config: click.Path,
211
- dry_run: bool,
212
- ):
213
- """Start full workflow for case ID."""
214
- analysis_api: BalsamicAnalysisAPI = context.obj.meta_apis["analysis_api"]
215
- analysis_api.prepare_fastq_files(case_id=case_id, dry_run=dry_run)
216
- LOG.info(f"Starting analysis for {case_id}")
217
- context.invoke(link, case_id=case_id, dry_run=dry_run)
218
- context.invoke(
219
- config_case,
220
- case_id=case_id,
221
- gender=gender,
222
- genome_version=genome_version,
223
- cache_version=cache_version,
224
- panel_bed=panel_bed,
225
- pon_cnn=pon_cnn,
226
- observations=observations,
227
- dry_run=dry_run,
228
- )
229
- context.invoke(
230
- run,
231
- case_id=case_id,
232
- cluster_config=cluster_config,
233
- slurm_quality_of_service=slurm_quality_of_service,
234
- dry_run=dry_run,
235
- )
236
-
237
-
238
- @balsamic.command("start-available")
239
- @DRY_RUN
240
- @LIMIT
241
- @click.pass_context
242
- def start_available(context: click.Context, dry_run: bool = False, limit: int | None = None):
243
- """Start full workflow for all cases ready for analysis"""
244
-
245
- analysis_api: AnalysisAPI = context.obj.meta_apis["analysis_api"]
246
-
247
- cases: list[Case] = analysis_api.get_cases_to_analyze(limit=limit)
248
- LOG.info(f"Starting {len(cases)} available Balsamic cases")
249
-
250
- exit_code: int = EXIT_SUCCESS
251
- for case in cases:
252
- try:
253
- context.invoke(start, case_id=case.internal_id, dry_run=dry_run)
254
- except AnalysisNotReadyError as error:
255
- LOG.error(error)
256
- except CgError as error:
257
- LOG.error(error)
258
- exit_code = EXIT_FAIL
259
- except Exception as error:
260
- LOG.error(f"Unspecified error occurred: {error}")
261
- exit_code = EXIT_FAIL
262
- if exit_code:
263
- raise click.Abort
264
-
265
-
266
104
  @balsamic.command("store")
267
105
  @ARGUMENT_CASE_ID
268
106
  @COMMENT
@@ -295,3 +133,79 @@ def store_available(context: click.Context, dry_run: bool) -> None:
295
133
  was_successful = False
296
134
  if not was_successful:
297
135
  raise click.Abort()
136
+
137
+
138
+ @balsamic.command("config-case")
139
+ @OPTION_PANEL_BED
140
+ @ARGUMENT_CASE_ID
141
+ @click.pass_obj
142
+ def config_case(cg_config: CGConfig, case_id: str, panel_bed: str | None):
143
+ """Configure a Balsamic case so that it is ready to be run.
144
+
145
+ \b
146
+ Creates the case config file:
147
+ - CASE_ID.json
148
+ """
149
+ factory = ConfiguratorFactory(cg_config)
150
+ configurator = cast(BalsamicConfigurator, factory.get_configurator(Workflow.BALSAMIC))
151
+ configurator.configure(case_id=case_id, panel_bed=panel_bed)
152
+
153
+
154
+ @balsamic.command("run")
155
+ @OPTION_WORKFLOW_PROFILE
156
+ @ARGUMENT_CASE_ID
157
+ @click.pass_obj
158
+ def run(cg_config: CGConfig, case_id: str, workflow_profile: click.Path | None):
159
+ """
160
+ Run a preconfigured Balsamic case.
161
+
162
+ \b
163
+ Assumes that case config file exist in the case run directory:
164
+ - CASE_ID.json
165
+ """
166
+ factory = AnalysisStarterFactory(cg_config)
167
+ analysis_starter: AnalysisStarter = factory.get_analysis_starter_for_workflow(Workflow.BALSAMIC)
168
+ analysis_starter.run(case_id=case_id, workflow_profile=workflow_profile)
169
+
170
+
171
+ @balsamic.command("start")
172
+ @OPTION_PANEL_BED
173
+ @OPTION_WORKFLOW_PROFILE
174
+ @ARGUMENT_CASE_ID
175
+ @click.pass_obj
176
+ def start(
177
+ cg_config: CGConfig,
178
+ case_id: str,
179
+ panel_bed: str | None,
180
+ workflow_profile: click.Path | None,
181
+ ):
182
+ """
183
+ Starts a Balsamic cases.
184
+
185
+ \b
186
+ Configures the case and creates the case config file:
187
+ - CASE_ID.json
188
+ and submits the job to slurm.
189
+ """
190
+ factory = AnalysisStarterFactory(cg_config)
191
+ analysis_starter: AnalysisStarter = factory.get_analysis_starter_for_workflow(Workflow.BALSAMIC)
192
+ analysis_starter.start(case_id=case_id, workflow_profile=workflow_profile, panel_bed=panel_bed)
193
+
194
+
195
+ @balsamic.command("start-available")
196
+ @click.pass_obj
197
+ def start_available(cg_config: CGConfig):
198
+ """
199
+ Starts all available Balsamic cases.
200
+
201
+ \b
202
+ Configures the individual case and creates its case config file:
203
+ - CASE_ID.json
204
+ and submits the job to slurm.
205
+ """
206
+ LOG.info("Starting Balsamic workflow for all available cases.")
207
+ factory = AnalysisStarterFactory(cg_config)
208
+ analysis_starter = factory.get_analysis_starter_for_workflow(Workflow.BALSAMIC)
209
+ succeeded: bool = analysis_starter.start_available()
210
+ if not succeeded:
211
+ raise click.Abort
@@ -1,59 +1,14 @@
1
1
  import rich_click as click
2
2
 
3
- from cg.constants.constants import GenomeVersion
4
- from cg.constants.priority import SlurmQos
5
- from cg.constants.subject import Sex
6
-
7
- OPTION_GENDER = click.option(
8
- "--gender",
9
- type=click.Choice([Sex.FEMALE, Sex.MALE]),
10
- required=False,
11
- help="Case associated gender. Set this option to override the one selected by the customer in StatusDB.",
12
- )
13
- OPTION_GENOME_VERSION = click.option(
14
- "--genome-version",
15
- show_default=True,
16
- default=GenomeVersion.HG19,
17
- type=click.Choice([GenomeVersion.HG19, GenomeVersion.HG38, GenomeVersion.CANFAM3]),
18
- help="Type and build version of the reference genome. Set this option to override the default.",
19
- )
20
3
  OPTION_PANEL_BED = click.option(
21
4
  "--panel-bed",
22
5
  required=False,
23
6
  help="Panel BED is determined based on capture kit \
24
7
  used for library prep. Set this option to override the default",
25
8
  )
26
- OPTION_QOS = click.option(
27
- "-qos",
28
- "--slurm-quality-of-service",
29
- type=click.Choice([SlurmQos.LOW, SlurmQos.NORMAL, SlurmQos.HIGH, SlurmQos.EXPRESS]),
30
- help="Job priority in SLURM. Setting this option will override the StatusDB case priority.",
31
- )
32
- OPTION_PON_CNN = click.option(
33
- "--pon-cnn",
34
- type=click.Path(exists=True),
35
- required=False,
36
- help="Panel of normal reference (.cnn) for CNVkit",
37
- )
38
- OPTION_OBSERVATIONS = click.option(
39
- "--observations",
40
- type=click.Path(exists=True),
41
- multiple=True,
42
- required=False,
43
- help="VCF paths of clinical and/or cancer SNVs and SVs observations (WHOLE_GENOME_SEQUENCING analysis only). Set this option to "
44
- "override the latest Loqusdb dump files.",
45
- )
46
-
47
- OPTION_CACHE_VERSION = click.option(
48
- "--cache-version",
49
- type=click.STRING,
50
- required=False,
51
- help="Cache version to be used for init or analysis. Use 'develop' or 'X.X.X'.",
52
- )
53
-
54
- OPTION_CLUSTER_CONFIG = click.option(
55
- "--cluster-config",
9
+ OPTION_WORKFLOW_PROFILE = click.option(
10
+ "--workflow-profile",
56
11
  type=click.Path(exists=True),
57
12
  required=False,
58
- help="Cluster resources configuration JSON file path used for analysis.",
13
+ help="Path to directory containing config.yaml.",
59
14
  )