def run(self, keep: bool, upload: bool, assume_clean: bool, sensor_plot: bool, plot_audio: bool) -> None: submission_hash = str(config["sauron.test_submission_hash"]) output_dir = config.get_output_dir(submission_hash) args = SubmissionRunnerArgs( dark=None, local=not upload, overwrite=True, keep_frames=False, store_to=None, assume_clean=assume_clean, sensor_plot=sensor_plot, plot_audio=plot_audio, halt_after_acquisition=False, ignore_prior=True, ) with Valar() as db: SauronxLock().lock(None) try: # these will get shut down afterward audio = SauronxAudio() audio.start() b = Board() b.init() ss = SubmissionRunner(args, db, audio, b) ss.run(submission_hash, True) if not keep: shutil.rmtree(output_dir, ignore_errors=True) finally: SauronxLock().unlock(ignore_warning=True)
def log(self) -> None: parser = argparse.ArgumentParser( description="Show the recent history of submissions on this Sauron." ) self._parse_args(parser) lookup = LookupTools() with Valar(): lookup.history()
def __init__( self, submission_hash: Optional[str], db: Optional[Valar] = None, acquisition_start: bool = False, ignore_warnings: bool = False, ) -> None: """If submission_hash is None, don't try to access submission_obj or status_obj or call update_status.""" self.db = None # type: Valar self._internal_db = None # type: bool self.submission_hash = None # type: Optional[str] self.submission_obj = None # type: Submissions self.sauron_obj = None # type: Saurons self.status_obj = None # type: Optional[SubmissionRecords] self._internal_db = db is None # don't close if it was built outside self.ignore_warnings = ignore_warnings self.acquisition_start = acquisition_start self.submission_hash = submission_hash if self.submission_hash is not None: processing_list = ProcessingList.now() if self.submission_hash in processing_list: warn_user( "Refusing to touch {}:".format(submission_hash), "Another SauronX process appears to be handling it.", "If this is wrong, run 'sauronx clear {}' to continue.". format(submission_hash), ) raise LockedError( "Refusing to touch {}: Another SauronX process appears to be handling it." .format(submission_hash)) if db is None: self.db = Valar() else: self.db = db try: if "connection.notification.slack_info_file" in config: with open( str(config["connection.notification.slack_info_file"]), "r") as file: self._slack_hook = file.readline() self._slack_user_dict = json.loads(file.readline()) except Exception: warn_user("Failed to call notification") logger.error("Failed to call notification", exc_info=True)
def lookup(self) -> None: parser = argparse.ArgumentParser( description="List something in Valar", usage= "sauronx lookup [users|experiments|submissions|history|batteries|assays|templates|configs|stimuli|audio_files|sensors|plates|runs|plate_types|saurons|locations]", ) parser.add_argument("what", help="What to list") args = self._parse_args(parser) with Valar(): getattr(LookupTools(), args.what)()
def update( self, text: str, when: datetime.datetime = datetime.datetime.now()) -> None: with Valar() as valar: import valarpy.model as model conf = model.SauronConfigs( datetime_changed=when, description=text, sauron=config.sauron_id, created=datetime.datetime.now(), ) conf.save() print(Fore.BLUE + "Created new sauron_config {}".format(conf.id))
def run( self, hashes: List[str], dark: Optional[int], local: bool, overwrite: bool, keep_frames: bool, store_to: Optional[str], assume_clean: bool, sensor_plot: bool, plot_audio: bool, halt_after_acquisition: bool, ignore_prior: bool, keep_lock: bool, ) -> None: args = SubmissionRunnerArgs( dark=dark, local=local, overwrite=overwrite, keep_frames=keep_frames, store_to=store_to, assume_clean=assume_clean, sensor_plot=sensor_plot, plot_audio=plot_audio, halt_after_acquisition=halt_after_acquisition, ignore_prior=ignore_prior, ) SauronxLock().lock(None) try: b = Board() b.init() audio = SauronxAudio() audio.start() with Valar() as db: ss = SubmissionRunner(args, db, audio, b) for i, h in enumerate(hashes): # TODO datetime_started only applies to the first hash. Is this really what we want? self._log_start(args.local, h) # we don't need a new process for the last run ss.run(h, i == len(hashes) - 1) self._log_finish(args.local, h, halt_after_acquisition) finally: if not keep_lock: SauronxLock().unlock(ignore_warning=True)
def clean(self) -> None: parser = argparse.ArgumentParser( description= "List SauronX output on this machine and decide whether to delete it" ) parser.add_argument( "--auto", action="store_true", help="Auto-accept each recommendation without prompting") parser.add_argument( "--skip-ignores", action="store_true", help="Don’t prompt when the recommendation is to ignore.", ) args, restrictions, options, base_dir = self._data_args(parser) with Valar() as db: if args.auto: DataManager(db).auto_clean(restrictions, options, base_dir) else: DataManager(db).clean(restrictions, options, base_dir, args.skip_ignores)
def connect(self) -> None: """Hidden connection test.""" with Valar(): pass
def data(self) -> None: parser = argparse.ArgumentParser( description="List SauronX output on this machine") args, restrictions, options, base_dir = self._data_args(parser) with Valar() as db: DataManager(db).data(restrictions, options, base_dir)
def print_info(self, extended: bool = False) -> None: with Valar() as valar: print() self._bannered( Style.BRIGHT + "Version information...", "Version ".ljust(20, ".") + " " + sauronx_version, "Hash ".ljust(20, ".") + " " + git_commit_hash(sauronx_home), ) obj = config.get_sauron_config() self._bannered( Style.BRIGHT + "Hardware config information...", "Date/time changed ".ljust(20, ".") + " " + "{}Z".format( config.sauron_number, obj.datetime_changed.strftime("%Y-%m-%d_%H-%M-%S")), "Description ".ljust(20, ".") + " " + obj.description, ) self._bannered( Style.BRIGHT + "Video information...", "QP ".ljust(20, ".") + " " + str(config["sauron.data.video.qp"]), "Keyframe interval ".ljust(20, ".") + " " + str(config["sauron.data.video.keyframe_interval"]), "Preset ".ljust(20, ".") + " " + config.get_str("sauron.data.video.preset"), "Custom params ".ljust(20, ".") + " " + "; ".join([ str(k) + "=" + str(v) for k, v in config["sauron.data.video.extra_x265_params"].items() ]), ) self._bannered( Style.BRIGHT + "Hardware information...", "FPS ".ljust(20, ".") + " " + str(config["sauron.hardware.camera.frames_per_second"]), "Exposure ".ljust(20, ".") + " " + str(config["sauron.hardware.camera.exposure"]), "Gain ".ljust(20, ".") + " " + str(config["sauron.hardware.camera.gain"]), "Gamma ".ljust(20, ".") + " " + str(config["sauron.hardware.camera.gamma"]), "Black level ".ljust(20, ".") + " " + str(config["sauron.hardware.camera.black_level"]), "Pre-padding ".ljust(20, ".") + " " + str(config[ "sauron.hardware.camera.padding_before_milliseconds"]), "Post-padding ".ljust(20, ".") + " " + str(config["sauron.hardware.camera.padding_after_milliseconds"] ), "Arduino chipset ".ljust(20, ".") + " " + config.get_str("sauron.hardware.arduino.chipset"), "Sample rate (ms) ".ljust(20, ".") + " " + str(config[ "sauron.hardware.sensors.sampling_interval_milliseconds"]), "Audio floor (dB) ".ljust(20, ".") + " " + str(config["sauron.hardware.stimuli.audio.audio_floor"]), "Registered sensors ".ljust(20, ".") + " " + "; ".join(config["sauron.hardware.sensors.registry"]), ) self._bannered( Style.BRIGHT + "Sauronx config information...", "Sauron ".ljust(20, ".") + " " + str(config.sauron_name), "Raw frames dir ".ljust(20, ".") + " " + config.raw_frames_root, "Output dir ".ljust(20, ".") + " " + config.output_dir_root, "Trash dir ".ljust(20, ".") + " " + config.trash_dir(), "Temp dir ".ljust(20, ".") + " " + config.temp_dir(), "Incubation dir ".ljust(20, ".") + " " + config.get_incubation_dir(), "Prototyping dir ".ljust(20, ".") + " " + config.get_prototyping_dir(), "Plate types ".ljust(20, ".") + " " + "; ".join(config.list_plate_types()), ) if extended: self._bannered( Style.BRIGHT + "Environment information...", *[((key + " ").ljust(40, ".") + " " + escape_for_properties(value)) for key, value in config.environment_info.items()], )
class SauronxAlive: """Handles a database connection from VALARPY_CONFIG and processing (submission) locks. For example, you can't prototype and submit at the same time. This should be reserved for commands that need the camera or Arduino board. """ def __init__( self, submission_hash: Optional[str], db: Optional[Valar] = None, acquisition_start: bool = False, ignore_warnings: bool = False, ) -> None: """If submission_hash is None, don't try to access submission_obj or status_obj or call update_status.""" self.db = None # type: Valar self._internal_db = None # type: bool self.submission_hash = None # type: Optional[str] self.submission_obj = None # type: Submissions self.sauron_obj = None # type: Saurons self.status_obj = None # type: Optional[SubmissionRecords] self._internal_db = db is None # don't close if it was built outside self.ignore_warnings = ignore_warnings self.acquisition_start = acquisition_start self.submission_hash = submission_hash if self.submission_hash is not None: processing_list = ProcessingList.now() if self.submission_hash in processing_list: warn_user( "Refusing to touch {}:".format(submission_hash), "Another SauronX process appears to be handling it.", "If this is wrong, run 'sauronx clear {}' to continue.". format(submission_hash), ) raise LockedError( "Refusing to touch {}: Another SauronX process appears to be handling it." .format(submission_hash)) if db is None: self.db = Valar() else: self.db = db try: if "connection.notification.slack_info_file" in config: with open( str(config["connection.notification.slack_info_file"]), "r") as file: self._slack_hook = file.readline() self._slack_user_dict = json.loads(file.readline()) except Exception: warn_user("Failed to call notification") logger.error("Failed to call notification", exc_info=True) def __enter__(self): self.start() return self def __exit__(self, t, value, traceback): self.finish() def start(self): if self._internal_db: self.db.open() self.sauron_obj = self._fetch_sauron() if self.submission_hash is not None: ProcessingSubmission.from_hash(self.submission_hash).create() try: self.submission_obj = self._fetch_submission() self._init_status() except: ProcessingSubmission.from_hash(self.submission_hash).destroy() raise @property def is_test(self): return self.submission_hash == "_" * 12 def finish(self): if self.submission_hash is not None: ProcessingSubmission.from_hash(self.submission_hash).destroy() if self._internal_db: self.db.close() def update_status(self, stat: StatusValue) -> None: if self.submission_hash is not None: self.status_obj = SubmissionRecords( submission=self.submission_obj, created=datetime_started_raw, datetime_modified=datetime.datetime.now(), sauron=self.sauron_obj.id, status=stat.name.lower(), ) try: self.status_obj.save() except: warn_user("Failed to update status to {}".format(stat)) logger.warning("Failed to update status to {}".format(stat)) def notify_finished(self) -> None: pass def _fetch_sauron(self): import valarpy.model as model matches = list( model.Saurons.select().where(model.Saurons.id == config.sauron_id)) if len(matches) != 1: raise ValueError("No Sauron with number {} exists!".format( config.sauron_name)) return matches[0] def _fetch_submission(self): import valarpy.model as model sub = (model.Submissions.select( model.Submissions, model.Experiments, model.Users).join( model.Users, on=(model.Submissions.user_id == model.Users.id)).switch( model.Submissions).join(model.Experiments).join( model.TemplatePlates).join(model.PlateTypes).switch( model.Experiments).join(model.Batteries).where( model.Submissions.lookup_hash == self.submission_hash).first()) if sub is None: raise ValarLookupError( "No SauronX submission exists in Valar with submission hash {}" .format(self.submission_hash)) # make sure it wasn't already used matching_run = (model.Runs.select(model.Runs, model.Submissions).join( model.Submissions).where(model.Submissions.id == sub.id).first()) if matching_run is not None: warn_user("Can't continue: {} was already used for run r{}".format( self.submission_hash, matching_run)) raise ValueError( "Submission hash {} was already used for run r{} ".format( self.submission_hash, matching_run)) return sub def _init_status(self) -> None: matches = list( SubmissionRecords.select(SubmissionRecords).where( SubmissionRecords.submission == self.submission_obj.id).order_by( SubmissionRecords.created.desc()) ) # type: List[SubmissionRecords] # warn about prior runs prev_run = Runs.select().where( Runs.submission == self.submission_obj).first() has_remote = any(m.status in REMOTE for m in matches) has_post_capture = any(m.status in POST_CAPTURE for m in matches) acquisition_starts = [ m.created for m in matches if m.status is StatusValue.CAPTURING ] # TODO warn if n_acquisition_starts > 0 if not self.ignore_warnings: if len(matches) > 0 and not SauronxLock().is_running_test(): logging.warning( "There are already {} submission record(s):\n{}\n".format( len(matches), _table( ["id", "status", "inserted", "run_started"], [[r.id, r.status, r.datetime_modified, r.created] for r in matches], ), )) msg = None # keep only the highest precedence warning, using elif if prev_run is not None: msg = "The submission is already attached to run r{}".format( prev_run.id) elif has_remote: msg = "The submission was captured and already uploaded to Valinor." elif has_post_capture and self.acquisition_start: # only worry if we're starting fresh msg = "The submission was already successfully captured." if msg is not None: warn_user("Refusing:", msg) print(Fore.RED + "Continue anyway? Not recommended. [yes/no]", end="") try: ans = Tools.prompt_yes_no("") except KeyboardInterrupt: raise RefusingRequestError(msg) if ans: logging.warning(msg) else: raise RefusingRequestError(msg) if len(acquisition_starts) > 0: logging.warning( "Acquisition started {} times before: {}".format( len(acquisition_starts), ", ".join(acquisition_starts))) self.update_status(StatusValue.STARTING)
import datetime import json import logging from enum import Enum from typing import List, Optional from pocketutils.full import Tools from valarpy.Valar import Valar from pocketutils.core.exceptions import LockedError, RefusingRequestError from pocketutils.misc.fancy_console import ColorMessages from loguru import logger Valar().open() from valarpy.model import * from sauronx import datetime_started_raw from .configuration import config from .locks import ProcessingList, ProcessingSubmission, SauronxLock class InterceptHandler(logging.Handler): """ Redirects standard logging to loguru. """ def emit(self, record): # Get corresponding Loguru level if it exists try: level = logger.level(record.levelname).name except ValueError: