From 0bd225b04b0d75c62e0377a87cabad755fcad920 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Thu, 7 May 2026 12:50:10 -0700 Subject: [PATCH 1/2] Replace SLIMS calls with subprocess call to waterlog app --- pyproject.toml | 1 - src/foraging_gui/Foraging.py | 174 ++++++++++++----------------------- 2 files changed, 59 insertions(+), 116 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f034a5e98..f29e8868a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ dependencies = [ "pyOSC3@git+https://github.com/glopesdev/pyosc3.git@master", "newscale@git+https://github.com/AllenNeuralDynamics/python-newscale@axes-on-target", "aind-auto-train@git+https://github.com/AllenNeuralDynamics/aind-foraging-behavior-bonsai-automatic-training.git@main", - "aind-slims-api@git+https://github.com/AllenNeuralDynamics/aind-slims-api@main", "aind-dynamic-foraging-models >= 0.12.2", "aind-dynamic-foraging-basic-analysis >= 0.3.34", "aind-behavior-services >=0.8, <0.9", diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 5379b37f4..3c1652229 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -31,7 +31,6 @@ from aind_auto_train.schema.task import TrainingStage from aind_behavior_services.session import AindBehaviorSessionModel from aind_data_schema.core.session import Session -from aind_slims_api import SlimsClient, models from matplotlib.backends.backend_qt5agg import ( NavigationToolbar2QT as NavigationToolbar, ) @@ -238,9 +237,6 @@ def __init__(self, parent=None, box_number=1, start_bonsai_ide=True): # Connect to Bonsai self._InitializeBonsai() - # connect to Slims - self._ConnectSlims() - # Set up threads self.threadpool = QThreadPool() # get animal response self.threadpool2 = QThreadPool() # get animal lick @@ -1966,6 +1962,7 @@ def _GetSettings(self): "clear_figure_after_save": True, "add_default_project_name": True, "check_schedule": False, + "waterlog_exe_path": "C://Program Files/AIBS_MPE/waterlog/waterlog.exe", } # Try to load the ForagingSettings.json file @@ -2097,71 +2094,8 @@ def _GetSettings(self): ] self.rig_name = "{}".format(self.current_box) - def _ConnectSlims(self): - """ - Connect to Slims - """ - try: - logging.info("Attempting to connect to Slims") - self.slims_client = SlimsClient( - username=os.environ["SLIMS_USERNAME"], - password=os.environ["SLIMS_PASSWORD"], - ) - except KeyError as e: - raise KeyError( - "SLIMS_USERNAME and SLIMS_PASSWORD do not exist as " - f"environment variables on machine. Please add. {e}" - ) - - try: - self.slims_client.fetch_model( - models.SlimsMouseContent, barcode="00000000" - ) - except Exception as e: - if "Status 401 – Unauthorized" in str( - e - ): # catch error if username and password are incorrect - raise Exception( - f"Exception trying to read from Slims: {e}.\n" - f" Please check credentials:\n" - f"Username: {os.environ['SLIMS_USERNAME']}\n" - f"Password: {os.environ['SLIMS_PASSWORD']}" - ) - elif "No record found" not in str( - e - ): # bypass if mouse doesn't exist - raise Exception(f"Exception trying to read from Slims: {e}.\n") - logging.info("Successfully connected to Slims") - - def _AddWaterLogResult(self, session: Session): - """ - Add WaterLogResult to slims based on current state of gui - - :param session: Session object to pull water information from - - """ - - try: # try and find mouse - logging.info( - f"Attempting to fetch mouse {session.subject_id} from Slims" - ) - mouse = self.slims_client.fetch_model( - models.SlimsMouseContent, barcode=session.subject_id - ) - except Exception as e: - if "No record found" in str( - e - ): # if no mouse found or validation errors on mouse - logging.warning( - f'"No record found" error while trying to fetch mouse {session.subject_id}. ' - f"Will not log water." - ) - return - else: - logging.error( - f"While fetching mouse {session.subject_id} model, unexpected error occurred: {e}" - ) - raise e + def _AddWaterlogResult(self, session: Session): + """Send weight/water information to databases via waterlog app cli""" # extract water information logging.info("Extracting water information from first stimulus epoch") @@ -2171,53 +2105,63 @@ def _AddWaterLogResult(self, session: Session): for k, v in water_json } - # extract software information + # extract software information - TODO: Add this to waterlog cli logging.info("Extracting software information from first data stream") software = session.stimulus_epochs[0].software[0] - # create model - logging.info( - "Creating SlimsWaterlogResult based on session information." - ) - print(water) - model = models.SlimsWaterlogResult( - mouse_pk=mouse.pk, - date=datetime.now(), - weight_g=session.animal_weight_post, - operator=self.behavior_session_model.experimenter[0], - water_earned_ml=water["water_in_session_total"], - water_supplement_delivered_ml=water["water_after_session"], - water_supplement_recommended_ml=None, - total_water_ml=water["water_in_session_total"]+water["water_after_session"], - comments=session.notes, - workstation=session.rig_id, - sw_source=software.url, - sw_version=software.version, - test_pk=self.slims_client.fetch_pk( - "Test", test_name="test_waterlog" - ), - ) + # TODO: validate user first + # experimenter_name = self.behavior_session_model.experimenter[0] + # try: + # resp = requests.get("http://aind-metadata-service/api/v2/active_directory/{experimenter_name}") + # resp.raise_for_status() + # validated_username = resp.json()['username'] + # except Exception: + # logging.warning( + # "Could not validate experimenter name against aind-metadata-service", + # exc_info=True + # ) + # validated_username = experimenter_name + + # TODO: Should we remove the suggested water calculation from DF? + # That would mean removing the last two arguments below + + waterlog_args = [ + self.Settings['waterlog_exe_path'], + '--username', + self.behavior_session_model.experimenter[0], + '--mouse-id', + session.subject_id, + '--mouse-weight', + session.animal_weight_post, + '--comment', + session.notes, + '--earned-water', + water["water_in_session_total"], + '--water-supplement-ml', + water["water_after_session"], + '--water-supplement-delivered', + ] - # check if mouse already has waterlog for at session time and if, so update model - logging.info( - f"Fetching previous waterlog for mouse {session.subject_id}" - ) - waterlog = self.slims_client.fetch_models( - models.SlimsWaterlogResult, mouse_pk=mouse.pk, start=0, end=1 - ) - if waterlog != [] and waterlog[0].date.strftime( - "%Y-%m-%d %H:%M:%S" - ) == session.session_start_time.astimezone(timezone.utc).strftime( - "%Y-%m-%d %H:%M:%S" - ): - logging.info( - "Waterlog information already exists for this session. Updating waterlog in Slims." - ) - model.pk = waterlog[0].pk - self.slims_client.update_model(model=model) - else: - logging.info("Adding waterlog to Slims.") - self.slims_client.add_model(model) + ### Leftover fields that were previously sent to SLIMS + ### TODO: validate that it's okay to leave them out + # total_water_ml=water["water_in_session_total"]+water["water_after_session"], + # workstation=session.rig_id, + # sw_source=software.url, + # sw_version=software.version, + + logging.info("Sending water info to waterlog") + process = subprocess.run([str(arg) for arg in waterlog_args]) + + # TODO: Add message to user to to over to waterlog and hit submit + + try: + process.check_returncode() + except Exception: + logging.warning( + f"Waterlog data for mouse {self.behavior_session_model.subject} cannot be sent to waterlog exe" + f", message: {process.stdout}, {process.stderr}", + exc_info=True, + ) def _InitializeBonsai(self): """ @@ -4171,7 +4115,7 @@ def _Save(self, ForceSave=0, SaveAs=0, SaveContinue=0, BackupSave=0): # save random reward parameters Obj['random_reward_par']=self.RandomReward_dialog.random_reward_par - # generate the metadata file and update slims + # generate the metadata file and update waterlog try: # save the metadata collected in the metadata dialogue self.Metadata_dialog._save_metadata_dialog_parameters() @@ -4220,10 +4164,10 @@ def _Save(self, ForceSave=0, SaveAs=0, SaveContinue=0, BackupSave=0): ): self._AddWaterLogResult(session) elif self.BaseWeight.text() == "" or self.WeightAfter.text() == "": - logging.warning(f"Waterlog for mouse {self.behavior_session_model.subject} cannot be added to slims" + logging.warning(f"Waterlog for mouse {self.behavior_session_model.subject} cannot be added to database" f" due do unrecorded weight information.") elif session is None: - logging.warning(f"Waterlog for mouse {self.behavior_session_model.subject} cannot be added to slims" + logging.warning(f"Waterlog for mouse {self.behavior_session_model.subject} cannot be added to database" f" due do metadata generation failure.") except Exception as e: logging.warning( From 2823f446455fbaedf5a580b9de0ef40f6a532581 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Mon, 11 May 2026 10:03:44 -0700 Subject: [PATCH 2/2] Clean up waterlog adapter communication --- src/foraging_gui/Foraging.py | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 3c1652229..e495c047c 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2105,25 +2105,10 @@ def _AddWaterlogResult(self, session: Session): for k, v in water_json } - # extract software information - TODO: Add this to waterlog cli + # extract software information to send to waterlog once it can accept it logging.info("Extracting software information from first data stream") software = session.stimulus_epochs[0].software[0] - - # TODO: validate user first - # experimenter_name = self.behavior_session_model.experimenter[0] - # try: - # resp = requests.get("http://aind-metadata-service/api/v2/active_directory/{experimenter_name}") - # resp.raise_for_status() - # validated_username = resp.json()['username'] - # except Exception: - # logging.warning( - # "Could not validate experimenter name against aind-metadata-service", - # exc_info=True - # ) - # validated_username = experimenter_name - - # TODO: Should we remove the suggested water calculation from DF? - # That would mean removing the last two arguments below + # Access sw name/version with (software.url, software.version) waterlog_args = [ self.Settings['waterlog_exe_path'], @@ -2142,17 +2127,10 @@ def _AddWaterlogResult(self, session: Session): '--water-supplement-delivered', ] - ### Leftover fields that were previously sent to SLIMS - ### TODO: validate that it's okay to leave them out - # total_water_ml=water["water_in_session_total"]+water["water_after_session"], - # workstation=session.rig_id, - # sw_source=software.url, - # sw_version=software.version, - logging.info("Sending water info to waterlog") process = subprocess.run([str(arg) for arg in waterlog_args]) - - # TODO: Add message to user to to over to waterlog and hit submit + + QMessageBox.information(self, "Waterlog", "Go to waterlog app to submit water information.") try: process.check_returncode()