From 70c81abdd86b67d6a792d13595b88e8bc5d00f95 Mon Sep 17 00:00:00 2001 From: David Perl Date: Fri, 31 Jul 2026 15:48:53 +0200 Subject: [PATCH 1/6] ci: add better linting --- .gitea/workflows/ci.yml | 17 +++++++++++++++++ pyproject.toml | 17 +++++++++++------ tests/unit/utils/test_beamline_dispatch.py | 4 +--- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 90e34916..91e9fbf1 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -30,6 +30,23 @@ jobs: source .venv/bin/activate ruff format --check + - name: Lint + run: | + source .venv/bin/activate + ruff check + + - name: Checkout pyright diff plugin + uses: actions/checkout@v5 + with: + repository: mx/diff_quality_basedpyright + path: diff_quality_basedpyright + + - name: Typecheck Diff + run: | + source .venv/bin/activate + uv pip install -e diff_quality_basedpyright + diff-quality --violations=basedpyright + test: runs-on: ubuntu-latest needs: "lint" diff --git a/pyproject.toml b/pyproject.toml index 0a8e1aa3..dd66e399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,13 +36,18 @@ dependencies = [ [project.optional-dependencies] test = [ -# "pytest==9.0.3", - "pytest-cov==7.1.0", - "pytest-mock==3.14.0", - "pytest-qt==4.4.0", - "pytest-asyncio==0.25.3", + "basedpyright", + "pytest", + "pytest-cov", + "pytest-mock", + "pytest-qt", + "pytest-asyncio", "pytest-timeout", - "ruff==0.15.*" + "ruff>=0.15" +] + +docs = [ + "zensical" ] [lint] diff --git a/tests/unit/utils/test_beamline_dispatch.py b/tests/unit/utils/test_beamline_dispatch.py index fa53ef6f..ff29f700 100644 --- a/tests/unit/utils/test_beamline_dispatch.py +++ b/tests/unit/utils/test_beamline_dispatch.py @@ -1,12 +1,10 @@ -import importlib +import importlib.util from unittest.mock import patch import pytest from aare.beamline_dispatch.beamline_dispatch import get_beamline_dispatch from aare.beamline_dispatch.simulated import SimulatedDispatch -from aare.beamline_dispatch.x06da.beamline_dispatch import X06daDispatch -from aare.beamline_dispatch.x06sa import X06saDispatch from aare.beamline_dispatch.x10sa import X10saDispatch -- 2.54.0 From f63107a1e0090ce51e8a2958d5ba1d2c4bc3574b Mon Sep 17 00:00:00 2001 From: David Perl Date: Fri, 31 Jul 2026 15:57:17 +0200 Subject: [PATCH 2/6] style: ruff check --fix --unsafe-fixes --- scripts/camera_stat_thread.py | 23 ++--- scripts/gui_desginer.py | 16 +-- scripts/redis_dump.py | 11 ++- src/aare/daq/aaredb.py | 29 +++--- src/aare/daq/auth.py | 3 +- src/aare/daq/beamcenterfit.py | 2 +- src/aare/daq/config.py | 13 +-- src/aare/daq/daq.py | 56 +++++------ src/aare/daq/mlbox.py | 32 +++--- .../daq/operations/common/ml_bounding_box.py | 12 +-- .../daq/operations/face_detection/utils.py | 16 ++- .../daq/operations/loop_centering/__init__.py | 10 +- src/aare/daq/operations/raster/__init__.py | 2 +- src/aare/daq/operations/raster/service.py | 2 +- src/aare/daq/server.py | 4 +- src/aare/daq/server_exception_handler.py | 9 +- src/aare/daq/tell_state_machine.py | 4 +- src/aare/daq/tellupdater.py | 4 +- src/aare/devices/aerotech.py | 25 +++-- src/aare/devices/area_detector.py | 3 +- src/aare/devices/bec_worker.py | 15 ++- src/aare/devices/enum_pv.py | 2 +- src/aare/devices/fluorimeter.py | 2 +- src/aare/devices/jfjoch.py | 37 ++++--- src/aare/devices/mx_lib.py | 19 ++-- src/aare/devices/set_get_pv.py | 6 +- src/aare/devices/smargon.py | 16 ++- src/aare/devices/tell_backend.py | 3 +- src/aare/devices/tell_client.py | 21 ++-- src/aare/devices/workflow_tools.py | 14 +-- src/aare/devices/zmq_client.py | 9 +- src/aare/gui/about.py | 2 +- src/aare/gui/gui.py | 4 +- src/aare/gui/main_window.py | 6 +- src/aare/gui/models/gui_state_manager.py | 1 + src/aare/gui/models/sample_queue_model.py | 4 +- src/aare/gui/models/user_sample_model.py | 2 +- src/aare/gui/panels/automation_panel.py | 4 +- src/aare/gui/panels/axis_video_panel.py | 2 +- src/aare/gui/panels/developer_help_dialog.py | 3 +- src/aare/gui/panels/loop_centering_panel.py | 2 +- src/aare/gui/panels/raster_data_collection.py | 2 +- src/aare/gui/panels/reference_tools_panel.py | 7 +- .../gui/panels/rotation_data_collection.py | 2 +- src/aare/gui/panels/scan_settings_panel.py | 2 +- src/aare/gui/panels/smargon_trace_panel.py | 2 +- src/aare/gui/panels/smart_rotation_panel.py | 7 +- src/aare/gui/panels/target_stability_panel.py | 5 +- .../gui/scan_logic/raster_grid_manager.py | 53 ++++------ src/aare/gui/threads/axis_video_thread.py | 8 +- src/aare/gui/threads/daq_worker.py | 97 +++++++++---------- src/aare/gui/threads/sse_client.py | 15 ++- .../gui/tutorials/controls_help_dialog.py | 2 +- src/aare/gui/tutorials/tutorial_manager.py | 12 +-- src/aare/gui/tutorials/tutorial_models.py | 6 +- src/aare/gui/widgets/automation_progress.py | 2 +- src/aare/gui/widgets/baton_request_dialog.py | 13 ++- src/aare/gui/widgets/camera_image.py | 14 ++- src/aare/gui/widgets/clickable_label.py | 2 +- .../widgets/local_contact_status_widget.py | 2 +- src/aare/gui/widgets/number_line_edit.py | 8 +- src/aare/gui/widgets/pgroup_dialog.py | 8 +- src/aare/gui/widgets/splash_screen.py | 2 +- src/aare/gui/widgets/status_bar.py | 2 - src/aare/gui/widgets/video_image.py | 6 +- tests/conftest.py | 2 +- tests/integration/daq/test_daq_server.py | 3 +- .../test_face_detection_logic.py | 19 ++-- .../test_loop_centering_service.py | 2 +- tests/unit/daq/test_autofocus.py | 3 +- tests/unit/daq/test_beamcenterfit.py | 6 +- tests/unit/daq/test_face_detection.py | 16 +-- tests/unit/daq/test_mlbox.py | 2 +- tests/unit/daq/test_mount.py | 6 +- tests/unit/daq/test_server.py | 1 - tests/unit/daq/test_spreadsheetupdater.py | 5 +- tests/unit/devices/test_enum_pv.py | 18 ++-- tests/unit/devices/test_mx_lib.py | 13 +-- tests/unit/devices/test_my_motor.py | 4 +- tests/unit/devices/test_workflow_tools.py | 1 + tests/unit/gui/test_auth_mock.py | 2 +- tests/unit/gui/test_axis_video_thread.py | 8 +- tests/unit/gui/test_login.py | 6 +- tests/unit/gui/test_message_box.py | 18 ++-- tests/unit/gui/test_sse_client.py | 4 +- tests/unit/gui/test_tutorials.py | 11 ++- tests/unit/utils/test_beamline_dispatch.py | 15 ++- 87 files changed, 413 insertions(+), 481 deletions(-) diff --git a/scripts/camera_stat_thread.py b/scripts/camera_stat_thread.py index 5f444731..0120b739 100644 --- a/scripts/camera_stat_thread.py +++ b/scripts/camera_stat_thread.py @@ -1,7 +1,7 @@ import json import threading import time -from typing import Any, Dict +from typing import Any import cv2 import numpy as np @@ -33,7 +33,7 @@ class ImageStatsReceiver: self.connection_attempts = 0 self.last_message_time = None - def calculate_projections(self, image: np.ndarray) -> Dict[str, Any]: + def calculate_projections(self, image: np.ndarray) -> dict[str, Any]: """Calculate X and Y projections of the image""" # Convert to grayscale if color image if len(image.shape) == 3: @@ -99,7 +99,7 @@ class ImageStatsReceiver: "y_coords": list(range(len(y_projection))), } - def calculate_radial_integration(self, image: np.ndarray, num_bins: int = 50) -> Dict[str, Any]: + def calculate_radial_integration(self, image: np.ndarray, num_bins: int = 50) -> dict[str, Any]: """Calculate radial integration of the image""" # Convert to grayscale if color image if len(image.shape) == 3: @@ -153,7 +153,7 @@ class ImageStatsReceiver: "num_bins": num_bins, } - def calculate_image_stats(self, image: np.ndarray) -> Dict[str, Any]: + def calculate_image_stats(self, image: np.ndarray) -> dict[str, Any]: """Calculate comprehensive statistics for an image""" image_float = image.astype(np.float64) @@ -250,7 +250,7 @@ class ImageStatsReceiver: else: print(f"[{time.strftime('%H:%M:%S')}] Waiting for image data...") - def print_formatted_stats(self, stats: Dict[str, Any]): + def print_formatted_stats(self, stats: dict[str, Any]): """Print formatted statistics in a compact terminal format""" timestamp = time.strftime("%H:%M:%S", time.localtime(stats["timestamp"])) @@ -295,7 +295,7 @@ class ImageStatsReceiver: ) # Optional: Print per-channel stats if available - if any(k.startswith("mean_ch") for k in stats.keys()): + if any(k.startswith("mean_ch") for k in stats): channels = [] i = 0 while f"mean_ch{i}" in stats: @@ -327,7 +327,7 @@ class ImageStatsReceiver: } return None - def save_radial_profile_to_file(self, filename: str = None): + def save_radial_profile_to_file(self, filename: str | None = None): """Save the current radial profile to a file""" if filename is None: filename = f"radial_profile_{int(time.time())}.txt" @@ -343,13 +343,10 @@ class ImageStatsReceiver: f.write(f"# Center: {radial['center']}\n") f.write("# Radius(pixels)\tMean_Intensity\tStd_Intensity\tPixel_Count\n") - for i in range(len(radial["r_centers"])): - f.write( - f"{radial['r_centers'][i]:.2f}\t" + f.writelines(f"{radial['r_centers'][i]:.2f}\t" f"{radial['radial_profile'][i]:.2f}\t" f"{radial['radial_std'][i]:.2f}\t" - f"{radial['pixel_counts'][i]}\n" - ) + f"{radial['pixel_counts'][i]}\n" for i in range(len(radial["r_centers"]))) print(f"Radial profile saved to {filename}") return filename @@ -425,7 +422,7 @@ def get_radial_profile(): return None -def save_current_radial_profile(filename: str = None): +def save_current_radial_profile(filename: str | None = None): """Save the current radial profile to a file""" global stats_receiver diff --git a/scripts/gui_desginer.py b/scripts/gui_desginer.py index dacc544d..3e606353 100644 --- a/scripts/gui_desginer.py +++ b/scripts/gui_desginer.py @@ -1,19 +1,19 @@ +import math +import sys + +from PySide6.QtCore import QPointF, QRectF, Qt +from PySide6.QtGui import QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPen from PySide6.QtWidgets import ( QApplication, - QWidget, - QVBoxLayout, + QFrame, QHBoxLayout, QLabel, - QFrame, QPushButton, QScrollArea, QStackedWidget, + QVBoxLayout, + QWidget, ) -from PySide6.QtCore import Qt, QPointF, QRectF -from PySide6.QtGui import QPainter, QColor, QPen, QLinearGradient, QFont, QFontMetrics -import sys -import math - # --------------------------------------------------------------------------- # Colour palette diff --git a/scripts/redis_dump.py b/scripts/redis_dump.py index 8db28f8f..84148556 100644 --- a/scripts/redis_dump.py +++ b/scripts/redis_dump.py @@ -1,9 +1,10 @@ # python -import sys import argparse -from typing import Any, Dict, Tuple -import yaml +import sys +from typing import Any + import redis +import yaml def decode_bulk(value: Any): @@ -19,7 +20,7 @@ def decode_bulk(value: Any): return value -def fetch_key(r: redis.Redis, key: bytes) -> Tuple[str, Any]: +def fetch_key(r: redis.Redis, key: bytes) -> tuple[str, Any]: decode_bulk(key) t = r.type(key) if isinstance(t, bytes): @@ -66,7 +67,7 @@ def dump_redis_to_yaml( ): r = redis.Redis(host=host, port=port, db=db, password=password) - dump: Dict[str, Any] = {"meta": {"host": host, "port": port, "db": db}, "data": {}} + dump: dict[str, Any] = {"meta": {"host": host, "port": port, "db": db}, "data": {}} cursor = 0 while True: diff --git a/src/aare/daq/aaredb.py b/src/aare/daq/aaredb.py index 2ceeec9a..fd119bb6 100644 --- a/src/aare/daq/aaredb.py +++ b/src/aare/daq/aaredb.py @@ -2,7 +2,6 @@ import datetime import io import json import os -from typing import List, Optional import aareDB import cv2 @@ -76,7 +75,7 @@ class AareWrapper: self._key_file = configuration.key_file @log_timing(logger, "AareDB call") - def set_pucks_beamline(self, input_list: List[PuckLoadedInfo]): + def set_pucks_beamline(self, input_list: list[PuckLoadedInfo]): o = [] for i in input_list: @@ -103,7 +102,7 @@ class AareWrapper: @log_timing(logger, "AareDB call") def send_sample_event( - self, sample_id: StrictInt, event_type: SampleEventType, comment: Optional[str] = None + self, sample_id: StrictInt, event_type: SampleEventType, comment: str | None = None ) -> None: if sample_id is None or sample_id < 0: if sample_id is None: @@ -123,7 +122,7 @@ class AareWrapper: @log_timing(logger, "AareDB call") def upload_image( - self, sample_id: int, filename: str, bgr_image: np.ndarray, message: Optional[str] = None + self, sample_id: int, filename: str, bgr_image: np.ndarray, message: str | None = None ): _, buffer = cv2.imencode(".jpg", bgr_image) jpeg_bytes = io.BytesIO(buffer) @@ -145,7 +144,7 @@ class AareWrapper: logger.debug(f"Response status code: {response.status_code}") @log_timing(logger, "AareDB call") - def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None): + def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: str | None = None): logger.debug(f"jppg_image of type: {type(jpg_image)}") url = f"{self._host}/protected_router/sample_runner/{sample_id}/upload-images" headers = { @@ -165,7 +164,7 @@ class AareWrapper: @log_timing(logger, "AareDB call") def create_rotation_run( - self, s: Optional[SampleShortInfo], r: RotationScanRequest, d: DAQStatusModel + self, s: SampleShortInfo | None, r: RotationScanRequest, d: DAQStatusModel ): if s is None: return @@ -238,7 +237,7 @@ class AareWrapper: @log_timing(logger, "AareDB call") def create_gridscan_run( - self, s: Optional[SampleShortInfo], r: RasterGridRequest, d: DAQStatusModel + self, s: SampleShortInfo | None, r: RasterGridRequest, d: DAQStatusModel ): if s is None: return @@ -301,11 +300,11 @@ class AareWrapper: @log_timing(logger, "AareDB call") def ingest_gridscan( self, - sample: Optional[SampleShortInfo], + sample: SampleShortInfo | None, raster_result: ScanResult, raster_request: RasterGridRequest, geom: SampleGeometryModel, - com: Optional[CenterOfMassModel], + com: CenterOfMassModel | None, beam_mark_pxl: tuple[float, float], ): @@ -339,11 +338,11 @@ class AareWrapper: def format_gridscan_payload( self, - sample: Optional[SampleShortInfo], + sample: SampleShortInfo | None, raster_result: ScanResult, raster_request: RasterGridRequest, geom: SampleGeometryModel, - com: Optional[CenterOfMassModel], + com: CenterOfMassModel | None, beam_mark_pxl: tuple[float, float], ) -> RasterPayloadModel | None: @@ -398,12 +397,12 @@ class AareWrapper: except Exception as e: logger.error(e) - raise e + raise @log_timing(logger, "AareDB call") def ingest_scan( self, - sample: Optional[SampleShortInfo], + sample: SampleShortInfo | None, result: ScanResult, geom: SampleGeometryModel, beam_mark_pxl: tuple[float, float], @@ -437,7 +436,7 @@ class AareWrapper: def format_scan_payload( self, - sample: Optional[SampleShortInfo], + sample: SampleShortInfo | None, result: ScanResult, geom: SampleGeometryModel, beam_mark_pxl: tuple[float, float], @@ -455,4 +454,4 @@ class AareWrapper: except Exception as e: logger.error(e) - raise e + raise diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index 927478b2..92c61938 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -5,7 +5,6 @@ import pwd import time import uuid from datetime import UTC, datetime, timedelta -from typing import List import jwt from aarecommon.errors.exception_handler import ( @@ -38,7 +37,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") class TokenData(BaseModel): sub: str # Username - pgroups: List[str] + pgroups: list[str] session: int staff: bool = False diff --git a/src/aare/daq/beamcenterfit.py b/src/aare/daq/beamcenterfit.py index de0f815c..280a2160 100644 --- a/src/aare/daq/beamcenterfit.py +++ b/src/aare/daq/beamcenterfit.py @@ -1,5 +1,5 @@ -import numpy as np import cv2 +import numpy as np from scipy.optimize import curve_fit diff --git a/src/aare/daq/config.py b/src/aare/daq/config.py index 47d71310..24ca2c9b 100644 --- a/src/aare/daq/config.py +++ b/src/aare/daq/config.py @@ -4,7 +4,6 @@ import json import time from dataclasses import asdict, is_dataclass from datetime import datetime -from typing import List, Tuple import numpy as np import redis @@ -263,7 +262,7 @@ class BeamlineConfig: holder_session = holder.session if holder is not None else None sessions: list[OpenGuiSessionInfo] = [] - for session_id in sorted((int(s) for s in session_ids)): + for session_id in sorted(int(s) for s in session_ids): payload = self._read_gui_session(session_id) if payload is not None: payload.holds_baton = payload.session == holder_session @@ -522,9 +521,7 @@ class BeamlineConfig: @property def commissioning_mode(self) -> bool: tmp = self._client.get(f"{self._bl}:commissioning_mode") - if tmp is None: - return False - return True + return tmp is not None @commissioning_mode.setter def commissioning_mode(self, commisioning_mode: bool) -> None: @@ -635,7 +632,7 @@ class BeamlineConfig: return float(np.log(lens_factor / (b * target_pixel_in_mm)) / a) @property - def beam_center(self) -> Tuple[float, float]: + def beam_center(self) -> tuple[float, float]: tmp_x = self._client.get(f"{self._bl}:beam_center_x") tmp_y = self._client.get(f"{self._bl}:beam_center_y") if tmp_x: @@ -649,7 +646,7 @@ class BeamlineConfig: return val_x, val_y @beam_center.setter - def beam_center(self, data: Tuple[float, float]): + def beam_center(self, data: tuple[float, float]): self._client.set(f"{self._bl}:beam_center_x", data[0]) self._client.set(f"{self._bl}:beam_center_y", data[1]) @@ -721,7 +718,7 @@ class BeamlineConfig: data_dict = json.loads(tmp) return SampleShortInfoList(**data_dict) - def spreadsheet_pgroup(self, pgroups: List[str]) -> SampleShortInfoList: + def spreadsheet_pgroup(self, pgroups: list[str]) -> SampleShortInfoList: sample = self.spreadsheet sample.s = list(filter(lambda x: x.user in pgroups, sample.s)) return sample diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 402b6bd9..0c6ee565 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -2,10 +2,10 @@ import copy import json import secrets import time -from datetime import datetime, timezone +from collections.abc import Callable +from datetime import UTC, datetime from math import ceil from pathlib import Path -from typing import Callable, List, Optional, Tuple from aarecommon.config.beamline import cfg_get from aarecommon.config.logger import setup_logger @@ -611,9 +611,8 @@ class AareDAQ: if ( tell_state.last_event_class == "Motion Sync" and tell_state.last_event_value == "Sample put on Puck" - ): - if state_ts is not None and state_ts >= started_at: - return True + ) and state_ts is not None and state_ts >= started_at: + return True for event in reversed(self._get_tell_events_from_redis()): if event.get("class") == "Motion Sync" and event.get("event") == "Sample put on Puck": @@ -943,10 +942,10 @@ class AareDAQ: def _handle_operation_error( self, operation: DAQOperation, - sample: Optional[SampleShortInfo], + sample: SampleShortInfo | None, error: Exception, event_type: SampleEventType = SampleEventType.FAILED, - additional_comment: Optional[str] = None, + additional_comment: str | None = None, ) -> None: """ Centralized databse maessage error handling for all operations. @@ -1006,7 +1005,7 @@ class AareDAQ: True if successful, False otherwise """ previous_sample = None - mount_started_at = datetime.now(timezone.utc) + mount_started_at = datetime.now(UTC) self._last_mount_error_message = "" try: @@ -1169,7 +1168,7 @@ class AareDAQ: step_size: int = 15, face_min_ratio: float = 0.3, report_error: bool = True, - sample: Optional[SampleShortInfo] = None, + sample: SampleShortInfo | None = None, ) -> FaceDetectionResult: """ Execute face detection sequence through the face detection service. @@ -1583,7 +1582,7 @@ class AareDAQ: # name = clean_filename(name) return name - def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str | None]: + def spreadsheet_params(self) -> tuple[SimpleScanParameters | None, str | None]: file_prefix = None if self.status.sample is None: @@ -1622,8 +1621,7 @@ class AareDAQ: new_res = 1 / ((1 / res) + 0.1) corrected_dtz = self.diffraction_geometry.calc_dtz_mm(new_res) logger.debug(f"corrected dtz: {corrected_dtz}") - if corrected_dtz < 108: - corrected_dtz = 108 + corrected_dtz = max(corrected_dtz, 108) params.dtz = round(corrected_dtz) osc = getattr(aaredb_params, "oscillation", None) @@ -1900,7 +1898,7 @@ class AareDAQ: logger.debug(f"Failed to change mounted sample: {e}") raise - def list_loaded_pucks(self) -> List[PuckLoadedInfo]: + def list_loaded_pucks(self) -> list[PuckLoadedInfo]: return [] def _auto_focus(self, settings: AutofocusSettings, settle_time_s: float = 1.0) -> float: @@ -2014,7 +2012,6 @@ class AareDAQ: ) self._devs.smargon_wait(timeout=180) - return def _build_fake_rotation_result(self, request: RotationScanRequest) -> CompletedRotationScan: start_angle = 0.0 @@ -2222,7 +2219,6 @@ class AareDAQ: except Exception: self._cfg.state_busy = False raise - pass def mark_beam(self, x_pxl: float, y_pxl: float): self._cfg.set_busy(BeamlineStateEnum.BeamLocation) @@ -2255,11 +2251,11 @@ class AareDAQ: return sample_geom @property - def beam_center(self) -> Tuple[float, float]: + def beam_center(self) -> tuple[float, float]: return self._cfg.beam_center @beam_center.setter - def beam_center(self, val: Tuple[float, float]): + def beam_center(self, val: tuple[float, float]): self._cfg.beam_center = val @property @@ -2329,7 +2325,7 @@ class AareDAQ: self._cfg.state_busy = False @log_timing(logger, "Auto loop center") - def auto_loop_center(self, sample: Optional[SampleShortInfo] = None) -> float: + def auto_loop_center(self, sample: SampleShortInfo | None = None) -> float: """ Automatically center the loop using ML-based detection. This performs a multi-step sequence including rotation and centering. @@ -2416,7 +2412,7 @@ class AareDAQ: ) def send_message_db( - self, db_id: int, event_type: SampleEventType, comment: Optional[str] = None + self, db_id: int, event_type: SampleEventType, comment: str | None = None ): self._aare.send_sample_event(db_id, event_type, comment) @@ -2524,7 +2520,7 @@ class AareDAQ: return params def get_collection_params(self, prefer_smart: bool = False) -> tuple[SimpleScanParameters, str]: - spreadsheet_params, file_prefix = self.spreadsheet_params() + spreadsheet_params, _file_prefix = self.spreadsheet_params() logger.debug(f"spreadsheet_params: {spreadsheet_params}") smart_params = self._cfg.auto_params default_params = SimpleScanParameters(exp_time_s=0.04, dtz=110, incr_omega_deg=0.2) @@ -2544,7 +2540,7 @@ class AareDAQ: def _end_operation( self, start: float, - operation: Optional[DAQOperation] = DAQOperation.AUTOMATION, + operation: DAQOperation | None = DAQOperation.AUTOMATION, error: bool = False, ) -> float: """ @@ -2603,9 +2599,7 @@ class AareDAQ: self._emit_automation_progress(progress) formatted_date = datetime.now().strftime("%Y%m%d") - sample_prefix = "{}/{}/{:02d}/{}".format( - formatted_date, sample.puck_name, sample.pin, sample.sample_name - ) + sample_prefix = f"{formatted_date}/{sample.puck_name}/{sample.pin:02d}/{sample.sample_name}" try: self._validate_automation_state(context="automation start") @@ -2697,9 +2691,7 @@ class AareDAQ: if raster_params.filename is not None: logger.info(f"Using filename {raster_params.filename}") - sample_prefix = "{filename}/{prefix}".format( - filename=raster_params.filename, prefix=sample.sample_name - ) + sample_prefix = f"{raster_params.filename}/{sample.sample_name}" raster_grid = RasterGridRequest( exp_time_s=raster_params.exp_time_s, @@ -2777,9 +2769,7 @@ class AareDAQ: if params.filename is not None: logger.info(f"Using filename {params.filename}") - sample_prefix = "{filename}/{prefix}".format( - filename=params.filename, prefix=sample.sample_name - ) + sample_prefix = f"{params.filename}/{sample.sample_name}" rotation_request = RotationScanRequest( start_omega_deg=start_omega, @@ -2883,7 +2873,7 @@ class AareDAQ: logger.error(f"Failed to mount sample: {e}") if e.critical: self._end_operation(start, operation=DAQOperation.AUTOMATION, error=True) - raise e + raise else: pass @@ -3324,7 +3314,7 @@ class AareDAQ: return self.beamline_status except Exception as e: # TODO add error message to send to GUI to say problem - logger.error(f"Failed to retrieve beamline status: {str(e)}") + logger.error(f"Failed to retrieve beamline status: {e!s}") raise def _safe_tell_state(self) -> TellStateModel | None: @@ -3352,7 +3342,7 @@ class AareDAQ: try: return self.diffraction_geometry except Exception as e: - logger.warning(f"Failed to retrieve diffraction geomtrey: {str(e)}") + logger.warning(f"Failed to retrieve diffraction geomtrey: {e!s}") # Must satisfy pydantic constraints in DiffractionGeometry return DiffractionGeometry( energy_keV=12.4, diff --git a/src/aare/daq/mlbox.py b/src/aare/daq/mlbox.py index 7fdb30fc..6a70ed08 100644 --- a/src/aare/daq/mlbox.py +++ b/src/aare/daq/mlbox.py @@ -1,6 +1,6 @@ import time +from collections.abc import Iterable from dataclasses import dataclass, field -from typing import Iterable, Optional import cv2 import numpy as np @@ -308,15 +308,13 @@ class MlBox: def box_filter_overlap( self, model: MLBoxModel, pin: MLBoxModel, overlap_parameter: float = 0.5 ) -> bool: - if self._check_overlap(model, pin) >= overlap_parameter: - return True - return False + return self._check_overlap(model, pin) >= overlap_parameter def _filter_predictions( self, predictions: MLOutputModel, - overlap_with_pin: Optional[float] = None, - confidence_min: Optional[float] = None, + overlap_with_pin: float | None = None, + confidence_min: float | None = None, ): pin = predictions.get_best_for_class(MLBoxType.PIN) @@ -337,7 +335,7 @@ class MlBox: predictions.boxes.pop(k, None) @staticmethod - def _best_by_class(results) -> Optional[MLOutputModel]: + def _best_by_class(results) -> MLOutputModel | None: out = MLOutputModel() for pred in results: try: @@ -375,7 +373,7 @@ class MlBox: @staticmethod def _all_from_prediction_model( prediction: LatestPredictionModel | None, - ) -> Optional[MLOutputModel]: + ) -> MLOutputModel | None: if prediction is None or not getattr(prediction, "boxes", None): return None @@ -406,9 +404,9 @@ class MlBox: @staticmethod def get_preferred_class_box_with_confidence_threshold( boxes: MLOutputModel, - preferred_class: Optional[Iterable[int] | int | MLBoxType] = None, + preferred_class: Iterable[int] | int | MLBoxType | None = None, loop_preference_margin: float = 0.1, - ) -> Optional[MLBoxModel]: + ) -> MLBoxModel | None: """ Get best box, preferring loops over pin even if pin has higher confidence, unless pin's confidence exceeds loops by the margin. @@ -466,8 +464,8 @@ class MlBox: @staticmethod def get_preferred_class_box( - boxes: MLOutputModel, preferred_class: Optional[Iterable[int] | int | MLBoxType] = None - ) -> Optional[MLBoxModel]: + boxes: MLOutputModel, preferred_class: Iterable[int] | int | MLBoxType | None = None + ) -> MLBoxModel | None: if preferred_class is None: order = (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN) else: @@ -525,8 +523,8 @@ class MlBox: def predict_best_no_filter( self, return_image: bool = False, return_bundle_meta: bool = False ) -> ( - Optional[MLOutputModel] - | tuple[Optional[MLOutputModel], np.ndarray | None] + MLOutputModel | None + | tuple[MLOutputModel | None, np.ndarray | None] | MLBoxPredictionsResult ): ml_bundle = self._collect_best_bundle() @@ -553,8 +551,8 @@ class MlBox: return_image: bool = False, return_bundle_meta: bool = False, ) -> ( - Optional[MLOutputModel] - | tuple[Optional[MLOutputModel], np.ndarray | None] + MLOutputModel | None + | tuple[MLOutputModel | None, np.ndarray | None] | MLBoxPredictionsResult ): ml_bundle = self._collect_best_bundle() @@ -630,7 +628,7 @@ class MlBox: overlap_with_pin: float | None = None, confidence_min: float | None = None, return_image: bool = False, - ) -> Optional[MLBoxModel] | tuple[Optional[MLBoxModel], np.ndarray | None]: + ) -> MLBoxModel | None | tuple[MLBoxModel | None, np.ndarray | None]: """ Request bounding boxes for the next N bundle fetches and return the best available box. """ diff --git a/src/aare/daq/operations/common/ml_bounding_box.py b/src/aare/daq/operations/common/ml_bounding_box.py index be364a20..77188e2d 100644 --- a/src/aare/daq/operations/common/ml_bounding_box.py +++ b/src/aare/daq/operations/common/ml_bounding_box.py @@ -1,7 +1,7 @@ import time +from collections.abc import Callable from dataclasses import dataclass from math import ceil, floor -from typing import Callable from aarecommon.config.beamline import cfg_get from aarecommon.config.logger_events import ( @@ -73,8 +73,8 @@ def scale_auto_raster_grid( physical_size_y_mm = n_y * grid_size.y scale = (image_count / max_images) ** 0.5 - scaled_n_x = max(1, int(floor(n_x / scale))) - scaled_n_y = max(1, int(floor(n_y / scale))) + scaled_n_x = max(1, floor(n_x / scale)) + scaled_n_y = max(1, floor(n_y / scale)) while scaled_n_x * scaled_n_y > max_images: if scaled_n_x >= scaled_n_y and scaled_n_x > 1: @@ -201,9 +201,9 @@ def _box_to_raster_request( frac_x = float(cfg_get("daq.auto_raster.grid_padding_fraction_x", 0.15)) frac_y_top = float(cfg_get("daq.auto_raster.grid_padding_fraction_y", 0.15)) frac_y_bottom = float(cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top)) - pad_x = max(1, int(ceil(frac_x * n_x))) - pad_y_top = max(1, int(ceil(frac_y_top * n_y))) - pad_y_bottom = max(1, int(ceil(frac_y_bottom * n_y))) + pad_x = max(1, ceil(frac_x * n_x)) + pad_y_top = max(1, ceil(frac_y_top * n_y)) + pad_y_bottom = max(1, ceil(frac_y_bottom * n_y)) x1 = x1 - pad_x * grid_size.x / geom.pixel_in_mm y1 = y1 - pad_y_top * grid_size.y / geom.pixel_in_mm n_x = n_x + 2 * pad_x diff --git a/src/aare/daq/operations/face_detection/utils.py b/src/aare/daq/operations/face_detection/utils.py index fe127170..998da418 100644 --- a/src/aare/daq/operations/face_detection/utils.py +++ b/src/aare/daq/operations/face_detection/utils.py @@ -3,7 +3,6 @@ import math import statistics import time import warnings -from typing import Dict, List, Optional, Tuple import numpy as np from aarecommon.config.logger import setup_logger @@ -13,7 +12,7 @@ logger = setup_logger("aareDAQ") def box_height_from_tuple(box: tuple[float, float, float, float]) -> float: - x1, y1, x2, y2 = box + _x1, y1, _x2, y2 = box return abs(y2 - y1) @@ -24,7 +23,7 @@ def box_area_from_tuple(box: tuple[float, float, float, float]) -> float: def prepare_samples( boxes_by_angle: dict[int, tuple[float, float, float, float]], area=False -) -> List[Tuple[float, float]]: +) -> list[tuple[float, float]]: # angles in degrees -> (theta_rad, height) samples = [] for deg, box in boxes_by_angle.items(): @@ -37,7 +36,7 @@ def cos_model(theta_deg: float | np.ndarray, A: float, B: float, phi_rad: float, return A + B * np.cos(C * np.deg2rad(theta_deg) - phi_rad) -def mad_filter(samples: List[Tuple[float, float]], k: float = 3.5) -> List[Tuple[float, float]]: +def mad_filter(samples: list[tuple[float, float]], k: float = 3.5) -> list[tuple[float, float]]: if not samples: return samples ys = [y for _, y in samples] @@ -54,7 +53,6 @@ def samples_to_json(samples): } with open("cos_test.json", "w") as f: json.dump(output_data, f, indent=2) - return def fit_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float, float]: @@ -67,7 +65,7 @@ def fit_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float, f return rmse, mae, r2 -def fit_cosine(samples: List[Tuple[float, float]]) -> dict: +def fit_cosine(samples: list[tuple[float, float]]) -> dict: samples = mad_filter(samples, k=3.5) if len(samples) < 3: A = sum(y for _, y in samples) / max(1, len(samples)) @@ -136,10 +134,10 @@ def get_samples_out(boxes): def choose_best_fit( - fits_by_name: Dict[str, Dict], -) -> Tuple[Optional[float], Optional[Dict], Optional[str]]: + fits_by_name: dict[str, dict], +) -> tuple[float | None, dict | None, str | None]: - def key(entry: Dict): + def key(entry: dict): params = entry.get("params") or {} rmse = params.get("rmse") mae = params.get("mae") diff --git a/src/aare/daq/operations/loop_centering/__init__.py b/src/aare/daq/operations/loop_centering/__init__.py index 2026c242..f6d0c2be 100644 --- a/src/aare/daq/operations/loop_centering/__init__.py +++ b/src/aare/daq/operations/loop_centering/__init__.py @@ -1,17 +1,17 @@ +from aare.daq.operations.loop_centering.analyzer import LoopCenteringAnalyzer from aare.daq.operations.loop_centering.models import ( - LoopCenteringContext, AngleAnalysis, AttemptSummary, + LoopCenteringContext, LoopCenteringSettings, ) from aare.daq.operations.loop_centering.service import LoopCenteringService -from aare.daq.operations.loop_centering.analyzer import LoopCenteringAnalyzer __all__ = [ - "LoopCenteringContext", "AngleAnalysis", "AttemptSummary", - "LoopCenteringSettings", - "LoopCenteringService", "LoopCenteringAnalyzer", + "LoopCenteringContext", + "LoopCenteringService", + "LoopCenteringSettings", ] diff --git a/src/aare/daq/operations/raster/__init__.py b/src/aare/daq/operations/raster/__init__.py index 5f87f03c..06414f52 100644 --- a/src/aare/daq/operations/raster/__init__.py +++ b/src/aare/daq/operations/raster/__init__.py @@ -1,4 +1,4 @@ -from aare.daq.operations.raster.models import RasterContext, RasterBoundingBoxResult +from aare.daq.operations.raster.models import RasterBoundingBoxResult, RasterContext from aare.daq.operations.raster.service import RasterService __all__ = ["RasterBoundingBoxResult", "RasterContext", "RasterService"] diff --git a/src/aare/daq/operations/raster/service.py b/src/aare/daq/operations/raster/service.py index 3d4e7c56..8d296b61 100644 --- a/src/aare/daq/operations/raster/service.py +++ b/src/aare/daq/operations/raster/service.py @@ -325,7 +325,7 @@ class RasterService: padded_height_mm = ( box_height_pxl * geom.pixel_in_mm * (1.0 + 2.0 * y_padding_fraction_each_side) ) - n_y = max(1, int(ceil(padded_height_mm / grid_size_mm.y))) + n_y = max(1, ceil(padded_height_mm / grid_size_mm.y)) self.logger.info( "Computed second auto-center raster y size from ML box height", extra=merge_log_context( diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 9a305cdf..d8e756d4 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -4,8 +4,8 @@ import importlib import json import os import time +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator, Optional import uvicorn from aarecommon.config.beamline import mx_beamline @@ -2487,7 +2487,7 @@ async def send_screenshot_db( async def send_message_db( db_id: int, event_type: SampleEventType, - comment: Optional[str] = None, + comment: str | None = None, token: str = Depends(oauth2_scheme), ): data = auth.parse_token(token) diff --git a/src/aare/daq/server_exception_handler.py b/src/aare/daq/server_exception_handler.py index 4aed5d8e..b78eed76 100644 --- a/src/aare/daq/server_exception_handler.py +++ b/src/aare/daq/server_exception_handler.py @@ -173,11 +173,10 @@ def register_exception_handlers(app) -> None: ) body = _error_body(exc, code_override=code_override) # TODO: migrate baton to its own error type - if isinstance(exc, UserRightsException): - if "do not hold the baton" in exc.message: - return JSONResponse( - status_code=status, content=body, headers=getattr(exc, "headers", None) - ) + if isinstance(exc, UserRightsException) and "do not hold the baton" in exc.message: + return JSONResponse( + status_code=status, content=body, headers=getattr(exc, "headers", None) + ) logger.warning( "AareAuthError: %s", body["message"], diff --git a/src/aare/daq/tell_state_machine.py b/src/aare/daq/tell_state_machine.py index 2bbe432d..53f8a97f 100644 --- a/src/aare/daq/tell_state_machine.py +++ b/src/aare/daq/tell_state_machine.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from datetime import datetime, timezone +from datetime import UTC, datetime from aarecommon.models.tell import TellActivityEnum, TellPhaseEnum, TellStateModel @@ -11,7 +11,7 @@ UNMOUNT_STATUS_RE = re.compile(r"^unmount:\s*") def _utc_now_iso() -> str: - return datetime.now(timezone.utc).isoformat() + return datetime.now(UTC).isoformat() def initial_tell_state() -> TellStateModel: diff --git a/src/aare/daq/tellupdater.py b/src/aare/daq/tellupdater.py index d459d203..d0ce3c06 100644 --- a/src/aare/daq/tellupdater.py +++ b/src/aare/daq/tellupdater.py @@ -4,7 +4,7 @@ import ssl import threading import time from collections import deque -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, cast import requests @@ -180,7 +180,7 @@ def set_tell_state_in_redis(state: TellStateModel) -> None: def record_tell_event(event_name, event_value): event_record = TellEventRecord( - timestamp=datetime.now(timezone.utc).isoformat(), class_=event_name, event=event_value + timestamp=datetime.now(UTC).isoformat(), class_=event_name, event=event_value ) latest_tell_events[event_name] = event_value diff --git a/src/aare/devices/aerotech.py b/src/aare/devices/aerotech.py index 5970f5ad..10732d2c 100644 --- a/src/aare/devices/aerotech.py +++ b/src/aare/devices/aerotech.py @@ -1,4 +1,3 @@ -from typing import Optional, Union from aarecommon.config.beamline import cfg_get, mx_beamline from aarecommon.config.logger import setup_logger @@ -23,7 +22,7 @@ AEROTECH_HOME = AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0) logger = setup_logger("aareDAQ") -class AerotechController(object): +class AerotechController: def __init__(self, bl: MXBeamline): if bl == MXBeamline.X06DA: self._simulated = False @@ -185,9 +184,9 @@ class AerotechController(object): def rotation_scan( self, - rotation_deg: float | int, - time_sec: float | int, - start_pos_deg: float | int, + rotation_deg: float, + time_sec: float, + start_pos_deg: float, run_async: bool = False, ): payload = RotationRequest( @@ -212,11 +211,11 @@ class AerotechController(object): def grid_scan( self, grid_elem_count_y: int, - grid_elem_size_y_um: int | float, - time_sec: int | float, - grid_elem_size_x_um: Optional[Union[float, int]] = None, - grid_elem_count_x: Optional[int] = None, - run_async: Optional[bool] = False, + grid_elem_size_y_um: float, + time_sec: float, + grid_elem_size_x_um: float | None = None, + grid_elem_count_x: int | None = None, + run_async: bool | None = False, ): payload = GridRequest( grid_elem_count_x=grid_elem_count_x, @@ -240,9 +239,9 @@ class AerotechController(object): def screening_scan( self, - rotation_deg: float | int, - wedge_deg: float | int, - time_sec: float | int, + rotation_deg: float, + wedge_deg: float, + time_sec: float, steps: int, run_async: bool = False, ): diff --git a/src/aare/devices/area_detector.py b/src/aare/devices/area_detector.py index b5104ed2..51427404 100644 --- a/src/aare/devices/area_detector.py +++ b/src/aare/devices/area_detector.py @@ -21,7 +21,7 @@ class AutoExposureSettings: exp_max: float = 30000.000 -class epicsAD(object): +class epicsAD: def __init__(self, prefix, cam="cam1:", image="image1:"): self.img = None self.monitored = False @@ -63,7 +63,6 @@ class epicsAD(object): epics.ca.pend_io() except Exception as e: print(f"EPICS error: {e}") - pass def _init_auto_exp(self, settings: AutoExposureSettings = AutoExposureSettings()): self.acquire.put(0) diff --git a/src/aare/devices/bec_worker.py b/src/aare/devices/bec_worker.py index f6649bbe..945fe909 100644 --- a/src/aare/devices/bec_worker.py +++ b/src/aare/devices/bec_worker.py @@ -1,6 +1,5 @@ import time from enum import Enum -from typing import List, Optional from aarecommon.config.beamline import cfg_get, mx_beamline from aarecommon.config.logger import setup_logger @@ -118,7 +117,7 @@ class BECClientWorker: raise Exception(f"Error initialising BEC devices: {e}") def _raise_bec_error( - self, exc: Exception, *, operation: str, tags: Optional[List[str]] = None + self, exc: Exception, *, operation: str, tags: list[str] | None = None ) -> None: message = f"BEC operation '{operation}' failed: {type(exc).__name__}: {exc}" # logger.exception(message) @@ -156,7 +155,7 @@ class BECClientWorker: raise BECCommunicationError(message, operation=operation, exception=exc) from exc - def _set_scilog_tags(self, tags: Optional[List[str]] = None): + def _set_scilog_tags(self, tags: list[str] | None = None): try: if tags: self.client.messaging.scilog.set_default_tags(tags) @@ -171,13 +170,13 @@ class BECClientWorker: message: str, error: bool = False, warning: bool = False, - error_message: Optional[str] = None, + error_message: str | None = None, attachments=None, bold: bool = False, italic: bool = False, - color: Optional[str] = None, - additonal_text: Optional[List[str]] = None, - tags: Optional[List[str]] = None, + color: str | None = None, + additonal_text: list[str] | None = None, + tags: list[str] | None = None, ): if color and color not in ["red", "green", "yellow", "blue", "pink"]: logger.warning("specified color not in allowed list,using default") @@ -375,7 +374,7 @@ class BECClientWorker: energy_kev = energy_ev / 1000 return energy_kev - def change_energy(self, value: float | int, plot: bool = False): + def change_energy(self, value: float, plot: bool = False): current_energy = self.check_current_energy() logger.info(f"Current energy: {current_energy:.1f} eV") logger.info(f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV") diff --git a/src/aare/devices/enum_pv.py b/src/aare/devices/enum_pv.py index f8219696..fd725e17 100755 --- a/src/aare/devices/enum_pv.py +++ b/src/aare/devices/enum_pv.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any -from aare.devices.set_get_pv import SetGetPV, MoveResult +from aare.devices.set_get_pv import MoveResult, SetGetPV class EnumPV(SetGetPV): diff --git a/src/aare/devices/fluorimeter.py b/src/aare/devices/fluorimeter.py index 13faa3e5..12c9f0a5 100644 --- a/src/aare/devices/fluorimeter.py +++ b/src/aare/devices/fluorimeter.py @@ -7,7 +7,7 @@ from epics import PV, poll logger = setup_logger("aareaDAQ") -class Fluorimeter(object): +class Fluorimeter: def __init__(self, beamline: MXBeamline, **kwargs): BEAMLINE = beamline.value.upper() diff --git a/src/aare/devices/jfjoch.py b/src/aare/devices/jfjoch.py index d4ecf250..46da0749 100644 --- a/src/aare/devices/jfjoch.py +++ b/src/aare/devices/jfjoch.py @@ -176,23 +176,22 @@ class JFJochWrapper: ) dataset_settings.xray_fluorescence_spectrum = xrf - if s.sample.aaredb_params: - if s.sample.aaredb_params.unitcell: - unit_cell_db = s.sample.aaredb_params.unitcell - unit_cell_split = unit_cell_db.replace(",", " ").split() - unit_cell_floats = [float(x) for x in unit_cell_split] - unit_cell = jfjoch_client.UnitCell( - a=unit_cell_floats[0], - b=unit_cell_floats[1], - c=unit_cell_floats[2], - alpha=unit_cell_floats[3], - beta=unit_cell_floats[4], - gamma=unit_cell_floats[5], - ) - dataset_settings.unit_cell = unit_cell - if s.sample.aaredb_params.spacegroupnumber: - space_group_number = s.sample.aaredb_params.spacegroupnumber - dataset_settings.space_group_number = space_group_number + if s.sample.aaredb_params and s.sample.aaredb_params.unitcell: + unit_cell_db = s.sample.aaredb_params.unitcell + unit_cell_split = unit_cell_db.replace(",", " ").split() + unit_cell_floats = [float(x) for x in unit_cell_split] + unit_cell = jfjoch_client.UnitCell( + a=unit_cell_floats[0], + b=unit_cell_floats[1], + c=unit_cell_floats[2], + alpha=unit_cell_floats[3], + beta=unit_cell_floats[4], + gamma=unit_cell_floats[5], + ) + dataset_settings.unit_cell = unit_cell + if s.sample.aaredb_params.spacegroupnumber: + space_group_number = s.sample.aaredb_params.spacegroupnumber + dataset_settings.space_group_number = space_group_number return dataset_settings @@ -230,7 +229,7 @@ class JFJochWrapper: def measure_raster(self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True): self._start_scan(ScanTypeEnum.RASTER, r, s, async_start=async_start) - def wait_till_running(self, timeout: int | float = 60): + def wait_till_running(self, timeout: float = 60): if self._simulated: return None try: @@ -244,7 +243,7 @@ class JFJochWrapper: endpoint="wait_until_running_post", ) - def wait_till_done(self, timeout: int | float) -> jfjoch_client.models.ScanResult | None: + def wait_till_done(self, timeout: float) -> jfjoch_client.models.ScanResult | None: if self._simulated: return None try: diff --git a/src/aare/devices/mx_lib.py b/src/aare/devices/mx_lib.py index 09bf2eab..1a28678a 100644 --- a/src/aare/devices/mx_lib.py +++ b/src/aare/devices/mx_lib.py @@ -1,6 +1,7 @@ import re import time -from typing import Callable, Union, Any +from collections.abc import Callable +from typing import Any from epics import PV, Motor, poll @@ -21,12 +22,12 @@ def wait_for_movement_to_finish(*motors): longest = 0.0 for motor in motors: time_to_target = motor.readback / motor.slew_speed - longest = time_to_target if time_to_target > longest else longest + longest = max(longest, time_to_target) timeout = time.time() + 1.5 * longest done = False while not done and time.time() < timeout: - done = all([m.done_moving for m in motors]) + done = all(m.done_moving for m in motors) if time.time() > timeout: print("TIMEOUT waiting for motors to be done moving; current motor positions:") @@ -102,7 +103,7 @@ def is_epics_type(pv: PV, pv_type: str) -> bool: def wait_string_condition( - pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1 + pv: PV, target: str | re.Pattern, *, timeout: float = 60.0, polling: float = 0.1 ): """wait until an epics.PV of type string reaches target :pv: epics.PV @@ -230,7 +231,7 @@ def wait_motor_position( def wait_enum_condition( - pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1 + pv: PV, value: str | int | re.Pattern, *, timeout: float = 60.0, polling=0.1 ): """wait until an epics.PV enum reaches value pv: epics.PV @@ -249,16 +250,16 @@ def wait_enum_condition( if not (isinstance(pv, PV) and pv.type.lower().endswith("enum")): raise AttributeError("argument 'pv' must be an epics.PV of type enum") - if not (isinstance(value, str) or isinstance(value, int) or isinstance(value, re.Pattern)): + if not (isinstance(value, (str, int, re.Pattern))): raise AttributeError("argument 'value' must be either an int, str, or re.Pattern") if type(value) is int: - tester = lambda pv: value == pv.get() # noqa: E731 + tester = lambda pv: value == pv.get() elif type(value) is str: - tester = lambda pv: str(value) == pv.get(as_string=True).lower() # noqa: E731 + tester = lambda pv: str(value) == pv.get(as_string=True).lower() value = str(value).lower() # it's already a str :-/ elif isinstance(value, re.Pattern): - tester = lambda pv: value.match(pv.get(as_string=True)) # noqa: E731 + tester = lambda pv: value.match(pv.get(as_string=True)) else: raise AttributeError("argument 'value' must be either an int, str, or re.Pattern") diff --git a/src/aare/devices/set_get_pv.py b/src/aare/devices/set_get_pv.py index 79fea92d..409b4038 100644 --- a/src/aare/devices/set_get_pv.py +++ b/src/aare/devices/set_get_pv.py @@ -1,9 +1,11 @@ from __future__ import annotations +from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import Any, Mapping, Optional, Union, Callable +from typing import Any, Union from epics import PV + from aare.devices.mx_lib import pv_wait RawValue = Union[str, float, int] @@ -16,7 +18,7 @@ ResolverValue = Union[ @dataclass class MoveResult: target: RawValue - name: Optional[str] = None + name: str | None = None class SetGetPV: diff --git a/src/aare/devices/smargon.py b/src/aare/devices/smargon.py index e870e277..9977b347 100644 --- a/src/aare/devices/smargon.py +++ b/src/aare/devices/smargon.py @@ -15,7 +15,7 @@ class SmargonMode(Enum): ERROR = 99 -class Smargon(object): +class Smargon: SMARGON_HOME = SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0) AERO_HOME = AerotechCoordinate(x=0, y=0, z=0, omega=0) @@ -175,13 +175,11 @@ class Smargon(object): target_string = "" if coord.sh_mm is not None: - target_string += "&SHX={:.5f}&SHY={:.5f}&SHZ={:.5f}".format( - coord.sh_mm.x, coord.sh_mm.y, coord.sh_mm.z - ) + target_string += f"&SHX={coord.sh_mm.x:.5f}&SHY={coord.sh_mm.y:.5f}&SHZ={coord.sh_mm.z:.5f}" if coord.chi_deg is not None: - target_string += "&CHI={:.5f}".format(coord.chi_deg) + target_string += f"&CHI={coord.chi_deg:.5f}" if coord.phi_deg is not None: - target_string += "&PHI={:.5f}".format(coord.phi_deg) + target_string += f"&PHI={coord.phi_deg:.5f}" if target_string: self.gonput(f"targetSCS?{target_string}") @@ -203,11 +201,9 @@ class Smargon(object): target_string = "" if coord.at_mm is not None: - target_string += "&GMX={:.5f}&GMY={:.5f}&GMZ={:.5f}".format( - coord.at_mm.x, coord.at_mm.y, coord.at_mm.z - ) + target_string += f"&GMX={coord.at_mm.x:.5f}&GMY={coord.at_mm.y:.5f}&GMZ={coord.at_mm.z:.5f}" if coord.omega_deg is not None: - target_string += "&GMU={:.5f}".format(coord.omega_deg) + target_string += f"&GMU={coord.omega_deg:.5f}" if target_string: self.gonput(f"targetAEROTECH?{target_string}") diff --git a/src/aare/devices/tell_backend.py b/src/aare/devices/tell_backend.py index 1ce95ff6..5a638305 100644 --- a/src/aare/devices/tell_backend.py +++ b/src/aare/devices/tell_backend.py @@ -1,7 +1,8 @@ import json import re import time -from typing import Any, Callable, Protocol +from collections.abc import Callable +from typing import Any, Protocol from urllib.parse import urlparse import requests diff --git a/src/aare/devices/tell_client.py b/src/aare/devices/tell_client.py index cf9ad4d8..c8590579 100644 --- a/src/aare/devices/tell_client.py +++ b/src/aare/devices/tell_client.py @@ -1,8 +1,8 @@ import ast import json import re +from datetime import UTC from enum import Enum -from typing import List from aarecommon.config.logger import setup_logger from aarecommon.errors.exception_handler import ( @@ -164,7 +164,7 @@ class TellClient: "Mount can't start: TELL doors are open", operation="mount_precheck" ) - def set_samples_info(self, info: List[PuckWithTellPosition]): + def set_samples_info(self, info: list[PuckWithTellPosition]): """sets the samples in the robot dewar based on the given list of PuckWithTellPosition objects and runs set_sample_info in the background""" @@ -205,9 +205,8 @@ class TellClient: result = self.get_result(self._last_cmd_id) logger.debug(f"getting result for command {self._last_cmd_id}: {result}") status = result["status"] - if "completed" != status: - if "removed" != status: - raise MountingFailed(f"{msg} {result}") + if "completed" != status and "removed" != status: + raise MountingFailed(f"{msg} {result}") return f"{msg} {result}" def estimate_mounting_time(self, segment) -> int: @@ -452,7 +451,7 @@ class TellClient: # return eval(status) return ast.literal_eval(status) - def get_detected_pucks(self) -> List[PuckLoadedInfo]: + def get_detected_pucks(self) -> list[PuckLoadedInfo]: j = json.loads(self.backend.eval("get_pucks_info()&")) output = [] @@ -482,7 +481,7 @@ class TellClient: return float(current) def set_current(self, current: float) -> float: - self.backend.eval("smart_magnet.set_current({:.1f})&".format(current)) + self.backend.eval(f"smart_magnet.set_current({current:.1f})&") current = self.backend.eval("smart_magnet.get_current_rb()&") return float(current) @@ -549,7 +548,7 @@ class TellClient: raise SmartMagnetFaultException except Exception as e: logger.error(f"check_smart_magnet_mounted failed: {e}") - raise e + raise def make_tell_client(bl: MXBeamline) -> TellClient: @@ -561,7 +560,7 @@ def make_tell_client(bl: MXBeamline) -> TellClient: if __name__ == "__main__": - from datetime import datetime, timezone + from datetime import datetime from aarecommon.config.beamline import mx_beamline @@ -574,8 +573,8 @@ if __name__ == "__main__": ts = float(tell_client.get_setting("dry_timestamp")) print("dry timestape: ", ts) - past = datetime.fromtimestamp(ts, tz=timezone.utc) - now = datetime.now(timezone.utc) + past = datetime.fromtimestamp(ts, tz=UTC) + now = datetime.now(UTC) seconds_ago = int((now - past).total_seconds()) print(seconds_ago) print("door closer :", tell_client.backend.eval("is_door_closed()&")) diff --git a/src/aare/devices/workflow_tools.py b/src/aare/devices/workflow_tools.py index 88cb8377..54675670 100755 --- a/src/aare/devices/workflow_tools.py +++ b/src/aare/devices/workflow_tools.py @@ -7,10 +7,10 @@ def wait_position(motor, target, tolerance=None, timeout=60.0): position = motor.readback if isinstance(position, float) and tolerance is None: tst = "%f == %s" - ltst = lambda x, y, z: x == y # noqa: E731 + ltst = lambda x, y, z: x == y elif isinstance(position, float) and tolerance is not None: tst = "abs(%f - %f) < %f" - ltst = lambda x, y, z: abs(x - y) < z # noqa: E731 + ltst = lambda x, y, z: abs(x - y) < z elif isinstance(position, (bytes, str)): tst = "'%s' == '%s'" if isinstance(position, bytes): @@ -38,18 +38,12 @@ def wait_position(motor, target, tolerance=None, timeout=60.0): if n > 20: n = 0 print( - "waiting_position test: %s (%s, %s, %s)" - % (tst, str(motor.readback), str(target), str(tolerance)) + f"waiting_position test: {tst} ({motor.readback!s}, {target!s}, {tolerance!s})" ) timeisup = timeout < time.time() condition = ltst(motor.readback, target, tolerance) if timeisup: - msg = "Timeout when waiting for %s to reach %s with tolerance %s. Device was at: %s" % ( - motor, - str(target), - str(tolerance), - str(motor.readback), - ) + msg = f"Timeout when waiting for {motor} to reach {target!s} with tolerance {tolerance!s}. Device was at: {motor.readback!s}" print(msg) raise RuntimeError(msg) diff --git a/src/aare/devices/zmq_client.py b/src/aare/devices/zmq_client.py index 2b4db0d9..15e52c9e 100755 --- a/src/aare/devices/zmq_client.py +++ b/src/aare/devices/zmq_client.py @@ -5,7 +5,6 @@ with fallback to area_detector if ZMQ is unavailable. """ import json -from typing import Optional import cv2 import numpy as np @@ -46,9 +45,9 @@ class ZMQCameraClient: raise Exception("unknown beamline") self._timeout_ms = timeout_ms - self._context: Optional[zmq.Context] = None - self._socket: Optional[zmq.Socket] = None - self._last_image: Optional[np.ndarray] = None + self._context: zmq.Context | None = None + self._socket: zmq.Socket | None = None + self._last_image: np.ndarray | None = None self._last_fetch_time: float = 0.0 self._connected = False @@ -81,7 +80,7 @@ class ZMQCameraClient: self._connected = False return False - def get_image(self, gray: bool = False) -> Optional[np.ndarray]: + def get_image(self, gray: bool = False) -> np.ndarray | None: """ Fetch the latest image from the ZMQ stream. diff --git a/src/aare/gui/about.py b/src/aare/gui/about.py index 0119fa04..c7d29e42 100644 --- a/src/aare/gui/about.py +++ b/src/aare/gui/about.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from aare.gui.threads.daq_worker import DAQWorker -def about_text(client: "DAQWorker"): +def about_text(client: DAQWorker): from aare.gui import gui try: diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index daaacbad..bd153f1a 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -173,7 +173,7 @@ def main(): QMessageBox.critical( None, "Authentication Error", - f"Cannot connect to AareDAQ server:\n{str(e)}\n\n Please check the server is running and your network connection.", + f"Cannot connect to AareDAQ server:\n{e!s}\n\n Please check the server is running and your network connection.", ) sys.exit(1) @@ -204,7 +204,7 @@ def main(): "Fatal Error", f"An error occurred during startup. See console for details." f"\nPlease check the server is running and your network connection." - f"\n\n{str(e)}\n\n", + f"\n\n{e!s}\n\n", ) sys.exit(1) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index d6825979..c6cb96e6 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -2454,16 +2454,14 @@ class MainWindow(QMainWindow): status = self._latest_daq_status if status is not None and bool(status.busy): return False - if self._is_automation_active(): - return False - return True + return not self._is_automation_active() @Slot() def _check_remote_close_deadline(self) -> None: if self._remote_close_deadline_ts is None: return - remaining = int(round(self._remote_close_deadline_ts - time.time())) + remaining = round(self._remote_close_deadline_ts - time.time()) if remaining > 0: if self._remote_close_banner_active: self.alert_banner_secondary.show_message( diff --git a/src/aare/gui/models/gui_state_manager.py b/src/aare/gui/models/gui_state_manager.py index 2cdaa90f..91608a2b 100644 --- a/src/aare/gui/models/gui_state_manager.py +++ b/src/aare/gui/models/gui_state_manager.py @@ -1,4 +1,5 @@ import json + from PySide6.QtCore import QSettings diff --git a/src/aare/gui/models/sample_queue_model.py b/src/aare/gui/models/sample_queue_model.py index 3ceedc1a..1e5d8115 100644 --- a/src/aare/gui/models/sample_queue_model.py +++ b/src/aare/gui/models/sample_queue_model.py @@ -78,9 +78,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel): return ["text/plain"] def canDropMimeData(self, data, action, row, column, parent): - if data.hasText(): - return True - return False + return bool(data.hasText()) def dropMimeData(self, data, action, row, column, parent): if not self.canDropMimeData(data, action, row, column, parent): diff --git a/src/aare/gui/models/user_sample_model.py b/src/aare/gui/models/user_sample_model.py index 9e410552..55e513cc 100644 --- a/src/aare/gui/models/user_sample_model.py +++ b/src/aare/gui/models/user_sample_model.py @@ -182,7 +182,7 @@ class UserSampleSpreadsheet(QAbstractTableModel): sample_data = SampleShortInfoList(s=[]) - for i in sorted(set(index.row() for index in indexes)): + for i in sorted({index.row() for index in indexes}): sample_data.s.append(self._sorted_samples[i]) mime_data.setText(sample_data.model_dump_json()) diff --git a/src/aare/gui/panels/automation_panel.py b/src/aare/gui/panels/automation_panel.py index e5bcf9cf..e9edb4c9 100644 --- a/src/aare/gui/panels/automation_panel.py +++ b/src/aare/gui/panels/automation_panel.py @@ -147,8 +147,8 @@ class AutomationProgressWidget(QWidget): if seconds is None or seconds <= 0: return "0m 00s" if seconds < 60: - return f"0m {int(round(seconds)):02d}s" - minutes, secs = divmod(int(round(seconds)), 60) + return f"0m {round(seconds):02d}s" + minutes, secs = divmod(round(seconds), 60) if minutes < 60: return f"{minutes}m {secs:02d}s" hours, minutes = divmod(minutes, 60) diff --git a/src/aare/gui/panels/axis_video_panel.py b/src/aare/gui/panels/axis_video_panel.py index bd8153a5..2bc5e5e8 100644 --- a/src/aare/gui/panels/axis_video_panel.py +++ b/src/aare/gui/panels/axis_video_panel.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Qt, Signal -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from aare.gui.widgets.busy_overlay import BusyOverlayStyle from aare.gui.widgets.video_image import VideoGraphicsView diff --git a/src/aare/gui/panels/developer_help_dialog.py b/src/aare/gui/panels/developer_help_dialog.py index 905b9c13..aefd0b36 100644 --- a/src/aare/gui/panels/developer_help_dialog.py +++ b/src/aare/gui/panels/developer_help_dialog.py @@ -3,7 +3,6 @@ from __future__ import annotations import json import logging import time -from typing import Dict from aarecommon.config.logger import attach_to_logger, find_existing_formatter from aarecommon.errors.codes import error_code_help @@ -40,7 +39,7 @@ class DeveloperHelpDialog(QDialog): self._daq = daq self._is_staff = bool(is_staff) - self._codes: Dict[str, str] = {} + self._codes: dict[str, str] = {} self._last_payload: dict = {} self._freeze_payload: bool = False self._always_highlight_last_error: bool = True diff --git a/src/aare/gui/panels/loop_centering_panel.py b/src/aare/gui/panels/loop_centering_panel.py index d3b4b947..15d9a179 100644 --- a/src/aare/gui/panels/loop_centering_panel.py +++ b/src/aare/gui/panels/loop_centering_panel.py @@ -1,4 +1,4 @@ -from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton +from PySide6.QtWidgets import QGridLayout, QPushButton, QWidget from aare.gui.widgets.title_label import TitleLabel diff --git a/src/aare/gui/panels/raster_data_collection.py b/src/aare/gui/panels/raster_data_collection.py index 3250f149..bbe4a16e 100644 --- a/src/aare/gui/panels/raster_data_collection.py +++ b/src/aare/gui/panels/raster_data_collection.py @@ -221,7 +221,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): def update_total_time_label(self): mins = int(self._total_time // 60) - secs = int(round(self._total_time % 60)) + secs = round(self._total_time % 60) if secs == 60: mins += 1 secs = 0 diff --git a/src/aare/gui/panels/reference_tools_panel.py b/src/aare/gui/panels/reference_tools_panel.py index 8fbf4882..711c582c 100644 --- a/src/aare/gui/panels/reference_tools_panel.py +++ b/src/aare/gui/panels/reference_tools_panel.py @@ -1,5 +1,4 @@ # reference_tools_panel.py -from typing import Optional from aarecommon.config.logger import setup_logger from aarecommon.models.models import DAQStatusModel, SampleShortInfo, SampleShortInfoList @@ -41,7 +40,7 @@ def get_entry(sample: SampleShortInfo, column: int): class ReferenceToolsModel(QAbstractTableModel): def __init__( self, - rows: Optional[list[SampleShortInfo]] | None = None, + rows: list[SampleShortInfo] | None = None, parent=None, current_reference: int | None = None, ): @@ -147,7 +146,7 @@ class ReferenceToolsModel(QAbstractTableModel): reverse=(self._sort_order == Qt.SortOrder.DescendingOrder), ) - def get_item(self, row: int) -> Optional[SampleShortInfo]: + def get_item(self, row: int) -> SampleShortInfo | None: """Get sample at the given row index.""" if 0 <= row < len(self._sorted_samples): return self._sorted_samples[row] @@ -227,7 +226,7 @@ class ReferenceToolsPanel(QFrame): self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection) - def _selected_item(self) -> Optional[SampleShortInfo]: + def _selected_item(self) -> SampleShortInfo | None: idx = self.table_view.currentIndex() if not idx.isValid(): return None diff --git a/src/aare/gui/panels/rotation_data_collection.py b/src/aare/gui/panels/rotation_data_collection.py index c01538eb..0e8dacf4 100644 --- a/src/aare/gui/panels/rotation_data_collection.py +++ b/src/aare/gui/panels/rotation_data_collection.py @@ -244,7 +244,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel): def update_total_time_label(self): mins = int(self._total_time // 60) - secs = int(round(self._total_time % 60)) + secs = round(self._total_time % 60) if secs == 60: mins += 1 secs = 0 diff --git a/src/aare/gui/panels/scan_settings_panel.py b/src/aare/gui/panels/scan_settings_panel.py index fcddb5ae..39285963 100644 --- a/src/aare/gui/panels/scan_settings_panel.py +++ b/src/aare/gui/panels/scan_settings_panel.py @@ -218,7 +218,7 @@ class ScanSettingsPanel(QWidget): def _res_to_dtz(self, res: float) -> float: dtz = self._diffraction.calc_dtz_mm(res) - return self.MIN_DTZ if dtz < self.MIN_DTZ else dtz + return max(dtz, self.MIN_DTZ) @Slot(float) def _on_dtz_value_changed(self, v: float): diff --git a/src/aare/gui/panels/smargon_trace_panel.py b/src/aare/gui/panels/smargon_trace_panel.py index e17c6d10..cd2bf95a 100644 --- a/src/aare/gui/panels/smargon_trace_panel.py +++ b/src/aare/gui/panels/smargon_trace_panel.py @@ -7,7 +7,7 @@ from pathlib import Path import numpy as np from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure -from PySide6.QtCore import QSettings, QTimer, Qt +from PySide6.QtCore import QSettings, Qt, QTimer from PySide6.QtWidgets import ( QCheckBox, QComboBox, diff --git a/src/aare/gui/panels/smart_rotation_panel.py b/src/aare/gui/panels/smart_rotation_panel.py index 9f850093..1a49a873 100644 --- a/src/aare/gui/panels/smart_rotation_panel.py +++ b/src/aare/gui/panels/smart_rotation_panel.py @@ -278,7 +278,7 @@ class SimpleRotationSettingsPanel(QWidget): def update_total_time_label(self): mins = int(self.total_time_s // 60) - secs = int(round(self.total_time_s % 60)) + secs = round(self.total_time_s % 60) if secs == 60: mins += 1 secs = 0 @@ -349,8 +349,7 @@ class SimpleRotationSettingsPanel(QWidget): self.transmission = 1.0 self.dtz = self._d.diffraction.calc_dtz_mm(d_tar) - if self.dtz < 108: - self.dtz = 108 + self.dtz = max(self.dtz, 108) self.transmission_label.setText(f"{self.transmission * 100:.1f}") self.image_time_label.setText(f"{self.image_time_s:.4f}") @@ -364,7 +363,7 @@ class SimpleRotationSettingsPanel(QWidget): self.dtz_label.setText(f"{self.dtz:.2f}") self.parameters = SimpleScanParameters( - dtz=int(round(self.dtz)), + dtz=round(self.dtz), exp_time_s=self.image_time_s, start_omega_deg=self.start_angle_enter.value, incr_omega_deg=image_angle, diff --git a/src/aare/gui/panels/target_stability_panel.py b/src/aare/gui/panels/target_stability_panel.py index 7bfdf029..c5bea872 100644 --- a/src/aare/gui/panels/target_stability_panel.py +++ b/src/aare/gui/panels/target_stability_panel.py @@ -1402,9 +1402,8 @@ class TargetStabilityPanel(QWidget): @staticmethod def _coerce_target_point(raw) -> tuple[float, float] | None: try: - if isinstance(raw, dict): - if "x" in raw and "y" in raw: - return float(raw["x"]), float(raw["y"]) + if isinstance(raw, dict) and "x" in raw and "y" in raw: + return float(raw["x"]), float(raw["y"]) if isinstance(raw, (list, tuple)) and len(raw) >= 2: return float(raw[0]), float(raw[1]) except Exception as e: diff --git a/src/aare/gui/scan_logic/raster_grid_manager.py b/src/aare/gui/scan_logic/raster_grid_manager.py index 64b292c1..5016fdf8 100644 --- a/src/aare/gui/scan_logic/raster_grid_manager.py +++ b/src/aare/gui/scan_logic/raster_grid_manager.py @@ -1,6 +1,5 @@ import math from enum import Enum -from typing import List, Tuple import numpy as np from aarecommon.config.beamline import cfg_get, get_jfjoch_url, mx_beamline @@ -148,29 +147,20 @@ class RasterGridManager(QObject): ), dtz=cfg_get("daq.data_collection_settings.default_raster_settings.dtz", 200.0), ) - self._completed_grids: List[CompletedRasterGridElem] = [] + self._completed_grids: list[CompletedRasterGridElem] = [] # Cache of pre-rendered heatmap bitmaps, keyed by (id(grid_elem), metric). # Each entry is (QImage, backing ndarray); the ndarray must be kept alive # because QImage shares its buffer without copying. Rebuilt only when the # data or metric changes, not on every repaint (sample move / zoom). - self._heatmap_cache: dict[tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray]] = {} + self._heatmap_cache: dict[tuple[int, RasterGridMetric], tuple[QImage, np.ndarray]] = {} @property def active_grid(self) -> RasterGridRequest: return self._active_grid def _is_grid_visible(self, grid: RasterGridRequest): - if ( - grid.visible - and abs(normalize_angle(grid.omega_deg - self._geom.omega_deg)) < 0.2 - and abs(grid.smargon_top_left.phi_deg - self._geom.smargon.phi_deg) < 0.2 - and abs(grid.smargon_top_left.chi_deg - self._geom.smargon.chi_deg) < 0.2 - and grid.n_x > 0 - and grid.n_y > 0 - ): - return True - return False + return bool(grid.visible and abs(normalize_angle(grid.omega_deg - self._geom.omega_deg)) < 0.2 and abs(grid.smargon_top_left.phi_deg - self._geom.smargon.phi_deg) < 0.2 and abs(grid.smargon_top_left.chi_deg - self._geom.smargon.chi_deg) < 0.2 and grid.n_x > 0 and grid.n_y > 0) def _grid_pixel_geometry( self, grid: RasterGridRequest @@ -210,10 +200,10 @@ class RasterGridManager(QObject): if visible_rect is None or visible_rect.isEmpty(): return 0, grid.n_x, 0, grid.n_y - min_x = max(0, int(math.floor((visible_rect.left() - start_x) / cell_w)) - 1) - max_x = min(grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1) - min_y = max(0, int(math.floor((visible_rect.top() - start_y) / cell_h)) - 1) - max_y = min(grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1) + min_x = max(0, math.floor((visible_rect.left() - start_x) / cell_w) - 1) + max_x = min(grid.n_x, math.ceil((visible_rect.right() - start_x) / cell_w) + 1) + min_y = max(0, math.floor((visible_rect.top() - start_y) / cell_h) - 1) + max_y = min(grid.n_y, math.ceil((visible_rect.bottom() - start_y) / cell_h) + 1) return min_x, max_x, min_y, max_y @@ -300,7 +290,7 @@ class RasterGridManager(QObject): 0, 0, self._active_grid.grid_size_mm.x, self._active_grid.grid_size_mm.y ) - def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> Tuple[int, int]: + def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> tuple[int, int]: point_bl = self._geom.picture_to_sample(Coordinate(x=point.x(), y=point.y())) delta = point_bl - self._geom.smargon_to_beamline(grid.smargon_top_left.sh_mm) @@ -542,7 +532,7 @@ class RasterGridManager(QObject): self._heatmap_cache.clear() def _heatmap_image( - self, cache_key: tuple, grid: RasterGridRequest, values: List[float] | List[int] + self, cache_key: tuple, grid: RasterGridRequest, values: list[float] | list[int] ) -> QImage | None: """Return a cached n_x*n_y heatmap bitmap for this grid, building it once on a cache miss. One pixel per cell; colours baked at full opacity with @@ -560,7 +550,7 @@ class RasterGridManager(QObject): return built[0] def _build_heatmap_image( - self, n_x: int, n_y: int, values: List[float] | List[int] + self, n_x: int, n_y: int, values: list[float] | list[int] ) -> tuple[QImage, np.ndarray] | None: if n_x <= 0 or n_y <= 0: return None @@ -597,7 +587,7 @@ class RasterGridManager(QObject): self, painter: QPainter, elem: CompletedRasterGridElem, - values: List[float] | List[int], + values: list[float] | list[int], alpha: int, visible_rect: QRectF | None, cache_key: tuple, @@ -648,7 +638,7 @@ class RasterGridManager(QObject): self, painter: QPainter, grid: RasterGridRequest, - values: List[float] | List[int] | None = None, + values: list[float] | list[int] | None = None, alpha: int = 127, visible_rect: QRectF | None = None, ): @@ -657,9 +647,8 @@ class RasterGridManager(QObject): if alpha < 0 or alpha > 255: return - if values is None: - if self._draw_active_grid_fast(painter, grid, visible_rect): - return + if values is None and self._draw_active_grid_fast(painter, grid, visible_rect): + return geo = self._grid_pixel_geometry(grid) if geo is None: @@ -759,8 +748,8 @@ class RasterGridManager(QObject): painter.drawRect(bounds) min_spacing_px = 4.0 - stride_x = max(1, int(math.ceil(min_spacing_px / max(cell_w, 1e-9)))) - stride_y = max(1, int(math.ceil(min_spacing_px / max(cell_h, 1e-9)))) + stride_x = max(1, math.ceil(min_spacing_px / max(cell_w, 1e-9))) + stride_y = max(1, math.ceil(min_spacing_px / max(cell_h, 1e-9))) min_ix, max_ix, min_iy, max_iy = self._visible_index_range( grid, visible_rect, start_x, start_y, cell_w, cell_h @@ -793,7 +782,7 @@ class RasterGridManager(QObject): self, painter: QPainter, grid: RasterGridRequest, - values: List[float] | List[int], + values: list[float] | list[int], alpha: int, visible_rect: QRectF | None, ) -> bool: @@ -823,8 +812,8 @@ class RasterGridManager(QObject): ) min_fill_px = 3.0 - stride_x = max(1, int(math.ceil(min_fill_px / max(cell_w, 1e-9)))) - stride_y = max(1, int(math.ceil(min_fill_px / max(cell_h, 1e-9)))) + stride_x = max(1, math.ceil(min_fill_px / max(cell_w, 1e-9))) + stride_y = max(1, math.ceil(min_fill_px / max(cell_h, 1e-9))) painter.save() painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) @@ -855,7 +844,7 @@ class RasterGridManager(QObject): return True def _block_value( - self, values: List[float] | List[int], grid_nx: int, x0: int, x1: int, y0: int, y1: int + self, values: list[float] | list[int], grid_nx: int, x0: int, x1: int, y0: int, y1: int ) -> float | None: best = None for y in range(y0, y1): @@ -967,5 +956,5 @@ class RasterGridManager(QObject): ) self.completed_grid_updated.emit() - def get_completed_grids(self) -> List[CompletedRasterGridElem]: + def get_completed_grids(self) -> list[CompletedRasterGridElem]: return self._completed_grids diff --git a/src/aare/gui/threads/axis_video_thread.py b/src/aare/gui/threads/axis_video_thread.py index 26f868ea..7a27ba8d 100644 --- a/src/aare/gui/threads/axis_video_thread.py +++ b/src/aare/gui/threads/axis_video_thread.py @@ -1,6 +1,6 @@ import cv2 -import requests import numpy as np +import requests from PySide6.QtCore import QThread, Signal from PySide6.QtGui import QImage @@ -76,9 +76,9 @@ class VideoThread(QThread): buffer = buffer[last_boundary:] except requests.exceptions.RequestException as e: - self.error_occurred.emit(f"Connection error: {str(e)}") + self.error_occurred.emit(f"Connection error: {e!s}") except Exception as e: - self.error_occurred.emit(f"Unexpected error: {str(e)}") + self.error_occurred.emit(f"Unexpected error: {e!s}") finally: self.running = False if self.session: @@ -119,7 +119,7 @@ class VideoThread(QThread): rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # Convert to QImage - height, width, channel = rgb_frame.shape + height, width, _channel = rgb_frame.shape bytes_per_line = 3 * width qt_image = QImage( rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888 diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index f08d8213..4683948f 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -352,7 +352,7 @@ class DAQWorker(QObject): self._last_status_request_ts = now request = QNetworkRequest(QUrl(f"{self._base_url}/status")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self.handle_status_response(reply)) @@ -739,7 +739,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/{url}")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) if str: request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -757,7 +757,7 @@ class DAQWorker(QObject): logger.info(f"PUT /{url}: {body}") return request = QNetworkRequest(QUrl(f"{self._base_url}/{url}")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) if str: request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.put(request, QByteArray(body.encode("utf-8"))) @@ -775,7 +775,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/{url}")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.deleteResource(request) reply.finished.connect(lambda: self.handle_req_response(reply)) @@ -874,7 +874,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/state/free_beamline")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = json.dumps({"confirmation_code": confirmation_code}) reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -889,7 +889,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/access/take_over_beamline")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = json.dumps({"confirmation_code": confirmation_code}) reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -904,7 +904,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/recovery/recover_beamline")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = json.dumps({"confirmation_code": confirmation_code}) reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -921,7 +921,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/recovery/unmount_sample")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = json.dumps({"confirmation_code": confirmation_code}) reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -964,7 +964,7 @@ class DAQWorker(QObject): self.staff_pgroups_loaded.emit([]) return request = QNetworkRequest(QUrl(f"{self._base_url}/access/all_pgroups")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.put(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_all_pgroups_response(reply)) @@ -1033,7 +1033,7 @@ class DAQWorker(QObject): self.run_number_incremented.emit() request = QNetworkRequest(QUrl(f"{self._base_url}/scan/rotation")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = r.model_dump_json() reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -1114,7 +1114,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/scan/raster?auto_center=false")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = r.model_dump_json() reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -1156,7 +1156,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/scan/raster?auto_center=true")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = r.model_dump_json() reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -1169,7 +1169,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/sample/spreadsheet")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self.handle_spreadsheet_response(reply)) @@ -1179,7 +1179,7 @@ class DAQWorker(QObject): logger.info("GET /sample/reference_tools") return request = QNetworkRequest(QUrl(f"{self._base_url}/sample/reference_tools")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self.handle_reference_tools_response(reply)) @@ -1314,14 +1314,7 @@ class DAQWorker(QObject): ): return True - if ( - "daq state error" in text - or "must be idle to start measurement" in text - or "must be idle" in text - ): - return True - - return False + return bool("daq state error" in text or "must be idle to start measurement" in text or "must be idle" in text) def handle_auto_scan_response(self, reply, sample_id: int): if reply.error() == QNetworkReply.NetworkError.NoError: @@ -1377,7 +1370,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/scan/auto")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = s.model_dump_json() reply = self._net_manager.post(request, QByteArray(body.encode("utf-8"))) @@ -1425,7 +1418,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/sample/resync")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_sample_resync_response(reply)) @@ -1437,7 +1430,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/resync/detector_metadata")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_detector_metadata_resync_response(reply)) @@ -1499,7 +1492,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/simulation_state")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_local_contact_simulation_state_response(reply)) @@ -1528,7 +1521,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/device_state")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_local_contact_device_state_response(reply)) @@ -1557,7 +1550,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/links")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_local_contact_links_response(reply)) @@ -1589,7 +1582,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/config")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_local_contact_config_response(reply)) @@ -1615,7 +1608,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/config")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") body = QByteArray(json.dumps(payload).encode("utf-8")) reply = self._net_manager.put(request, body) @@ -1657,7 +1650,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/bec/user_macros")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_bec_user_macros_response(reply)) @@ -1680,7 +1673,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/bec/devices")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_bec_devices_response(reply)) @@ -1768,7 +1761,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/alc/ml_bounding_box")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self.handle_ml_box_response(reply)) @@ -2006,7 +1999,7 @@ class DAQWorker(QObject): self._automation_progress_buffer = "" request = QNetworkRequest(QUrl(f"{self._base_url}/sse/automation_progress")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.readyRead.connect(lambda: self._read_automation_progress_stream(reply)) reply.finished.connect(self._restart_automation_progress_stream) @@ -2023,7 +2016,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/sse/face_detection")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.readyRead.connect(lambda: self._read_face_detection_stream(reply)) reply.finished.connect(self._restart_face_detection_stream) @@ -2045,7 +2038,7 @@ class DAQWorker(QObject): request = QNetworkRequest( QUrl(f"{self._base_url}/face_detection/run?steps={steps}&step_size={step_size}") ) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_face_detection_response(reply)) @@ -2071,7 +2064,7 @@ class DAQWorker(QObject): if emit_status: # fetch status and bkg in parallel (simple sequential here) status_req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/status")) - status_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + status_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) status_reply = self._net_manager.get(status_req) status_reply.finished.connect( lambda: self._handle_fluorimeter_status_and_emit(data, status_reply) @@ -2087,7 +2080,7 @@ class DAQWorker(QObject): s_payload = self.handle_response(status_reply) s = int(s_payload) if s_payload not in ("", "null") else -1 b_req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/background")) - b_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + b_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) b_reply = self._net_manager.get(b_req) b_reply.finished.connect(lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply)) except Exception as e: @@ -2110,7 +2103,7 @@ class DAQWorker(QObject): if self._base_url is None: return req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/spectrum")) - req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) req.setRawHeader(b"Content-Type", b"application/json") body = f.model_dump_json() reply = self._net_manager.post(req, QByteArray(body.encode("utf-8"))) @@ -2130,7 +2123,7 @@ class DAQWorker(QObject): if self._base_url is None: return req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/data")) - req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(req) reply.finished.connect(lambda: self._handle_fluorimeter_data(reply, emit_status=True)) @@ -2139,7 +2132,7 @@ class DAQWorker(QObject): if self._base_url is None: return request = QNetworkRequest(QUrl(f"{self._base_url}/sse/fluorimeter")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.readyRead.connect(lambda: self._read_fluorimeter_stream(reply)) reply.finished.connect(lambda: reply.deleteLater()) @@ -2173,7 +2166,7 @@ class DAQWorker(QObject): if isinstance(v, dict): group = str(k) for kk, vv in v.items(): - out[f"{group}.{str(kk)}"] = str(vv) + out[f"{group}.{kk!s}"] = str(vv) else: out[str(k)] = str(v) return out @@ -2191,13 +2184,13 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/meta/error-codes")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_error_codes_response(reply)) def _retry_error_codes_legacy(self) -> None: request = QNetworkRequest(QUrl(f"{self._base_url}/meta/error-codes/flat")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_error_codes_response(reply)) @@ -2255,7 +2248,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/sse/baton")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.readyRead.connect(lambda: self._read_baton_stream(reply)) reply.finished.connect(self._restart_baton_stream) @@ -2312,7 +2305,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/baton/request")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_baton_request_response(reply)) @@ -2353,7 +2346,7 @@ class DAQWorker(QObject): request = QNetworkRequest( QUrl(f"{self._base_url}/baton/respond?accept={str(accept).lower()}") ) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_baton_response_result(reply)) @@ -2390,7 +2383,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/baton/check_timeout")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_baton_timeout_response(reply)) @@ -2453,7 +2446,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/admin/gui_sessions")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.get(request) reply.finished.connect(lambda: self._handle_gui_sessions_response(reply)) @@ -2470,7 +2463,7 @@ class DAQWorker(QObject): f"{self._base_url}/admin/gui_sessions/{session_id}/request_close?grace_seconds={grace_seconds}" ) ) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply)) @@ -2482,7 +2475,7 @@ class DAQWorker(QObject): return request = QNetworkRequest(QUrl(f"{self._base_url}/admin/gui_sessions/{session_id}")) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) reply = self._net_manager.deleteResource(request) reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply)) @@ -2508,7 +2501,7 @@ class DAQWorker(QObject): request = QNetworkRequest( QUrl(f"{self._base_url}/admin/gui_sessions/{session_id}/interaction") ) - request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) request.setRawHeader(b"Content-Type", b"application/json") reply = self._net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: reply.deleteLater()) diff --git a/src/aare/gui/threads/sse_client.py b/src/aare/gui/threads/sse_client.py index 7264b955..63f32295 100644 --- a/src/aare/gui/threads/sse_client.py +++ b/src/aare/gui/threads/sse_client.py @@ -1,6 +1,6 @@ -from typing import Optional, Dict -from PySide6.QtCore import QObject, Signal, Slot, QUrl, QTimer, QByteArray -from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QSslError + +from PySide6.QtCore import QByteArray, QObject, QTimer, QUrl, Signal, Slot +from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest, QSslError class SSEClient(QObject): @@ -15,13 +15,13 @@ class SSEClient(QObject): super().__init__(parent) self._network_manager = QNetworkAccessManager(self) self._network_manager.sslErrors.connect(self._handle_ssl_errors) - self._reply: Optional[QNetworkReply] = None + self._reply: QNetworkReply | None = None self._reconnect_timer = QTimer(self) self._reconnect_timer.setSingleShot(True) self._reconnect_timer.timeout.connect(self._attempt_reconnect) self._url = QUrl() - self._headers: Dict[str, str] = {} + self._headers: dict[str, str] = {} self._buffer = QByteArray() self._connected = False self._reconnect_delay = 1000 # Start with 1 second @@ -32,7 +32,7 @@ class SSEClient(QObject): self._current_data = "" self._current_id = "" - def connect_to_sse(self, url: str, headers: Optional[Dict[str, str]] = None): + def connect_to_sse(self, url: str, headers: dict[str, str] | None = None): """Connect to SSE endpoint""" self._url = QUrl(url) self._headers = headers or {} @@ -155,8 +155,7 @@ class SSEClient(QObject): value = line[colon_index + 1 :] # Remove leading space from value - if value.startswith(" "): - value = value[1:] + value = value.removeprefix(" ") if field == "data": if self._current_data: diff --git a/src/aare/gui/tutorials/controls_help_dialog.py b/src/aare/gui/tutorials/controls_help_dialog.py index 30ef8f59..0883e97a 100644 --- a/src/aare/gui/tutorials/controls_help_dialog.py +++ b/src/aare/gui/tutorials/controls_help_dialog.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Qt -from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QTabWidget, QWidget +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QTabWidget, QTextEdit, QVBoxLayout, QWidget class ControlsHelpDialog(QDialog): diff --git a/src/aare/gui/tutorials/tutorial_manager.py b/src/aare/gui/tutorials/tutorial_manager.py index 005a4dc5..c9fa6ae8 100644 --- a/src/aare/gui/tutorials/tutorial_manager.py +++ b/src/aare/gui/tutorials/tutorial_manager.py @@ -5,10 +5,10 @@ from dataclasses import dataclass from typing import Any from PySide6.QtCore import ( + Property, QEasingCurve, QObject, QPropertyAnimation, - Property, QRect, Qt, QTimer, @@ -460,8 +460,7 @@ class TutorialManager(QObject): self.tutorial_stopped.emit(scenario_id) return - if new_index < 0: - new_index = 0 + new_index = max(new_index, 0) self.runtime_state.active_step_index = new_index step = self.current_scenario.steps[new_index] @@ -688,9 +687,8 @@ class TutorialManager(QObject): step.skippable and self.current_scenario is not None and self.current_scenario.allow_skip - ): - if self.runtime_state is not None: - self._advance_to_index(self.runtime_state.active_step_index + 1) - return + ) and self.runtime_state is not None: + self._advance_to_index(self.runtime_state.active_step_index + 1) + return self.overlay.next_button.setEnabled(True) diff --git a/src/aare/gui/tutorials/tutorial_models.py b/src/aare/gui/tutorials/tutorial_models.py index d0ab205c..fe2bae5c 100644 --- a/src/aare/gui/tutorials/tutorial_models.py +++ b/src/aare/gui/tutorials/tutorial_models.py @@ -51,7 +51,7 @@ class CompletionKind(str, Enum): class CompletionRule: kind: CompletionKind value: Any = None - children: list["CompletionRule"] = field(default_factory=list) + children: list[CompletionRule] = field(default_factory=list) description: TutorialTextRef | None = None @@ -121,7 +121,7 @@ class TutorialScenario: title: TutorialTextRef description: TutorialTextRef mode: TutorialMode - steps: list["TutorialStepDefinition"] + steps: list[TutorialStepDefinition] version: str = "1.0" tags: set[str] = field(default_factory=set) @@ -146,6 +146,6 @@ class TutorialContext: completed_step_ids: list[str] = field(default_factory=list) state: dict[str, Any] = field(default_factory=dict) - event_log: list["TutorialEvent"] = field(default_factory=list) + event_log: list[TutorialEvent] = field(default_factory=list) metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/aare/gui/widgets/automation_progress.py b/src/aare/gui/widgets/automation_progress.py index 80e0663a..4c88baab 100644 --- a/src/aare/gui/widgets/automation_progress.py +++ b/src/aare/gui/widgets/automation_progress.py @@ -112,7 +112,7 @@ class CompactAutomationProgressStrip(QFrame): def _format_duration(seconds: float | None) -> str: if seconds is None or seconds <= 0: return "0m 00s" - total = int(round(seconds)) + total = round(seconds) minutes, secs = divmod(total, 60) if minutes < 60: return f"{minutes}m {secs:02d}s" diff --git a/src/aare/gui/widgets/baton_request_dialog.py b/src/aare/gui/widgets/baton_request_dialog.py index 16043c8b..4ec6ae4d 100644 --- a/src/aare/gui/widgets/baton_request_dialog.py +++ b/src/aare/gui/widgets/baton_request_dialog.py @@ -1,14 +1,14 @@ -from PySide6.QtCore import Qt, Signal, QTimer +from PySide6.QtCore import Qt, QTimer, Signal +from PySide6.QtGui import QFont from PySide6.QtWidgets import ( QDialog, - QVBoxLayout, + QFrame, QHBoxLayout, QLabel, - QPushButton, QProgressBar, - QFrame, + QPushButton, + QVBoxLayout, ) -from PySide6.QtGui import QFont class BatonRequestDialog(QDialog): @@ -317,8 +317,7 @@ class BatonPendingDialog(QDialog): def _tick(self): self._remaining -= 1 - if self._remaining < 0: - self._remaining = 0 + self._remaining = max(self._remaining, 0) self.progress.setValue(self._remaining) self.time_label.setText(f"{self._remaining} seconds remaining") diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 7d94d217..6cae2c24 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -405,12 +405,11 @@ class SampleCameraImageLabel(QGraphicsView): self._state = SampleCameraImageState.RESIZE_RASTER_GRID else: self._state = SampleCameraImageState.DRAWING_RASTER_GRID - elif event.button() == Qt.MouseButton.LeftButton: - if ( - self._raster_mgr.is_part_of_active_grid(self.start_point) - and not ctrl_override_move - ): - self._state = SampleCameraImageState.MOVING_RASTER_GRID + elif event.button() == Qt.MouseButton.LeftButton and ( + self._raster_mgr.is_part_of_active_grid(self.start_point) + and not ctrl_override_move + ): + self._state = SampleCameraImageState.MOVING_RASTER_GRID def _update_grid(self): now = time.monotonic() @@ -613,8 +612,7 @@ class SampleCameraImageLabel(QGraphicsView): ratio_h = self.viewport().size().height() / self.pixmap_item.boundingRect().height() ratio = min(ratio_w, ratio_h) - if ratio < 0.1: - ratio = 0.1 + ratio = max(ratio, 0.1) if ratio >= 1.0: # Don't enable scaling when gain in ratio is < 5% (to avoid back-and-forth) diff --git a/src/aare/gui/widgets/clickable_label.py b/src/aare/gui/widgets/clickable_label.py index 97d34aa8..eb3b2aac 100644 --- a/src/aare/gui/widgets/clickable_label.py +++ b/src/aare/gui/widgets/clickable_label.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import Signal, Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QLabel diff --git a/src/aare/gui/widgets/local_contact_status_widget.py b/src/aare/gui/widgets/local_contact_status_widget.py index e9bae8db..c8253b2d 100644 --- a/src/aare/gui/widgets/local_contact_status_widget.py +++ b/src/aare/gui/widgets/local_contact_status_widget.py @@ -351,7 +351,7 @@ class LocalContactStatusWidget(QFrame): def _refresh(self) -> None: if self._last_status is None: - for _key, (_title, value) in self._row_widgets.items(): + for (_title, value) in self._row_widgets.values(): value.setText(self._badge("WAITING", tone="neutral")) return diff --git a/src/aare/gui/widgets/number_line_edit.py b/src/aare/gui/widgets/number_line_edit.py index fd79f088..168d68b4 100644 --- a/src/aare/gui/widgets/number_line_edit.py +++ b/src/aare/gui/widgets/number_line_edit.py @@ -1,6 +1,6 @@ -from PySide6.QtCore import Signal, Slot, Qt +from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QDoubleValidator -from PySide6.QtWidgets import QLineEdit, QWidget, QCheckBox, QHBoxLayout +from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QLineEdit, QWidget class NumberLineEdit(QLineEdit): @@ -23,7 +23,7 @@ class NumberLineEdit(QLineEdit): self.setValidator(self.validator) self.setAlignment(Qt.AlignmentFlag.AlignRight) self.setToolTip( - "Minimum: {:s}\nMaximum: {:s}".format(self.to_string(min_val), self.to_string(max_val)) + f"Minimum: {self.to_string(min_val):s}\nMaximum: {self.to_string(max_val):s}" ) # Connect the textChanged signal to a custom slot to check validity @@ -75,7 +75,7 @@ class NumberLineEdit(QLineEdit): def update_limits(self, min_val: float, max_val: float): self.validator.setRange(min_val, max_val, self.decimal_count) self.setToolTip( - "Minimum: {:s}\nMaximum: {:s}".format(self.to_string(min_val), self.to_string(max_val)) + f"Minimum: {self.to_string(min_val):s}\nMaximum: {self.to_string(max_val):s}" ) def validate(self, text) -> bool: diff --git a/src/aare/gui/widgets/pgroup_dialog.py b/src/aare/gui/widgets/pgroup_dialog.py index 11e9b0ea..6d467abf 100644 --- a/src/aare/gui/widgets/pgroup_dialog.py +++ b/src/aare/gui/widgets/pgroup_dialog.py @@ -1,12 +1,12 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QDialog, - QVBoxLayout, - QPushButton, - QLabel, QComboBox, QCompleter, + QDialog, + QLabel, QMessageBox, + QPushButton, + QVBoxLayout, ) diff --git a/src/aare/gui/widgets/splash_screen.py b/src/aare/gui/widgets/splash_screen.py index 2c7da0d4..91761223 100644 --- a/src/aare/gui/widgets/splash_screen.py +++ b/src/aare/gui/widgets/splash_screen.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Qt -from PySide6.QtWidgets import QSplashScreen, QProgressBar, QApplication +from PySide6.QtWidgets import QApplication, QProgressBar, QSplashScreen class LoadingSplashScreen(QSplashScreen): diff --git a/src/aare/gui/widgets/status_bar.py b/src/aare/gui/widgets/status_bar.py index 34506549..b182b531 100644 --- a/src/aare/gui/widgets/status_bar.py +++ b/src/aare/gui/widgets/status_bar.py @@ -538,7 +538,6 @@ class StatusBar(QStatusBar): self.staff_pgroups_loaded.disconnect(_on_loaded) except Exception as e: logger.debug(f"Error disconnecting: {e}") - pass self.staff_pgroups_loaded.connect(_on_loaded) self._list_staff_pgroups() @@ -586,7 +585,6 @@ class StatusBar(QStatusBar): def _list_staff_pgroups(self): self.get_all_pgroups.emit() - return def _generate_pgroup_dialogue(self, curr: str | None = None, pgroups: list | None = None): logger.info(pgroups) diff --git a/src/aare/gui/widgets/video_image.py b/src/aare/gui/widgets/video_image.py index bd03c132..78d9134d 100644 --- a/src/aare/gui/widgets/video_image.py +++ b/src/aare/gui/widgets/video_image.py @@ -1,6 +1,6 @@ -from PySide6.QtCore import Qt, Slot, QRectF -from PySide6.QtGui import QPainter, QPixmap, QImage, QFont, QColor, QPen, QFontMetrics -from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem +from PySide6.QtCore import QRectF, Qt, Slot +from PySide6.QtGui import QColor, QFont, QFontMetrics, QImage, QPainter, QPen, QPixmap +from PySide6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsView from aare.gui.widgets.busy_overlay import BusyOverlayStyle diff --git a/tests/conftest.py b/tests/conftest.py index c73d1cbc..b43e2a01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -50,7 +50,7 @@ def sample_info(): # ------------------------- @pytest.fixture(scope="session") def server_module(): - import aare.daq.server as server + from aare.daq import server return server diff --git a/tests/integration/daq/test_daq_server.py b/tests/integration/daq/test_daq_server.py index c9f26964..0fc50dca 100644 --- a/tests/integration/daq/test_daq_server.py +++ b/tests/integration/daq/test_daq_server.py @@ -1,5 +1,6 @@ -import pytest import os + +import pytest from fastapi.testclient import TestClient # We need to set the environment variable before importing the app diff --git a/tests/unit/daq/operations/face_detection/test_face_detection_logic.py b/tests/unit/daq/operations/face_detection/test_face_detection_logic.py index 4f430cfb..ca58fd43 100644 --- a/tests/unit/daq/operations/face_detection/test_face_detection_logic.py +++ b/tests/unit/daq/operations/face_detection/test_face_detection_logic.py @@ -1,17 +1,18 @@ import numpy as np import pytest + from aare.daq.operations.face_detection.utils import ( - box_height_from_tuple, box_area_from_tuple, - prepare_samples, - cos_model, - mad_filter, - fit_metrics, - fit_cosine, - get_samples_out, + box_height_from_tuple, choose_best_fit, - get_flat_face, chose_best_angle, + cos_model, + fit_cosine, + fit_metrics, + get_flat_face, + get_samples_out, + mad_filter, + prepare_samples, ) @@ -101,7 +102,7 @@ def test_choose_best_fit(): "Height": {"angle": 45, "params": {"rmse": 0.1, "mae": 0.1, "r2": 0.95}}, "Area": {"angle": 50, "params": {"rmse": 0.05, "mae": 0.05, "r2": 0.98}}, } - angle, fit, name = choose_best_fit(fits) + angle, _fit, name = choose_best_fit(fits) assert name == "Area" assert angle == 50.0 diff --git a/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py b/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py index 2dc7e809..8059a644 100644 --- a/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py +++ b/tests/unit/daq/operations/loop_centering/test_loop_centering_service.py @@ -88,7 +88,7 @@ def test_service_succeeds_when_correction_pass_has_valid_target(monkeypatch, con return AngleAnalysis( angle_deg=kwargs["angle"], classes=[MLBoxType.CRYSTAL.value] if kwargs["angle"] == 0 else [], - has_valid_target=True if kwargs["angle"] == 0 else False, + has_valid_target=kwargs["angle"] == 0, ignore_only=False, ) return AngleAnalysis( diff --git a/tests/unit/daq/test_autofocus.py b/tests/unit/daq/test_autofocus.py index 77e398c0..ce108fbd 100644 --- a/tests/unit/daq/test_autofocus.py +++ b/tests/unit/daq/test_autofocus.py @@ -1,5 +1,6 @@ -import numpy as np import cv2 +import numpy as np + from aare.daq.autofocus import calculate_focus_measure diff --git a/tests/unit/daq/test_beamcenterfit.py b/tests/unit/daq/test_beamcenterfit.py index c290f09e..8155dcb9 100644 --- a/tests/unit/daq/test_beamcenterfit.py +++ b/tests/unit/daq/test_beamcenterfit.py @@ -1,6 +1,7 @@ -import pytest import numpy as np -from aare.daq.beamcenterfit import beamcenter_fit, Gaussian2Dfit +import pytest + +from aare.daq.beamcenterfit import Gaussian2Dfit, beamcenter_fit def create_synthetic_beam_image( @@ -50,7 +51,6 @@ def test_beamcenter_fit_no_converge(): # but we want to test the failure path. # To truly force non-convergence we might need a more extreme case, # but return None is better than exit() anyway. - pass def test_beamcenter_fit_no_contours(): diff --git a/tests/unit/daq/test_face_detection.py b/tests/unit/daq/test_face_detection.py index e1075e09..887beb29 100644 --- a/tests/unit/daq/test_face_detection.py +++ b/tests/unit/daq/test_face_detection.py @@ -77,21 +77,9 @@ def test_public_face_detection_uses_execute_face_detection(monkeypatch): daq = object.__new__(AareDAQ) cfg = types.SimpleNamespace(try_set_busy=lambda timeout=360: None, state_busy=False) - setattr(daq, "_cfg", cfg) + daq._cfg = cfg - setattr( - daq, - "_execute_face_detection", - lambda **kwargs: FaceDetectionResult( - success=True, - payload={ - "running": False, - "samples": [{"angle": 45}], - "height_fit": {}, - "area_fit": {}, - }, - ), - ) + daq._execute_face_detection = lambda **kwargs: FaceDetectionResult(success=True, payload={"running": False, "samples": [{"angle": 45}], "height_fit": {}, "area_fit": {}}) result = daq.face_detection(steps=7, step_size=30) diff --git a/tests/unit/daq/test_mlbox.py b/tests/unit/daq/test_mlbox.py index 98ef9a9e..650e8c09 100644 --- a/tests/unit/daq/test_mlbox.py +++ b/tests/unit/daq/test_mlbox.py @@ -70,7 +70,7 @@ def test_filter_predictions(mlbox): mlbox._filter_predictions(out, confidence_min=0.5) assert len(out.boxes) == 1 - assert list(out.boxes.values())[0].conf == 0.8 + assert next(iter(out.boxes.values())).conf == 0.8 @patch("cv2.imdecode") diff --git a/tests/unit/daq/test_mount.py b/tests/unit/daq/test_mount.py index 808272fd..2abdc2f1 100644 --- a/tests/unit/daq/test_mount.py +++ b/tests/unit/daq/test_mount.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock @@ -61,13 +61,13 @@ def test_was_previous_sample_unmounted_since_prefers_tell_phase_confirmation(): previous_sample = _make_sample(1, "old") daq = _make_daq(previous_sample) - started_at = datetime.now(timezone.utc) - timedelta(seconds=5) + started_at = datetime.now(UTC) - timedelta(seconds=5) daq._safe_tell_state = MagicMock( return_value=TellStateModel( activity=TellActivityEnum.MOUNTING, operation="mount", phase=TellPhaseEnum.PICKING_NEW_SAMPLE, - last_update_ts=datetime.now(timezone.utc).isoformat(), + last_update_ts=datetime.now(UTC).isoformat(), last_event_class="Motion Sync", last_event_value="Sample get on Puck", ) diff --git a/tests/unit/daq/test_server.py b/tests/unit/daq/test_server.py index 7a535775..c1343cad 100644 --- a/tests/unit/daq/test_server.py +++ b/tests/unit/daq/test_server.py @@ -2,7 +2,6 @@ import os from types import SimpleNamespace from unittest.mock import patch - os.environ["JWT_AAREDAQ_KEY"] = "test_key_for_unit_testing" diff --git a/tests/unit/daq/test_spreadsheetupdater.py b/tests/unit/daq/test_spreadsheetupdater.py index 2255bb87..56ce46d9 100644 --- a/tests/unit/daq/test_spreadsheetupdater.py +++ b/tests/unit/daq/test_spreadsheetupdater.py @@ -25,9 +25,8 @@ def test_get_ws_headers_success(): def test_get_ws_headers_fail(): - with patch("os.getenv", return_value=None): - with pytest.raises(ValueError): - get_ws_headers() + with patch("os.getenv", return_value=None), pytest.raises(ValueError): + get_ws_headers() def test_set_spreadsheet_in_redis(mock_config): diff --git a/tests/unit/devices/test_enum_pv.py b/tests/unit/devices/test_enum_pv.py index 6ae6cd4d..89658a5e 100644 --- a/tests/unit/devices/test_enum_pv.py +++ b/tests/unit/devices/test_enum_pv.py @@ -1,6 +1,8 @@ -import pytest -from unittest.mock import patch from enum import Enum +from unittest.mock import patch + +import pytest + from aare.devices.enum_pv import EnumPV @@ -29,14 +31,14 @@ def mock_pvs(): def test_enum_pv_init_fail(mock_pvs): - set_pv, get_pv = mock_pvs + set_pv, _get_pv = mock_pvs set_pv.enum_strs = None with pytest.raises(RuntimeError): EnumPV("test", "SET", "GET") def test_enum_pv_init_success(mock_pvs): - set_pv, get_pv = mock_pvs + set_pv, _get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") assert epv.name == "test" @@ -51,7 +53,7 @@ def test_enum_pv_position(mock_pvs): def test_enum_pv_resolve_enum(mock_pvs): - set_pv, get_pv = mock_pvs + set_pv, _get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") @@ -63,7 +65,7 @@ def test_enum_pv_resolve_enum(mock_pvs): def test_enum_pv_resolve_int(mock_pvs): - set_pv, get_pv = mock_pvs + set_pv, _get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") @@ -75,7 +77,7 @@ def test_enum_pv_resolve_int(mock_pvs): def test_enum_pv_resolve_str(mock_pvs): - set_pv, get_pv = mock_pvs + set_pv, _get_pv = mock_pvs set_pv.enum_strs = (" State1 ", "State2") epv = EnumPV("test", "SET", "GET") @@ -87,7 +89,7 @@ def test_enum_pv_resolve_str(mock_pvs): def test_enum_pv_resolve_invalid_type(mock_pvs): - set_pv, get_pv = mock_pvs + set_pv, _get_pv = mock_pvs set_pv.enum_strs = ("State1", "State2") epv = EnumPV("test", "SET", "GET") with pytest.raises(TypeError): diff --git a/tests/unit/devices/test_mx_lib.py b/tests/unit/devices/test_mx_lib.py index e7158f1c..4baeb224 100644 --- a/tests/unit/devices/test_mx_lib.py +++ b/tests/unit/devices/test_mx_lib.py @@ -1,7 +1,9 @@ -import pytest from unittest.mock import MagicMock, patch -from aare.devices.mx_lib import wait_for_movement_to_finish, pv_wait, is_epics_type, clean_filename -from epics import Motor, PV + +import pytest +from epics import PV, Motor + +from aare.devices.mx_lib import clean_filename, is_epics_type, pv_wait, wait_for_movement_to_finish def test_clean_filename(): @@ -47,9 +49,8 @@ def test_wait_for_movement_to_finish_timeout(mock_poll): mock_motor.units = "mm" mock_motor._prefix = "MOT1:" - with patch("time.time", side_effect=[0, 0, 100, 101]): - with pytest.raises(TimeoutError): - wait_for_movement_to_finish(mock_motor) + with patch("time.time", side_effect=[0, 0, 100, 101]), pytest.raises(TimeoutError): + wait_for_movement_to_finish(mock_motor) @patch("aare.devices.mx_lib.wait_motor_position") diff --git a/tests/unit/devices/test_my_motor.py b/tests/unit/devices/test_my_motor.py index eefd4cad..c6cb1258 100644 --- a/tests/unit/devices/test_my_motor.py +++ b/tests/unit/devices/test_my_motor.py @@ -34,7 +34,7 @@ def test_my_motor_init(mock_motor_base): def test_my_motor_speed(mock_motor_base): - _, mock_get, mock_put, _ = mock_motor_base + _, mock_get, _mock_put, _ = mock_motor_base m = MyMotor("MTR") with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: mock_name.return_value = "MTR" @@ -94,7 +94,7 @@ def test_my_motor_units(mock_motor_base): def test_my_motor_limits(mock_motor_base): - _, mock_get, mock_put, _ = mock_motor_base + _, mock_get, _mock_put, _ = mock_motor_base m = MyMotor("MTR") with patch.object(MyMotor, "name", create=True, new_callable=PropertyMock) as mock_name: mock_name.return_value = "MTR" diff --git a/tests/unit/devices/test_workflow_tools.py b/tests/unit/devices/test_workflow_tools.py index d25419a0..c8136951 100644 --- a/tests/unit/devices/test_workflow_tools.py +++ b/tests/unit/devices/test_workflow_tools.py @@ -1,4 +1,5 @@ import pytest + from aare.devices.workflow_tools import wait_position diff --git a/tests/unit/gui/test_auth_mock.py b/tests/unit/gui/test_auth_mock.py index 8685fa43..836ef743 100644 --- a/tests/unit/gui/test_auth_mock.py +++ b/tests/unit/gui/test_auth_mock.py @@ -15,7 +15,7 @@ def test_auth_success(mocker): assert token == "fake_token_abc.123.xyz" mock_run.assert_called_once() - args, kwargs = mock_run.call_args + args, _kwargs = mock_run.call_args assert "curl" in args[0] assert "--cacert" in args[0] assert "/tmp/test-cert.pem" in args[0] diff --git a/tests/unit/gui/test_axis_video_thread.py b/tests/unit/gui/test_axis_video_thread.py index 828fb594..44ec9270 100644 --- a/tests/unit/gui/test_axis_video_thread.py +++ b/tests/unit/gui/test_axis_video_thread.py @@ -1,8 +1,10 @@ -import pytest -import numpy as np -import cv2 from unittest.mock import MagicMock, patch + +import cv2 +import numpy as np +import pytest from PySide6.QtGui import QImage + from aare.gui.threads.axis_video_thread import VideoThread diff --git a/tests/unit/gui/test_login.py b/tests/unit/gui/test_login.py index 0b761a1f..12f2d50c 100644 --- a/tests/unit/gui/test_login.py +++ b/tests/unit/gui/test_login.py @@ -1,8 +1,10 @@ +from unittest.mock import MagicMock, patch + +import jwt import pytest from PySide6.QtCore import Qt + from aare.gui.widgets.login import LoginDialog -from unittest.mock import MagicMock, patch -import jwt @pytest.fixture diff --git a/tests/unit/gui/test_message_box.py b/tests/unit/gui/test_message_box.py index 8f2464fd..5ed7923f 100644 --- a/tests/unit/gui/test_message_box.py +++ b/tests/unit/gui/test_message_box.py @@ -1,13 +1,15 @@ from unittest.mock import MagicMock, patch -from PySide6.QtWidgets import QMessageBox -from aare.gui.widgets.message_box import ( - reply_box, - timer_box, - ring_current_low_check, - experiment_hutch_shutter_check, - ring_current_auto_check, -) + import pytest +from PySide6.QtWidgets import QMessageBox + +from aare.gui.widgets.message_box import ( + experiment_hutch_shutter_check, + reply_box, + ring_current_auto_check, + ring_current_low_check, + timer_box, +) def test_reply_box(qtbot): diff --git a/tests/unit/gui/test_sse_client.py b/tests/unit/gui/test_sse_client.py index b1e20f1d..866c208d 100644 --- a/tests/unit/gui/test_sse_client.py +++ b/tests/unit/gui/test_sse_client.py @@ -1,8 +1,10 @@ +from unittest.mock import MagicMock + import pytest from PySide6.QtCore import QByteArray from PySide6.QtNetwork import QNetworkReply + from aare.gui.threads.sse_client import SSEClient -from unittest.mock import MagicMock @pytest.fixture diff --git a/tests/unit/gui/test_tutorials.py b/tests/unit/gui/test_tutorials.py index 2955477e..3f049cfb 100644 --- a/tests/unit/gui/test_tutorials.py +++ b/tests/unit/gui/test_tutorials.py @@ -1,19 +1,20 @@ import pytest from PySide6.QtWidgets import QWidget + from aare.gui.tutorials.tutorial_manager import TutorialManager from aare.gui.tutorials.tutorial_models import ( + StepKind, + TargetKind, + TutorialMode, TutorialScenario, TutorialStepDefinition, - TutorialMode, - StepKind, - TutorialTextRef, TutorialTarget, - TargetKind, + TutorialTextRef, ) from aare.gui.tutorials.tutorial_runtime import ( DictionaryTextResolver, - NoOpActionExecutor, DictTargetResolver, + NoOpActionExecutor, TutorialEventBus, ) diff --git a/tests/unit/utils/test_beamline_dispatch.py b/tests/unit/utils/test_beamline_dispatch.py index ff29f700..8feadae6 100644 --- a/tests/unit/utils/test_beamline_dispatch.py +++ b/tests/unit/utils/test_beamline_dispatch.py @@ -9,9 +9,8 @@ from aare.beamline_dispatch.x10sa import X10saDispatch def test_dispatch_cannot_be_instantiated_without_env(): - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(ValueError) as e: - _ = get_beamline_dispatch() + with patch.dict("os.environ", {}, clear=True), pytest.raises(ValueError) as e: + _ = get_beamline_dispatch() assert e.match("Please set the BEAMLINE environment variable") @@ -23,9 +22,8 @@ def test_simulated_dispatch_can_be_instantiated(): @pytest.mark.skipif(importlib.util.find_spec("pxi_bec") is None, reason="run only for pxi flavour") def test_pxi_dispatch_can_be_instantiated(): - with patch.dict("os.environ", {"BEAMLINE": "X06SA"}): - with pytest.raises(TypeError) as e: - _ = get_beamline_dispatch() + with patch.dict("os.environ", {"BEAMLINE": "X06SA"}), pytest.raises(TypeError) as e: + _ = get_beamline_dispatch() assert e.match("Can't instantiate abstract class") @@ -42,7 +40,6 @@ def test_pxii_dispatch_can_be_instantiated(): importlib.util.find_spec("pxiii_bec") is None, reason="run only for pxiii flavour" ) def test_pxiii_dispatch_can_be_instantiated(): - with patch.dict("os.environ", {"BEAMLINE": "X06DA"}): - with pytest.raises(TypeError) as e: - _ = get_beamline_dispatch() + with patch.dict("os.environ", {"BEAMLINE": "X06DA"}), pytest.raises(TypeError) as e: + _ = get_beamline_dispatch() assert e.match("Can't instantiate abstract class") -- 2.54.0 From 0135129c89c377084cc87ee9704e21b67a94265a Mon Sep 17 00:00:00 2001 From: David Perl Date: Mon, 3 Aug 2026 09:24:32 +0200 Subject: [PATCH 3/6] fix: as many ruff errors as possible, some type --- pyproject.toml | 8 ++ scripts/camera_stat_thread.py | 28 ++-- scripts/gui_desginer.py | 3 +- scripts/pshell_client_old.py | 8 +- .../default/beamline_dispatch.py | 2 +- src/aare/daq/aaredb.py | 18 +-- src/aare/daq/auth.py | 2 + src/aare/daq/config.py | 33 ++--- src/aare/daq/daq.py | 84 ++++++------ src/aare/daq/devices.py | 4 +- src/aare/daq/mlbox.py | 38 ++++-- .../daq/operations/face_detection/service.py | 2 +- .../daq/operations/face_detection/utils.py | 8 +- .../daq/operations/loop_centering/service.py | 5 +- src/aare/daq/operations/mounting/service.py | 14 +- src/aare/daq/operations/raster/service.py | 10 +- src/aare/daq/server.py | 27 ++-- src/aare/daq/spreadsheetupdater.py | 13 +- src/aare/daq/tell_state_machine.py | 33 +++-- src/aare/daq/workflows.py | 4 +- src/aare/devices/aerotech.py | 9 +- src/aare/devices/area_detector.py | 10 +- src/aare/devices/bec_worker.py | 102 +++++++-------- src/aare/devices/enum_pv.py | 0 src/aare/devices/fluorimeter.py | 8 +- src/aare/devices/jfjoch.py | 41 +++--- src/aare/devices/mx_lib.py | 8 +- src/aare/devices/set_get_pv.py | 12 +- src/aare/devices/smargon.py | 10 +- src/aare/devices/tell_backend.py | 2 +- src/aare/devices/tell_client.py | 9 +- src/aare/devices/workflow_tools.py | 4 +- src/aare/devices/zmq_client.py | 12 +- src/aare/gui/about.py | 7 + src/aare/gui/auth.py | 1 + src/aare/gui/gui.py | 11 +- src/aare/gui/log.py | 2 +- src/aare/gui/main_window.py | 80 ++++++------ src/aare/gui/models/gui_state_manager.py | 13 +- src/aare/gui/models/sample_queue_model.py | 9 +- src/aare/gui/models/user_sample_model.py | 6 + .../gui/panels/beamline_recovery_panel.py | 6 + src/aare/gui/panels/developer_help_dialog.py | 7 +- src/aare/gui/panels/face_detection_panel.py | 1 + src/aare/gui/panels/fluorescence_panel.py | 17 +-- src/aare/gui/panels/local_contact_panel.py | 3 +- .../gui/panels/{LogPanel.py => log_panel.py} | 4 +- src/aare/gui/panels/portrait_mode.py | 6 +- .../gui/panels/prediction_metrics_panel.py | 9 +- src/aare/gui/panels/reference_tools_panel.py | 5 +- src/aare/gui/panels/sample_queue_panel.py | 4 +- src/aare/gui/panels/smargon_trace_panel.py | 6 + src/aare/gui/panels/target_stability_panel.py | 2 +- src/aare/gui/panels/tell_sample_panel.py | 1 + .../gui/scan_logic/raster_grid_manager.py | 26 +++- src/aare/gui/threads/axis_video_thread.py | 15 ++- src/aare/gui/threads/camera_thread.py | 9 ++ src/aare/gui/threads/daq_worker.py | 121 ++++++++++-------- src/aare/gui/threads/jfjoch_viewer.py | 9 +- src/aare/gui/threads/prediction_subscriber.py | 9 +- src/aare/gui/threads/sse_client.py | 3 +- src/aare/gui/tutorials/tutorial_manager.py | 18 ++- src/aare/gui/tutorials/tutorial_runtime.py | 5 + src/aare/gui/widgets/camera_image.py | 41 +++--- .../widgets/local_contact_status_widget.py | 10 +- src/aare/gui/widgets/message_box.py | 4 +- src/aare/gui/widgets/status_bar.py | 10 +- tests/conftest.py | 4 +- tests/unit/daq/test_aaredb.py | 16 ++- tests/unit/daq/test_auth.py | 8 +- tests/unit/daq/test_face_detection.py | 5 +- tests/unit/daq/test_tellupdater.py | 12 +- 72 files changed, 647 insertions(+), 449 deletions(-) mode change 100755 => 100644 src/aare/devices/enum_pv.py mode change 100755 => 100644 src/aare/devices/workflow_tools.py mode change 100755 => 100644 src/aare/devices/zmq_client.py rename src/aare/gui/panels/{LogPanel.py => log_panel.py} (98%) diff --git a/pyproject.toml b/pyproject.toml index dd66e399..225e4ed7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,5 +86,13 @@ multi_line_output = 3 [tool.ruff] line-length = 100 +[tool.ruff.lint] +ignore = [ + # Timestamps here are local beamline wall-clock and are stored/displayed as + # naive ISO strings; adding a tzinfo would change what aaredb and the GUI see. + "DTZ005", + "DTZ006", +] + [tool.ruff.format] skip-magic-trailing-comma = true diff --git a/scripts/camera_stat_thread.py b/scripts/camera_stat_thread.py index 0120b739..2d22e451 100644 --- a/scripts/camera_stat_thread.py +++ b/scripts/camera_stat_thread.py @@ -6,6 +6,9 @@ from typing import Any import cv2 import numpy as np import zmq +from aarecommon.config.logger import setup_logger + +logger = setup_logger("aareDAQ") class ImageStatsReceiver: @@ -229,8 +232,8 @@ class ImageStatsReceiver: except zmq.Again: return None - except Exception as e: - print(f"Error processing ZMQ message: {e}") + except Exception: + logger.exception("Error processing ZMQ message") return None def receiver_thread(self): @@ -343,10 +346,13 @@ class ImageStatsReceiver: f.write(f"# Center: {radial['center']}\n") f.write("# Radius(pixels)\tMean_Intensity\tStd_Intensity\tPixel_Count\n") - f.writelines(f"{radial['r_centers'][i]:.2f}\t" - f"{radial['radial_profile'][i]:.2f}\t" - f"{radial['radial_std'][i]:.2f}\t" - f"{radial['pixel_counts'][i]}\n" for i in range(len(radial["r_centers"]))) + f.writelines( + f"{radial['r_centers'][i]:.2f}\t" + f"{radial['radial_profile'][i]:.2f}\t" + f"{radial['radial_std'][i]:.2f}\t" + f"{radial['pixel_counts'][i]}\n" + for i in range(len(radial["r_centers"])) + ) print(f"Radial profile saved to {filename}") return filename @@ -389,8 +395,8 @@ def start_image_stats_receiver(zmq_url: str = "tcp://localhost:5555"): stats_receiver = ImageStatsReceiver(zmq_url) stats_receiver.start() print(f"Started image statistics monitoring on {zmq_url}") - except Exception as e: - print(f"Failed to start image stats receiver: {e}") + except Exception: + logger.exception("Failed to start image stats receiver") stats_receiver = None @@ -405,8 +411,6 @@ def stop_image_stats_receiver(): def get_latest_image_stats(): """Get the latest image statistics""" - global stats_receiver - if stats_receiver and stats_receiver.latest_stats: with stats_receiver.stats_lock: return stats_receiver.latest_stats.copy() @@ -415,8 +419,6 @@ def get_latest_image_stats(): def get_radial_profile(): """Get just the radial profile data""" - global stats_receiver - if stats_receiver: return stats_receiver.get_radial_profile_summary() return None @@ -424,8 +426,6 @@ def get_radial_profile(): def save_current_radial_profile(filename: str | None = None): """Save the current radial profile to a file""" - global stats_receiver - if stats_receiver: return stats_receiver.save_radial_profile_to_file(filename) return None diff --git a/scripts/gui_desginer.py b/scripts/gui_desginer.py index 3e606353..ba48ca9a 100644 --- a/scripts/gui_desginer.py +++ b/scripts/gui_desginer.py @@ -1,5 +1,6 @@ import math import sys +from typing import ClassVar from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPen @@ -114,7 +115,7 @@ class OrbitWidget(QWidget): # LED step indicator # --------------------------------------------------------------------------- class LEDStages(QWidget): - STEPS = ["Mount", "Centre", "Raster", "Collect"] + STEPS: ClassVar[list[str]] = ["Mount", "Centre", "Raster", "Collect"] def __init__(self, active_step: int = 1, parent=None): """ diff --git a/scripts/pshell_client_old.py b/scripts/pshell_client_old.py index 50ee1614..22c3116b 100644 --- a/scripts/pshell_client_old.py +++ b/scripts/pshell_client_old.py @@ -13,12 +13,12 @@ class PShellClient: def _get_response(self, response, is_json=True): if response.status_code != 200: - raise Exception(response.text) + raise RuntimeError(response.text) return json.loads(response.text) if is_json else response.text def _get_binary_response(self, response): if response.status_code != 200: - raise Exception(response.text) + raise RuntimeError(response.text) return response.raw.read() def get_version(self): @@ -292,8 +292,8 @@ class PShellClient: def print_logs(self): for log_line in self.get_logs(): - print("%s %s %-20s %-8s %s" % tuple(log_line)) + print("{} {} {:<20} {:<8} {}".format(*log_line)) def print_devices(self): for log_line in self.get_devices(): - print("%-16s %-32s %-10s %-32s %s" % tuple(log_line)) + print("{:<16} {:<32} {:<10} {:<32} {}".format(*log_line)) diff --git a/src/aare/beamline_dispatch/default/beamline_dispatch.py b/src/aare/beamline_dispatch/default/beamline_dispatch.py index 64daae3a..c432da92 100644 --- a/src/aare/beamline_dispatch/default/beamline_dispatch.py +++ b/src/aare/beamline_dispatch/default/beamline_dispatch.py @@ -6,7 +6,7 @@ from aare.beamline_dispatch.protocols import AuthDispatch, BeamlineDispatch class DefaultAuthDispatch(AuthDispatch): def get_jwt_key(self) -> str: if (key := os.environ.get("JWT_AAREDAQ_KEY")) is None: - raise Exception( + raise RuntimeError( "JWT_AAREDAQ_KEY environment variable not set, cannot guarantee safe authentication." ) return key diff --git a/src/aare/daq/aaredb.py b/src/aare/daq/aaredb.py index fd119bb6..70f14953 100644 --- a/src/aare/daq/aaredb.py +++ b/src/aare/daq/aaredb.py @@ -97,8 +97,8 @@ class AareWrapper: try: s.db_id = self._sample_api.insert_sample(manual_sample).id - except Exception as e: - logger.error(f"Error inserting sample: {e}") + except Exception: + logger.exception("Error inserting sample") @log_timing(logger, "AareDB call") def send_sample_event( @@ -117,8 +117,8 @@ class AareWrapper: sample_id=sample_id, sample_event_create=SampleEventCreate(event_type=event_type, comment=comment), ) - except Exception as e: - logger.error(f"Error sending sample event {event_type!s} to db: {e}") + except Exception: + logger.exception(f"Error sending sample event {event_type!s} to db") @log_timing(logger, "AareDB call") def upload_image( @@ -232,8 +232,8 @@ class AareWrapper: sample_id=s.db_id, experiment_parameters_create=experiment_params_payload ) # logger.debug("Experiment parameters created:", response) - except Exception as e: - logger.error(e) + except Exception: + logger.exception("Error creating experiment parameters for rotation run") @log_timing(logger, "AareDB call") def create_gridscan_run( @@ -295,7 +295,7 @@ class AareWrapper: ) # logger.info("Experiment parameters created:", response) except Exception as e: - logger.debug(e) + logger.debug(e, exc_info=True) @log_timing(logger, "AareDB call") def ingest_gridscan( @@ -376,7 +376,9 @@ class AareWrapper: for img in raster_result.images ] except Exception as e: - logger.warning(f"raster score computation failed, sending null score: {e}") + logger.warning( + f"raster score computation failed, sending null score: {e}", exc_info=True + ) score = None payload = RasterPayloadModel( diff --git a/src/aare/daq/auth.py b/src/aare/daq/auth.py index 92c61938..f7ca9696 100644 --- a/src/aare/daq/auth.py +++ b/src/aare/daq/auth.py @@ -135,6 +135,7 @@ def check_jwt_rw(cfg: BeamlineConfig, data: TokenData) -> None: cfg.try_extend_active_session(data.session, SESSION_EXPIRE_SECONDS) except Exception: # In case something is wrong but you are holder (maybe redis expiry?) + logger.debug("Could not extend the active session; claiming it instead", exc_info=True) cfg.try_set_active_session(data.session, SESSION_EXPIRE_SECONDS) @@ -158,6 +159,7 @@ def check_jwt_staff(cfg: BeamlineConfig, data: TokenData) -> None: try: cfg.try_extend_active_session(data.session, SESSION_EXPIRE_SECONDS) except Exception: + logger.debug("Could not extend the active session; claiming it instead", exc_info=True) cfg.try_set_active_session(data.session, SESSION_EXPIRE_SECONDS) diff --git a/src/aare/daq/config.py b/src/aare/daq/config.py index 24ca2c9b..421c1c91 100644 --- a/src/aare/daq/config.py +++ b/src/aare/daq/config.py @@ -147,6 +147,7 @@ class BeamlineConfig: try: ttl = int(self._client.ttl(self._gui_session_key(session))) except Exception: + logger.debug("Could not read the GUI session TTL", exc_info=True) return None if ttl > 0: @@ -160,7 +161,7 @@ class BeamlineConfig: try: return OpenGuiSessionInfo(**json.loads(raw)) except Exception: - logger.warning(f"Failed to parse GUI session info for session {session}") + logger.warning(f"Failed to parse GUI session info for session {session}", exc_info=True) return None def touch_gui_session( @@ -347,7 +348,7 @@ class BeamlineConfig: if active is None: self._client.set(f"{self._bl}:active_session", session) elif active != session: - raise Exception( + raise RuntimeError( "There is already active session with different id. Try again later." ) self._client.expire(f"{self._bl}:active_session", expiry_sec) @@ -357,7 +358,7 @@ class BeamlineConfig: with redis_lock.Lock(self._client, f"{self._bl}:active_session_lock", expire=10): active = self.active_session if active is None: - raise Exception("There is no active session with given id. Try again later.") + raise RuntimeError("There is no active session with given id. Try again later.") # if active == session: # self._client.expire(f"{self._bl}:active_session", expiry_sec, gt=True) @@ -371,7 +372,7 @@ class BeamlineConfig: if current_ttl is None or current_ttl < 0 or current_ttl < expiry_sec: self._client.expire(key, expiry_sec) else: - raise Exception( + raise RuntimeError( "There is already active session with different id. Try again later." ) @@ -401,6 +402,7 @@ class BeamlineConfig: try: return BatonHolderInfo(**json.loads(tmp)) except Exception: + logger.debug("Could not parse the baton holder info", exc_info=True) return None @baton_holder.setter @@ -419,6 +421,7 @@ class BeamlineConfig: try: return BatonRequest(**json.loads(tmp)) except Exception: + logger.debug("Could not parse the pending baton request", exc_info=True) return None def set_pending_baton_request( @@ -444,6 +447,7 @@ class BeamlineConfig: try: return BatonTransferQueue(**json.loads(tmp)) except Exception: + logger.debug("Could not parse the queued baton transfer", exc_info=True) return None @queued_baton_transfer.setter @@ -456,12 +460,9 @@ class BeamlineConfig: def can_transfer_baton_now(self) -> bool: """Check if baton can be transferred (beamline not mid-operation).""" # Can't transfer while beamline is busy - if self.state_busy: - return False # Add automation queue check here when you implement it - # if self.automation_queue_running: - # return False - return True + # return not (self.state_busy or self.automation_queue_running) + return not self.state_busy def execute_baton_transfer( self, @@ -545,7 +546,7 @@ class BeamlineConfig: curr_state = self.state if curr_state != target: self.state_busy = False - raise Exception("Beamline is not in a proper state") + raise RuntimeError("Beamline is not in a proper state") def start_moving( self, target: BeamlineStateEnum, timeout: int | None = None @@ -817,7 +818,7 @@ class BeamlineConfig: def zoom_settings(self) -> ZoomModel: mode = self.zoom_mode if not mode or not isinstance(mode, ZoomModeEnum): - raise Exception("incorrect zoom settings mode used") + raise ValueError("incorrect zoom settings mode used") tmp = self._client.get(f"{self._bl}:{self.zoom_setting_string(mode)}") if tmp is None: return zoom_manager(mode, self._mxb) @@ -828,7 +829,7 @@ class BeamlineConfig: def zoom_settings(self, data: ZoomModel): mode = self.zoom_mode if not mode or not isinstance(mode, ZoomModeEnum): - raise Exception("incorrect zoom settings mode used") + raise ValueError("incorrect zoom settings mode used") self._client.set(f"{self._bl}:{self.zoom_setting_string(mode)}", data.model_dump_json()) def save_zoom_camera_setting( @@ -1010,8 +1011,8 @@ class BeamlineConfig: try: data_dict = json.loads(tmp) return SimpleScanParameters(**data_dict) - except Exception as e: - logger.error(f"failed to parse auto_params from redis: {e}; raw={tmp}") + except Exception: + logger.exception(f"failed to parse auto_params from redis; raw={tmp}") return None @auto_params.setter @@ -1181,7 +1182,7 @@ class BeamlineConfig: payload = json.loads(str(raw)) return payload if isinstance(payload, dict) else {} except Exception as e: - logger.warning(f"Failed to read detector metadata from Redis: {e}") + logger.warning(f"Failed to read detector metadata from Redis: {e}", exc_info=True) return {} def set_detector_metadata(self, payload: dict) -> dict: @@ -1252,7 +1253,7 @@ class BeamlineConfig: return LocalContactConfigModel.model_validate_json(str(raw_value)) except Exception as e: - logger.warning(f"Failed to read Local Contact config from Redis: {e}") + logger.warning(f"Failed to read Local Contact config from Redis: {e}", exc_info=True) return default def set_local_contact_config( diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 0c6ee565..bf6acedc 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -583,7 +583,7 @@ class AareDAQ: parsed = json.loads(str(raw_value)) return parsed if isinstance(parsed, list) else [] except Exception as e: - logger.debug(f"Failed to read tell_events from Redis: {e}") + logger.debug(f"Failed to read tell_events from Redis: {e}", exc_info=True) return [] @staticmethod @@ -609,9 +609,13 @@ class AareDAQ: return True if ( - tell_state.last_event_class == "Motion Sync" - and tell_state.last_event_value == "Sample put on Puck" - ) and state_ts is not None and state_ts >= started_at: + ( + tell_state.last_event_class == "Motion Sync" + and tell_state.last_event_value == "Sample put on Puck" + ) + and state_ts is not None + and state_ts >= started_at + ): return True for event in reversed(self._get_tell_events_from_redis()): @@ -636,7 +640,7 @@ class AareDAQ: try: self._face_detection_progress_cb(payload) except Exception as e: - logger.warning(f"Failed to emit face detection progress: {e}") + logger.warning(f"Failed to emit face detection progress: {e}", exc_info=True) def _emit_automation_progress(self, progress: AutomationProgress) -> None: if self._automation_progress_cb is None: @@ -644,7 +648,7 @@ class AareDAQ: try: self._automation_progress_cb(progress) except Exception as e: - logger.warning(f"Failed to emit automation progress: {e}") + logger.warning(f"Failed to emit automation progress: {e}", exc_info=True) def _record_best_effort_step_failure( self, @@ -1017,7 +1021,8 @@ class AareDAQ: ) except Exception as sync_error: logger.warning( - f"Failed to reconcile previous sample from TELL before mount: {sync_error}" + f"Failed to reconcile previous sample from TELL before mount: {sync_error}", + exc_info=True, ) if previous_sample is not None and previous_sample.db_id is not None: @@ -1078,7 +1083,7 @@ class AareDAQ: except Exception as e: self._last_mount_error_message = str(e) or "Mount failed" - logger.error(f"Mount failed: {e}") + logger.exception("Mount failed") previous_sample_unmounted = ( previous_sample is not None @@ -1150,7 +1155,7 @@ class AareDAQ: return True except Exception as e: - logger.error(f"Loop centering failed: {e}") + logger.exception("Loop centering failed") if result is not None and result.error is not None: additional_comment = result.comment if result.comment is not None else "" self._handle_operation_error( @@ -1185,7 +1190,7 @@ class AareDAQ: sample = self.sample logger.debug(f"No sample provided, using current sample from DAQ {sample}") except Exception: - logger.error("Failed to get current sample") + logger.exception("Failed to get current sample") sample = None aare = getattr(self, "_aare", None) @@ -1212,12 +1217,13 @@ class AareDAQ: return result except Exception as e: - logger.error(f"Face detection failed: {e}") + logger.exception("Face detection failed") additional_comment = f"{e}" if e is not None else "" try: sample = self.sample except Exception: + logger.debug("Could not read the current sample for face detection", exc_info=True) sample = None if report_error: @@ -1447,7 +1453,7 @@ class AareDAQ: f.flush() except Exception as e: - logger.warning(f"Failed to append smargon trace: {e}") + logger.warning(f"Failed to append smargon trace: {e}", exc_info=True) def _sample_matches_mounted_address( self, sample: SampleShortInfo | None, mounted_address @@ -1554,7 +1560,7 @@ class AareDAQ: "Cannot explicitly move to busy state", extra={"target": target, "state": self._cfg.state}, ) - raise Exception("Cannot explicitly move to busy state") + raise RuntimeError("Cannot explicitly move to busy state") start = time.perf_counter() self._cfg.try_set_busy(timeout=300) @@ -1653,10 +1659,9 @@ class AareDAQ: try: self._devs.aerotech_omega = val self._cfg.state_busy = False - except Exception as e: - logger.error(f"Omega error: {e}") + except Exception: + logger.exception("Omega move timed out") self._cfg.state_busy = False - logger.error("Omega move timed out") else: self._cfg.state_busy = False logger.error("Omega has to be between -2000 and 2000 degrees") @@ -1799,7 +1804,7 @@ class AareDAQ: curr_sample = self._cfg.current_sample if curr_sample is not None and curr_sample.location is not None: - raise Exception("Sample from TELL is loaded") + raise RuntimeError("Sample from TELL is loaded") self._aare.create_manual_sample(target) self._cfg.current_sample = target @@ -1832,8 +1837,8 @@ class AareDAQ: def tell_toggle_blower(self): try: self._devs.tell.toggle_blower() - except Exception as e: - logger.error(f"Failed to turn off blower: {e}") + except Exception: + logger.exception("Failed to turn off blower") def initialise_smargon(self): self._cfg.try_set_busy(timeout=360) @@ -2018,7 +2023,7 @@ class AareDAQ: try: start_angle = float(self.omega) except Exception: - pass + logger.debug("Could not read omega for the fake rotation result", exc_info=True) return build_fake_rotation_result(request, start_angle=start_angle) @@ -2154,10 +2159,8 @@ class AareDAQ: try: if self._cfg.state_busy and self._cfg.state != BeamlineStateEnum.Maintenance: self._set_state(BeamlineStateEnum.SampleAlignment) - except Exception as cleanup_error: - logger.exception( - f"Failed to restore SampleAlignment after rotation: {cleanup_error}" - ) + except Exception: + logger.exception("Failed to restore SampleAlignment after rotation") finally: self._cfg.state_busy = False @@ -2411,9 +2414,7 @@ class AareDAQ: default_message=self._default_screenshot_message(sample.db_id), ) - def send_message_db( - self, db_id: int, event_type: SampleEventType, comment: str | None = None - ): + def send_message_db(self, db_id: int, event_type: SampleEventType, comment: str | None = None): self._aare.send_sample_event(db_id, event_type, comment) @property @@ -2506,8 +2507,8 @@ class AareDAQ: try: params.dtz = self.diffraction_geometry.calc_dtz_mm(res) logger.debug(f"dtz: {params.dtz}") - except Exception as e: - logger.error(f"Failed to calculate dtz for resolution {res}: {e}") + except Exception: + logger.exception(f"Failed to calculate dtz for resolution {res}") # Adjust exposure time based on resolution if res <= 1.5: @@ -2899,7 +2900,7 @@ class AareDAQ: event_type=SampleEventType.FAILED, ) self._end_operation(start, operation=DAQOperation.AUTOMATION, error=True) - raise Exception(f"Critical Error in automation: {e}") from e + raise RuntimeError(f"Critical Error in automation: {e}") from e self._record_completed_sample_time(progress, time.time() - sample_started_at) self._mark_progress_finished(progress, True, "Automation complete") @@ -2927,7 +2928,7 @@ class AareDAQ: ) if not self._cfg.state_busy: - raise Exception("Beamline should be busy") + raise RuntimeError("Beamline should be busy") if target == BeamlineStateEnum.Maintenance: self._cfg.state = BeamlineStateEnum.Maintenance @@ -3121,8 +3122,8 @@ class AareDAQ: raise except Exception as e: logger.exception( - f"Exception during state transition : {curr_state} -> {target}." - f"Changing state to maintenance due to error: {e}", + f"Exception during state transition : {curr_state} -> {target}. " + "Changing state to maintenance due to error.", extra={"from_state": curr_state, "to_state": target}, ) self._cfg.state = BeamlineStateEnum.Maintenance @@ -3165,7 +3166,8 @@ class AareDAQ: ) except Exception as e: logger.warning( - f"Falling back to default diffraction geometry because cached detector metadata is unavailable: {e}" + f"Falling back to default diffraction geometry because cached detector metadata is unavailable: {e}", + exc_info=True, ) energy = self._devs.energy_kev dtz = self._devs.dtz @@ -3236,8 +3238,8 @@ class AareDAQ: pss_alarm=pss_alarm, ) - except Exception as e: - logger.exception(f"Failed to retrieve beamline status: {e}") + except Exception: + logger.exception("Failed to retrieve beamline status") raise def _safe_sample(self) -> tuple[SampleShortInfo | None, bool, str | None]: @@ -3249,7 +3251,7 @@ class AareDAQ: except TellCommunicationError as e: return self._cfg.current_sample, False, str(e) except Exception as e: - logger.warning(f"error in status sample info call: {e}") + logger.warning(f"error in status sample info call: {e}", exc_info=True) # Keep status flowing even if Tell code throws something unexpected return self._cfg.current_sample, False, f"TELL unavailable: {e}" @@ -3259,7 +3261,7 @@ class AareDAQ: try: _ = self._devs.aerotech.status() except Exception as e: - logger.warning(f"Aerotech error in _aerotech_status: {e}") + logger.warning(f"Aerotech error in _aerotech_status: {e}", exc_info=True) aerotech_ok = False aerotech_err = f"Cannot connect to Aerotech: {e}" return aerotech_ok, aerotech_err @@ -3289,7 +3291,7 @@ class AareDAQ: self._devs.exp_shutter.close() aerotech_connected = False except Exception as e: - logger.warning(f"Unexpected error in _safe_geom: {e}") + logger.warning(f"Unexpected error in _safe_geom: {e}", exc_info=True) smargon_error = f"Safe geometry failed: {e}" aerotech_error = f"Safe geometry failed: {e}" aerotech_connected = False @@ -3335,14 +3337,14 @@ class AareDAQ: return TellStateModel.model_validate_json(str(raw_value)) except Exception as e: - logger.warning(f"Failed to read tell_state from Redis: {e}") + logger.warning(f"Failed to read tell_state from Redis: {e}", exc_info=True) return None def _safe_diffraction_geometry(self) -> DiffractionGeometry: try: return self.diffraction_geometry except Exception as e: - logger.warning(f"Failed to retrieve diffraction geomtrey: {e!s}") + logger.warning(f"Failed to retrieve diffraction geomtrey: {e!s}", exc_info=True) # Must satisfy pydantic constraints in DiffractionGeometry return DiffractionGeometry( energy_keV=12.4, diff --git a/src/aare/daq/devices.py b/src/aare/daq/devices.py index d3ee4055..252d5121 100644 --- a/src/aare/daq/devices.py +++ b/src/aare/daq/devices.py @@ -115,7 +115,9 @@ class BeamlineDevices: try: self.bec_worker.shutdown_client() except Exception as e: - logger.warning(f"Failed to shutdown previous BEC worker cleanly: {e}") + logger.warning( + f"Failed to shutdown previous BEC worker cleanly: {e}", exc_info=True + ) finally: beamline = MXBeamline.SIMULATED if simulated else self._beamline logger.info(f"Restarting BEC worker with simulated={simulated}") diff --git a/src/aare/daq/mlbox.py b/src/aare/daq/mlbox.py index 6a70ed08..33621bc3 100644 --- a/src/aare/daq/mlbox.py +++ b/src/aare/daq/mlbox.py @@ -92,7 +92,7 @@ class MlBox: return None return image except Exception as e: - logger.warning(f"Failed to decode prediction bundle image: {e}") + logger.warning(f"Failed to decode prediction bundle image: {e}", exc_info=True) return None @staticmethod @@ -106,7 +106,7 @@ class MlBox: return LatestPredictionModel.model_validate(metadata) return LatestPredictionModel.model_validate(metadata.model_dump()) except Exception as e: - logger.warning(f"Failed to validate prediction metadata: {e}") + logger.warning(f"Failed to validate prediction metadata: {e}", exc_info=True) return None @staticmethod @@ -126,7 +126,8 @@ class MlBox: except Exception as e: last_error = e logger.warning( - f"Prediction bundle fetch failed on attempt {attempt}/{self.RETRY_COUNT}: {e}" + f"Prediction bundle fetch failed on attempt {attempt}/{self.RETRY_COUNT}: {e}", + exc_info=True, ) if attempt < self.RETRY_COUNT: time.sleep(self.RETRY_SLEEP_S) @@ -156,7 +157,9 @@ class MlBox: if isinstance(raw, (list, tuple)) and len(raw) >= 2: return float(raw[0]), float(raw[1]) except Exception as e: - logger.warning(f"Failed to parse target_point from prediction metadata: {e}") + logger.warning( + f"Failed to parse target_point from prediction metadata: {e}", exc_info=True + ) return None @@ -171,7 +174,9 @@ class MlBox: if raw_focus is not None: focus = float(raw_focus) except Exception as e: - logger.warning(f"Failed to parse focus_score from prediction metadata: {e}") + logger.warning( + f"Failed to parse focus_score from prediction metadata: {e}", exc_info=True + ) return MLBundleMeta(target_point=target_point, focus=focus) @@ -328,9 +333,12 @@ class MlBox: continue if model.cls == MLBoxType.PIN: continue - if overlap_with_pin is not None and pin: - if self._check_overlap(model, pin) >= overlap_with_pin: - keys_to_remove.append(key) + if ( + overlap_with_pin is not None + and pin + and self._check_overlap(model, pin) >= overlap_with_pin + ): + keys_to_remove.append(key) for k in keys_to_remove: predictions.boxes.pop(k, None) @@ -348,7 +356,7 @@ class MlBox: y2 = float(box.get("y2")) except Exception as e: - logger.debug(f"Failed to parse prediction: {e}") + logger.debug(f"Failed to parse prediction: {e}", exc_info=True) continue base_key = MLOutputModel.get_class_str(cls=cls) @@ -387,7 +395,7 @@ class MlBox: x2 = float(det.x2) y2 = float(det.y2) except Exception as e: - logger.debug(f"Failed to parse bundle detection: {e}") + logger.debug(f"Failed to parse bundle detection: {e}", exc_info=True) continue out.add_box(cls=cls, box_tuple=(x1, y1, x2, y2), conf=conf) @@ -523,7 +531,8 @@ class MlBox: def predict_best_no_filter( self, return_image: bool = False, return_bundle_meta: bool = False ) -> ( - MLOutputModel | None + MLOutputModel + | None | tuple[MLOutputModel | None, np.ndarray | None] | MLBoxPredictionsResult ): @@ -551,7 +560,8 @@ class MlBox: return_image: bool = False, return_bundle_meta: bool = False, ) -> ( - MLOutputModel | None + MLOutputModel + | None | tuple[MLOutputModel | None, np.ndarray | None] | MLBoxPredictionsResult ): @@ -657,7 +667,9 @@ class MlBox: logger.debug(f"Frame {frame_idx + 1}/{n_frames}: no detection") except Exception as e: - logger.warning(f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}") + logger.warning( + f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}", exc_info=True + ) continue if best_box is None: diff --git a/src/aare/daq/operations/face_detection/service.py b/src/aare/daq/operations/face_detection/service.py index 8e91d7aa..d75776ed 100644 --- a/src/aare/daq/operations/face_detection/service.py +++ b/src/aare/daq/operations/face_detection/service.py @@ -210,7 +210,7 @@ class FaceDetectionService: return FaceDetectionResult(success=True, payload=payload) except Exception as e: - self.logger.error(f"error in face detection sequence {e}") + self.logger.exception("error in face detection sequence") payload = self._emit_empty_result() return FaceDetectionResult( success=False, payload=payload, error=e, comment="Face detection sequence failed" diff --git a/src/aare/daq/operations/face_detection/utils.py b/src/aare/daq/operations/face_detection/utils.py index 998da418..6aec307f 100644 --- a/src/aare/daq/operations/face_detection/utils.py +++ b/src/aare/daq/operations/face_detection/utils.py @@ -82,6 +82,7 @@ def fit_cosine(samples: list[tuple[float, float]]) -> dict: beta, _, _, _ = np.linalg.lstsq(X, ys, rcond=None) A0, C0, S0 = beta.tolist() except Exception: + logger.debug("Cosine fit seeding failed; falling back to the mean", exc_info=True) A0, C0, S0 = float(np.mean(ys)), 0.0, 0.0 B0 = float(math.hypot(C0, S0)) @@ -109,7 +110,7 @@ def fit_cosine(samples: list[tuple[float, float]]) -> dict: except Exception as e: # Fallback to initial - logger.info(f"error in curve fit {e}") + logger.info(f"error in curve fit {e}", exc_info=True) yhat0 = cos_model(degs, A0, max(0.0, B0), phi0, C0) rmse0, mae0, r2_0 = fit_metrics(ys, yhat0) return { @@ -133,9 +134,7 @@ def get_samples_out(boxes): return samples_out -def choose_best_fit( - fits_by_name: dict[str, dict], -) -> tuple[float | None, dict | None, str | None]: +def choose_best_fit(fits_by_name: dict[str, dict]) -> tuple[float | None, dict | None, str | None]: def key(entry: dict): params = entry.get("params") or {} @@ -150,6 +149,7 @@ def choose_best_fit( logger.info(f"rmse: {rmse}, mae: {mae}, r2: {r2}") return float(rmse), float(mae), -float(r2) except Exception: + logger.debug("Could not score a candidate fit", exc_info=True) return None best_name = None diff --git a/src/aare/daq/operations/loop_centering/service.py b/src/aare/daq/operations/loop_centering/service.py index e1baeca0..e1f586c8 100644 --- a/src/aare/daq/operations/loop_centering/service.py +++ b/src/aare/daq/operations/loop_centering/service.py @@ -1,5 +1,4 @@ import time -import traceback from aarecommon.config.logger_events import log_duration from aarecommon.errors.exception_handler import LoopCenteringFailed @@ -203,7 +202,5 @@ class LoopCenteringService: f"Needle: {found_classes_count.get(5, 0)}" ) - self.logger.error(f"ALC exception: {alc_comment}") - self.logger.error(traceback.format_exc()) - self.logger.error(f"Error in loop centering: {e}") + self.logger.exception(f"ALC exception: {alc_comment}") return LoopCenteringResult(success=False, comment=alc_comment, error=e) diff --git a/src/aare/daq/operations/mounting/service.py b/src/aare/daq/operations/mounting/service.py index 13960895..1f11058c 100644 --- a/src/aare/daq/operations/mounting/service.py +++ b/src/aare/daq/operations/mounting/service.py @@ -43,7 +43,9 @@ class MountingService: self.logger.error( "Goniometer didn't reach position based on magnet position sensor readout" ) - raise Exception("Goniometer is not in position based on magnet position sensor readout") + raise RuntimeError( + "Goniometer is not in position based on magnet position sensor readout" + ) def _handle_consecutive_mount_failure(self) -> None: count = self.ctx.deps.cfg.increment_mount_failure_streak() @@ -53,8 +55,8 @@ class MountingService: self.logger.warning(f"Mount failed {count} times in a row; drying") try: self.dry(park=False) - except Exception as e: - self.logger.exception(f"Failed to dry after mount failure: {e}") + except Exception: + self.logger.exception("Failed to dry after mount failure") if count >= STOP_AFTER_FAIL_COUNT: self.logger.error(f"Mount failed {count} times in a row, stopping automation") @@ -62,8 +64,8 @@ class MountingService: try: self._unmount_current_sample(timeout=60.0) self.dry(park=True) - except Exception as e: - self.logger.exception(f"Failed to clean up after repeated mount failure: {e}") + except Exception: + self.logger.exception("Failed to clean up after repeated mount failure") raise MountingFailed( f"Mount failed {count} times in a row, stopping automation.", critical=True @@ -235,7 +237,7 @@ class MountingService: ) except Exception as e: - self.logger.error(f"Mount failed: {e}") + self.logger.exception("Mount failed") return MountingResult( success=False, mounted_sample=None, diff --git a/src/aare/daq/operations/raster/service.py b/src/aare/daq/operations/raster/service.py index 8d296b61..38708811 100644 --- a/src/aare/daq/operations/raster/service.py +++ b/src/aare/daq/operations/raster/service.py @@ -121,7 +121,9 @@ class RasterService: if bits: comment = f"Raster diffraction ({', '.join(bits)})" except Exception: - pass + self.logger.debug( + "Could not build the raster diffraction preview comment", exc_info=True + ) self.ctx.deps.aare.upload_jpg(sample_id, filename, diffraction_image, message=comment) @@ -647,9 +649,9 @@ class RasterService: request=copy.deepcopy(request), result=scan_result, centre_of_mass=com ) - except Exception as e: + except Exception: self.logger.exception( - f"Failed during raster: {e}", + "Failed during raster", extra=merge_log_context( sample_log_context(self.ctx.sample), raster_request_log_context(request), @@ -661,7 +663,7 @@ class RasterService: def execute_auto_center(self, request: RasterGridRequest) -> CompletedRasterGrid | None: sample = self.ctx.sample if sample is None: - raise Exception("Sample must be mounted to auto center") + raise RuntimeError("Sample must be mounted to auto center") old_prefix = request.file_prefix geom = self.ctx.sample_geometry diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index d8e756d4..e0f5f3e7 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -6,6 +6,7 @@ import os import time from collections.abc import AsyncGenerator from contextlib import asynccontextmanager +from typing import Any, ClassVar import uvicorn from aarecommon.config.beamline import mx_beamline @@ -84,7 +85,10 @@ _automation_progress_state_lock = asyncio.Lock() class AareUvicornWorker(UvicornWorker): # CONFIG_KWARGS merged last into uvicorn Config (uvicorn/workers.py:69) → # keeps our access-log filter + proxy_headers under gunicorn. - CONFIG_KWARGS = {"log_config": get_uvicorn_logging_config(), "proxy_headers": False} + CONFIG_KWARGS: ClassVar[dict[str, Any]] = { + "log_config": get_uvicorn_logging_config(), + "proxy_headers": False, + } @asynccontextmanager @@ -109,16 +113,16 @@ async def lifespan(application: FastAPI): try: cfg.reset_automation_progress() except Exception as e: - logger.warning(f"Failed to reset automation progress Redis keys: {e}") + logger.warning(f"Failed to reset automation progress Redis keys: {e}", exc_info=True) try: daq.refresh_detector_metadata_cache() except Exception as e: - logger.warning(f"Initial hardware metadata refresh failed: {e}") + logger.warning(f"Initial hardware metadata refresh failed: {e}", exc_info=True) # ── Initial TELL sync ── try: daq.sync_current_sample_from_tell(force=True) except Exception as e: - logger.warning(f"Initial sample sync from TELL failed: {e}") + logger.warning(f"Initial sample sync from TELL failed: {e}", exc_info=True) # ── Wire callbacks ── daq.set_face_detection_progress_callback(_push_face_detection_progress) daq.set_automation_progress_callback(_push_automation_progress) @@ -184,6 +188,7 @@ def _sample_is_mounted() -> bool: try: return daq.sample is not None except Exception: + logger.debug("Could not determine whether a sample is mounted", exc_info=True) return False @@ -199,7 +204,7 @@ def _push_face_detection_progress(payload: dict) -> None: next_seq = int(_face_detection_state.get("seq", 0)) + 1 _face_detection_state = {"seq": next_seq, **payload} except Exception as e: - logger.warning(f"Failed to update face detection progress: {e}") + logger.warning(f"Failed to update face detection progress: {e}", exc_info=True) def _get_automation_progress_state() -> dict: @@ -209,7 +214,7 @@ def _get_automation_progress_state() -> dict: try: return cfg.get_automation_progress_state() except Exception as e: - logger.warning(f"Failed to read automation progress from Redis: {e}") + logger.warning(f"Failed to read automation progress from Redis: {e}", exc_info=True) return {"seq": 0, "progress": None} @@ -228,7 +233,9 @@ def _push_automation_progress(progress: AutomationProgress) -> None: f"current_step={state.get('progress', {}).get('current_step')}" ) except Exception as e: - logger.warning(f"Failed to update automation progress: {type(e).__name__}: {e}") + logger.warning( + f"Failed to update automation progress: {type(e).__name__}: {e}", exc_info=True + ) async def face_detection_event_stream() -> AsyncGenerator[str, None]: @@ -2289,8 +2296,8 @@ async def get_all_pgroups(token: str = Depends(oauth2_scheme)): names.sort(key=lambda d: int(d[1:])) _all_pgroups_cache[base_path] = (names, now) return names - except Exception as e: - logger.error(f"Failed to list pgroups: {e}") + except Exception: + logger.exception("Failed to list pgroups") return [] @@ -2450,7 +2457,7 @@ async def sse_fluorimeter(token: str = Depends(oauth2_scheme)): daq.fluorimeter_start(erase=False) except Exception: # Ignore if already running or start not needed - pass + logger.debug("Fluorimeter already running or start not needed", exc_info=True) return StreamingResponse( fluorimeter_stream(), media_type="text/event-stream", diff --git a/src/aare/daq/spreadsheetupdater.py b/src/aare/daq/spreadsheetupdater.py index 2c8a86e9..c499ab12 100644 --- a/src/aare/daq/spreadsheetupdater.py +++ b/src/aare/daq/spreadsheetupdater.py @@ -3,11 +3,14 @@ import os import time from aarecommon.config.beamline import mx_beamline +from aarecommon.config.logger import setup_logger from aarecommon.models.models import DewarAddress, SampleShortInfo, SampleShortInfoList from aareDB.models import PuckWithTellPosition from aare.daq.config import BeamlineConfig +logger = setup_logger("aareDAQ") + beamline = mx_beamline() config = BeamlineConfig(bl=beamline) @@ -105,10 +108,10 @@ def on_message(ws, message): config._client.delete(ref_key) print("[REDIS][INFO] Cleared reference tools key:", ref_key) except Exception: - pass + logger.debug("Could not clear the reference tools key", exc_info=True) - except Exception as exc: - print("[WS][ERROR] Failed to parse or convert message:", exc) + except Exception: + logger.exception("[WS] Failed to parse or convert message") def on_error(ws, error): @@ -178,8 +181,8 @@ def main(): print(f"[WS][INFO] Connecting to {WS_URL}...") ws.run_forever(sslopt=ssl_opt) - except Exception as e: - print(f"[MAIN][ERROR] WebSocket connection failed: {e}") + except Exception: + logger.exception("[MAIN] WebSocket connection failed") time.sleep(5) diff --git a/src/aare/daq/tell_state_machine.py b/src/aare/daq/tell_state_machine.py index 53f8a97f..9de7e8d0 100644 --- a/src/aare/daq/tell_state_machine.py +++ b/src/aare/daq/tell_state_machine.py @@ -248,11 +248,7 @@ def advance_tell_state( if event_name == "Motion Sync": if value == "Sample get from Gonio": - next_phase = ( - TellPhaseEnum.RETURNING_OLD_SAMPLE - if state.operation == "mount" - else TellPhaseEnum.RETURNING_OLD_SAMPLE - ) + next_phase = TellPhaseEnum.RETURNING_OLD_SAMPLE next_operation = "mount" if state.operation == "mount" else "unmount" return _update_state( state, @@ -324,18 +320,21 @@ def advance_tell_state( mount_error="", ) - if event_name == "Gripper detection": - if value in {"No Pin in Gripper", "Pin still in Gripper", "Pin is lost"}: - return _update_state( - state, - activity=TellActivityEnum.ERROR, - message=value, - event_name=event_name, - event_value=value, - mount_success=False, - mount_error=value, - phase=TellPhaseEnum.FAILED, - ) + if event_name == "Gripper detection" and value in { + "No Pin in Gripper", + "Pin still in Gripper", + "Pin is lost", + }: + return _update_state( + state, + activity=TellActivityEnum.ERROR, + message=value, + event_name=event_name, + event_value=value, + mount_success=False, + mount_error=value, + phase=TellPhaseEnum.FAILED, + ) if event_name == "state": if value == "Ready": diff --git a/src/aare/daq/workflows.py b/src/aare/daq/workflows.py index 0b9a54ca..41cd0c48 100644 --- a/src/aare/daq/workflows.py +++ b/src/aare/daq/workflows.py @@ -266,8 +266,8 @@ def common2dh(devs: BeamlineDevices, cfg: BeamlineConfig): if devs.tell.get_mounted_sample() is not None: try: devs.tell.unmount(wait=True) - except Exception as e: - print(f"Error for unmounting: {e}") + except Exception: + logger.exception("Error for unmounting") devs.tell.dry(wait_cold=-1, wait=False) diff --git a/src/aare/devices/aerotech.py b/src/aare/devices/aerotech.py index 10732d2c..afa95cfa 100644 --- a/src/aare/devices/aerotech.py +++ b/src/aare/devices/aerotech.py @@ -1,4 +1,3 @@ - from aarecommon.config.beamline import cfg_get, mx_beamline from aarecommon.config.logger import setup_logger from aarecommon.errors.exception_handler import AerotechCommunicationError @@ -41,7 +40,7 @@ class AerotechController: self._pos = AEROTECH_HOME self._vel = 0 else: - raise Exception("unknown beamline") + raise ValueError("unknown beamline") if not self._simulated: self._client = ApiClient(Configuration(host=self._base)) @@ -183,11 +182,7 @@ class AerotechController: ) from e def rotation_scan( - self, - rotation_deg: float, - time_sec: float, - start_pos_deg: float, - run_async: bool = False, + self, rotation_deg: float, time_sec: float, start_pos_deg: float, run_async: bool = False ): payload = RotationRequest( rotation_deg=rotation_deg, diff --git a/src/aare/devices/area_detector.py b/src/aare/devices/area_detector.py index 51427404..74e67076 100644 --- a/src/aare/devices/area_detector.py +++ b/src/aare/devices/area_detector.py @@ -3,6 +3,9 @@ from enum import Enum import epics import numpy as np +from aarecommon.config.logger import setup_logger + +logger = setup_logger("aareDAQ") class AutoEnum(Enum): @@ -61,10 +64,11 @@ class epicsAD: try: epics.ca.pend_io() - except Exception as e: - print(f"EPICS error: {e}") + except Exception: + logger.exception("EPICS error") - def _init_auto_exp(self, settings: AutoExposureSettings = AutoExposureSettings()): + def _init_auto_exp(self, settings: AutoExposureSettings | None = None): + settings = settings or AutoExposureSettings() self.acquire.put(0) self.aoi_start_x.put(settings.aoi_offset_x) self.aoi_start_y.put(settings.aoi_offset_y) diff --git a/src/aare/devices/bec_worker.py b/src/aare/devices/bec_worker.py index 945fe909..ac652e36 100644 --- a/src/aare/devices/bec_worker.py +++ b/src/aare/devices/bec_worker.py @@ -1,3 +1,4 @@ +import sys import time from enum import Enum @@ -89,9 +90,9 @@ class BECClientWorker: self._set_scilog_tags() try: self._init_beamline_environment() - except Exception as e: - logger.error(f"Error initialising BEC devices: {e}") - exit(1) + except Exception: + logger.exception("Error initialising BEC devices") + sys.exit(1) logger.debug(f"simulated is {self.simulated}") def _init_beamline_environment(self): @@ -111,14 +112,15 @@ class BECClientWorker: self._zoom = self.dev.scam_zoom self._ring_current = self.dev.sls_current except Exception as e: - logger.error(f"Error initialising zoom and ring_current: {e}") + logger.exception("Error initialising zoom and ring_current") self._zoom = None self.ring_current = None - raise Exception(f"Error initialising BEC devices: {e}") + raise RuntimeError(f"Error initialising BEC devices: {e}") from e - def _raise_bec_error( + def _bec_error( self, exc: Exception, *, operation: str, tags: list[str] | None = None - ) -> None: + ) -> BECCommunicationError: + """Report a failed BEC operation and build the error; callers ``raise ... from`` it.""" message = f"BEC operation '{operation}' failed: {type(exc).__name__}: {exc}" # logger.exception(message) if tags is None: @@ -133,8 +135,8 @@ class BECClientWorker: last_alarm = self.client.show_last_alarm() message += f"\n\nAlarm: {last_alarm}\n\n" logger.error(f"last_alarm: {last_alarm}") - except Exception as e: - logger.error(f"Couldn't raise BEC alarms: {e}") + except Exception: + logger.exception("Couldn't raise BEC alarms") try: self.scilog_msg( @@ -143,17 +145,17 @@ class BECClientWorker: error_message=f"Error during '{operation}': {exc}", tags=tags, ) - except Exception as e: - logger.error(f"Error sending scilog message: {e}") + except Exception: + logger.exception("Error sending scilog message") if isinstance(exc, AssertionError): - raise BECCommunicationError( + return BECCommunicationError( f"BEC internal assertion failed during '{operation}'", operation=operation, exception=exc, - ) from exc + ) - raise BECCommunicationError(message, operation=operation, exception=exc) from exc + return BECCommunicationError(message, operation=operation, exception=exc) def _set_scilog_tags(self, tags: list[str] | None = None): try: @@ -162,8 +164,8 @@ class BECClientWorker: else: self.client.messaging.scilog.set_default_tags(["AareDAQ"]) except Exception as e: - logger.error(f"Error setting scilog tags: {e}") - self._raise_bec_error(e, operation="set scilog tags") + logger.exception("Error setting scilog tags") + raise self._bec_error(e, operation="set scilog tags") from e def scilog_msg( self, @@ -184,7 +186,7 @@ class BECClientWorker: try: msg = self.client.messaging.scilog.new() except Exception as e: - self._raise_bec_error(e, operation="scilog_msg") + raise self._bec_error(e, operation="scilog_msg") from e try: msg.add_text(message, bold=bold, italic=italic, color=color) if error: @@ -192,32 +194,32 @@ class BECClientWorker: elif warning: msg.add_text(error_message, bold=True, color="yellow") except Exception as e: - logger.error(f"Error adding text: {e}") + logger.exception("Error adding text") msg.add_text(f"Error adding text: {e}") try: if attachments: for attachment in attachments: msg.add_attachment(attachment) except Exception as e: - logger.error(f"Error adding attachment: {e}") + logger.exception("Error adding attachment") msg.add_text(f"Error adding attachment: {e}") try: if additonal_text: for text in additonal_text: msg.add_text(text) except Exception as e: - logger.error(f"Error adding additional text: {e}") + logger.exception("Error adding additional text") msg.add_text(f"Error adding additional text: {e}") try: if tags: msg.add_tags(tags) except Exception as e: - logger.error(f"Error setting scilog tags: {e}") + logger.exception("Error setting scilog tags") msg.add_text(f"Error setting scilog tags: {e}") try: msg.send() - except Exception as e: - logger.error(f"Error sending scilog message: {e}") + except Exception: + logger.exception("Error sending scilog message") def run_macro(self, macro_name: str, *args, queue: str = "default", **kwargs): if self.simulated: @@ -226,7 +228,7 @@ class BECClientWorker: try: return self.client.proc.run_macro(macro_name, *args, queue=queue) except Exception as e: - self._raise_bec_error(e, operation=f"run_macro:{macro_name}") + raise self._bec_error(e, operation=f"run_macro:{macro_name}") from e def run_macro_blocked(self, macro_name: str, *args, queue: str = "default", **kwargs): if self.simulated: @@ -239,7 +241,7 @@ class BECClientWorker: print(status) return status except Exception as e: - self._raise_bec_error(e, operation=f"run_macro_blocked:{macro_name}") + raise self._bec_error(e, operation=f"run_macro_blocked:{macro_name}") from e @log_timing(logger, "BEC move_to") def move_to(self, state: BeamlineState): @@ -262,7 +264,7 @@ class BECClientWorker: ) return False except Exception as e: - self._raise_bec_error(e, operation=f"planner.move_to:{state.value}") + raise self._bec_error(e, operation=f"planner.move_to:{state.value}") from e def is_state(self, state: BeamlineState): if self.simulated: @@ -297,7 +299,7 @@ class BECClientWorker: return [] return [str(macro) for macro in raw_macros] except Exception as e: - self._raise_bec_error(e, operation="list_all_user_macros") + raise self._bec_error(e, operation="list_all_user_macros") from e def _list_all_macros(self): result = self.client.macros.list_user_macros() @@ -312,7 +314,7 @@ class BECClientWorker: try: return self._load_user_macros() except Exception as e: - self._raise_bec_error(e, operation="load_user_macros") + raise self._bec_error(e, operation="load_user_macros") from e def _load_user_macros(self): result = self.macros.load_all_user_macros() @@ -344,9 +346,9 @@ class BECClientWorker: logger.info(f"Reinitialised BEC planner and position devices using method={method}") return self.list_position_devices() except Exception as e: - self._raise_bec_error( + raise self._bec_error( e, operation=f"reinitialise_planner_and_position_devices:{method}" - ) + ) from e def shutdown_client(self): self.client.shutdown() @@ -355,7 +357,7 @@ class BECClientWorker: try: self.macros.mono_pitch_scan(plot) except Exception as e: - self._raise_bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"]) + raise self._bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"]) from e if self.beamline is MXBeamline.X06DA: addtional_text = [f"New dcm_pitch position: {self.dev.dcm_pitch.position:5f}"] else: @@ -381,11 +383,11 @@ class BECClientWorker: try: self.macros.bl_energy(value, move_gap=False, mono_scan=True, plot=plot) except Exception as e: - self._raise_bec_error( + raise self._bec_error( e, operation=f"Requested energy change from:{current_energy:.1f} to {value} eV", tags=["energy_change"], - ) + ) from e if abs(value - self.check_current_energy()) > 1: logger.warning( @@ -419,7 +421,7 @@ class BECClientWorker: try: return self.dev.det_z.position except Exception as e: - self._raise_bec_error(e, operation="get_det_z", tags=["det_z"]) + raise self._bec_error(e, operation="get_det_z", tags=["det_z"]) from e def det_z(self, value: float, timeout: int | None = None): """timeout is None or integer in s""" @@ -429,13 +431,13 @@ class BECClientWorker: status.wait(timeout=timeout) return status except Exception as e: - self._raise_bec_error(e, operation=f"scans.mv:det_z:{value}", tags=["det_z"]) + raise self._bec_error(e, operation=f"scans.mv:det_z:{value}", tags=["det_z"]) from e def get_det_y(self): try: return self.dev.det_y.position except Exception as e: - self._raise_bec_error(e, operation="get_det_z", tags=["det_z"]) + raise self._bec_error(e, operation="get_det_z", tags=["det_z"]) from e def det_y(self, value: float, timeout: int | None = None): """timeout is None or integer in s""" @@ -445,7 +447,7 @@ class BECClientWorker: status.wait(timeout=timeout) return status except Exception as e: - self._raise_bec_error(e, operation=f"scans.mv:det_y:{value}", tags=["det_z"]) + raise self._bec_error(e, operation=f"scans.mv:det_y:{value}", tags=["det_z"]) from e @property def backlight_brightness(self) -> BrightnessEnum: @@ -454,11 +456,11 @@ class BECClientWorker: try: return BrightnessEnum(self._backlight_brightness.actual) except Exception as e: - self._raise_bec_error( + raise self._bec_error( e, operation="backlight brightness, could not get backlight brightness", tags=["backlight"], - ) + ) from e raise @backlight_brightness.setter @@ -469,7 +471,9 @@ class BECClientWorker: try: self._backlight_brightness.move(value) except Exception as e: - self._raise_bec_error(e, operation=f"backlight_brightness:{value}", tags=["backlight"]) + raise self._bec_error( + e, operation=f"backlight_brightness:{value}", tags=["backlight"] + ) from e raise def get_backlight_pos(self) -> BrightnessEnum: @@ -488,11 +492,11 @@ class BECClientWorker: target = BrightnessEnum.ON self.backlight_brightness = target except Exception as e: - self._raise_bec_error( + raise self._bec_error( e, operation="backlight toggle, could not change backlight on/off ", tags=["backlight"], - ) + ) from e def save_current_bs_pos(self): self.macros.save_current_position(self.dev.bs_z, "safe") @@ -534,13 +538,12 @@ if __name__ == "__main__": beamline = mx_beamline() try: client = BECClientWorker(beamline) - except Exception as e: - print(f"Error: {e}") + except Exception: + logger.exception("Failed to start BEC client") try: client.shutdown_client() - except Exception as e: - import sys - + except Exception: + logger.exception("Failed to shut the BEC client down cleanly") sys.exit(1) # print(client.get_det_cov(actual=True)) # print(client.is_state(BeamlineState.ROBOT_SAMPLE_EXCHANGE)) @@ -580,10 +583,9 @@ if __name__ == "__main__": # print(client.backlight_brightness) # client.scilog_msg("Testing scilog messages with color = yellow and italic", italic=True, # color="green", warning=False) - except Exception as e: - # client._raise_bec_error(e, operation="send message") + except Exception: client.shutdown_client() - print(f"Error: {e}") + logger.exception("BEC client smoke test failed") # try: # det_value = 980 diff --git a/src/aare/devices/enum_pv.py b/src/aare/devices/enum_pv.py old mode 100755 new mode 100644 diff --git a/src/aare/devices/fluorimeter.py b/src/aare/devices/fluorimeter.py index 12c9f0a5..4e6eb823 100644 --- a/src/aare/devices/fluorimeter.py +++ b/src/aare/devices/fluorimeter.py @@ -108,8 +108,8 @@ class Fluorimeter: raise ValueError(f"Invalid preset mode: {mode}") try: self._preset_mode.put(mode) - except Exception as e: - logger.error(f"Error setting preset mode to {mode}: {e}") + except Exception: + logger.exception(f"Error setting preset mode to {mode}") def save_file(self, filename: str | None = None, timeout_s: float = 30.0): if not filename: @@ -122,8 +122,8 @@ class Fluorimeter: if time.time() > timeout: raise TimeoutError("Timed out waiting for save to complete") poll(0.1) - except Exception as e: - logger.error(f"Error saving file {filename}: {e}") + except Exception: + logger.exception(f"Error saving file {filename}") @property def real_time(self): diff --git a/src/aare/devices/jfjoch.py b/src/aare/devices/jfjoch.py index 46da0749..a0a7de36 100644 --- a/src/aare/devices/jfjoch.py +++ b/src/aare/devices/jfjoch.py @@ -4,6 +4,7 @@ from enum import Enum import jfjoch_client from aarecommon.config.beamline import get_jfjoch_url +from aarecommon.config.logger import setup_logger from aarecommon.errors.exception_handler import JFJochCommunicationError from aarecommon.models.beamline import MXBeamline from aarecommon.models.models import DAQStatusModel, FluorescenceSpectrumOutputModel @@ -12,6 +13,8 @@ from aarecommon.models.rotation_scan import RotationScanRequest from jfjoch_client.api.default_api import DefaultApi from jfjoch_client.api_client import ApiClient +logger = setup_logger("aareDAQ") + class ScanTypeEnum(Enum): RASTER = "Raster" @@ -53,32 +56,33 @@ class JFJochWrapper: return None - def _raise_jfjoch_error( + def _jfjoch_error( self, message: str, *, error: Exception, operation: str, endpoint: str - ) -> None: - raise JFJochCommunicationError( + ) -> JFJochCommunicationError: + """Build the error for a failed JFJoch call; callers ``raise ... from`` it.""" + return JFJochCommunicationError( message, operation=operation, endpoint=endpoint, base_url=self._url, status_code=self._extract_status_code(error), - ) from error + ) def initialize(self): try: self._api.initialize_post() except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( "JFJoch initialize failed", error=e, operation="POST", endpoint="initialize_post" - ) + ) from e def cancel(self): try: self._api.cancel_post() except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( "JFJoch cancel failed", error=e, operation="POST", endpoint="cancel_post" - ) + ) from e def is_idle(self) -> bool: status = self._api.status_get() @@ -207,12 +211,12 @@ class JFJochWrapper: try: self._api.start_post(dataset_settings=dataset_settings) except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( f"JFJoch data collection failed to initialize for {scan_type.value} scan with exception: {e}", error=e, operation="POST", endpoint="start_post", - ) + ) from e def measure_rotation( self, @@ -236,12 +240,12 @@ class JFJochWrapper: self._api.wait_until_running_post_with_http_info(timeout=math.ceil(timeout)) return True except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( f"JFJoch wait until running returned an exception: {e}", error=e, operation="POST", endpoint="wait_until_running_post", - ) + ) from e def wait_till_done(self, timeout: float) -> jfjoch_client.models.ScanResult | None: if self._simulated: @@ -250,23 +254,23 @@ class JFJochWrapper: self._api.wait_till_done_post_with_http_info(timeout=math.ceil(timeout)) return self._api.result_scan_get() except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( f"JFJoch wait till done retrieval returned an exception: {e}", error=e, operation="POST", endpoint="wait_till_done_post / result_scan_get", - ) + ) from e def detector(self) -> jfjoch_client.models.DetectorListElement: try: detector_list = self._api.config_select_detector_get() except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( f"JFJoch detector configuration retrieval failed: {e}", error=e, operation="GET", endpoint="config_select_detector_get", - ) + ) from e if len(detector_list.detectors) == 0: raise JFJochCommunicationError( @@ -280,12 +284,12 @@ class JFJochWrapper: try: return detector_list.detectors[detector_list.current_id] except Exception as e: - self._raise_jfjoch_error( + raise self._jfjoch_error( "JFJoch returned an invalid selected detector entry", error=e, operation="GET", endpoint="config_select_detector_get", - ) + ) from e def take_pedestal(self): raise NotImplementedError( @@ -311,6 +315,7 @@ class JFJochWrapper: show_beam_center=show_beam_center, ) except Exception as e: + logger.debug("Diffraction image retrieval attempt failed; retrying", exc_info=True) last_error = e time.sleep(wait_between_retries_s) raise last_error diff --git a/src/aare/devices/mx_lib.py b/src/aare/devices/mx_lib.py index 1a28678a..c0807eda 100644 --- a/src/aare/devices/mx_lib.py +++ b/src/aare/devices/mx_lib.py @@ -3,8 +3,11 @@ import time from collections.abc import Callable from typing import Any +from aarecommon.config.logger import setup_logger from epics import PV, Motor, poll +logger = setup_logger("aareDAQ") + def wait_for_movement_to_finish(*motors): """ @@ -210,11 +213,12 @@ def wait_motor_position( raises: TimeoutError if a timeout occurs """ if not callable(tester): - raise RuntimeError("argument 'tester' must be a function") + raise TypeError("argument 'tester' must be a function") try: move_time = abs(motor.drive - motor.readback) / motor.speed except Exception: + logger.debug("Could not compute the motor move time; using the 1 s default", exc_info=True) move_time = 1.0 # in case of unusual motor record tout = move_time + time.time() + timeout @@ -251,7 +255,7 @@ def wait_enum_condition( raise AttributeError("argument 'pv' must be an epics.PV of type enum") if not (isinstance(value, (str, int, re.Pattern))): - raise AttributeError("argument 'value' must be either an int, str, or re.Pattern") + raise TypeError("argument 'value' must be either an int, str, or re.Pattern") if type(value) is int: tester = lambda pv: value == pv.get() diff --git a/src/aare/devices/set_get_pv.py b/src/aare/devices/set_get_pv.py index 409b4038..fa849073 100644 --- a/src/aare/devices/set_get_pv.py +++ b/src/aare/devices/set_get_pv.py @@ -2,17 +2,17 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import Any, Union +from typing import Any from epics import PV from aare.devices.mx_lib import pv_wait -RawValue = Union[str, float, int] -ResolverValue = Union[ - RawValue, - tuple[Callable[..., RawValue], tuple[Any, ...]], # (func, args) pattern you already use -] +RawValue = str | float | int +ResolverValue = ( + RawValue + | tuple[Callable[..., RawValue], tuple[Any, ...]] # (func, args) pattern you already use +) @dataclass diff --git a/src/aare/devices/smargon.py b/src/aare/devices/smargon.py index 9977b347..6fc9abad 100644 --- a/src/aare/devices/smargon.py +++ b/src/aare/devices/smargon.py @@ -31,7 +31,7 @@ class Smargon: elif bl == MXBeamline.SIMULATED: self._simulated = True else: - raise Exception("unknown beamline") + raise ValueError("unknown beamline") self._pos = self.SMARGON_HOME self._pos_aero = self.AERO_HOME @@ -175,7 +175,9 @@ class Smargon: target_string = "" if coord.sh_mm is not None: - target_string += f"&SHX={coord.sh_mm.x:.5f}&SHY={coord.sh_mm.y:.5f}&SHZ={coord.sh_mm.z:.5f}" + target_string += ( + f"&SHX={coord.sh_mm.x:.5f}&SHY={coord.sh_mm.y:.5f}&SHZ={coord.sh_mm.z:.5f}" + ) if coord.chi_deg is not None: target_string += f"&CHI={coord.chi_deg:.5f}" if coord.phi_deg is not None: @@ -201,7 +203,9 @@ class Smargon: target_string = "" if coord.at_mm is not None: - target_string += f"&GMX={coord.at_mm.x:.5f}&GMY={coord.at_mm.y:.5f}&GMZ={coord.at_mm.z:.5f}" + target_string += ( + f"&GMX={coord.at_mm.x:.5f}&GMY={coord.at_mm.y:.5f}&GMZ={coord.at_mm.z:.5f}" + ) if coord.omega_deg is not None: target_string += f"&GMU={coord.omega_deg:.5f}" if target_string: diff --git a/src/aare/devices/tell_backend.py b/src/aare/devices/tell_backend.py index 5a638305..8b7f25c1 100644 --- a/src/aare/devices/tell_backend.py +++ b/src/aare/devices/tell_backend.py @@ -326,7 +326,7 @@ class SimTellBackend: } ) except Exception: - logger.warning("Failed to load simulated samples info") + logger.warning("Failed to load simulated samples info", exc_info=True) def abort(self) -> None: self._state = "Ready" diff --git a/src/aare/devices/tell_client.py b/src/aare/devices/tell_client.py index c8590579..baccfd14 100644 --- a/src/aare/devices/tell_client.py +++ b/src/aare/devices/tell_client.py @@ -94,7 +94,7 @@ class TellClient: raise TellConnectionException("Tell reconnecting") elif state == "Closing": raise TellConnectionException("Tell is disconnecting") - raise Exception("Invalid state: " + str(state)) + raise RuntimeError("Invalid state: " + str(state)) def set_in_mount_position(self, value): """tells the robot that the beamlien is safe and to set the in mount position flag allowing mounting @@ -142,6 +142,7 @@ class TellClient: if not self.is_remote_mode(): reasons.append("TELL is not in remote mode") except Exception as e: + logger.debug("Failed to check TELL remote mode", exc_info=True) reasons.append(f"failed to check remote mode: {e}") if reasons: @@ -158,7 +159,7 @@ class TellClient: except Exception as e: raise TellCommunicationError( f"Mount can't start: failed to check door status: {e}", operation="mount_precheck" - ) + ) from e if not door_closed: raise TellCommunicationError( "Mount can't start: TELL doors are open", operation="mount_precheck" @@ -232,6 +233,7 @@ class TellClient: needs_drying = mount_needs_drying + unmount_needs_drying return needs_cooling * 30 + needs_drying * 120 except Exception: + logger.debug("Could not estimate the mounting time", exc_info=True) return 0 def mount( @@ -357,7 +359,7 @@ class TellClient: except TellCommunicationError: raise except Exception as e: - logger.error(f"Exception occurred: {e}") + logger.exception("Exception occurred") raise TellCommunicationError( message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True ) @@ -473,6 +475,7 @@ class TellClient: try: offset = float(self.backend.eval("get_pin_offset()&")) except Exception: + logger.debug("Could not read the pin offset; assuming 0", exc_info=True) offset = 0.0 return offset diff --git a/src/aare/devices/workflow_tools.py b/src/aare/devices/workflow_tools.py old mode 100755 new mode 100644 index 54675670..a75eea0b --- a/src/aare/devices/workflow_tools.py +++ b/src/aare/devices/workflow_tools.py @@ -37,9 +37,7 @@ def wait_position(motor, target, tolerance=None, timeout=60.0): n = n + 1 if n > 20: n = 0 - print( - f"waiting_position test: {tst} ({motor.readback!s}, {target!s}, {tolerance!s})" - ) + print(f"waiting_position test: {tst} ({motor.readback!s}, {target!s}, {tolerance!s})") timeisup = timeout < time.time() condition = ltst(motor.readback, target, tolerance) diff --git a/src/aare/devices/zmq_client.py b/src/aare/devices/zmq_client.py old mode 100755 new mode 100644 index 15e52c9e..da8d389b --- a/src/aare/devices/zmq_client.py +++ b/src/aare/devices/zmq_client.py @@ -42,7 +42,7 @@ class ZMQCameraClient: self._simulated = True self._zmq_url = None else: - raise Exception("unknown beamline") + raise ValueError("unknown beamline") self._timeout_ms = timeout_ms self._context: zmq.Context | None = None @@ -76,7 +76,7 @@ class ZMQCameraClient: logger.debug(f"ZMQ camera connected to {self._zmq_url}") return True except Exception as e: - logger.warning(f"Failed to connect ZMQ camera to {self._zmq_url}: {e}") + logger.warning(f"Failed to connect ZMQ camera to {self._zmq_url}: {e}", exc_info=True) self._connected = False return False @@ -113,6 +113,7 @@ class ZMQCameraClient: header = decoded break except Exception: + logger.debug("Skipping an unparsable ZMQ message part", exc_info=True) continue if header and header.get("type") == "uint8": @@ -137,7 +138,7 @@ class ZMQCameraClient: logger.debug("ZMQ camera: timeout waiting for frame") return self._last_image except Exception as e: - logger.warning(f"ZMQ camera error: {e}") + logger.warning(f"ZMQ camera error: {e}", exc_info=True) self._connected = False return self._last_image @@ -160,6 +161,7 @@ class ZMQCameraClient: self._socket.setsockopt(zmq.RCVTIMEO, old_timeout) return False except Exception: + logger.debug("Camera ZMQ socket is not available", exc_info=True) return False def close(self): @@ -168,13 +170,13 @@ class ZMQCameraClient: try: self._socket.close() except Exception: - pass + logger.debug("Error closing the camera ZMQ socket", exc_info=True) self._socket = None if self._context: try: self._context.term() except Exception: - pass + logger.debug("Error terminating the camera ZMQ context", exc_info=True) self._context = None self._connected = False diff --git a/src/aare/gui/about.py b/src/aare/gui/about.py index c7d29e42..8152fdee 100644 --- a/src/aare/gui/about.py +++ b/src/aare/gui/about.py @@ -4,6 +4,12 @@ import importlib import sys from typing import TYPE_CHECKING +from aarecommon.config.logger import setup_logger + +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + if TYPE_CHECKING: from aare.gui.threads.daq_worker import DAQWorker @@ -14,6 +20,7 @@ def about_text(client: DAQWorker): try: version = importlib.metadata.version("aaredaq") except Exception: + logger.debug("Could not resolve the installed package version", exc_info=True) version = "Not found - package is not installed into the environment." server_version, server_file_path = client.server_about_info() diff --git a/src/aare/gui/auth.py b/src/aare/gui/auth.py index 235d0cd5..92201ca6 100644 --- a/src/aare/gui/auth.py +++ b/src/aare/gui/auth.py @@ -27,6 +27,7 @@ def auth(base_url: str | None, cert_path: str | None) -> str: capture_output=True, text=True, timeout=120.0, + check=False, ) except subprocess.TimeoutExpired as e: logger.error(f"Token request curl timed out: {e}") diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index bd153f1a..5cb233c9 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -1,6 +1,5 @@ import os import sys -import traceback from aarecommon.config.beamline import cfg_get, mx_beamline from aarecommon.config.logger import setup_logger @@ -25,8 +24,8 @@ def main(): banner_path = os.path.join(basedir, "graphics/aare_banner.png") splash_pix = QtGui.QPixmap(banner_path) splash = LoadingSplashScreen(splash_pix) - except Exception as e: - logger.error(f"Failed to load resources for splash screen: {e}") + except Exception: + logger.exception("Failed to load resources for splash screen") sys.exit(1) try: # define application @@ -168,8 +167,7 @@ def main(): splash.set_progress(80, "Authentication successful...") except Exception as e: splash.finish(None) - logger.error(f"Cannot connect to AareDAQ server. Exiting. {e}") - logger.error(traceback.format_exc()) + logger.exception("Cannot connect to AareDAQ server. Exiting.") QMessageBox.critical( None, "Authentication Error", @@ -196,8 +194,7 @@ def main(): except Exception as e: splash.finish(None) - logger.error(f"Error starting GUI: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") + logger.exception("Error starting GUI") QMessageBox.critical( None, diff --git a/src/aare/gui/log.py b/src/aare/gui/log.py index 7bb6bb7d..815b007e 100644 --- a/src/aare/gui/log.py +++ b/src/aare/gui/log.py @@ -16,5 +16,5 @@ class QtLogHandler(logging.Handler): try: msg = self.format(record) self.emitter.message.emit(msg) - except Exception: + except Exception: # noqa: BLE001 - logging here would recurse into this handler self.handleError(record) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index c6cb96e6..061f11df 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -45,7 +45,7 @@ from aare.gui.panels.fluorescence_panel import FluorescencePanel from aare.gui.panels.local_contact_panel import LocalContactDialog # panels -from aare.gui.panels.LogPanel import LogDock +from aare.gui.panels.log_panel import LogDock from aare.gui.panels.loop_centering_panel import LoopCenteringPanel from aare.gui.panels.manual_sample_panel import ManualSamplePanel from aare.gui.panels.portrait_mode import PortraitModePanel @@ -169,8 +169,8 @@ class MainWindow(QMainWindow): payload = jwt.decode(token_str, options={"verify_signature": False}) self._decoded_token = TokenData(**payload) - except Exception as e: - logger.error(f"Failed to decode authentication token: {e}", exc_info=True) + except Exception: + logger.exception("Failed to decode authentication token") QMessageBox.critical( None, "Authentication Error", @@ -934,7 +934,7 @@ class MainWindow(QMainWindow): if hasattr(self, "content_stack") and hasattr(self, "_standard_main_page"): self.content_stack.setCurrentWidget(self._standard_main_page) except Exception as e: - logger.warning(f"Failed to return to main view during shutdown: {e}") + logger.warning(f"Failed to return to main view during shutdown: {e}", exc_info=True) def _restore_samcam_overlay_settings(self) -> None: settings = QSettings("PSI", "AareGUI") @@ -1011,7 +1011,7 @@ class MainWindow(QMainWindow): try: thread.stop() except Exception as e: - logger.warning(f"Failed to stop {attr_name}: {e}") + logger.warning(f"Failed to stop {attr_name}: {e}", exc_info=True) setattr(self, attr_name, None) @@ -1028,6 +1028,7 @@ class MainWindow(QMainWindow): try: activity_name = tell_state.activity.display_name() except Exception: + logger.debug("Could not derive the axis busy text from status", exc_info=True) activity_name = activity_value.capitalize() if activity_value else "Busy" return f"TELL {activity_name}".upper() @@ -1144,7 +1145,7 @@ class MainWindow(QMainWindow): self.portrait_sample_camera._autoscale = True self.portrait_sample_camera._scaling() except Exception: - pass + logger.debug("Could not autoscale the portrait-mode camera", exc_info=True) # ── Hide all chrome that contributes to window width ──────────────── if self.status_bar is not None: @@ -1222,7 +1223,7 @@ class MainWindow(QMainWindow): settings.get("show_overlay_legend", True) ) except Exception: - pass + logger.debug("Could not restore the camera overlay legend", exc_info=True) if hasattr(self, "_pre_portrait_geometry") and self._pre_portrait_geometry: self.restoreGeometry(self._pre_portrait_geometry) @@ -1742,14 +1743,14 @@ class MainWindow(QMainWindow): try: if self.job_list_panel is not None and self.job_list_panel.is_running(): self.job_list_panel.pause_automation() - except Exception as e: - logger.error(f"Failed to pause automation after operation failure: {e}") + except Exception: + logger.exception("Failed to pause automation after operation failure") try: show = QMessageBox.critical if critical else QMessageBox.warning show(self, title, message) - except Exception as e: - logger.error(f"Failed to show operation failure popup: {e}") + except Exception: + logger.exception("Failed to show operation failure popup") def _hutch_blocks_mount(self) -> str | None: """Reason the hutch PSS currently blocks a mount, or None if OK. @@ -1780,8 +1781,8 @@ class MainWindow(QMainWindow): logger.warning(f"Manual mount blocked by hutch PSS: {reason}") try: QMessageBox.critical(self, "Mounting Failed", reason) - except Exception as e: - logger.error(f"Failed to show mount-blocked popup: {e}") + except Exception: + logger.exception("Failed to show mount-blocked popup") return self.daq.mount(sample, reference) @@ -1792,8 +1793,8 @@ class MainWindow(QMainWindow): logger.warning(f"Manual unmount blocked by hutch PSS: {reason}") try: QMessageBox.critical(self, "Unmounting Failed", reason) - except Exception as e: - logger.error(f"Failed to show unmount-blocked popup: {e}") + except Exception: + logger.exception("Failed to show unmount-blocked popup") return self.daq.unmount() @@ -1837,8 +1838,8 @@ class MainWindow(QMainWindow): f"Details:\n{message}" ), ) - except Exception as e: - logger.error(f"Failed to show manual collection detector popup: {e}") + except Exception: + logger.exception("Failed to show manual collection detector popup") @Slot(str) def _on_automation_critical_failure(self, message: str) -> None: @@ -1864,8 +1865,8 @@ class MainWindow(QMainWindow): self.job_list_panel.pause_automation() if self.daq is not None: self.daq.send_status_request() - except Exception as e: - logger.error(f"Failed to pause automation queue after critical failure: {e}") + except Exception: + logger.exception("Failed to pause automation queue after critical failure") # 2. Mark the automation progress widget as finished-with-error so # _is_automation_active() returns False and idle/close timers behave. @@ -1903,8 +1904,8 @@ class MainWindow(QMainWindow): progress.finished = True progress.success = False self.automation_progress_panel.set_progress(progress) - except Exception as e: - logger.error(f"Failed to update automation progress after critical failure: {e}") + except Exception: + logger.exception("Failed to update automation progress after critical failure") self._show_runtime_notification( title="Automation paused", message=message, level="error", sticky=True @@ -1954,8 +1955,8 @@ class MainWindow(QMainWindow): "Please contact your local contact to recover the beamline." ), ) - except Exception as e: - logger.error(f"Failed to surface recovery UI after critical failure: {e}") + except Exception: + logger.exception("Failed to surface recovery UI after critical failure") def show_beamline_recovery(self) -> None: if not bool(getattr(self._decoded_token, "staff", False)): @@ -2035,6 +2036,7 @@ class MainWindow(QMainWindow): ) break except Exception: + logger.debug("Skipping an unreadable GUI session entry", exc_info=True) continue else: if self._remote_close_deadline_ts is not None: @@ -2290,7 +2292,7 @@ class MainWindow(QMainWindow): try: self._return_to_main_view_for_shutdown() except Exception as e: - logger.warning(f"Failed to restore main view before close: {e}") + logger.warning(f"Failed to restore main view before close: {e}", exc_info=True) try: # TODO put all setting related handling into state_manager @@ -2300,18 +2302,18 @@ class MainWindow(QMainWindow): self._save_theme_settings() except Exception as e: - logger.warning(f"Failed to save main window state: {e}") + logger.warning(f"Failed to save main window state: {e}", exc_info=True) # End session before closing so the backend removes this GUI from Redis immediately try: self.daq.end_session_on_close() except Exception as e: - logger.warning(f"Failed to end session on close: {e}") + logger.warning(f"Failed to end session on close: {e}", exc_info=True) try: self.cleanup() except Exception as e: - logger.warning(f"Cleanup during closeEvent failed: {e}") + logger.warning(f"Cleanup during closeEvent failed: {e}", exc_info=True) super().closeEvent(event) @@ -2322,7 +2324,7 @@ class MainWindow(QMainWindow): try: self._return_to_main_view_for_shutdown() except Exception as e: - logger.warning(f"Failed to restore main view during cleanup: {e}") + logger.warning(f"Failed to restore main view during cleanup: {e}", exc_info=True) self._cleanup_done = True @@ -2330,19 +2332,19 @@ class MainWindow(QMainWindow): if hasattr(self, "_samcam_source_timer") and self._samcam_source_timer is not None: self._samcam_source_timer.stop() except Exception as e: - logger.warning(f"Failed to stop _samcam_source_timer: {e}") + logger.warning(f"Failed to stop _samcam_source_timer: {e}", exc_info=True) try: if hasattr(self, "_idle_timer") and self._idle_timer is not None: self._idle_timer.stop() except Exception as e: - logger.warning(f"Failed to stop _idle_timer: {e}") + logger.warning(f"Failed to stop _idle_timer: {e}", exc_info=True) try: if hasattr(self, "_remote_close_timer") and self._remote_close_timer is not None: self._remote_close_timer.stop() except Exception as e: - logger.warning(f"Failed to stop _remote_close_timer: {e}") + logger.warning(f"Failed to stop _remote_close_timer: {e}", exc_info=True) try: if ( @@ -2351,13 +2353,13 @@ class MainWindow(QMainWindow): ): self._axis_camera_refresh_timer.stop() except Exception as e: - logger.warning(f"Failed to stop _axis_camera_refresh_timer: {e}") + logger.warning(f"Failed to stop _axis_camera_refresh_timer: {e}", exc_info=True) try: if hasattr(self, "daq") and self.daq is not None: self.daq.cleanup() except Exception as e: - logger.warning(f"Failed to clean up DAQ worker: {e}") + logger.warning(f"Failed to clean up DAQ worker: {e}", exc_info=True) self._stop_axis_camera_threads() @@ -2371,7 +2373,7 @@ class MainWindow(QMainWindow): try: thread.stop() except Exception as e: - logger.warning(f"Failed to stop {attr_name}: {e}") + logger.warning(f"Failed to stop {attr_name}: {e}", exc_info=True) setattr(self, attr_name, None) @@ -2380,14 +2382,14 @@ class MainWindow(QMainWindow): if hasattr(self, "job_list_panel") and self.job_list_panel is not None: return bool(self.job_list_panel.is_running()) except Exception: - pass + logger.debug("Could not read the job list panel running state", exc_info=True) try: progress = getattr(self.automation_progress_panel, "_progress", None) if progress is not None and not bool(getattr(progress, "finished", False)): return True except Exception: - pass + logger.debug("Could not read the automation progress state", exc_info=True) return False @@ -2405,14 +2407,14 @@ class MainWindow(QMainWindow): try: self.daq.report_gui_interaction(int(self._decoded_token.session)) except Exception as e: - logger.debug(f"Failed to report GUI interaction: {e}") + logger.debug(f"Failed to report GUI interaction: {e}", exc_info=True) def _mark_user_interaction(self) -> None: self._refresh_idle_activity(report_backend=True) def eventFilter(self, obj, event): try: - if event is not None and event.type() in { + if event.type() in { QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease, QEvent.Type.MouseMove, @@ -2425,7 +2427,7 @@ class MainWindow(QMainWindow): }: self._mark_user_interaction() except Exception as e: - logger.debug(f"GUI interaction event filter error: {e}") + logger.debug(f"GUI interaction event filter error: {e}", exc_info=True) return super().eventFilter(obj, event) def _start_remote_close_countdown( diff --git a/src/aare/gui/models/gui_state_manager.py b/src/aare/gui/models/gui_state_manager.py index 91608a2b..07131a6d 100644 --- a/src/aare/gui/models/gui_state_manager.py +++ b/src/aare/gui/models/gui_state_manager.py @@ -1,7 +1,12 @@ import json +from aarecommon.config.logger import setup_logger from PySide6.QtCore import QSettings +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + class UIStateManager: def __init__(self, organization="PSI", app="AareGUI"): @@ -25,8 +30,8 @@ class UIStateManager: data = model.to_list() self.settings.setValue(key, json.dumps(data)) - except Exception as e: - print(f"Failed to save model {key}: {e}") + except Exception: + logger.warning(f"Failed to save model {key}", exc_info=True) def restore_model(self, key: str, model): try: @@ -38,8 +43,8 @@ class UIStateManager: model.from_list(data) - except Exception as e: - print(f"Failed to restore model {key}: {e}") + except Exception: + logger.warning(f"Failed to restore model {key}", exc_info=True) def save_value(self, key: str, value): self.settings.setValue(key, value) diff --git a/src/aare/gui/models/sample_queue_model.py b/src/aare/gui/models/sample_queue_model.py index 1e5d8115..f4b19e1a 100644 --- a/src/aare/gui/models/sample_queue_model.py +++ b/src/aare/gui/models/sample_queue_model.py @@ -1,7 +1,12 @@ +from aarecommon.config.logger import setup_logger from aarecommon.models.models import SampleShortInfo, SampleShortInfoList from PySide6.QtCore import QAbstractTableModel, Qt from PySide6.QtGui import QBrush, QColor +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + def get_entry(sample: SampleShortInfo, column: int, *, show_user: bool = False): if show_user: @@ -105,8 +110,8 @@ class SampleQueueSpreadsheet(QAbstractTableModel): updated_samples.insert(updated_row, sample) self.samples = updated_samples self.endResetModel() - except Exception as e: - print(e) + except Exception: + logger.warning("Failed to handle dropped sample queue rows", exc_info=True) return False return True diff --git a/src/aare/gui/models/user_sample_model.py b/src/aare/gui/models/user_sample_model.py index 55e513cc..14aee66b 100644 --- a/src/aare/gui/models/user_sample_model.py +++ b/src/aare/gui/models/user_sample_model.py @@ -1,9 +1,14 @@ import re +from aarecommon.config.logger import setup_logger from aarecommon.models.models import SampleShortInfo, SampleShortInfoList from PySide6.QtCore import QAbstractTableModel, QMimeData, Qt from PySide6.QtGui import QBrush, QColor +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + def get_entry(sample: SampleShortInfo, column: int): if column == 0: @@ -275,6 +280,7 @@ class UserSampleSpreadsheet(QAbstractTableModel): ) ) except Exception: + logger.debug("Falling back to a plain sort for the column values", exc_info=True) out.sort() else: out.sort() diff --git a/src/aare/gui/panels/beamline_recovery_panel.py b/src/aare/gui/panels/beamline_recovery_panel.py index c83590b5..17933205 100644 --- a/src/aare/gui/panels/beamline_recovery_panel.py +++ b/src/aare/gui/panels/beamline_recovery_panel.py @@ -1,5 +1,6 @@ from __future__ import annotations +from aarecommon.config.logger import setup_logger from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import Slot from PySide6.QtWidgets import ( @@ -14,8 +15,11 @@ from PySide6.QtWidgets import ( QWidget, ) +from aare.gui.constants import LOGGER_NAME from aare.gui.threads.daq_worker import DAQWorker +logger = setup_logger(LOGGER_NAME) + class RecoveryPanel(QWidget): def __init__(self, *, daq: DAQWorker, parent=None): @@ -161,12 +165,14 @@ class RecoveryPanel(QWidget): try: return self._last_status is not None and self._last_status.sample is not None except Exception: + logger.debug("Could not determine whether a sample is mounted", exc_info=True) return False def _beamline_appears_busy(self) -> bool: try: return self._last_status is not None and bool(self._last_status.busy) except Exception: + logger.debug("Could not determine whether the beamline is busy", exc_info=True) return False def _refresh_buttons(self) -> None: diff --git a/src/aare/gui/panels/developer_help_dialog.py b/src/aare/gui/panels/developer_help_dialog.py index aefd0b36..ef1fd449 100644 --- a/src/aare/gui/panels/developer_help_dialog.py +++ b/src/aare/gui/panels/developer_help_dialog.py @@ -4,7 +4,7 @@ import json import logging import time -from aarecommon.config.logger import attach_to_logger, find_existing_formatter +from aarecommon.config.logger import attach_to_logger, find_existing_formatter, setup_logger from aarecommon.errors.codes import error_code_help from aarecommon.models.models import OpenGuiSessionInfo from PySide6.QtCore import Qt, QUrl, Slot @@ -29,9 +29,12 @@ from PySide6.QtWidgets import ( QWidget, ) +from aare.gui.constants import LOGGER_NAME from aare.gui.log import QtLogEmitter, QtLogHandler from aare.gui.threads.daq_worker import DAQWorker +logger = setup_logger(LOGGER_NAME) + class DeveloperHelpDialog(QDialog): def __init__(self, *, daq: DAQWorker, is_staff: bool, parent=None): @@ -389,6 +392,7 @@ class DeveloperHelpDialog(QDialog): try: pretty = json.dumps(p or {}, indent=2, sort_keys=True, default=str) except Exception: + logger.debug("Could not pretty-print the error payload", exc_info=True) pretty = str(p) blocks.append(f"#{i}\n{pretty}") self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)") @@ -595,6 +599,7 @@ class DeveloperHelpDialog(QDialog): try: return int(value) except Exception: + logger.debug("Could not read the selected GUI session id", exc_info=True) return None @Slot() diff --git a/src/aare/gui/panels/face_detection_panel.py b/src/aare/gui/panels/face_detection_panel.py index 2f9e2ef2..46c9e5e2 100644 --- a/src/aare/gui/panels/face_detection_panel.py +++ b/src/aare/gui/panels/face_detection_panel.py @@ -70,6 +70,7 @@ class FaceDetectionPanel(QWidget): self.face_detection_button.setEnabled(False) self.face_detection.emit(int(self.steps), int(self.step_size)) except Exception as e: + logger.warning("Face detection run failed", exc_info=True) self._manual_run_requested = False self.status_lbl.setText(f"Error: {e}") self.face_detection_button.setEnabled(True) diff --git a/src/aare/gui/panels/fluorescence_panel.py b/src/aare/gui/panels/fluorescence_panel.py index e1304508..49e2ccb7 100644 --- a/src/aare/gui/panels/fluorescence_panel.py +++ b/src/aare/gui/panels/fluorescence_panel.py @@ -112,7 +112,7 @@ class FluorescencePanel(QWidget): self._update_vline(pt.x()) return False except Exception as e: - logger.debug(f"eventFilter error: {e}") + logger.debug(f"eventFilter error: {e}", exc_info=True) return False def _show_context_menu(self, pos): @@ -153,10 +153,10 @@ class FluorescencePanel(QWidget): for x, y in zip(x_vals, y_vals): writer.writerow([f"{x:.6f}", f"{y:.6f}"]) logger.info(f"Spectrum saved to {path}") - except Exception as e: - logger.error(f"Failed to save CSV: {e}") - except Exception as e: - logger.error(f"Context menu error: {e}") + except Exception: + logger.exception("Failed to save CSV") + except Exception: + logger.exception("Context menu error") def _update_vline(self, x_val: float): # Draw vertical line spanning current Y axis range at x_val @@ -172,7 +172,7 @@ class FluorescencePanel(QWidget): self._vline.replace(1, x_val, ymax) self._vline.setVisible(True) except Exception as e: - logger.debug(f"vline error: {e}") + logger.debug(f"vline error: {e}", exc_info=True) def _nearest_index(self, x_val: float) -> int: n = self.series.count() @@ -213,6 +213,7 @@ class FluorescencePanel(QWidget): else: self.avg_dead_label.setText("Average dead time: -") except Exception: + logger.debug("Could not compute the average dead time", exc_info=True) self.avg_dead_label.setText("Average dead time: -") # Default peak label @@ -251,8 +252,8 @@ class FluorescencePanel(QWidget): ymax = ymin + 1.0 self.axis_x.setRange(xmin, xmax) self.axis_y.setRange(ymin, ymax) - except Exception as e: - logger.error(f"Update plot error: {e}") + except Exception: + logger.exception("Update plot error") @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): diff --git a/src/aare/gui/panels/local_contact_panel.py b/src/aare/gui/panels/local_contact_panel.py index 52716c54..7b6630c4 100644 --- a/src/aare/gui/panels/local_contact_panel.py +++ b/src/aare/gui/panels/local_contact_panel.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import ClassVar from aarecommon.config.logger import setup_logger from aarecommon.models.models import DAQStatusModel @@ -44,7 +45,7 @@ class LocalContactPanel(QFrame): TAB_DETECTOR = "Detector" TAB_CONFIG = "Config" - DEVICE_TITLES = { + DEVICE_TITLES: ClassVar[dict[str, str]] = { "bec": "BEC", "detector": "Detector", "tell": "TELL", diff --git a/src/aare/gui/panels/LogPanel.py b/src/aare/gui/panels/log_panel.py similarity index 98% rename from src/aare/gui/panels/LogPanel.py rename to src/aare/gui/panels/log_panel.py index e2ad35e9..1de7958c 100644 --- a/src/aare/gui/panels/LogPanel.py +++ b/src/aare/gui/panels/log_panel.py @@ -169,7 +169,9 @@ class LogDock(QDockWidget): def __init__(self, title="Log", parent=None): super().__init__(title, parent) self.setAllowedAreas( - Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea + Qt.DockWidgetArea.BottomDockWidgetArea + | Qt.DockWidgetArea.RightDockWidgetArea + | Qt.DockWidgetArea.LeftDockWidgetArea ) self.container = QWidget(self) diff --git a/src/aare/gui/panels/portrait_mode.py b/src/aare/gui/panels/portrait_mode.py index f3ef9d4d..08b21114 100644 --- a/src/aare/gui/panels/portrait_mode.py +++ b/src/aare/gui/panels/portrait_mode.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import ClassVar + from aarecommon.config.logger import setup_logger from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal, Slot @@ -39,10 +41,10 @@ ACTIVE_STEP = "#FFFFFF" # LED step indicator # --------------------------------------------------------------------------- class LEDStages(QWidget): - STEPS = ["Mount", "Centre", "Raster", "Collect"] + STEPS: ClassVar[list[str]] = ["Mount", "Centre", "Raster", "Collect"] # WorkflowStateKind → LED index - _KIND_TO_INDEX: dict[WorkflowStateKind, int] = { + _KIND_TO_INDEX: ClassVar[dict[WorkflowStateKind, int]] = { WorkflowStateKind.MOUNT: 0, WorkflowStateKind.LOOP_CENTRE: 1, WorkflowStateKind.RASTER: 2, diff --git a/src/aare/gui/panels/prediction_metrics_panel.py b/src/aare/gui/panels/prediction_metrics_panel.py index 906ab9b9..dce257d6 100644 --- a/src/aare/gui/panels/prediction_metrics_panel.py +++ b/src/aare/gui/panels/prediction_metrics_panel.py @@ -13,6 +13,7 @@ import csv import time from collections import deque from dataclasses import dataclass, field +from typing import ClassVar from aarecommon.config.logger import setup_logger from aarecommon.models.models import MLBoxType @@ -110,8 +111,8 @@ def get_class_color(class_name: str) -> str: class ConfidenceHistogramWidget(QWidget): """Real-time histogram of prediction confidence scores.""" - BINS = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] - BIN_COLORS = ["#d62728", "#ff7f0e", "#ffbb78", "#98df8a", "#2ca02c"] + BINS: ClassVar[list[float]] = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] + BIN_COLORS: ClassVar[list[str]] = ["#d62728", "#ff7f0e", "#ffbb78", "#98df8a", "#2ca02c"] def __init__(self, parent=None): super().__init__(parent) @@ -810,5 +811,5 @@ class PredictionMetricsPanel(QWidget): logger.info(f"Exported {len(self._history)} frames to {path}") - except Exception as e: - logger.error(f"Failed to export prediction metrics: {e}") + except Exception: + logger.exception("Failed to export prediction metrics") diff --git a/src/aare/gui/panels/reference_tools_panel.py b/src/aare/gui/panels/reference_tools_panel.py index 711c582c..8708c91b 100644 --- a/src/aare/gui/panels/reference_tools_panel.py +++ b/src/aare/gui/panels/reference_tools_panel.py @@ -64,10 +64,10 @@ class ReferenceToolsModel(QAbstractTableModel): else: self._sorted_samples = [] - def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + def rowCount(self, parent: QModelIndex | None = None) -> int: return len(self._sorted_samples) - def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + def columnCount(self, parent: QModelIndex | None = None) -> int: return len(self.header) def data(self, index: QModelIndex, role=None): @@ -279,4 +279,5 @@ class ReferenceToolsPanel(QFrame): f"Current sample: {sample.sample_name} ({sample.location.segment}{sample.location.pos}-{sample.pin})" ) except Exception as e: + logger.debug("Could not update the current sample label", exc_info=True) self.curr_sample_label.setText(f"Confusing information :/ {e}") diff --git a/src/aare/gui/panels/sample_queue_panel.py b/src/aare/gui/panels/sample_queue_panel.py index 72dc0c8b..a7de0c68 100644 --- a/src/aare/gui/panels/sample_queue_panel.py +++ b/src/aare/gui/panels/sample_queue_panel.py @@ -279,8 +279,8 @@ class SampleQueuePanel(QFrame): try: if self.loop_restart_requested(): return - except Exception as e: - logger.exception(e) + except Exception: + logger.exception("Loop restart request failed") if self.park_and_dry_when_cleared.isChecked(): logger.info("Automation queue empty; parking and drying TELL.") diff --git a/src/aare/gui/panels/smargon_trace_panel.py b/src/aare/gui/panels/smargon_trace_panel.py index cd2bf95a..d1b1fb8e 100644 --- a/src/aare/gui/panels/smargon_trace_panel.py +++ b/src/aare/gui/panels/smargon_trace_panel.py @@ -5,6 +5,7 @@ from math import sqrt from pathlib import Path import numpy as np +from aarecommon.config.logger import setup_logger from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure from PySide6.QtCore import QSettings, Qt, QTimer @@ -23,6 +24,10 @@ from PySide6.QtWidgets import ( QWidget, ) +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + class SmargonTracePanel(QWidget): HOME_X_MM = 0.0 @@ -274,6 +279,7 @@ class SmargonTracePanel(QWidget): ) except Exception as e: + logger.warning(f"Failed to load the smargon trace from {csv_path}", exc_info=True) self._last_rows = [] self._last_distances = [] self._last_lengths = [] diff --git a/src/aare/gui/panels/target_stability_panel.py b/src/aare/gui/panels/target_stability_panel.py index c5bea872..fdbeb00d 100644 --- a/src/aare/gui/panels/target_stability_panel.py +++ b/src/aare/gui/panels/target_stability_panel.py @@ -1407,5 +1407,5 @@ class TargetStabilityPanel(QWidget): if isinstance(raw, (list, tuple)) and len(raw) >= 2: return float(raw[0]), float(raw[1]) except Exception as e: - logger.warning(f"Failed to parse target point {raw}: {e}") + logger.warning(f"Failed to parse target point {raw}: {e}", exc_info=True) return None diff --git a/src/aare/gui/panels/tell_sample_panel.py b/src/aare/gui/panels/tell_sample_panel.py index 2669bd75..035c487f 100644 --- a/src/aare/gui/panels/tell_sample_panel.py +++ b/src/aare/gui/panels/tell_sample_panel.py @@ -236,6 +236,7 @@ class TellSamplePanel(QFrame): current_puck=sample.puck_name, current_sample=sample.db_id ) except Exception as e: + logger.debug("Could not build the TELL sample panel text", exc_info=True) base_text = f"Confusing information :/ {e}" if tell_details: diff --git a/src/aare/gui/scan_logic/raster_grid_manager.py b/src/aare/gui/scan_logic/raster_grid_manager.py index 5016fdf8..c6d7d163 100644 --- a/src/aare/gui/scan_logic/raster_grid_manager.py +++ b/src/aare/gui/scan_logic/raster_grid_manager.py @@ -160,7 +160,22 @@ class RasterGridManager(QObject): return self._active_grid def _is_grid_visible(self, grid: RasterGridRequest): - return bool(grid.visible and abs(normalize_angle(grid.omega_deg - self._geom.omega_deg)) < 0.2 and abs(grid.smargon_top_left.phi_deg - self._geom.smargon.phi_deg) < 0.2 and abs(grid.smargon_top_left.chi_deg - self._geom.smargon.chi_deg) < 0.2 and grid.n_x > 0 and grid.n_y > 0) + if ( + grid.smargon_top_left is None + or grid.smargon_top_left.phi_deg is None + or grid.smargon_top_left.chi_deg is None + or self._geom.smargon.phi_deg is None + or self._geom.smargon.chi_deg is None + ): + raise ValueError(f"Cannot compute visibility for {grid}.") + return bool( + grid.visible + and abs(normalize_angle(grid.omega_deg - self._geom.omega_deg)) < 0.2 + and abs(grid.smargon_top_left.phi_deg - self._geom.smargon.phi_deg) < 0.2 + and abs(grid.smargon_top_left.chi_deg - self._geom.smargon.chi_deg) < 0.2 + and grid.n_x > 0 + and grid.n_y > 0 + ) def _grid_pixel_geometry( self, grid: RasterGridRequest @@ -666,9 +681,12 @@ class RasterGridManager(QObject): ): return - if values is not None and (cell_w < 3.0 or cell_h < 3.0): - if self._draw_completed_grid_fast(painter, grid, values, alpha, visible_rect): - return + if ( + values is not None + and (cell_w < 3.0 or cell_h < 3.0) + and self._draw_completed_grid_fast(painter, grid, values, alpha, visible_rect) + ): + return painter.save() painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) diff --git a/src/aare/gui/threads/axis_video_thread.py b/src/aare/gui/threads/axis_video_thread.py index 7a27ba8d..a132393a 100644 --- a/src/aare/gui/threads/axis_video_thread.py +++ b/src/aare/gui/threads/axis_video_thread.py @@ -1,9 +1,14 @@ import cv2 import numpy as np import requests +from aarecommon.config.logger import setup_logger from PySide6.QtCore import QThread, Signal from PySide6.QtGui import QImage +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + class VideoThread(QThread): frame_ready = Signal(QImage) @@ -78,6 +83,7 @@ class VideoThread(QThread): except requests.exceptions.RequestException as e: self.error_occurred.emit(f"Connection error: {e!s}") except Exception as e: + logger.warning("Axis video stream failed", exc_info=True) self.error_occurred.emit(f"Unexpected error: {e!s}") finally: self.running = False @@ -85,7 +91,7 @@ class VideoThread(QThread): try: self.session.close() except Exception: - pass + logger.debug("Error closing the Axis camera session", exc_info=True) self.session = None def _process_buffer(self, buffer, boundary): @@ -131,6 +137,7 @@ class VideoThread(QThread): except Exception: # If frame processing fails, use last good frame if available + logger.debug("Frame processing failed; reusing the last good frame", exc_info=True) if self.last_good_frame is not None: self.frame_ready.emit(self.last_good_frame) @@ -143,16 +150,16 @@ class VideoThread(QThread): try: session.close() except Exception: - pass + logger.debug("Error closing the Axis camera session", exc_info=True) finally: self.session = None try: self.quit() except Exception: - pass + logger.debug("Error while quitting the Axis video thread", exc_info=True) try: self.wait(5000) except Exception: - pass + logger.debug("Error while waiting for the Axis video thread to finish", exc_info=True) diff --git a/src/aare/gui/threads/camera_thread.py b/src/aare/gui/threads/camera_thread.py index 310a84dc..e8fd54e9 100644 --- a/src/aare/gui/threads/camera_thread.py +++ b/src/aare/gui/threads/camera_thread.py @@ -4,11 +4,16 @@ import time import cv2 import numpy as np import zmq +from aarecommon.config.logger import setup_logger from aarecommon.math.autofocus import focus_measure_edges from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import QThread, Signal, Slot from PySide6.QtGui import QImage, QPixmap +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + class SampleCameraThread(QThread): # Define a signal to communicate messages from the thread to the main GUI @@ -97,6 +102,9 @@ class SampleCameraThread(QThread): header = decoded break except Exception: + logger.debug( + "Skipping an unparsable camera ZMQ message part", exc_info=True + ) continue if header: @@ -160,6 +168,7 @@ class SampleCameraThread(QThread): self._set_camera_available(False, "Sample camera feed unavailable") continue # Check self.running again except Exception as e: + logger.warning("Sample camera feed unavailable", exc_info=True) self._set_camera_available(False, f"Sample camera feed unavailable: {e}") self.running = False diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index 4683948f..ca0e7765 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -8,7 +8,7 @@ import time from collections import deque from dataclasses import dataclass from datetime import datetime -from typing import Literal, cast +from typing import ClassVar, Literal, cast from aarecommon.config.logger import setup_logger from aarecommon.errors.codes import AareErrorCode, AuthErrorCode, export_error_codes @@ -367,7 +367,7 @@ class DAQWorker(QObject): file = str(file_reply.readAll()) if file_reply.waitForReadyRead(500) else "Not connected" return version, file - _SELF_SIGNED_ERRORS = { + _SELF_SIGNED_ERRORS: ClassVar[set[QSslError.SslError]] = { QSslError.SslError.SelfSignedCertificate, QSslError.SslError.SelfSignedCertificateInChain, } @@ -564,7 +564,7 @@ class DAQWorker(QObject): if aerotech_detail: err_msg = aerotech_detail except Exception: - logger.error(f"Exception reading status response: {e}") + logger.exception("Exception reading status response") if status == 403: self._last_status_can_read = False @@ -592,7 +592,7 @@ class DAQWorker(QObject): self._last_smargon_connected = None self._last_aerotech_connected = None - logger.error(f"Exception from status response: {e}") + logger.exception("Exception from status response") @Slot(QNetworkReply) def handle_spreadsheet_response(self, reply: QNetworkReply): @@ -601,7 +601,7 @@ class DAQWorker(QObject): parsed_response = SampleShortInfoList.model_validate_json(response_data) self.spreadsheet.emit(parsed_response) except Exception as e: - logger.error(f"Exception from spreadsheet response: {e}") + logger.exception("Exception from spreadsheet response") self.http_error.emit(str(e)) @Slot(QNetworkReply) @@ -611,7 +611,7 @@ class DAQWorker(QObject): parsed_response = SampleShortInfoList.model_validate_json(response_data) self.reference_tools.emit(parsed_response) except Exception as e: - logger.error(f"Exception from reference tools response: {e}") + logger.exception("Exception from reference tools response") self.http_error.emit(str(e)) def handle_req_response(self, reply: QNetworkReply): @@ -623,11 +623,12 @@ class DAQWorker(QObject): try: raw_body = reply.readAll().data().decode("utf-8") except Exception: - pass + logger.debug("Could not read the raw reply body", exc_info=True) try: url = reply.request().url().toString() except Exception: + logger.debug("Could not read the request URL", exc_info=True) url = "" net_err = reply.error() @@ -691,7 +692,7 @@ class DAQWorker(QObject): self.sample_resync_completed.emit(message) self.send_status_request() except Exception as e: - logger.error(f"Sample resync failed: {e}") + logger.exception("Sample resync failed") self.http_error.emit(str(e)) def _handle_detector_metadata_resync_response(self, reply: QNetworkReply): @@ -705,7 +706,7 @@ class DAQWorker(QObject): self.send_status_request() self.load_local_contact_device_state() except Exception as e: - logger.error(f"Hardware metadata resync failed: {e}") + logger.exception("Hardware metadata resync failed") self.http_error.emit(str(e)) def _handle_recovery_action_response(self, reply: QNetworkReply, default_message: str): @@ -718,7 +719,7 @@ class DAQWorker(QObject): self.recovery_action_completed.emit(message) self.send_status_request() except Exception as e: - logger.error(f"Recovery action failed: {e}") + logger.exception("Recovery action failed") self.http_error.emit(str(e)) def _emit_placeholder_local_contact_action(self, action_name: str) -> None: @@ -949,12 +950,12 @@ class DAQWorker(QObject): arr = json.loads(response_data) if response_data else [] if not isinstance(arr, list): - raise RuntimeError("Invalid all_pgroups payload") + raise TypeError("Invalid all_pgroups payload") # Ensure list[str] out = [str(x) for x in arr if isinstance(x, (str, int))] self.staff_pgroups_loaded.emit(out) except Exception as e: - logger.error(f"Exception from all_pgroups response: {e}") + logger.exception("Exception from all_pgroups response") self.http_error.emit(str(e)) @Slot() @@ -1017,7 +1018,7 @@ class DAQWorker(QObject): parsed_response = CompletedRotationScan.model_validate_json(response_data) self.standard_scan_completed.emit(parsed_response) except Exception as e: - logger.error(f"Exception from rotation scan response: {e}") + logger.exception("Exception from rotation scan response") self.http_error.emit(str(e)) finally: reply.deleteLater() @@ -1072,7 +1073,7 @@ class DAQWorker(QObject): parsed_response = CompletedRasterGrid.model_validate_json(response_data) self.raster_scan_completed.emit(parsed_response) except Exception as e: - logger.error(f"Exception from raster scan response: {e}") + logger.exception("Exception from raster scan response") self.http_error.emit(str(e)) finally: reply.deleteLater() @@ -1242,9 +1243,10 @@ class DAQWorker(QObject): else: err_msg = raw_body except Exception: + logger.debug("Could not parse the error body as JSON", exc_info=True) err_msg = raw_body except Exception: - pass + logger.debug("Could not extract error details from the reply body", exc_info=True) return status, str(err_msg), body_json @@ -1278,6 +1280,7 @@ class DAQWorker(QObject): try: return code in {item.value for item in AuthErrorCode} except Exception: + logger.debug("Could not classify the reply as an auth error", exc_info=True) return False @staticmethod @@ -1296,6 +1299,7 @@ class DAQWorker(QObject): try: status_int = int(status) if status is not None else None except Exception: + logger.debug("Could not read the reply status code", exc_info=True) status_int = None code = None @@ -1314,7 +1318,11 @@ class DAQWorker(QObject): ): return True - return bool("daq state error" in text or "must be idle to start measurement" in text or "must be idle" in text) + return bool( + "daq state error" in text + or "must be idle to start measurement" in text + or "must be idle" in text + ) def handle_auto_scan_response(self, reply, sample_id: int): if reply.error() == QNetworkReply.NetworkError.NoError: @@ -1470,6 +1478,7 @@ class DAQWorker(QObject): try: url = reply.request().url().toString() except Exception: + logger.debug("Could not read the request URL", exc_info=True) url = "unknown-url" return f"{context}\n\nURL: {url}\nHTTP status: {status}\nError: {exc}" @@ -1501,9 +1510,10 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else {} if not isinstance(payload, dict): - raise RuntimeError("Invalid local contact simulation state payload") + raise TypeError("Invalid local contact simulation state payload") self.local_contact_simulation_state_loaded.emit(payload) except Exception as e: + logger.warning("Local Contact simulation state request failed", exc_info=True) message = self._build_local_contact_error_message( "Error transferring information from DAQ while loading Local Contact simulation state.", reply, @@ -1530,9 +1540,10 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else {} if not isinstance(payload, dict): - raise RuntimeError("Invalid local contact device state payload") + raise TypeError("Invalid local contact device state payload") self.local_contact_device_state_loaded.emit(payload) except Exception as e: + logger.warning("Local Contact device state request failed", exc_info=True) message = self._build_local_contact_error_message( "Error transferring information from DAQ while loading Local Contact device state.", reply, @@ -1559,9 +1570,10 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else {} if not isinstance(payload, dict): - raise RuntimeError("Invalid local contact links payload") + raise TypeError("Invalid local contact links payload") self.local_contact_links_loaded.emit(payload) except Exception as e: + logger.warning("Local Contact links request failed", exc_info=True) message = self._build_local_contact_error_message( "Error transferring information from DAQ while loading Local Contact links.", reply, @@ -1591,14 +1603,14 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else {} if not isinstance(payload, dict): - raise RuntimeError("Invalid local contact config payload") + raise TypeError("Invalid local contact config payload") self.local_contact_config_loaded.emit(payload) except Exception as e: message = ( "Error transferring information from DAQ while loading Local Contact config.\n\n" f"{e}" ) - logger.error(message) + logger.exception(message) self.local_contact_transfer_error.emit(message) @Slot(dict) @@ -1619,7 +1631,7 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else {} if not isinstance(payload, dict): - raise RuntimeError("Invalid local contact config response") + raise TypeError("Invalid local contact config response") self.local_contact_config_saved.emit(payload) self.local_contact_config_loaded.emit(payload) self.status_message.emit("Local Contact config saved.", False) @@ -1627,7 +1639,7 @@ class DAQWorker(QObject): message = ( f"Error transferring information from DAQ while saving Local Contact config.\n\n{e}" ) - logger.error(message) + logger.exception(message) self.local_contact_transfer_error.emit(message) @Slot(str, bool) @@ -1659,10 +1671,10 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else [] if not isinstance(payload, list): - raise RuntimeError("Invalid BEC user macros payload") + raise TypeError("Invalid BEC user macros payload") self.bec_user_macros_loaded.emit([str(item) for item in payload]) except Exception as e: - logger.error(f"Failed to list BEC user macros: {e}") + logger.exception("Failed to list BEC user macros") self.http_error.emit(str(e)) @Slot() @@ -1682,10 +1694,10 @@ class DAQWorker(QObject): response_data = self.handle_response(reply) payload = json.loads(response_data) if response_data else [] if not isinstance(payload, list): - raise RuntimeError("Invalid BEC devices payload") + raise TypeError("Invalid BEC devices payload") self.bec_devices_loaded.emit([str(item) for item in payload]) except Exception as e: - logger.error(f"Failed to list BEC devices: {e}") + logger.exception("Failed to list BEC devices") self.http_error.emit(str(e)) @Slot(str) @@ -1748,7 +1760,7 @@ class DAQWorker(QObject): parsed_response = RasterGridRequest.model_validate_json(response_data) self.raster_generated_by_ml.emit(parsed_response) except Exception as e: - logger.error(f"Error in ml box: {e}") + logger.exception("Error in ml box") self.http_error.emit(str(e)) @Slot() @@ -1776,7 +1788,7 @@ class DAQWorker(QObject): data = json.loads(payload) self.face_detection_result.emit(data) except Exception as e: - logger.error(f"Error in face detection: {e}") + logger.exception("Error in face detection") self.http_error.emit(str(e)) finally: reply.deleteLater() @@ -1790,8 +1802,8 @@ class DAQWorker(QObject): if payload: data = json.loads(payload) self.face_detection_result.emit(data) - except Exception as e: - logger.error(f"Face detection stream parse error: {e}") + except Exception: + logger.exception("Face detection stream parse error") def _restart_face_detection_stream(self): reply = self._face_detection_stream_reply @@ -1802,6 +1814,7 @@ class DAQWorker(QObject): try: status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) except Exception: + logger.debug("Could not read the face detection stream status", exc_info=True) status = None if status == 403: @@ -1961,8 +1974,8 @@ class DAQWorker(QObject): self._automation_progress_buffer += chunk self._process_automation_progress_buffer() - except Exception as e: - logger.error(f"Automation progress stream parse error: {e}") + except Exception: + logger.exception("Automation progress stream parse error") def _restart_automation_progress_stream(self): reply = self._automation_progress_stream_reply @@ -1974,6 +1987,7 @@ class DAQWorker(QObject): try: status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) except Exception: + logger.debug("Could not read the automation progress stream status", exc_info=True) status = None if status == 403: @@ -2072,7 +2086,7 @@ class DAQWorker(QObject): else: self.fluorimeter_update.emit(data, [], -1) except Exception as e: - logger.error(f"Fluorimeter data error: {e}") + logger.exception("Fluorimeter data error") self.http_error.emit(str(e)) def _handle_fluorimeter_status_and_emit(self, data, status_reply: QNetworkReply): @@ -2084,7 +2098,7 @@ class DAQWorker(QObject): b_reply = self._net_manager.get(b_req) b_reply.finished.connect(lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply)) except Exception as e: - logger.error(f"Fluorimeter status error: {e}") + logger.exception("Fluorimeter status error") self.http_error.emit(str(e)) def _emit_fluorimeter_with_bkg(self, data, s, b_reply: QNetworkReply): @@ -2095,7 +2109,7 @@ class DAQWorker(QObject): bkg = json.loads(bkg_json) if bkg_json else [] self.fluorimeter_update.emit(data, bkg, s) except Exception as e: - logger.error(f"Fluorimeter background error: {e}") + logger.exception("Fluorimeter background error") self.http_error.emit(str(e)) @Slot() @@ -2115,7 +2129,7 @@ class DAQWorker(QObject): parsed_response = FluorescenceSpectrumOutputModel.model_validate_json(response_data) self.fluorimeter_spectrum_update.emit(parsed_response) except Exception as e: - logger.error(f"Exception from fluorimeter spectrum: {e}") + logger.exception("Exception from fluorimeter spectrum") self.http_error.emit(str(e)) @Slot() @@ -2149,8 +2163,8 @@ class DAQWorker(QObject): bkg = obj.get("background") or [] status = obj.get("status", -1) self.fluorimeter_update.emit(data, bkg, status) - except Exception as e: - logger.error(f"SSE parse error: {e}") + except Exception: + logger.exception("SSE parse error") @staticmethod def _flatten_error_codes_payload(obj: dict) -> dict[str, str]: @@ -2202,6 +2216,7 @@ class DAQWorker(QObject): try: url = reply.request().url().toString() except Exception: + logger.debug("Could not read the request URL", exc_info=True) url = "" if int(status) == 404 and url.endswith("/meta/error-codes"): @@ -2212,11 +2227,11 @@ class DAQWorker(QObject): payload = self.handle_response(reply) obj = json.loads(payload) if payload else {} if not isinstance(obj, dict): - raise RuntimeError("Invalid error-codes payload (expected JSON object)") + raise TypeError("Invalid error-codes payload (expected JSON object)") out = self._flatten_error_codes_payload(obj) self.error_codes_loaded.emit(out) except Exception as e: - logger.error(f"Failed to load error codes: {e}") + logger.exception("Failed to load error codes") self.http_error.emit(str(e)) @Slot(str, str) @@ -2292,8 +2307,8 @@ class DAQWorker(QObject): self._last_baton_status = status self.baton_status_changed.emit(status) - except Exception as e: - logger.error(f"Baton stream parse error: {e}") + except Exception: + logger.exception("Baton stream parse error") @Slot() def request_baton(self): @@ -2328,7 +2343,7 @@ class DAQWorker(QObject): elif result.get("error"): self.status_message.emit(result.get("message", "Request failed"), True) except Exception as e: - logger.error(f"Baton request failed: {e}") + logger.exception("Baton request failed") self.http_error.emit(str(e)) @Slot(bool) @@ -2363,7 +2378,7 @@ class DAQWorker(QObject): self.start_baton_stream() self._restart_blocked_sse_streams_if_access_restored() except Exception as e: - logger.error(f"Baton response failed: {e}") + logger.exception("Baton response failed") self.http_error.emit(str(e)) @Slot() @@ -2398,7 +2413,7 @@ class DAQWorker(QObject): self.start_baton_stream() self._restart_blocked_sse_streams_if_access_restored() except Exception as e: - logger.error(f"Baton timeout check failed: {e}") + logger.exception("Baton timeout check failed") self.http_error.emit(str(e)) def release_baton_on_close(self): @@ -2427,7 +2442,7 @@ class DAQWorker(QObject): logger.info("End session requested on GUI close") except Exception as e: - logger.warning(f"Error ending session on close: {e}") + logger.warning(f"Error ending session on close: {e}", exc_info=True) def _handle_gui_sessions_response(self, reply: QNetworkReply): try: @@ -2436,7 +2451,7 @@ class DAQWorker(QObject): sessions = [OpenGuiSessionInfo.model_validate(item) for item in payload] self.gui_sessions_loaded.emit(sessions) except Exception as e: - logger.error(f"Failed to load GUI sessions: {e}") + logger.exception("Failed to load GUI sessions") self.http_error.emit(str(e)) @Slot() @@ -2489,7 +2504,7 @@ class DAQWorker(QObject): _ = self.handle_response(reply) self.load_gui_sessions() except Exception as e: - logger.error(f"GUI session mutation failed: {e}") + logger.exception("GUI session mutation failed") self.http_error.emit(str(e)) self.load_gui_sessions() @@ -2515,13 +2530,13 @@ class DAQWorker(QObject): if hasattr(self, "_baton_timeout_timer") and self._baton_timeout_timer is not None: self._baton_timeout_timer.stop() except Exception as e: - logger.warning(f"Failed to stop _baton_timeout_timer: {e}") + logger.warning(f"Failed to stop _baton_timeout_timer: {e}", exc_info=True) try: if hasattr(self, "_timer") and self._timer is not None: self._timer.stop() except Exception as e: - logger.warning(f"Failed to stop __timer: {e}") + logger.warning(f"Failed to stop __timer: {e}", exc_info=True) for attr_name in ( "_baton_stream_reply", @@ -2535,12 +2550,12 @@ class DAQWorker(QObject): try: reply.abort() except Exception as e: - logger.warning(f"Failed to abort {attr_name}: {e}") + logger.warning(f"Failed to abort {attr_name}: {e}", exc_info=True) try: reply.deleteLater() except Exception as e: - logger.warning(f"Failed to delete {attr_name}: {e}") + logger.warning(f"Failed to delete {attr_name}: {e}", exc_info=True) setattr(self, attr_name, None) diff --git a/src/aare/gui/threads/jfjoch_viewer.py b/src/aare/gui/threads/jfjoch_viewer.py index 36407b36..102ef3b0 100644 --- a/src/aare/gui/threads/jfjoch_viewer.py +++ b/src/aare/gui/threads/jfjoch_viewer.py @@ -1,7 +1,12 @@ from aarecommon.config.beamline import get_jfjoch_url, mx_beamline +from aarecommon.config.logger import setup_logger from PySide6.QtCore import QObject, Slot from PySide6.QtDBus import QDBusConnection, QDBusInterface +from aare.gui.constants import LOGGER_NAME + +logger = setup_logger(LOGGER_NAME) + class JFJochDBusClient(QObject): def __init__(self): @@ -25,8 +30,8 @@ class JFJochDBusClient(QObject): else: print("D-Bus not available") - except Exception as e: - print(f"D-Bus not available: {e}.") + except Exception: + logger.warning("D-Bus not available", exc_info=True) def _ensure_interface(self): """Ensure we have a valid interface, creating one if needed.""" diff --git a/src/aare/gui/threads/prediction_subscriber.py b/src/aare/gui/threads/prediction_subscriber.py index 2d03e4d5..044fe99f 100644 --- a/src/aare/gui/threads/prediction_subscriber.py +++ b/src/aare/gui/threads/prediction_subscriber.py @@ -91,6 +91,7 @@ class PredictionSubscriber(QThread): decoded = json.loads(part.decode("utf-8")) return decoded if isinstance(decoded, dict) else None except Exception: + logger.debug("Could not parse the prediction payload as JSON", exc_info=True) return None def _decode_rgb_image(self, header: dict, data: bytes) -> np.ndarray | None: @@ -231,13 +232,13 @@ class PredictionSubscriber(QThread): except Exception as e: if self.running: self._set_camera_available(False, f"Sample camera feed unavailable: {e}") - logger.exception(f"PredictionSubscriber error: {e}") + logger.exception("PredictionSubscriber error") finally: try: if self._sock is not None: self._sock.close(0) except Exception: - pass + logger.debug("Error closing the prediction ZMQ socket", exc_info=True) finally: self._sock = None @@ -245,7 +246,7 @@ class PredictionSubscriber(QThread): if self._ctx is not None: self._ctx.term() except Exception: - pass + logger.debug("Error terminating the prediction ZMQ context", exc_info=True) finally: self._ctx = None @@ -257,7 +258,7 @@ class PredictionSubscriber(QThread): if self._sock is not None: self._sock.close(0) except Exception: - pass + logger.debug("Error while stopping the prediction subscriber", exc_info=True) if not self.wait(1500): logger.warning("PredictionSubscriber did not stop within timeout") diff --git a/src/aare/gui/threads/sse_client.py b/src/aare/gui/threads/sse_client.py index 63f32295..78f3ca8e 100644 --- a/src/aare/gui/threads/sse_client.py +++ b/src/aare/gui/threads/sse_client.py @@ -1,3 +1,4 @@ +from typing import ClassVar from PySide6.QtCore import QByteArray, QObject, QTimer, QUrl, Signal, Slot from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest, QSslError @@ -61,7 +62,7 @@ class SSEClient(QObject): """Check if connected to SSE""" return self._connected and self._reply and self._reply.isOpen() - _SELF_SIGNED_ERRORS = { + _SELF_SIGNED_ERRORS: ClassVar[set[QSslError.SslError]] = { QSslError.SslError.SelfSignedCertificate, QSslError.SslError.SelfSignedCertificateInChain, } diff --git a/src/aare/gui/tutorials/tutorial_manager.py b/src/aare/gui/tutorials/tutorial_manager.py index c9fa6ae8..87745a4d 100644 --- a/src/aare/gui/tutorials/tutorial_manager.py +++ b/src/aare/gui/tutorials/tutorial_manager.py @@ -4,6 +4,7 @@ import time from dataclasses import dataclass from typing import Any +from aarecommon.config.logger import setup_logger from PySide6.QtCore import ( Property, QEasingCurve, @@ -17,6 +18,7 @@ from PySide6.QtCore import ( from PySide6.QtGui import QColor, QPainter, QPen from PySide6.QtWidgets import QLabel, QPushButton, QWidget +from aare.gui.constants import LOGGER_NAME from aare.gui.tutorials.tutorial_models import ( StepFlow, StepStatus, @@ -36,6 +38,8 @@ from aare.gui.tutorials.tutorial_runtime import ( ensure_step_runtime_state, ) +logger = setup_logger(LOGGER_NAME) + def _now_ms() -> int: return int(time.time() * 1000) @@ -169,11 +173,14 @@ class TutorialOverlay(QWidget): super().keyPressEvent(event) def mousePressEvent(self, event) -> None: - if self._waiting_for_click and self.current_rect.isValid(): - if self.current_rect.contains(event.pos()): - self.highlight_clicked.emit() - event.accept() - return + if ( + self._waiting_for_click + and self.current_rect.isValid() + and self.current_rect.contains(event.pos()) + ): + self.highlight_clicked.emit() + event.accept() + return super().mousePressEvent(event) def paintEvent(self, event) -> None: @@ -534,6 +541,7 @@ class TutorialManager(QObject): try: return self.target_resolver.resolve_target(step.target, self.context) except Exception: + logger.debug("Could not resolve the tutorial step target", exc_info=True) return None def _resolved_target_rect(self, resolved: ResolvedTutorialTarget | None) -> QRect | None: diff --git a/src/aare/gui/tutorials/tutorial_runtime.py b/src/aare/gui/tutorials/tutorial_runtime.py index 27d1b996..1d8f1bf1 100644 --- a/src/aare/gui/tutorials/tutorial_runtime.py +++ b/src/aare/gui/tutorials/tutorial_runtime.py @@ -3,9 +3,11 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Protocol, runtime_checkable +from aarecommon.config.logger import setup_logger from PySide6.QtCore import QObject, QRect, Signal from PySide6.QtWidgets import QWidget +from aare.gui.constants import LOGGER_NAME from aare.gui.tutorials.tutorial_models import ( CompletionKind, CompletionRule, @@ -19,6 +21,8 @@ from aare.gui.tutorials.tutorial_models import ( TutorialTextRef, ) +logger = setup_logger(LOGGER_NAME) + @dataclass(slots=True) class ResolvedTutorialTarget: @@ -148,6 +152,7 @@ class DictionaryTextResolver: try: return template.format(**text.args) except Exception: + logger.debug("Could not resolve the tutorial text template", exc_info=True) return template diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 6cae2c24..5d66ea2f 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -193,7 +193,7 @@ class SampleCameraImageLabel(QGraphicsView): self._det_shape = payload.get("shape", None) self._detections = payload.get("boxes", []) or [] except Exception as e: - logger.debug(f"Exception in update_detections: {e}") + logger.debug(f"Exception in update_detections: {e}", exc_info=True) self._detections = [] self.update() @@ -224,7 +224,7 @@ class SampleCameraImageLabel(QGraphicsView): ) self._last_target_update_ts = now except Exception as e: - logger.debug(f"Exception in update_target_point: {e}") + logger.debug(f"Exception in update_target_point: {e}", exc_info=True) self._target_point = None self._smoothed_target_point = None self.update() @@ -445,13 +445,14 @@ class SampleCameraImageLabel(QGraphicsView): self.mapToGlobal(event.pos()), f"{mouse_pos.x():.0f}, {mouse_pos.y():.0f} pxl", self ) - if self._state == SampleCameraImageState.IDLE: - if event.modifiers() & Qt.KeyboardModifier.ShiftModifier: - if not self.raster_timer.isActive(): - self.load_image.emit(mouse_pos) - self.raster_timer.start(self.raster_timer_interval) - else: - self._pending_load_pos = mouse_pos + if self._state == SampleCameraImageState.IDLE and ( + event.modifiers() & Qt.KeyboardModifier.ShiftModifier + ): + if not self.raster_timer.isActive(): + self.load_image.emit(mouse_pos) + self.raster_timer.start(self.raster_timer_interval) + else: + self._pending_load_pos = mouse_pos self.end_point = mouse_pos @@ -734,7 +735,7 @@ class SampleCameraImageLabel(QGraphicsView): label = str(det.get("label", "")).lower() conf = det.get("conf", 0.0) except Exception as e: - logger.debug(f"Error in draw detection {det}: {e}") + logger.debug(f"Error in draw detection {det}: {e}", exc_info=True) continue color = color_map.get(label, QColor("magenta")) @@ -774,7 +775,7 @@ class SampleCameraImageLabel(QGraphicsView): if isinstance(raw, (list, tuple)) and len(raw) >= 2: return float(raw[0]), float(raw[1]) except Exception as e: - logger.debug(f"Error parsing target point {raw}: {e}") + logger.debug(f"Error parsing target point {raw}: {e}", exc_info=True) return None def _draw_target_point(self, painter: QPainter): @@ -797,7 +798,10 @@ class SampleCameraImageLabel(QGraphicsView): img_h, img_w = int(shape[0]), int(shape[1]) tx, ty = self._smoothed_target_point except Exception as e: - logger.debug(f"Error using smoothed target point {self._smoothed_target_point}: {e}") + logger.debug( + f"Error using smoothed target point {self._smoothed_target_point}: {e}", + exc_info=True, + ) return sx = pix.width() / float(img_w) @@ -1120,11 +1124,14 @@ class SampleCameraImageLabel(QGraphicsView): mouse_view_pos = self.mapFromGlobal(mouse_global_pos) mouse_scene_pos = self.mapToScene(mouse_view_pos) - if event.key() == Qt.Key.Key_Shift: - if self._state == SampleCameraImageState.IDLE and self._camera_interaction_enabled(): - if not self.raster_timer.isActive(): - self.load_image.emit(mouse_scene_pos) - self.raster_timer.start(self.raster_timer_interval) + if ( + event.key() == Qt.Key.Key_Shift + and self._state == SampleCameraImageState.IDLE + and self._camera_interaction_enabled() + and not self.raster_timer.isActive() + ): + self.load_image.emit(mouse_scene_pos) + self.raster_timer.start(self.raster_timer_interval) super().keyPressEvent(event) diff --git a/src/aare/gui/widgets/local_contact_status_widget.py b/src/aare/gui/widgets/local_contact_status_widget.py index c8253b2d..328e9157 100644 --- a/src/aare/gui/widgets/local_contact_status_widget.py +++ b/src/aare/gui/widgets/local_contact_status_widget.py @@ -1,16 +1,21 @@ from __future__ import annotations from collections.abc import Iterable +from typing import ClassVar +from aarecommon.config.logger import setup_logger from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import Qt, Slot from PySide6.QtWidgets import QFrame, QGridLayout, QLabel, QSizePolicy, QVBoxLayout +from aare.gui.constants import LOGGER_NAME from aare.gui.widgets.title_label import TitleLabel +logger = setup_logger(LOGGER_NAME) + class LocalContactStatusWidget(QFrame): - FIELD_TITLES = { + FIELD_TITLES: ClassVar[dict[str, str]] = { "beamline_state": "Beamline state", "busy": "Busy", "sample": "Sample", @@ -202,6 +207,7 @@ class LocalContactStatusWidget(QFrame): try: sample_loc = f" ({status.sample.loc_str()})" except Exception: + logger.debug("Could not format the sample location", exc_info=True) sample_loc = "" if sample_name: return f"{self._badge('MOUNTED', tone='info')} {sample_name}{sample_loc}" @@ -351,7 +357,7 @@ class LocalContactStatusWidget(QFrame): def _refresh(self) -> None: if self._last_status is None: - for (_title, value) in self._row_widgets.values(): + for _title, value in self._row_widgets.values(): value.setText(self._badge("WAITING", tone="neutral")) return diff --git a/src/aare/gui/widgets/message_box.py b/src/aare/gui/widgets/message_box.py index 80692363..87c38fac 100644 --- a/src/aare/gui/widgets/message_box.py +++ b/src/aare/gui/widgets/message_box.py @@ -114,9 +114,9 @@ def timer_box( # Auto-accept (as if user pressed Yes) box.done(QMessageBox.StandardButton.Yes) stop_timer() - except Exception as e: + except Exception: # Never let errors in condition_func block UI - logger.error(f"Error in condition_func: {e}") + logger.exception("Error in condition_func") box.accepted.connect(stop_timer) box.rejected.connect(stop_timer) diff --git a/src/aare/gui/widgets/status_bar.py b/src/aare/gui/widgets/status_bar.py index b182b531..8a600382 100644 --- a/src/aare/gui/widgets/status_bar.py +++ b/src/aare/gui/widgets/status_bar.py @@ -230,8 +230,8 @@ class StatusBar(QStatusBar): html_content_session = f"""Session: {session_flag}""" self.session_label.setText(html_content_session) - except Exception as e: - logger.error(f"Error updating DAQ status in status bar: {e}") + except Exception: + logger.exception("Error updating DAQ status in status bar") @Slot(BatonStatus) def update_baton_status(self, status: BatonStatus): @@ -263,8 +263,8 @@ class StatusBar(QStatusBar): logger.info("Baton request no longer incoming, closing local dialog reference") try: self._baton_request_dialog.close() - except Exception as e: - logger.error(f"Error closing baton request dialog: {e}") + except Exception: + logger.exception("Error closing baton request dialog") self._baton_request_dialog = None def _show_pgroup_after_baton_grant(self) -> None: @@ -537,7 +537,7 @@ class StatusBar(QStatusBar): try: self.staff_pgroups_loaded.disconnect(_on_loaded) except Exception as e: - logger.debug(f"Error disconnecting: {e}") + logger.debug(f"Error disconnecting: {e}", exc_info=True) self.staff_pgroups_loaded.connect(_on_loaded) self._list_staff_pgroups() diff --git a/tests/conftest.py b/tests/conftest.py index b43e2a01..bba854dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,9 +90,9 @@ def client(server_module, mock_backend, auth_token_data): patch.object(server_module, "mx_beamline", return_value=mock_backend["bl"]), patch.object(server_module, "BeamlineConfig", return_value=mock_backend["cfg"]), patch.object(server_module, "AareDAQ", return_value=mock_backend["daq"]), + TestClient(server_module.app) as c, ): - with TestClient(server_module.app) as c: - yield c + yield c @pytest.fixture diff --git a/tests/unit/daq/test_aaredb.py b/tests/unit/daq/test_aaredb.py index 311a352f..e3ab9dc2 100644 --- a/tests/unit/daq/test_aaredb.py +++ b/tests/unit/daq/test_aaredb.py @@ -130,7 +130,9 @@ def test_create_manual_sample_error(mock_sample, mock_api, mock_bl, sample_info, wrapper = AareWrapper(bl=mock_bl) mock_sample.return_value.insert_sample.side_effect = Exception("DB Error") wrapper.create_manual_sample(sample_info) - assert "Error inserting sample: DB Error" in caplog.text + assert "Error inserting sample" in caplog.text + # the cause now arrives via the logged traceback rather than the message + assert "DB Error" in caplog.text @patch("aareDB.ApiClient") @@ -348,25 +350,25 @@ def test_ingest_gridscan_payload_none(mock_api, mock_bl, sample_info, geom_model # Lines 351-352 in aaredb.py: if payload is None: return # This happens if format_gridscan_payload returns None. with patch.object(AareWrapper, "format_gridscan_payload", return_value=None): - wrapper.ingest_gridscan(sample_info, None, None, geom_model, None, (0, 0)) + wrapper.ingest_gridscan(sample_info, None, None, geom_model, None, (0, 0)) # type: ignore # intended with patch.object(AareWrapper, "format_scan_payload", return_value=None): - wrapper.ingest_scan(sample_info, None, geom_model, (0, 0)) + wrapper.ingest_scan(sample_info, None, geom_model, (0, 0)) # type: ignore # intended @patch("aareDB.ApiClient") def test_format_gridscan_payload_error(mock_api, mock_bl, sample_info, caplog): wrapper = AareWrapper(bl=mock_bl) # Passing None for geom_model should trigger an error in smargon_to_picture - with pytest.raises(Exception): - wrapper.format_gridscan_payload(sample_info, None, None, None, None, (0, 0)) + with pytest.raises(AttributeError): + wrapper.format_gridscan_payload(sample_info, None, None, None, None, (0, 0)) # type: ignore # intended assert "NoneType" in caplog.text @patch("aareDB.ApiClient") def test_format_scan_payload_error(mock_api, mock_bl, sample_info, caplog): wrapper = AareWrapper(bl=mock_bl) - with pytest.raises(Exception): + with pytest.raises(AttributeError): # Passing None for geom should trigger error when accessing geom.beam_size_mm - wrapper.format_scan_payload(sample_info, None, None, (0, 0)) + wrapper.format_scan_payload(sample_info, None, None, (0, 0)) # type: ignore # intended assert "NoneType" in caplog.text diff --git a/tests/unit/daq/test_auth.py b/tests/unit/daq/test_auth.py index c99ad461..d779a677 100644 --- a/tests/unit/daq/test_auth.py +++ b/tests/unit/daq/test_auth.py @@ -97,9 +97,11 @@ def test_parse_token(): def test_parse_token_invalid(): - with patch("aare.daq.auth.jwt_key", return_value="test_secret"): - with pytest.raises(AuthenticationException): - parse_token("invalid.token.here") + with ( + patch("aare.daq.auth.jwt_key", return_value="test_secret"), + pytest.raises(AuthenticationException), + ): + parse_token("invalid.token.here") def test_check_jwt_ro(mock_cfg, token_data): diff --git a/tests/unit/daq/test_face_detection.py b/tests/unit/daq/test_face_detection.py index 887beb29..72a55736 100644 --- a/tests/unit/daq/test_face_detection.py +++ b/tests/unit/daq/test_face_detection.py @@ -79,7 +79,10 @@ def test_public_face_detection_uses_execute_face_detection(monkeypatch): cfg = types.SimpleNamespace(try_set_busy=lambda timeout=360: None, state_busy=False) daq._cfg = cfg - daq._execute_face_detection = lambda **kwargs: FaceDetectionResult(success=True, payload={"running": False, "samples": [{"angle": 45}], "height_fit": {}, "area_fit": {}}) + daq._execute_face_detection = lambda **kwargs: FaceDetectionResult( + success=True, + payload={"running": False, "samples": [{"angle": 45}], "height_fit": {}, "area_fit": {}}, + ) result = daq.face_detection(steps=7, step_size=30) diff --git a/tests/unit/daq/test_tellupdater.py b/tests/unit/daq/test_tellupdater.py index 2fbfc91a..894325c5 100644 --- a/tests/unit/daq/test_tellupdater.py +++ b/tests/unit/daq/test_tellupdater.py @@ -180,8 +180,10 @@ def test_on_message(mock_ws_app): } ] ) - with patch("aare.daq.tellupdater.ws_update_samples_info") as mock_update: - with patch("aare.daq.tellupdater.handle_tell_change_event"): - tellupdater.current_pucks = [] - tellupdater.on_message(None, message) - mock_update.assert_called_once() + with ( + patch("aare.daq.tellupdater.ws_update_samples_info") as mock_update, + patch("aare.daq.tellupdater.handle_tell_change_event"), + ): + tellupdater.current_pucks = [] + tellupdater.on_message(None, message) + mock_update.assert_called_once() -- 2.54.0 From 7c28b5f023dba7e5fa952130b485398cffd90c4a Mon Sep 17 00:00:00 2001 From: David Perl Date: Mon, 3 Aug 2026 09:29:49 +0200 Subject: [PATCH 4/6] ci: make sure main is available for diff --- .gitea/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 91e9fbf1..405276b4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v5 + with: + fetch-depth: 0 - name: Setup uses: ./.gitea/actions/install -- 2.54.0 From 28dec7a3fefe82ee1246cf227e90149b14737c77 Mon Sep 17 00:00:00 2001 From: David Perl Date: Mon, 3 Aug 2026 10:41:36 +0200 Subject: [PATCH 5/6] chore: update common dep --- pyproject.toml | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 225e4ed7..ddaf69f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,24 +7,24 @@ requires-python = ">=3.11" dependencies = [ "uv", "gunicorn", - "aarecommon>=0.2.0", - "pydantic==2.11.4", - "numpy==2.2.5", + "aarecommon>=0.2.2", + "pydantic>=2.11", + "numpy", "jfjoch_client==1.0.0rc146", - "pyJWT==2.10.1", - "pyzmq==26.4.0", - "opencv-python-headless==4.11.0.86", + "pyJWT", + "pyzmq", + "opencv-python-headless", "PySide6==6.9.0", - "requests==2.32.4", - "pyepics==3.5.8", - "redis==6.2.0", - "python-redis-lock==4.0.0", - "fastapi==0.115.13", + "requests", + "pyepics~=3.5", + "redis", + "python-redis-lock", + "fastapi", "uvicorn", "aaredb>=0.2", - "python_multipart==0.0.20", - "websocket-client==1.8.0", - "sseclient==0.0.27", #Was sseclient-py==1.8.0 but this was broken??? + "python_multipart", + "websocket-client", + "sseclient", "psi-pshell==2.1.0", "bec_lib>=3.130.3", "bec-ipython-client>=3.130.3", @@ -53,6 +53,19 @@ docs = [ [lint] ignore = ["F401", "F541", "W503", "W504"] +[tool.uv] +# Transitive deps (aarecommon -> aarelc_infer) ask for the non-headless +# `opencv-python`. It unpacks into the same `cv2/` directory as the +# `opencv-python-headless` we depend on, so whichever installs last wins for the +# overlapping files and you get a mixed install -- the Python half of one wheel +# next to the native half of the other. That surfaces as +# `AttributeError: partially initialized module 'cv2' has no attribute +# 'gapi_wip_gst_GStreamerPipeline'` on `import cv2`, and it bites in the +# test-with-beamline-plugins job because installing the plugins re-resolves and +# flips the install order. A marker that never matches drops the requirement, +# leaving headless as the only cv2 provider. +override-dependencies = ["opencv-python; sys_platform == 'nonexistent'"] + [tool.uv.sources] aaredb = { index = "psi"} -- 2.54.0 From 8def022d4c68ac928a22b4ab214b9e7df9eb279f Mon Sep 17 00:00:00 2001 From: David Perl Date: Mon, 3 Aug 2026 14:13:19 +0200 Subject: [PATCH 6/6] fix: add coverage comment --- .gitea/workflows/ci.yml | 67 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 405276b4..48adc9cd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: https://github.com/actions/checkout@v5 with: fetch-depth: 0 @@ -38,7 +38,7 @@ jobs: ruff check - name: Checkout pyright diff plugin - uses: actions/checkout@v5 + uses: https://github.com/actions/checkout@v5 with: repository: mx/diff_quality_basedpyright path: diff_quality_basedpyright @@ -51,14 +51,13 @@ jobs: test: runs-on: ubuntu-latest - needs: "lint" strategy: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout code - uses: actions/checkout@v5 + uses: https://github.com/actions/checkout@v5 - name: Setup uses: ./.gitea/actions/install @@ -77,14 +76,13 @@ jobs: test-with-beamline-plugins: runs-on: ubuntu-latest - needs: ["lint", "test"] strategy: matrix: plugin_repo: ["pxi_bec", "pxii_bec", "pxiii_bec"] steps: - name: Checkout code - uses: actions/checkout@v5 + uses: https://github.com/actions/checkout@v5 - name: Setup uses: ./.gitea/actions/install @@ -111,11 +109,10 @@ jobs: test-with-coverage: runs-on: ubuntu-latest - needs: ["lint", "test", "test-with-beamline-plugins"] steps: - name: Checkout code - uses: actions/checkout@v5 + uses: https://github.com/actions/checkout@v5 - name: Setup uses: ./.gitea/actions/install @@ -131,3 +128,57 @@ jobs: run: | source .venv/bin/activate pytest --cov=aare --cov-config=./pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./tests/unit + + - name: Build coverage summary + run: | + source .venv/bin/activate + { + echo "" + echo "### Coverage report" + echo + echo "**Total line + branch coverage: $(coverage report --format=total)%**" + echo + echo "
Per-file breakdown" + echo + coverage report --format=markdown + echo + echo "
" + } > coverage-summary.md + + # Only the rendered summary is uploaded. coverage.xml (~1 MB) is rejected + # with an HTML 403 by the proxy in front of gitea.psi.ch. + - name: Upload coverage + uses: https://github.com/actions/upload-artifact@v3 + with: + name: coverage + path: coverage-summary.md + if-no-files-found: error + + coverage-analysis: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + needs: ["test-with-coverage"] + if: github.event_name == 'pull_request' + steps: + - name: Download coverage + uses: https://github.com/actions/download-artifact@v3 + with: + name: coverage + + - name: Find Comment + uses: https://github.com/peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Create or update comment + uses: https://github.com/peter-evans/create-or-update-comment@v5 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: coverage-summary.md + edit-mode: replace -- 2.54.0