"""FlameIQ exception hierarchy.
All FlameIQ exceptions derive from :class:`FlameIQError`. This lets callers
catch the entire FlameIQ surface with a single ``except FlameIQError``, or
target specific classes for fine-grained handling.
**Rule:** Never raise a bare ``Exception`` anywhere in the FlameIQ codebase.
Always raise from this hierarchy.
Hierarchy::
FlameIQError
├── ValidationError
│ └── SchemaVersionError
├── ConfigurationError
│ └── ThresholdConfigError
├── BaselineError
│ ├── BaselineNotFoundError
│ └── BaselineCorruptedError
├── ProviderError
│ ├── ProviderNotFoundError
│ └── MetricsFileNotFoundError
├── ComparisonError
│ └── InsufficientSamplesError
└── StorageError
└── MigrationError
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------
[docs]
class FlameIQError(Exception):
"""Base class for all FlameIQ exceptions."""
# ---------------------------------------------------------------------------
# Schema & Validation
# ---------------------------------------------------------------------------
[docs]
class ValidationError(FlameIQError):
"""Raised when a snapshot fails schema validation."""
[docs]
class SchemaVersionError(ValidationError):
"""Raised when an unsupported schema version is encountered.
Args:
version: The unsupported version number that was encountered.
"""
[docs]
def __init__(self, version: int) -> None:
"""Initialize the error with the unsupported version number."""
super().__init__(
f"Unsupported schema version: {version}. FlameIQ v1.0 supports schema_version 1 only."
)
self.version = version
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
[docs]
class ConfigurationError(FlameIQError):
"""Raised when flameiq.yaml is missing, malformed, or invalid."""
[docs]
class ThresholdConfigError(ConfigurationError):
"""Raised when a threshold value cannot be parsed or is out of range.
Args:
key: The metric key the threshold applies to.
value: The raw invalid threshold string.
reason: Human-readable explanation of the error.
"""
[docs]
def __init__(self, key: str, value: str, reason: str) -> None:
"""Initialize the error with the invalid threshold details."""
super().__init__(f"Invalid threshold for '{key}: {value}' — {reason}")
self.key = key
self.value = value
# ---------------------------------------------------------------------------
# Baseline
# ---------------------------------------------------------------------------
[docs]
class BaselineError(FlameIQError):
"""Raised for baseline management failures."""
[docs]
class BaselineNotFoundError(BaselineError):
"""Raised when no baseline snapshot exists for the current context.
Args:
path: The filesystem path where the baseline was expected.
"""
[docs]
def __init__(self, path: str) -> None:
"""Initialize the error with the expected baseline path."""
super().__init__(
f"No baseline found at '{path}'. Run: flameiq baseline set --metrics <file>"
)
self.path = path
[docs]
class BaselineCorruptedError(BaselineError):
"""Raised when a baseline file exists but cannot be deserialised.
Args:
path: Path to the corrupted file.
reason: Description of the parse failure.
"""
[docs]
def __init__(self, path: str, reason: str) -> None:
"""Initialize the error with the corrupted file details."""
super().__init__(f"Baseline at '{path}' is corrupted: {reason}")
self.path = path
# ---------------------------------------------------------------------------
# Providers
# ---------------------------------------------------------------------------
[docs]
class ProviderError(FlameIQError):
"""Raised when a metric provider fails to collect or normalise data."""
[docs]
class ProviderNotFoundError(ProviderError):
"""Raised when a requested provider name is not registered.
Args:
name: The requested provider name.
"""
[docs]
def __init__(self, name: str) -> None:
"""Initialize the error with the missing provider name."""
super().__init__(
f"Provider '{name}' is not registered. Use: flameiq validate --list-providers"
)
self.name = name
[docs]
class MetricsFileNotFoundError(ProviderError):
"""Raised when the metrics source file does not exist.
Args:
path: The path that was not found.
"""
[docs]
def __init__(self, path: str) -> None:
"""Initialize the error with the missing file path."""
super().__init__(f"Metrics file not found: '{path}'")
self.path = path
# ---------------------------------------------------------------------------
# Comparison
# ---------------------------------------------------------------------------
[docs]
class ComparisonError(FlameIQError):
"""Raised when a comparison cannot be completed."""
[docs]
class InsufficientSamplesError(ComparisonError):
"""Raised when statistical mode is enabled but sample count is too low.
Args:
metric: The metric name with insufficient samples.
got: Number of samples available.
required: Minimum samples required.
"""
[docs]
def __init__(self, metric: str, got: int, required: int) -> None:
"""Initialize the error with the insufficient samples details."""
super().__init__(
f"Insufficient samples for '{metric}': "
f"got {got}, need at least {required} for statistical comparison."
)
self.metric = metric
self.got = got
self.required = required
# ---------------------------------------------------------------------------
# Storage
# ---------------------------------------------------------------------------
[docs]
class StorageError(FlameIQError):
"""Raised for storage read/write failures."""
[docs]
class MigrationError(StorageError):
"""Raised when a storage schema migration fails."""