Coverage for src / tracekit / core / exceptions.py: 100%
137 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""TraceKit exception hierarchy with helpful error messages.
3This module provides custom exception classes that follow a consistent
4template for error messages: WHAT, WHY, HOW TO FIX, DOCUMENTATION LINK.
7Example:
8 >>> try:
9 ... raise UnsupportedFormatError(".xyz", ["wfm", "csv", "npz"])
10 ... except UnsupportedFormatError as e:
11 ... print(e)
12 Unsupported file format: .xyz
13 ...
14"""
16from __future__ import annotations
18from typing import Any
20# Documentation base URL
21# Points to GitHub repository docs directory as primary documentation source.
22# Update this when official documentation hosting is configured (e.g., ReadTheDocs).
23#
24# To verify/update this URL:
25# 1. Check if https://tracekit.readthedocs.io exists and is active
26# 2. If not, use GitHub repository docs: https://github.com/lair-click-bats/tracekit/tree/main/docs
27# 3. Or local docs path for development: file:///path/to/docs/
28DOCS_BASE_URL = "https://github.com/lair-click-bats/tracekit/tree/main/docs"
31class TraceKitError(Exception):
32 """Base exception for all TraceKit errors.
34 All TraceKit exceptions inherit from this class, providing a
35 consistent interface for error handling.
37 Attributes:
38 message: Brief description of the error.
39 details: Additional context about the error.
40 fix_hint: Suggestion for how to fix the error.
41 docs_path: Path to relevant documentation.
43 Example:
44 >>> raise TraceKitError("Something went wrong")
45 TraceKitError: Something went wrong
46 """
48 docs_path: str = "errors"
50 def __init__(
51 self,
52 message: str,
53 *,
54 details: str | None = None,
55 fix_hint: str | None = None,
56 docs_path: str | None = None,
57 ) -> None:
58 """Initialize TraceKitError.
60 Args:
61 message: Brief description of the error.
62 details: Additional context about what caused the error.
63 fix_hint: Suggestion for how to fix the error.
64 docs_path: Path to relevant documentation (appended to base URL).
65 """
66 self.message = message
67 self.details = details
68 self.fix_hint = fix_hint
69 if docs_path is not None:
70 self.docs_path = docs_path
71 super().__init__(self._format_message())
73 def _format_message(self) -> str:
74 """Format the full error message with template.
76 Returns:
77 Formatted error message with WHAT, WHY, HOW TO FIX, DOCS.
78 """
79 parts = [self.message]
81 if self.details:
82 parts.append(f"Details: {self.details}")
84 if self.fix_hint:
85 parts.append(f"Fix: {self.fix_hint}")
87 parts.append(f"Docs: {DOCS_BASE_URL}/{self.docs_path}")
89 return "\n".join(parts)
92class LoaderError(TraceKitError):
93 """Error loading trace data from file.
95 Raised when a file cannot be read, parsed, or converted to
96 a TraceKit data structure.
98 Attributes:
99 file_path: Path to the file that failed to load.
100 """
102 docs_path: str = "errors#loader"
104 def __init__(
105 self,
106 message: str,
107 *,
108 file_path: str | None = None,
109 details: str | None = None,
110 fix_hint: str | None = None,
111 ) -> None:
112 """Initialize LoaderError.
114 Args:
115 message: Brief description of the error.
116 file_path: Path to the file that failed to load.
117 details: Additional context about the error.
118 fix_hint: Suggestion for how to fix the error.
119 """
120 self.file_path = file_path
121 if file_path and not details:
122 details = f"File: {file_path}"
123 elif file_path and details:
124 details = f"File: {file_path}. {details}"
125 super().__init__(
126 message,
127 details=details,
128 fix_hint=fix_hint,
129 docs_path=self.docs_path,
130 )
133class UnsupportedFormatError(LoaderError):
134 """File format not recognized or unsupported.
136 Raised when attempting to load a file with an unsupported
137 extension or format.
139 Attributes:
140 format_ext: The unsupported format extension.
141 supported_formats: List of supported format extensions.
142 """
144 docs_path: str = "errors#unsupported-format"
146 def __init__(
147 self,
148 format_ext: str,
149 supported_formats: list[str] | None = None,
150 *,
151 file_path: str | None = None,
152 ) -> None:
153 """Initialize UnsupportedFormatError.
155 Args:
156 format_ext: The unsupported format extension (e.g., ".xyz").
157 supported_formats: List of supported format extensions.
158 file_path: Path to the file that failed to load.
159 """
160 self.format_ext = format_ext
161 self.supported_formats = supported_formats or []
163 message = f"Unsupported file format: {format_ext}"
165 if self.supported_formats:
166 formats_str = ", ".join(self.supported_formats)
167 details = f"Supported formats: {formats_str}"
168 else:
169 details = None
171 fix_hint = "Convert the file to a supported format or use a custom loader."
173 super().__init__(
174 message,
175 file_path=file_path,
176 details=details,
177 fix_hint=fix_hint,
178 )
181class FormatError(LoaderError):
182 """File format is invalid or corrupted.
184 Raised when a file has the correct extension but invalid content.
185 """
187 docs_path: str = "errors#format-error"
189 def __init__(
190 self,
191 message: str,
192 *,
193 file_path: str | None = None,
194 expected: str | None = None,
195 got: str | None = None,
196 details: str | None = None,
197 fix_hint: str | None = None,
198 ) -> None:
199 """Initialize FormatError.
201 Args:
202 message: Brief description of the error.
203 file_path: Path to the file that failed to load.
204 expected: What was expected in the file.
205 got: What was actually found.
206 details: Additional context about the error (overrides expected/got).
207 fix_hint: Suggestion for how to fix the error.
208 """
209 # Build details from expected/got if not provided directly
210 if details is None:
211 if expected and got:
212 details = f"Expected: {expected}. Got: {got}"
213 elif expected:
214 details = f"Expected: {expected}"
215 elif got:
216 details = f"Found: {got}"
218 # Use default fix_hint if not provided
219 if fix_hint is None:
220 fix_hint = "Verify the file is not corrupted and matches the expected format."
222 super().__init__(
223 message,
224 file_path=file_path,
225 details=details,
226 fix_hint=fix_hint,
227 )
230class AnalysisError(TraceKitError):
231 """Error during signal analysis.
233 Raised when an analysis function encounters invalid data
234 or cannot compute a result.
235 """
237 docs_path: str = "errors#analysis"
239 def __init__(
240 self,
241 message: str,
242 *,
243 analysis_type: str | None = None,
244 details: str | None = None,
245 fix_hint: str | None = None,
246 ) -> None:
247 """Initialize AnalysisError.
249 Args:
250 message: Brief description of the error.
251 analysis_type: Type of analysis that failed (e.g., "rise_time").
252 details: Additional context about the error.
253 fix_hint: Suggestion for how to fix the error.
254 """
255 self.analysis_type = analysis_type
256 if analysis_type and not details:
257 details = f"Analysis: {analysis_type}"
258 elif analysis_type and details:
259 details = f"Analysis: {analysis_type}. {details}"
260 super().__init__(
261 message,
262 details=details,
263 fix_hint=fix_hint,
264 docs_path=self.docs_path,
265 )
268class InsufficientDataError(AnalysisError):
269 """Not enough data points for the requested analysis.
271 Raised when a signal is too short or lacks sufficient
272 features (edges, periods, etc.) for analysis.
274 Attributes:
275 required: Minimum data points or features required.
276 available: Actual data points or features available.
277 """
279 docs_path: str = "errors#insufficient-data"
281 def __init__(
282 self,
283 message: str,
284 *,
285 required: int | None = None,
286 available: int | None = None,
287 analysis_type: str | None = None,
288 ) -> None:
289 """Initialize InsufficientDataError.
291 Args:
292 message: Brief description of the error.
293 required: Minimum number of samples or features required.
294 available: Actual number available.
295 analysis_type: Type of analysis that failed.
296 """
297 self.required = required
298 self.available = available
300 details = None
301 if required is not None and available is not None:
302 details = f"Required: {required}. Available: {available}"
303 elif required is not None:
304 details = f"Minimum required: {required}"
306 fix_hint = "Acquire more data or reduce analysis window."
308 super().__init__(
309 message,
310 analysis_type=analysis_type,
311 details=details,
312 fix_hint=fix_hint,
313 )
316class SampleRateError(AnalysisError):
317 """Invalid or missing sample rate.
319 Raised when sample rate is invalid (zero, negative) or
320 insufficient for the requested analysis.
322 Attributes:
323 required_rate: Minimum sample rate required.
324 actual_rate: Actual sample rate provided.
325 """
327 docs_path: str = "errors#sample-rate"
329 def __init__(
330 self,
331 message: str,
332 *,
333 required_rate: float | None = None,
334 actual_rate: float | None = None,
335 ) -> None:
336 """Initialize SampleRateError.
338 Args:
339 message: Brief description of the error.
340 required_rate: Minimum sample rate required in Hz.
341 actual_rate: Actual sample rate in Hz.
342 """
343 self.required_rate = required_rate
344 self.actual_rate = actual_rate
346 details = None
347 if required_rate is not None and actual_rate is not None:
348 details = f"Required: {required_rate:.2e} Hz. Got: {actual_rate:.2e} Hz"
349 elif actual_rate is not None:
350 details = f"Got: {actual_rate:.2e} Hz"
352 fix_hint = "Ensure sample_rate is positive and sufficient for the analysis."
354 super().__init__(
355 message,
356 details=details,
357 fix_hint=fix_hint,
358 )
361class ConfigurationError(TraceKitError):
362 """Invalid configuration parameters.
364 Raised when configuration is invalid, missing required fields,
365 or contains invalid values.
367 Attributes:
368 config_key: The configuration key that is invalid.
369 expected_type: Expected type or format.
370 actual_value: The invalid value that was provided.
371 """
373 docs_path: str = "errors#configuration"
375 def __init__(
376 self,
377 message: str,
378 *,
379 config_key: str | None = None,
380 expected_type: str | None = None,
381 actual_value: Any = None,
382 details: str | None = None,
383 fix_hint: str | None = None,
384 ) -> None:
385 """Initialize ConfigurationError.
387 Args:
388 message: Brief description of the error.
389 config_key: The configuration key that is invalid.
390 expected_type: Expected type or format.
391 actual_value: The invalid value that was provided.
392 details: Additional context about the error.
393 fix_hint: Suggestion for how to fix the error.
394 """
395 self.config_key = config_key
396 self.expected_type = expected_type
397 self.actual_value = actual_value
399 # Build details from parts if not provided directly
400 if details is None:
401 details_parts = []
402 if config_key:
403 details_parts.append(f"Key: {config_key}")
404 if expected_type:
405 details_parts.append(f"Expected: {expected_type}")
406 if actual_value is not None:
407 details_parts.append(f"Got: {actual_value!r}")
408 details = ". ".join(details_parts) if details_parts else None
410 if fix_hint is None:
411 fix_hint = "Check configuration file and ensure all values are valid."
413 super().__init__(
414 message,
415 details=details,
416 fix_hint=fix_hint,
417 docs_path=self.docs_path,
418 )
421class ValidationError(TraceKitError):
422 """Data validation failed.
424 Raised when input data does not meet validation requirements.
426 Attributes:
427 field: The field that failed validation.
428 constraint: The constraint that was violated.
429 value: The value that failed validation.
430 """
432 docs_path: str = "errors#validation"
434 def __init__(
435 self,
436 message: str,
437 *,
438 field: str | None = None,
439 constraint: str | None = None,
440 value: Any = None,
441 ) -> None:
442 """Initialize ValidationError.
444 Args:
445 message: Brief description of the error.
446 field: The field that failed validation.
447 constraint: The constraint that was violated.
448 value: The value that failed validation.
449 """
450 self.field = field
451 self.constraint = constraint
452 self.value = value
454 details_parts = []
455 if field:
456 details_parts.append(f"Field: {field}")
457 if constraint:
458 details_parts.append(f"Constraint: {constraint}")
459 if value is not None:
460 details_parts.append(f"Value: {value!r}")
462 details = ". ".join(details_parts) if details_parts else None
463 fix_hint = "Ensure input data meets all validation requirements."
465 super().__init__(
466 message,
467 details=details,
468 fix_hint=fix_hint,
469 docs_path=self.docs_path,
470 )
473class ExportError(TraceKitError):
474 """Error exporting data.
476 Raised when data cannot be exported to the requested format.
478 Attributes:
479 export_format: The format that failed.
480 output_path: Path where export was attempted.
481 """
483 docs_path: str = "errors#export"
485 def __init__(
486 self,
487 message: str,
488 *,
489 export_format: str | None = None,
490 output_path: str | None = None,
491 details: str | None = None,
492 ) -> None:
493 """Initialize ExportError.
495 Args:
496 message: Brief description of the error.
497 export_format: The format that failed (e.g., "csv", "hdf5").
498 output_path: Path where export was attempted.
499 details: Additional context about the error.
500 """
501 self.export_format = export_format
502 self.output_path = output_path
504 details_parts = []
505 if export_format:
506 details_parts.append(f"Format: {export_format}")
507 if output_path:
508 details_parts.append(f"Path: {output_path}")
509 if details:
510 details_parts.append(details)
512 combined_details = ". ".join(details_parts) if details_parts else None
513 fix_hint = "Check output path is writable and data is valid for export."
515 super().__init__(
516 message,
517 details=combined_details,
518 fix_hint=fix_hint,
519 docs_path=self.docs_path,
520 )
523__all__ = [
524 "DOCS_BASE_URL",
525 "AnalysisError",
526 "ConfigurationError",
527 "ExportError",
528 "FormatError",
529 "InsufficientDataError",
530 "LoaderError",
531 "SampleRateError",
532 "TraceKitError",
533 "UnsupportedFormatError",
534 "ValidationError",
535]