Exceptions (pyobs.utils.exceptions)

The pyobs exception hierarchy, used both for local errors and to carry failures across the RPC boundary between modules.

Two independent axes live in this hierarchy:

  • Domain exceptions (everything above RemoteError below) mean “the operation failed for reason X.” These deliberately multiply into fine-grained leaves (MoveError, FocusError, GrabImageError, …) so a caller can react differently to each one, rather than catching one broad type and inspecting a message string.

  • `RemoteError` and its small subtree mean “the call itself didn’t reach/return” (a transport failure), not “the remote operation failed for a domain reason.” That distinction doesn’t benefit from the same fine-grained treatment, so it stays deliberately small.

A domain exception raised on the far side of an RPC call is looked up in the registry below by its fully-qualified name and re-raised locally as its real type, tagged with remote_module (see comm/xmpp/rpc.py’s fault reconstruction) – it does not travel wrapped in a RemoteError. If the type can’t be resolved because its defining module was never imported in this process, it falls back to the nearest of its base classes that is registered here (the fault carries their names, see comm/xmpp/rpc.py; not in 2.14.2), e.g. MotionError for a driver’s own MotionError subclass, with the original type name preserved as original_type. If there is no such base class, or the exception was never a PyobsError to begin with, it arrives as UnclassifiedError, again with original_type. Driver authors should therefore subclass the closest class of this module, not PyobsError itself. See the “Exceptions across modules” guide in the documentation.

Every subclass registers itself automatically via __init_subclass__ (PyobsError._registry); there’s nothing to do at definition time beyond subclassing the right branch of the hierarchy.

PyobsError

exception PyobsError(message: str | None = None, **context: Any)[source]

Base class for all exceptions.

Define your own errors by subclassing the closest class of this module (e.g. MotionError), not PyobsError itself: a caller that did not import your module then still receives the nearest of these classes, otherwise it gets UnclassifiedError.

log(log: Logger, level: str, message: str, **kwargs: Any) → None[source]
classmethod resolve(qualified_name: str) → type[PyobsError] | None[source]

AbortedError

exception AbortedError(message: str | None = None, **context: Any)[source]

AcquisitionError

exception AcquisitionError(message: str | None = None, **context: Any)[source]

ArchiveError

exception ArchiveError(message: str | None = None, **context: Any)[source]

An archive backend couldn’t answer a query – it was unreachable, or it returned a non-200 response. Distinct from an archive that answers fine but has no matching frames, which is a normal empty result rather than an error. Deliberately one type across list_options/ list_frames and every backend, not split by failure mode: callers react the same way to all of them (back off and retry later), and only the message distinguishes “HTTP 502 from <url>” from “connection refused”.

DeviceBusyError

exception DeviceBusyError(message: str | None = None, **context: Any)[source]

The device can’t service this request right now because it’s already busy with another operation (e.g. an exposure/sequence already running, or another motion in progress) – back off and retry, as opposed to GrabImageError/MoveError/etc., which mean the operation was actually attempted and failed. Deliberately one type across camera/telescope/roof/focuser modules alike, not split by device or by which specific operation was busy – no caller reacts differently to those variants.

ExceptionHandler

exception ExceptionHandler(exc_type, limit, timespan, module, callback)[source]

Create new instance of ExceptionHandler(exc_type, limit, timespan, module, callback)

callback: Callable[[PyobsError], Coroutine[Any, Any, None]] | None

Alias for field number 4

exc_type: type[PyobsError]

Alias for field number 0

limit: int

Alias for field number 1

module: str | None

Alias for field number 3

timespan: float | None

Alias for field number 2

ForbiddenError

exception ForbiddenError(message: str | None = None, **context: Any)[source]

Raised when a caller is not permitted to invoke a method under the target module’s ACL policy.

FocusError

exception FocusError(message: str | None = None, **context: Any)[source]

GeneralError

exception GeneralError(message: str | None = None, **context: Any)[source]

GrabImageError

exception GrabImageError(message: str | None = None, **context: Any)[source]

ImageError

exception ImageError(message: str | None = None, **context: Any)[source]

InitError

exception InitError(message: str | None = None, **context: Any)[source]

InvalidArgumentError

exception InvalidArgumentError(message: str | None = None, **context: Any)[source]

The caller passed an argument this method rejects (unknown name, out-of-range value, …). Unlike a plain ValueError, this survives an RPC round trip as itself, so except exc.InvalidArgumentError: around a proxy call actually catches it – a bare ValueError would silently degrade to UnclassifiedError the moment the call crosses XMPP, even though it works fine locally (LocalComm, direct calls, tests), which is exactly the kind of inconsistency that only shows up once code goes from a local test to a networked deployment. Deliberately one type across every setter-shaped method, not split per method or per argument – no caller reacts differently to “unknown filter” vs. “invalid focus value.

LoggedException

exception LoggedException(time, exception)[source]

Create new instance of LoggedException(time, exception)

exception: PyobsError

Alias for field number 1

time: float

Alias for field number 0

ModuleError

exception ModuleError(message: str | None = None, **context: Any)[source]

ModuleStartingError

exception ModuleStartingError(message: str | None = None, **context: Any)[source]

The module hasn’t finished its own startup yet (still inside open(), e.g. connecting to hardware) and isn’t safe to command – distinct from DeviceBusyError, which means the module is fully up but its device is occupied with another operation. Back off and retry once the module reports ModuleState.READY, rather than treating this as a domain failure.

MotionError

exception MotionError(message: str | None = None, **context: Any)[source]

MoveError

exception MoveError(message: str | None = None, **context: Any)[source]

NotSupportedError

exception NotSupportedError(message: str | None = None, **context: Any)[source]

This module doesn’t support the requested operation at all – a capability the module only optionally implements (e.g. an alt/az-only telescope asked to move_radec) isn’t available, as opposed to a specific attempt at that operation failing. Cross-cutting: any module with an optional-capability mixin can raise this, not just telescopes.

ParkError

exception ParkError(message: str | None = None, **context: Any)[source]

PointingLimitError

exception PointingLimitError(message: str | None = None, **context: Any)[source]

The requested destination is outside the configured pointing limits (today the minimum altitude, later e.g. sun distance or dome limits). Nothing was attempted and the device is fine, so this is an expected condition: defer or skip the target, don’t alert and don’t retry right away. Deliberately not a MotionError, so that it never counts towards a module’s repeated-motion-failure escalation. Remote rejections also do not count towards broad RemoteError severity handlers; explicit handlers for this class or its ancestors still apply.

RemoteError

exception RemoteError(message: str | None = None, **context: Any)[source]

The call itself didn’t reach/return – a transport failure, not a domain exception raised by the remote operation (see the module docstring for that distinction). Kept deliberately small: “the call failed to even happen” doesn’t need the same fine-grained treatment as a domain failure.

RemoteTimeoutError

exception RemoteTimeoutError(message: str | None = None, **context: Any)[source]

UnclassifiedError

exception UnclassifiedError(message: str | None = None, **context: Any)[source]

Wraps an exception that isn’t part of the deliberate PyobsError contract – either it never was a PyobsError to begin with (a builtin, a vendor SDK exception) and escaped a module’s own method body unconverted, or it crossed an RPC boundary and couldn’t be reconstructed as its real type because its defining module was never imported in this process and none of its base classes (other than PyobsError) is known here either. The original type name survives as original_type even when the class itself doesn’t.