def __init__(self, reference_genome, config, log, charon_connector=None, tracking_connector=None, process_connector=None): """ Create an instance of SarekAnalysis. :param reference_genome: a type of ReferenceGenome indicating the reference genome to use :param config: a dict object with configuration options :param log: a log handle to use for logging :param charon_connector: a CharonConnector instance to use for the database connection. If not specified, a new connector will be created :param tracking_connector: a TrackingConnector instance to use for connections to the local tracking database. If not specified, a new connector will be created :param process_connector: a ProcessConnector instance to use for starting the analysis. If not specified, a new connector for local execution will be created """ self.reference_genome = reference_genome self.config = config self.log = log merged_configs = self.configure_analysis( opts={"sarek": { "genome": self.reference_genome }}) self.sarek_config = merged_configs["sarek"] self.nextflow_config = merged_configs["nextflow"] self.charon_connector = charon_connector or CharonConnector( self.config, self.log) self.tracking_connector = tracking_connector or TrackingConnector( self.config, self.log) self.process_connector = process_connector or ProcessConnector( cwd=os.curdir)
def update_charon_with_local_jobs_status(config=None, log=None, tracking_connector=None, charon_connector=None, **kwargs): """ Update Charon with the local changes in the SQLite tracking database. :param config: optional dict with configuration options. If not specified, the global configuration will be used instead :param log: optional log instance. If not specified, a new log instance will be created :param tracking_connector: optional connector to the tracking database. If not specified, a new connector will be created :param charon_connector: optional connector to the charon database. If not specified, a new connector will be created :param kwargs: placeholder for additional, unused options :return: None """ log = log or minimal_logger(__name__, debug=True) tracking_connector = tracking_connector or TrackingConnector(config, log) charon_connector = charon_connector or CharonConnector(config, log) log.debug("updating Charon status for locally tracked jobs") # iterate over the analysis processes tracked in the local database for analysis in tracking_connector.tracked_analyses(): try: log.debug( "checking status for analysis of {}:{} with {}:{}, having {}". format( analysis.project_id, analysis.sample_id, analysis.engine, analysis.workflow, "pid {}".format(analysis.process_id) if analysis.process_id is not None else "sbatch job id {}".format(analysis.slurm_job_id))) # create an AnalysisTracker instance for the analysis analysis_tracker = AnalysisTracker(analysis, charon_connector, tracking_connector, log, config) # recreate the analysis_sample from disk/analysis analysis_tracker.recreate_analysis_sample() # poll the system for the analysis status analysis_tracker.get_analysis_status() # set the analysis status analysis_tracker.report_analysis_status() # set the analysis results analysis_tracker.report_analysis_results() # remove the analysis entry from the local db analysis_tracker.remove_analysis() # do cleanup analysis_tracker.cleanup() except Exception as e: log.error( "exception raised when processing sample {} in project {}, please review: {}" .format(analysis.sample_id, analysis.project_id, e))
def analyze(analysis_object): """ This is the main entry point for launching the Sarek analysis pipeline. This gets called by NGI pipeline for projects having the corresponding best_practice_analysis in Charon. It's called per project and the passed analysis object contains some parameters for the analysis, while others are looked up in the config. Refer to the ngi_pipeline.conductor.launchers.launch_analysis method to see how the analysis object is created. :param analysis_object: an ngi_pipeline.conductor.classes.NGIAnalysis object holding parameters for the analysis :return: None """ # get a SlurmConnector that will take care of submitting analysis jobs slurm_project_id = analysis_object.config["environment"]["project_id"] slurm_mail_user = analysis_object.config["mail"]["recipient"] slurm_conector = SlurmConnector(slurm_project_id, slurm_mail_user, cwd="/scratch", slurm_mail_events="TIME_LIMIT_80", **analysis_object.config.get("slurm", {})) # get a CharonConnector that will interface with the Charon database charon_connector = CharonConnector(analysis_object.config, analysis_object.log) # get a TrackingConnector that will interface with the local SQLite database tracking_connector = TrackingConnector(analysis_object.config, analysis_object.log) # get a SarekAnlaysis instance that matches the analysis specified in Charon (e.g. germline) analysis_object.log.info("Launching SAREK analysis for {}".format( analysis_object.project.project_id)) analysis_engine = SarekAnalysis.get_analysis_instance_for_project( analysis_object.project.project_id, analysis_object.config, analysis_object.log, charon_connector=charon_connector, tracking_connector=tracking_connector, process_connector=slurm_conector) # iterate over the samples in the project and launch analysis for each for sample_object in analysis_object.project: try: analysis_engine.analyze_sample(sample_object, analysis_object) except SampleNotValidForAnalysisError as e: analysis_object.log.error(e) # finally, let's force a sync of the local SQLite DB and Charon time.sleep(5) update_charon_with_local_jobs_status(config=analysis_object.config, log=analysis_object.log, tracking_connector=tracking_connector, charon_connector=charon_connector)
def get_analysis_instance_for_project(projectid, config, log, charon_connector=None, tracking_connector=None, process_connector=None): """ Factory method returning a SarekAnalysis subclass instance corresponding to the best practice analysis specified for the supplied project. :param projectid: the projectid of the project to get an analysis instance for :param config: a config dict :param log: a log handle :param charon_connector: a connector instance to the charon database. If None, the default connector will be used :param tracking_connector: a TrackingConnector instance to use for connections to the local tracking database. If not specified, a new connector will be created :param process_connector: a ProcessConnector instance to use for starting the analysis. If not specified, a new connector for local execution will be created :return: an instance of a SarekAnalysis subclass """ charon_connector = charon_connector or CharonConnector(config, log) tracking_connector = tracking_connector or TrackingConnector( config, log) process_connector = process_connector or ProcessConnector( cwd=os.curdir) # fetch the best practice analysis pipeline and reference specified in Charon analysis_pipeline = charon_connector.analysis_pipeline(projectid) analysis_type = charon_connector.best_practice_analysis(projectid) analysis_reference = charon_connector.analysis_reference(projectid) reference_genome = ReferenceGenome.get_instance(analysis_reference) # we can only run Sarek analyses in this module if analysis_pipeline.lower() != "sarek": raise BestPracticeAnalysisNotRecognized(analysis_pipeline) # currently, we use the same analysis pipeline for targeted and wgs data if analysis_type.lower() in ["exome_germline", "wgs_germline"]: return SarekGermlineAnalysis(reference_genome, config, log, charon_connector, tracking_connector, process_connector) elif analysis_type in ["exome_somatic", "wgs_somatic"]: raise NotImplementedError( "best-practice.analysis for {} is not implemented".format( analysis_type)) raise BestPracticeAnalysisNotRecognized(analysis_type)
def _get_charon_connector(self, charon_session): self.charon_connector = CharonConnector(self.config, self.log, charon_session=charon_session)
class TestCharonConnector(unittest.TestCase): CONFIG = {} def setUp(self): self.log = minimal_logger(__name__, to_file=False, debug=True) self.config = TestCharonConnector.CONFIG self.project_id = "this-is-a-project-id" self.sample_id = "this-is-a-sample-id" self.libprep_id = "this-is-a-libprep-id" self.libpreps = [ {"libprepid": "this-is-a-libprep-1"}, {"libprepid": "this-is-a-libprep-2"}, {"libprepid": "this-is-a-libprep-3"}] self.seqruns = [ {"seqrunid": "this-is-a-seqrun-1"}, {"seqrunid": "this-is-a-seqrun-2"}, {"seqrunid": "this-is-a-seqrun-3"}] def _get_charon_connector(self, charon_session): self.charon_connector = CharonConnector(self.config, self.log, charon_session=charon_session) def test_best_practice_analysis(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.project_get.return_value = {} with self.assertRaises(BestPracticeAnalysisNotSpecifiedError): self.charon_connector.best_practice_analysis(self.project_id) expected_bp_analysis = "this-is-a-best-practice-analysis" self.charon_connector.charon_session.project_get.return_value = { "best_practice_analysis": expected_bp_analysis} observed_bp_analysis = self.charon_connector.best_practice_analysis(self.project_id) self.assertEqual(expected_bp_analysis, observed_bp_analysis) def test_sample_analysis_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.sample_get.return_value = {} with self.assertRaises(SampleAnalysisStatusNotFoundError): self.charon_connector.sample_analysis_status(self.project_id, self.sample_id) expected_sample_analysis_status = "this-is-a-sample-analysis-status" self.charon_connector.charon_session.sample_get.return_value = { "analysis_status": expected_sample_analysis_status} observed_sample_analysis_status = self.charon_connector.sample_analysis_status( self.project_id, self.sample_id) self.assertEqual(expected_sample_analysis_status, observed_sample_analysis_status) def libpreps_seqruns_helper(self, test_fn, test_args, expected_list, element_key): # simplest case, no restricting self.assertListEqual( expected_list, test_fn(*test_args)) # filtering for a non-existing element should exclude everything self.assertListEqual( [], test_fn(*test_args, restrict_to=["this-element-does-not-exist"])) # filtering for existing elements should return only those self.assertListEqual( expected_list[0:2], test_fn(*test_args, restrict_to=map(lambda x: x[element_key], expected_list[0:2]))) self.assertListEqual( expected_list[0:1], test_fn(*test_args, restrict_to=map(lambda x: x[element_key], expected_list[0:1]))) def test_sample_libpreps(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.sample_get_libpreps.return_value = {} with self.assertRaises(SampleLookupError) as sle: self.charon_connector.sample_libpreps(self.project_id, self.sample_id) expected_libpreps = self.libpreps self.charon_connector.charon_session.sample_get_libpreps.return_value = {"libpreps": expected_libpreps} self.libpreps_seqruns_helper( self.charon_connector.sample_libpreps, [self.project_id, self.sample_id], expected_libpreps, "libprepid") def test_libprep_seqruns(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.libprep_get_seqruns.return_value = {} with self.assertRaises(SampleLookupError) as sle: self.charon_connector.libprep_seqruns(self.project_id, self.sample_id, self.libprep_id) expected_seqruns = self.seqruns self.charon_connector.charon_session.libprep_get_seqruns.return_value = {"seqruns": expected_seqruns} self.libpreps_seqruns_helper( self.charon_connector.libprep_seqruns, [self.project_id, self.sample_id, self.libprep_id], expected_seqruns, "seqrunid") def test_analysis_status_from_process_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) with self.assertRaises(AnalysisStatusForProcessStatusNotFoundError) as e: self.charon_connector.analysis_status_from_process_status(ProcessStopped) expected_status = CharonConnector._ANALYSIS_STATUS_FROM_PROCESS_STATUS.values() self.assertListEqual( expected_status, map( lambda p: self.charon_connector.analysis_status_from_process_status(p), CharonConnector._ANALYSIS_STATUS_FROM_PROCESS_STATUS.keys())) def test_alignment_status_from_analysis_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) with self.assertRaises(AlignmentStatusForAnalysisStatusNotFoundError) as e: self.charon_connector.alignment_status_from_analysis_status("this-status-does-not-exist") expected_status = CharonConnector._ALIGNMENT_STATUS_FROM_ANALYSIS_STATUS.values() self.assertListEqual( expected_status, map( lambda p: self.charon_connector.alignment_status_from_analysis_status(p), CharonConnector._ALIGNMENT_STATUS_FROM_ANALYSIS_STATUS.keys())) def _configure_sample_attribute_update(self, charon_session_mock): # set up some mocks self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.sample_get_libpreps.return_value = {"libpreps": self.libpreps} self.charon_connector.charon_session.libprep_get_seqruns.return_value = {"seqruns": self.seqruns} expected_libpreps = self.libpreps[-1].values() expected_seqruns = {lp.values()[0]: self.seqruns[1].values() for lp in self.libpreps} return expected_libpreps, expected_seqruns def test_set_sample_analysis_status(self, charon_session_mock): expected_libpreps, expected_seqruns = self._configure_sample_attribute_update(charon_session_mock) analysis_status = "FAILED" alignment_status = self.charon_connector.alignment_status_from_analysis_status(analysis_status) sample_update_kwargs = {"analysis_status": analysis_status} seqrun_update_kwargs = {"alignment_status": alignment_status} # set the analysis and alignment status for sample and seqrun, respectively update_args = (analysis_status, self.project_id, self.sample_id) update_kwargs = { "recurse": True, "restrict_to_libpreps": expected_libpreps, "restrict_to_seqruns": expected_seqruns} self.charon_connector.set_sample_analysis_status(*update_args, **update_kwargs) self.charon_connector.charon_session.sample_update.assert_called_once_with( self.project_id, self.sample_id, **sample_update_kwargs) self.charon_connector.charon_session.seqrun_update.assert_called_once_with( self.project_id, self.sample_id, expected_libpreps[0], expected_seqruns.values()[0][0], **seqrun_update_kwargs) # have exceptions raised for update_fn in [self.charon_connector.charon_session.sample_update, self.charon_connector.charon_session.seqrun_update]: update_fn.side_effect = CharonError("raised CharonError") with self.assertRaises(SampleAnalysisStatusNotSetError): self.charon_connector.set_sample_analysis_status(*update_args, **update_kwargs) update_fn.side_effect = None def test_set_sample_attribute(self, charon_session_mock): expected_libpreps, expected_seqruns = self._configure_sample_attribute_update(charon_session_mock) expected_return_value = "this-is-the-return-value" self.charon_connector.charon_session.sample_update = mock.MagicMock() self.charon_connector.charon_session.sample_update.return_value = expected_return_value sample_update_kwargs = {"sample_attribute": "this-is-the-attribute-value"} seqrun_update_kwargs = {"seqrun_attribute": "this-is-the-attribute-value"} update_args = (self.project_id, self.sample_id) update_kwargs = { "recurse": False, "sample_update_kwargs": sample_update_kwargs, "seqrun_update_kwargs": seqrun_update_kwargs, "restrict_to_libpreps": expected_libpreps, "restrict_to_seqruns": expected_seqruns} # update a sample attribute observed_return_value = self.charon_connector.set_sample_attribute(*update_args, **update_kwargs) self.assertEqual( expected_return_value, observed_return_value) self.charon_connector.charon_session.seqrun_update.assert_not_called() self.charon_connector.charon_session.sample_update.assert_called_once_with( *update_args, **sample_update_kwargs) self.charon_connector.charon_session.sample_update.reset_mock() # update a sample attribute and seqrun recursively update_kwargs["recurse"] = True observed_return_value = self.charon_connector.set_sample_attribute(*update_args, **update_kwargs) self.assertEqual( expected_return_value, observed_return_value) self.charon_connector.charon_session.sample_update.assert_called_once_with( *update_args, **sample_update_kwargs) self.charon_connector.charon_session.seqrun_update.assert_called_once_with( update_args[0], update_args[1], expected_libpreps[0], expected_seqruns.values()[0][0], **seqrun_update_kwargs) # exception encountered during sample update self.charon_connector.charon_session.sample_update.side_effect = CharonError("raised CharonError") with self.assertRaises(SampleUpdateError): self.charon_connector.set_sample_attribute( self.project_id, self.sample_id, sample_update_kwargs=sample_update_kwargs) # exception encountered during seqrun update self.charon_connector.charon_session.seqrun_update.side_effect = CharonError("raised CharonError") with self.assertRaises(SeqrunUpdateError): self.charon_connector.set_sample_attribute( self.project_id, self.sample_id, sample_update_kwargs=sample_update_kwargs, seqrun_update_kwargs=seqrun_update_kwargs, recurse=True) def _set_metric_helper(self, charon_session_mock, update_fn, update_attribute, attribute_value): self._configure_sample_attribute_update(charon_session_mock) update_args = [self.project_id, self.sample_id] getattr(self.charon_connector, update_fn)(attribute_value, *update_args) self.charon_connector.charon_session.sample_update.assert_called_once_with( *update_args, **{update_attribute: attribute_value}) def test_set_sample_duplication(self, charon_session_mock): self._set_metric_helper( charon_session_mock, "set_sample_duplication", "duplication_pc", 12.45) def test_set_sample_autosomal_coverage(self, charon_session_mock): self._set_metric_helper( charon_session_mock, "set_sample_autosomal_coverage", "total_autosomal_coverage", 30.9) def test_set_sample_total_reads(self, charon_session_mock): self._set_metric_helper( charon_session_mock, "set_sample_total_reads", "total_sequenced_reads", 123456789)
def _alignment_status_wrapper(status, *args): return CharonConnector( self.config, self.log, charon_session="charon-session" ).alignment_status_from_analysis_status(status)
class TestCharonConnector(unittest.TestCase): CONFIG = {} def setUp(self): self.log = minimal_logger(__name__, to_file=False, debug=True) self.config = TestCharonConnector.CONFIG self.project_id = "this-is-a-project-id" self.sample_id = "this-is-a-sample-id" self.libprep_id = "this-is-a-libprep-id" self.libpreps = [{ "libprepid": "this-is-a-libprep-1" }, { "libprepid": "this-is-a-libprep-2" }, { "libprepid": "this-is-a-libprep-3" }] self.seqruns = [{ "seqrunid": "this-is-a-seqrun-1" }, { "seqrunid": "this-is-a-seqrun-2" }, { "seqrunid": "this-is-a-seqrun-3" }] def _get_charon_connector(self, charon_session): self.charon_connector = CharonConnector(self.config, self.log, charon_session=charon_session) def test_best_practice_analysis(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.project_get.return_value = {} with self.assertRaises(BestPracticeAnalysisNotSpecifiedError): self.charon_connector.best_practice_analysis(self.project_id) expected_bp_analysis = "this-is-a-best-practice-analysis" self.charon_connector.charon_session.project_get.return_value = { "best_practice_analysis": expected_bp_analysis } observed_bp_analysis = self.charon_connector.best_practice_analysis( self.project_id) self.assertEqual(expected_bp_analysis, observed_bp_analysis) def test_sample_analysis_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.sample_get.return_value = {} with self.assertRaises(SampleAnalysisStatusNotFoundError): self.charon_connector.sample_analysis_status( self.project_id, self.sample_id) expected_sample_analysis_status = "this-is-a-sample-analysis-status" self.charon_connector.charon_session.sample_get.return_value = { "analysis_status": expected_sample_analysis_status } observed_sample_analysis_status = self.charon_connector.sample_analysis_status( self.project_id, self.sample_id) self.assertEqual(expected_sample_analysis_status, observed_sample_analysis_status) def libpreps_seqruns_helper(self, test_fn, test_args, expected_list, element_key): # simplest case, no restricting self.assertListEqual(expected_list, test_fn(*test_args)) # filtering for a non-existing element should exclude everything self.assertListEqual([], test_fn( *test_args, restrict_to=["this-element-does-not-exist"])) # filtering for existing elements should return only those self.assertListEqual( expected_list[0:2], test_fn(*test_args, restrict_to=[x[element_key] for x in expected_list[0:2]])) self.assertListEqual( expected_list[0:1], test_fn(*test_args, restrict_to=[x[element_key] for x in expected_list[0:1]])) def test_sample_libpreps(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.sample_get_libpreps.return_value = {} with self.assertRaises(SampleLookupError) as sle: self.charon_connector.sample_libpreps(self.project_id, self.sample_id) expected_libpreps = self.libpreps self.charon_connector.charon_session.sample_get_libpreps.return_value = { "libpreps": expected_libpreps } self.libpreps_seqruns_helper(self.charon_connector.sample_libpreps, [self.project_id, self.sample_id], expected_libpreps, "libprepid") def test_libprep_seqruns(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.libprep_get_seqruns.return_value = {} with self.assertRaises(SampleLookupError) as sle: self.charon_connector.libprep_seqruns(self.project_id, self.sample_id, self.libprep_id) expected_seqruns = self.seqruns self.charon_connector.charon_session.libprep_get_seqruns.return_value = { "seqruns": expected_seqruns } self.libpreps_seqruns_helper( self.charon_connector.libprep_seqruns, [self.project_id, self.sample_id, self.libprep_id], expected_seqruns, "seqrunid") def test_analysis_status_from_process_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) with self.assertRaises( AnalysisStatusForProcessStatusNotFoundError) as e: self.charon_connector.analysis_status_from_process_status( ProcessStopped) expected_status = list( CharonConnector._ANALYSIS_STATUS_FROM_PROCESS_STATUS.values()) self.assertListEqual(expected_status, [ self.charon_connector.analysis_status_from_process_status(p) for p in list( CharonConnector._ANALYSIS_STATUS_FROM_PROCESS_STATUS.keys()) ]) def test_alignment_status_from_analysis_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) with self.assertRaises( AlignmentStatusForAnalysisStatusNotFoundError) as e: self.charon_connector.alignment_status_from_analysis_status( "this-status-does-not-exist") expected_status = list( CharonConnector._ALIGNMENT_STATUS_FROM_ANALYSIS_STATUS.values()) self.assertListEqual(expected_status, [ self.charon_connector.alignment_status_from_analysis_status(p) for p in list( CharonConnector._ALIGNMENT_STATUS_FROM_ANALYSIS_STATUS.keys()) ]) def _configure_sample_attribute_update(self, charon_session_mock): # set up some mocks self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.sample_get_libpreps.return_value = { "libpreps": self.libpreps } self.charon_connector.charon_session.libprep_get_seqruns.return_value = { "seqruns": self.seqruns } expected_libpreps = list(self.libpreps[-1].values()) expected_seqruns = { list(lp.values())[0]: list(self.seqruns[1].values()) for lp in self.libpreps } return expected_libpreps, expected_seqruns def test_set_sample_analysis_status(self, charon_session_mock): expected_libpreps, expected_seqruns = self._configure_sample_attribute_update( charon_session_mock) analysis_status = "FAILED" alignment_status = self.charon_connector.alignment_status_from_analysis_status( analysis_status) sample_update_kwargs = {"analysis_status": analysis_status} seqrun_update_kwargs = {"alignment_status": alignment_status} # set the analysis and alignment status for sample and seqrun, respectively update_args = (analysis_status, self.project_id, self.sample_id) update_kwargs = { "recurse": True, "restrict_to_libpreps": expected_libpreps, "restrict_to_seqruns": expected_seqruns } self.charon_connector.set_sample_analysis_status( *update_args, **update_kwargs) self.charon_connector.charon_session.sample_update.assert_called_once_with( self.project_id, self.sample_id, **sample_update_kwargs) self.charon_connector.charon_session.seqrun_update.assert_called_once_with( self.project_id, self.sample_id, expected_libpreps[0], list(expected_seqruns.values())[0][0], **seqrun_update_kwargs) # have exceptions raised for update_fn in [ self.charon_connector.charon_session.sample_update, self.charon_connector.charon_session.seqrun_update ]: update_fn.side_effect = CharonError("raised CharonError") with self.assertRaises(SampleAnalysisStatusNotSetError): self.charon_connector.set_sample_analysis_status( *update_args, **update_kwargs) update_fn.side_effect = None def test_set_sample_attribute(self, charon_session_mock): expected_libpreps, expected_seqruns = self._configure_sample_attribute_update( charon_session_mock) expected_return_value = "this-is-the-return-value" self.charon_connector.charon_session.sample_update = mock.MagicMock() self.charon_connector.charon_session.sample_update.return_value = expected_return_value sample_update_kwargs = { "sample_attribute": "this-is-the-attribute-value" } seqrun_update_kwargs = { "seqrun_attribute": "this-is-the-attribute-value" } update_args = (self.project_id, self.sample_id) update_kwargs = { "recurse": False, "sample_update_kwargs": sample_update_kwargs, "seqrun_update_kwargs": seqrun_update_kwargs, "restrict_to_libpreps": expected_libpreps, "restrict_to_seqruns": expected_seqruns } # update a sample attribute observed_return_value = self.charon_connector.set_sample_attribute( *update_args, **update_kwargs) self.assertEqual(expected_return_value, observed_return_value) self.charon_connector.charon_session.seqrun_update.assert_not_called() self.charon_connector.charon_session.sample_update.assert_called_once_with( *update_args, **sample_update_kwargs) self.charon_connector.charon_session.sample_update.reset_mock() # update a sample attribute and seqrun recursively update_kwargs["recurse"] = True observed_return_value = self.charon_connector.set_sample_attribute( *update_args, **update_kwargs) self.assertEqual(expected_return_value, observed_return_value) self.charon_connector.charon_session.sample_update.assert_called_once_with( *update_args, **sample_update_kwargs) self.charon_connector.charon_session.seqrun_update.assert_called_once_with( update_args[0], update_args[1], expected_libpreps[0], list(expected_seqruns.values())[0][0], **seqrun_update_kwargs) # exception encountered during sample update self.charon_connector.charon_session.sample_update.side_effect = CharonError( "raised CharonError") with self.assertRaises(SampleUpdateError): self.charon_connector.set_sample_attribute( self.project_id, self.sample_id, sample_update_kwargs=sample_update_kwargs) # exception encountered during seqrun update self.charon_connector.charon_session.seqrun_update.side_effect = CharonError( "raised CharonError") with self.assertRaises(SeqrunUpdateError): self.charon_connector.set_sample_attribute( self.project_id, self.sample_id, sample_update_kwargs=sample_update_kwargs, seqrun_update_kwargs=seqrun_update_kwargs, recurse=True) def _set_metric_helper(self, charon_session_mock, update_fn, update_attribute, attribute_value): self._configure_sample_attribute_update(charon_session_mock) update_args = [self.project_id, self.sample_id] getattr(self.charon_connector, update_fn)(attribute_value, *update_args) self.charon_connector.charon_session.sample_update.assert_called_once_with( *update_args, **{update_attribute: attribute_value}) def test_set_sample_duplication(self, charon_session_mock): self._set_metric_helper(charon_session_mock, "set_sample_duplication", "duplication_pc", 12.45) def test_set_sample_autosomal_coverage(self, charon_session_mock): self._set_metric_helper(charon_session_mock, "set_sample_autosomal_coverage", "total_autosomal_coverage", 30.9) def test_set_sample_total_reads(self, charon_session_mock): self._set_metric_helper(charon_session_mock, "set_sample_total_reads", "total_sequenced_reads", 123456789) def test_libprep_qc_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.libprep_get.return_value = { 'qc': 'qc_status' } qc_status = self.charon_connector.libprep_qc_status( self.project_id, self.sample_id, self.libprep_id) self.assertEqual(qc_status, 'qc_status') self.charon_connector.charon_session.libprep_get.return_value = {} with self.assertRaises(SampleLookupError): qc_status = self.charon_connector.libprep_qc_status( self.project_id, self.sample_id, self.libprep_id) def test_seqrun_alignment_status(self, charon_session_mock): self._get_charon_connector(charon_session_mock.return_value) self.charon_connector.charon_session.seqrun_get.return_value = { 'alignment_status': 'alignment_status' } alignment_status = self.charon_connector.seqrun_alignment_status( self.project_id, self.sample_id, self.libprep_id, 'seqrun_id') self.assertEqual(alignment_status, 'alignment_status') self.charon_connector.charon_session.seqrun_get.return_value = {} with self.assertRaises(SampleLookupError): alignment_status = self.charon_connector.seqrun_alignment_status( self.project_id, self.sample_id, self.libprep_id, 'seqrun_id')