DAQ: starting exception_handler.py rework
This commit is contained in:
@@ -1,15 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import ClassVar
|
||||
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.error_codes import AuthErrorCode, DAQErrorCode
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
class TransformationInvalidException(Exception):
|
||||
def __init__(self, message: str = "Transformation is not implemented"):
|
||||
super().__init__(message)
|
||||
|
||||
class AareException(Exception):
|
||||
"""Root of the aare exception hierarchy.
|
||||
|
||||
Class-level ``critical`` is the default; pass ``critical=...`` to the
|
||||
constructor of an "optionally critical" subclass to override on a single
|
||||
raise site.
|
||||
"""
|
||||
|
||||
critical: ClassVar[bool] = False
|
||||
|
||||
def __init__(self, *args, critical: bool | None = None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if critical is not None:
|
||||
self.critical = critical
|
||||
|
||||
|
||||
class AutomationError(AareException):
|
||||
"""Errors that happen during a DAQ operation; route through automation flows."""
|
||||
|
||||
|
||||
class AareUserError(AareException):
|
||||
"""Bad input / mode misuse; routes through the input-correction flow."""
|
||||
|
||||
|
||||
class AareAuthError(AareException):
|
||||
"""Authentication / authorization failures; routes through re-auth flow."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Device-family base classes
|
||||
#
|
||||
# These exist purely so watchers can match a whole device family via
|
||||
# ``isinstance(e, TellException)`` etc. Per the plan they are intentionally
|
||||
# empty -- do not add behavior.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TellException(AutomationError):
|
||||
pass
|
||||
|
||||
|
||||
class SmargonException(AutomationError):
|
||||
pass
|
||||
|
||||
|
||||
class AerotechException(AutomationError):
|
||||
pass
|
||||
|
||||
|
||||
class JFJochException(AutomationError):
|
||||
pass
|
||||
|
||||
|
||||
class BECException(AutomationError):
|
||||
pass
|
||||
|
||||
|
||||
class BeamlineStateException(AutomationError):
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Concrete automation exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TransformationInvalidException(AutomationError):
|
||||
def __init__(self, message: str = "Transformation is not implemented", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -17,9 +83,9 @@ class TransformationInvalidException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class StateTransitionFailed(Exception):
|
||||
def __init__(self, message: str = "Beamline state transition failed"):
|
||||
super().__init__(message)
|
||||
class StateTransitionFailed(BeamlineStateException):
|
||||
def __init__(self, message: str = "Beamline state transition failed", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -27,9 +93,9 @@ class StateTransitionFailed(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class MaintenanceStateException(Exception):
|
||||
def __init__(self, message: str = "Beamline is in Maintenance state"):
|
||||
super().__init__(message)
|
||||
class MaintenanceStateException(BeamlineStateException):
|
||||
def __init__(self, message: str = "Beamline is in Maintenance state", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -37,31 +103,33 @@ class MaintenanceStateException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class DataCollectionException(Exception):
|
||||
def __init__(self, message: str = "Data collection failed"):
|
||||
super().__init__(message)
|
||||
class DataCollectionException(AutomationError):
|
||||
"""Group parent for data-collection-time exceptions; also raisable directly."""
|
||||
|
||||
def __init__(self, message: str = "Data collection failed", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class RasterScanException(Exception):
|
||||
def __init__(self, message: str = "Data collection failed"):
|
||||
super().__init__(message)
|
||||
class RasterScanException(DataCollectionException):
|
||||
def __init__(self, message: str = "Data collection failed", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class AutoRasterSampleSkipped(Exception):
|
||||
class AutoRasterSampleSkipped(AutomationError):
|
||||
"""Raised when automation should skip the current sample because auto-raster is too large."""
|
||||
|
||||
|
||||
class LoopCenteringFailed(Exception):
|
||||
def __init__(self, message: str = "Loop Centering did not detect a sample"):
|
||||
super().__init__(message)
|
||||
class LoopCenteringFailed(AutomationError):
|
||||
def __init__(self, message: str = "Loop Centering did not detect a sample", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -69,9 +137,11 @@ class LoopCenteringFailed(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class UnmountingFailed(Exception):
|
||||
def __init__(self, message: str = "A sample was not unmounted"):
|
||||
super().__init__(message)
|
||||
class UnmountingFailed(TellException):
|
||||
"""Sample failed to unmount. Optionally critical via instance flag."""
|
||||
|
||||
def __init__(self, message: str = "A sample was not unmounted", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -79,9 +149,14 @@ class UnmountingFailed(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class MountingFailed(Exception):
|
||||
def __init__(self, message: str = "A sample was not mounted"):
|
||||
super().__init__(message)
|
||||
class MountingFailed(TellException):
|
||||
"""Sample failed to mount. Optionally critical via instance flag.
|
||||
|
||||
Absorbs ``TellMountFailedException`` (still present for back-compat during
|
||||
migration; will be removed in Phase 3)."""
|
||||
|
||||
def __init__(self, message: str = "A sample was not mounted", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -89,10 +164,10 @@ class MountingFailed(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class ManualMountException(Exception):
|
||||
class ManualMountException(AareUserError):
|
||||
"""Custom exception for manual mounting"""
|
||||
def __init__(self, message: str = "Manual mounting failed"):
|
||||
super().__init__(message)
|
||||
def __init__(self, message: str = "Manual mounting failed", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -100,10 +175,10 @@ class ManualMountException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class SmartMagnetFaultException(Exception):
|
||||
class SmartMagnetFaultException(AutomationError):
|
||||
"""Custom exception for smart magnet fault"""
|
||||
def __init__(self, message: str = "Smart magnet fault"):
|
||||
super().__init__(message)
|
||||
def __init__(self, message: str = "Smart magnet fault", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -111,11 +186,13 @@ class SmartMagnetFaultException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class TellMountFailedException(Exception):
|
||||
"""Custom exception for mount failure"""
|
||||
class TellMountFailedException(TellException):
|
||||
"""Deprecated -- will be absorbed by MountingFailed in Phase 3.
|
||||
|
||||
def __init__(self, message: str = "Tell mount failed"):
|
||||
super().__init__(message)
|
||||
Currently kept as a TellException family member for back-compat."""
|
||||
|
||||
def __init__(self, message: str = "Tell mount failed", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -123,11 +200,11 @@ class TellMountFailedException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class TellCommandWhileBusyException(Exception):
|
||||
class TellCommandWhileBusyException(TellException):
|
||||
"""Custom exception for trying to move Tell when it is busy"""
|
||||
|
||||
def __init__(self, message: str = "Tell is busy"):
|
||||
super().__init__(message)
|
||||
def __init__(self, message: str = "Tell is busy", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -135,11 +212,11 @@ class TellCommandWhileBusyException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class TellConnectionException(Exception):
|
||||
class TellConnectionException(TellException):
|
||||
"""Custom exception for connection problems"""
|
||||
|
||||
def __init__(self, message: str = "Lost connection to Tell"):
|
||||
super().__init__(message)
|
||||
def __init__(self, message: str = "Lost connection to Tell", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -147,9 +224,9 @@ class TellConnectionException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class WarningTellException(Exception):
|
||||
def __init__(self, message: str = "Warning error in TELL"):
|
||||
super().__init__(message)
|
||||
class WarningTellException(TellException):
|
||||
def __init__(self, message: str = "Warning error in TELL", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -157,9 +234,9 @@ class WarningTellException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class CriticalTellException(Exception):
|
||||
def __init__(self, message: str = "Critical error in TELL"):
|
||||
super().__init__(message)
|
||||
class CriticalTellException(TellException):
|
||||
def __init__(self, message: str = "Critical error in TELL", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -167,9 +244,9 @@ class CriticalTellException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class AXCFailed(Exception):
|
||||
def __init__(self, message: str = "Auto X-ray centering failed"):
|
||||
super().__init__(message)
|
||||
class AXCFailed(AutomationError):
|
||||
def __init__(self, message: str = "Auto X-ray centering failed", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -177,9 +254,9 @@ class AXCFailed(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class BeamlineBusyException(Exception):
|
||||
def __init__(self, message: str = "Beamline is in busy state"):
|
||||
super().__init__(message)
|
||||
class BeamlineBusyException(BeamlineStateException):
|
||||
def __init__(self, message: str = "Beamline is in busy state", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -187,9 +264,9 @@ class BeamlineBusyException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class BeamlineBusyTimeoutException(Exception):
|
||||
def __init__(self, message: str = "Beamline busy state expired during operation"):
|
||||
super().__init__(message)
|
||||
class BeamlineBusyTimeoutException(BeamlineStateException):
|
||||
def __init__(self, message: str = "Beamline busy state expired during operation", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -197,9 +274,15 @@ class BeamlineBusyTimeoutException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class SampleException(Exception):
|
||||
def __init__(self, message: str = "Sample not found"):
|
||||
super().__init__(message)
|
||||
class SampleException(AareUserError):
|
||||
"""Requested sample could not be located.
|
||||
|
||||
NOTE: not listed in the redesign hierarchy (§1.2); parented under
|
||||
AareUserError pending confirmation -- "sample not found" reads as bad
|
||||
input rather than a runtime failure."""
|
||||
|
||||
def __init__(self, message: str = "Sample not found", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
@@ -207,14 +290,19 @@ class SampleException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class AuthenticationException(Exception):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AuthenticationException(AareAuthError):
|
||||
def __init__(self,
|
||||
message: str = "Authentication failed.",
|
||||
*,
|
||||
status_code: int = 401,
|
||||
headers: dict[str, str] | None = None,
|
||||
code: AuthErrorCode = AuthErrorCode.AUTHENTICATION_FAILED):
|
||||
super().__init__(message)
|
||||
code: AuthErrorCode = AuthErrorCode.AUTHENTICATION_FAILED,
|
||||
critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
@@ -225,16 +313,18 @@ class AuthenticationException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class UserRightsException(Exception):
|
||||
class UserRightsException(AareAuthError):
|
||||
_last_log_ts_by_message: dict[str, float] = {}
|
||||
_throttle_window_s = 30.0
|
||||
|
||||
def __init__(self,
|
||||
message: str = "User does not have rights to perform this action.",
|
||||
*,
|
||||
status_code: int = 403,
|
||||
headers: dict[str, str] | None = None,
|
||||
code: AuthErrorCode = AuthErrorCode.FORBIDDEN):
|
||||
super().__init__(message)
|
||||
code: AuthErrorCode = AuthErrorCode.FORBIDDEN,
|
||||
critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
@@ -250,7 +340,11 @@ class UserRightsException(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class SmargonCommunicationError(Exception):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Device communication exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SmargonCommunicationError(SmargonException):
|
||||
"""
|
||||
Raised when Smargon HTTP communication fails (connection refused, timeout, bad HTTP status, etc).
|
||||
Keep the original exception in `__cause__` by using `raise ... from e`.
|
||||
@@ -262,10 +356,11 @@ class SmargonCommunicationError(Exception):
|
||||
*,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
operation: str | None = None, # e.g. "GET" / "PUT"
|
||||
operation: str | None = None,
|
||||
status_code: int | None = None,
|
||||
critical: bool | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.endpoint = endpoint
|
||||
self.base_url = base_url
|
||||
@@ -286,7 +381,7 @@ class SmargonCommunicationError(Exception):
|
||||
return self.message
|
||||
|
||||
|
||||
class TellCommunicationError(Exception):
|
||||
class TellCommunicationError(TellException):
|
||||
"""
|
||||
Raised when TELL HTTP/PShell communication fails (timeouts, connection refused, etc).
|
||||
Intended to be caught centrally by FastAPI exception handlers.
|
||||
@@ -298,9 +393,10 @@ class TellCommunicationError(Exception):
|
||||
*,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
operation: str | None = None, # e.g. "GET"
|
||||
operation: str | None = None,
|
||||
critical: bool | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.endpoint = endpoint
|
||||
self.base_url = base_url
|
||||
@@ -318,7 +414,8 @@ class TellCommunicationError(Exception):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
class JFJochCommunicationError(Exception):
|
||||
|
||||
class JFJochCommunicationError(JFJochException):
|
||||
"""
|
||||
Raised when JFJoch HTTP/API communication fails.
|
||||
Intended for scan-time fallbacks and GUI-visible alerts.
|
||||
@@ -332,8 +429,9 @@ class JFJochCommunicationError(Exception):
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
status_code: int | None = None,
|
||||
critical: bool | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.operation = operation
|
||||
self.endpoint = endpoint
|
||||
@@ -353,8 +451,13 @@ class JFJochCommunicationError(Exception):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
class AareDBCommunicationError(Exception):
|
||||
"""Raised when AareDB HTTPS communication fails."""
|
||||
|
||||
class AareDBCommunicationError(AutomationError):
|
||||
"""Raised when AareDB HTTPS communication fails.
|
||||
|
||||
Default not critical -- DB hiccups don't always halt automation. Raise with
|
||||
``critical=True`` at call sites where a DB read failure should escalate."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "AareDB communication error",
|
||||
@@ -363,8 +466,9 @@ class AareDBCommunicationError(Exception):
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
status_code: int | None = None,
|
||||
critical: bool | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.operation = operation
|
||||
self.endpoint = endpoint
|
||||
@@ -384,7 +488,8 @@ class AareDBCommunicationError(Exception):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
class AerotechCommunicationError(Exception):
|
||||
|
||||
class AerotechCommunicationError(AerotechException):
|
||||
"""
|
||||
Raised when Aerotech HTTP/API communication fails (connection refused, timeout, bad HTTP status, etc).
|
||||
Keep the original exception in `__cause__` by using `raise ... from e`.
|
||||
@@ -398,8 +503,9 @@ class AerotechCommunicationError(Exception):
|
||||
base_url: str | None = None,
|
||||
operation: str | None = None,
|
||||
status_code: int | None = None,
|
||||
critical: bool | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.endpoint = endpoint
|
||||
self.base_url = base_url
|
||||
@@ -419,9 +525,10 @@ class AerotechCommunicationError(Exception):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
class MagnetPositionSensorErorr(Exception):
|
||||
def __init__(self, message: str = "Magnet position sensor error"):
|
||||
super().__init__(message)
|
||||
|
||||
class MagnetPositionSensorErorr(AutomationError):
|
||||
def __init__(self, message: str = "Magnet position sensor error", *, critical: bool | None = None):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
logger.error(
|
||||
message,
|
||||
@@ -431,7 +538,8 @@ class MagnetPositionSensorErorr(Exception):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
class BECCommunicationError(Exception):
|
||||
|
||||
class BECCommunicationError(BECException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "BEC communication error",
|
||||
@@ -440,7 +548,9 @@ class BECCommunicationError(Exception):
|
||||
operation: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
critical: bool | None = None,
|
||||
):
|
||||
super().__init__(message, critical=critical)
|
||||
self.message = message
|
||||
self.operation = operation
|
||||
self.exception = exception
|
||||
|
||||
Reference in New Issue
Block a user