from __future__ import annotations from enum import StrEnum class AuthErrorCode(StrEnum): """ Stable, machine-readable error codes used across the API. Rules: - never rename an existing value (treat as public API) - only add new values """ # Generic / defaults AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR" AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED" FORBIDDEN = "FORBIDDEN" HTTP_ERROR = "HTTP_ERROR" INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR" # Auth/JWT INVALID_TOKEN = "INVALID_TOKEN" SESSION_ALREADY_ACTIVE = "SESSION_ALREADY_ACTIVE" # Authorization NOT_STAFF = "NOT_STAFF" NOT_IN_ACTIVE_PGROUP = "NOT_IN_ACTIVE_PGROUP" NOT_BATON_HOLDER = "NOT_BATON_HOLDER" class DAQErrorCode(StrEnum): BEAMLINE_BUSY = "BEAMLINE_BUSY" MOUNTING_FAILED = "MOUNTING_FAILED" UNMOUNTING_FAILED = "UNMOUNTING_FAILED" LOOP_CENTERING_FAILED = "LOOP_CENTERING_FAILED" AXC_FAILED = "AXC_FAILED" TRANSFORMATION_INVALID = "TRANSFORMATION_INVALID" AUTOMATION_CRITICAL = "AUTOMATION_CRITICAL" SAMPLE_NOT_FOUND = "SAMPLE_NOT_FOUND" DATA_COLLECTION_FAILED = "DATA_COLLECTION_FAILED" RASTER_SCAN_FAILED = "RASTER_SCAN_FAILED" TELL_WARNING = "TELL_WARNING" TELL_CRITICAL = "TELL_CRITICAL" SMARGON_UNAVAILABLE = "SMARGON_UNAVAILABLE" TELL_UNAVAILABLE = "TELL_UNAVAILABLE" TELL_CONNECTION_ERROR = "TELL_CONNECTION_ERROR" TELL_MOUNT_FAILED = "TELL_MOUNT_FAILED" TELL_BUSY = "TELL_BUSY" MANUAL_MOUNT_REQUIRED = "MANUAL_MOUNT_REQUIRED" SMART_MAGNET_FAULT = "SMART_MAGNET_FAULT" JFJOCH_UNAVAILABLE = "JFJOCH_UNAVAILABLE" AEROTECH_UNAVAILABLE = "AEROTECH_UNAVAILABLE" AAREDB_UNAVAILABLE = "AAREDB_UNAVAILABLE" MAGNET_POSITION_SENSOR_ERROR = "MAGNET_POSITION_SENSOR_ERROR" _ERROR_CODE_HELP: dict[str, str] = { # Auth/JWT AuthErrorCode.AUTHENTICATION_ERROR: ( "Generic authentication problem. Usually means the request lacked valid credentials " "(expired/invalid token, missing Authorization header, etc.)." ), AuthErrorCode.AUTHENTICATION_FAILED: ( "Authentication failed during login/token creation. Typically incorrect credentials " "or an inability to validate the user." ), AuthErrorCode.INVALID_TOKEN: ( "The provided token could not be decoded/validated (bad signature, expired, malformed). " "Re-authenticate to obtain a new token." ), AuthErrorCode.SESSION_ALREADY_ACTIVE: ( "A different session currently owns control. Use “force current session” (if allowed) " "or wait for the active session to expire/end." ), AuthErrorCode.NOT_BATON_HOLDER: ( "A different session currently owns the baton. Request the baton or wait for active session to expire/end"), # Authorization AuthErrorCode.FORBIDDEN: ( "Generic permissions failure. The user is authenticated but not allowed to perform this action." ), AuthErrorCode.NOT_STAFF: ( "This action requires staff privileges. Log in with a staff account or ask staff to perform it." ), AuthErrorCode.NOT_IN_ACTIVE_PGROUP: ( "You are not a member of the currently active p-group. Change p-group or use an account " "that belongs to the active group." ), # Generic AuthErrorCode.HTTP_ERROR: ( "Generic HTTP error wrapper. The server returned an HTTPException that wasn’t mapped to a more specific code." ), AuthErrorCode.INTERNAL_SERVER_ERROR: ( "Unhandled server error. Check server logs for a stack trace and context." ), DAQErrorCode.BEAMLINE_BUSY: ( "Beamline state is set to Busy by prior action. If this state persists an additional error may have occurred, " "preventing the state from being released, this should timeout within 10 minutes." "If this occurs please seek assistance from your local contact." ), DAQErrorCode.MOUNTING_FAILED: ( "The requested sample could not be mounted. Check TELL/robot state, sample location, and hardware readiness." ), DAQErrorCode.UNMOUNTING_FAILED: ( "The mounted sample could not be unmounted. Check robot state and whether the sample changer is ready." ), DAQErrorCode.LOOP_CENTERING_FAILED: ( "Automatic loop centering failed to locate or position the sample reliably." ), DAQErrorCode.AXC_FAILED: ( "Automatic X-ray centering failed. Verify sample visibility, alignment, and detector/beam conditions." ), DAQErrorCode.TRANSFORMATION_INVALID: ( "The requested beamline state transition is not allowed from the current state." ), DAQErrorCode.AUTOMATION_CRITICAL: ( "A critical failure occurred during fully automated measurement. The GUI should stop " "dispatching further queue items and staff/operator recovery may be required." ), DAQErrorCode.SAMPLE_NOT_FOUND: ( "The requested sample could not be found in the available sample lists." ), DAQErrorCode.DATA_COLLECTION_FAILED: ( "Rotation/data collection failed before a valid result was produced." ), DAQErrorCode.RASTER_SCAN_FAILED: ( "Raster scan failed before a valid result was produced." ), DAQErrorCode.TELL_WARNING: ( "TELL reported a warning condition. The request may not have completed cleanly." ), DAQErrorCode.TELL_CRITICAL: ( "TELL reported a critical error condition requiring operator attention." ), DAQErrorCode.SMARGON_UNAVAILABLE: ( "Smargon communication failed or the controller is unavailable." ), DAQErrorCode.TELL_UNAVAILABLE: ( "TELL communication failed or the service is unavailable." ), DAQErrorCode.TELL_CONNECTION_ERROR: ( "The server could not establish or maintain a connection to TELL." ), DAQErrorCode.TELL_MOUNT_FAILED: ( "TELL reported that the mount/unmount operation failed." ), DAQErrorCode.TELL_BUSY: ( "A TELL command was requested while TELL was still busy processing another action." ), DAQErrorCode.MANUAL_MOUNT_REQUIRED: ( "Automatic mounting could not proceed and manual intervention is required." ), DAQErrorCode.SMART_MAGNET_FAULT: ( "The smart magnet system reported a fault or unsafe condition." ), DAQErrorCode.JFJOCH_UNAVAILABLE: ( "JFJoch detector communication failed or the detector service is unavailable." ), DAQErrorCode.AEROTECH_UNAVAILABLE: ( "Aerotech communication failed or the motion controller is unavailable." ), DAQErrorCode.AAREDB_UNAVAILABLE: ( "AareDB communication failed or the database service is unavailable." ), DAQErrorCode.MAGNET_POSITION_SENSOR_ERROR: ( "The magnet position sensor reported an invalid or unsafe state." ), } def error_code_help(code: str) -> str | None: """ Return a human help message for a code string, if known. Accepts either enum value strings or raw strings. """ if not code: return None return _ERROR_CODE_HELP.get(str(code)) def export_error_code_help() -> dict[str, str]: """ Export help text as {"CODE": "help text", ...} """ return {str(k): str(v) for k, v in _ERROR_CODE_HELP.items()} def export_error_codes_grouped() -> dict[str, dict[str, str]]: """ Export codes grouped by enum class name: { "AuthErrorCode": {"INVALID_TOKEN": "INVALID_TOKEN", ...}, "DAQErrorCode": {"BEAMLINE_BUSY": "BEAMLINE_BUSY", ...} } """ enums: tuple[type[StrEnum], ...] = (AuthErrorCode, DAQErrorCode) return {e.__name__: {c.name: str(c.value) for c in e} for e in enums} def export_error_codes() -> dict[str, str]: """ Backwards-compatible, flat export used by older clients/tests/docs: {"INVALID_TOKEN": "INVALID_TOKEN", ...} NOTE: This intentionally exports only AuthErrorCode to avoid breaking existing consumers that assume a flat map and/or specific keys. """ return {c.name: str(c.value) for c in AuthErrorCode}