Example #1
0
    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)
Example #2
0
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))
Example #3
0
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)
Example #4
0
    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)
Example #7
0
 def _alignment_status_wrapper(status, *args):
     return CharonConnector(
         self.config, self.log, charon_session="charon-session"
     ).alignment_status_from_analysis_status(status)
Example #8
0
 def _get_charon_connector(self, charon_session):
     self.charon_connector = CharonConnector(self.config,
                                             self.log,
                                             charon_session=charon_session)
Example #9
0
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')