Exception Handling and Exit Codes¶
This document describes BatteryProc's exception hierarchy and how exceptions map to CLI exit codes.
Overview¶
BatteryProc uses a structured exception hierarchy to categorize different types of processing failures. This enables: - Clean error messages at the CLI level - Deterministic exit codes for scripting and automation - Programmatic error handling via the public API
Exception Hierarchy¶
All domain-specific exceptions inherit from BatteryAnalysisError:
BatteryAnalysisError (base class)
├── ParserError # File parsing failures
├── BadCellError # Data quality issues
└── WrongDataTypeError # Type/format mismatches
BatteryAnalysisError¶
Location: batteryproc.errors.BatteryAnalysisError
Base exception for all BatteryProc domain errors. Use this when: - You need to catch any BatteryProc-specific error - You're implementing generic error handling
Example:
from batteryproc.errors import BatteryAnalysisError
try:
result = process_battery_data(file_path)
except BatteryAnalysisError as e:
logger.error(f"BatteryProc error: {e}")
ParserError¶
Location: batteryproc.errors.ParserError
Raised when file parsing fails. Common scenarios: - Missing required sheets or columns in Excel files - Invalid file format or structure - Corrupt or unreadable data - Unsupported file types
CLI Behavior: Exit code 2 (FileErrorCategory.PARSE)
Example:
from batteryproc.errors import ParserError
def parse_neware_file(path):
if not path.exists():
raise ParserError(f"File not found: {path}")
# Check for required sheets
if 'record' not in sheets:
raise ParserError("Missing required sheet: 'record'")
BadCellError¶
Location: batteryproc.errors.BadCellError
Raised when battery test data fails quality checks. Common scenarios: - Insufficient charge/discharge capacity - Missing or invalid cycle data - Anomalous electrochemical behavior - Data quality below processing thresholds
CLI Behavior: Exit code 3 (FileErrorCategory.DATA_QUALITY)
Example:
from batteryproc.errors import BadCellError
def check_capacity(capacity_df):
max_discharge = capacity_df['discharge_mAh_per_g'].max()
if max_discharge < 10:
raise BadCellError(
f"Discharge capacity ({max_discharge:.1f} mAh/g) "
f"below 10 mAh/g threshold"
)
WrongDataTypeError¶
Location: batteryproc.errors.WrongDataTypeError
Raised when data types or formats don't match expectations. Common scenarios: - Expected DataFrame but received dict - Required numeric column contains strings - Data structure incompatible with operation - Missing expected attributes or methods
CLI Behavior: Exit code 4 (FileErrorCategory.WRONG_DATA_TYPE)
Example:
import pandas as pd
from batteryproc.errors import WrongDataTypeError
def process_cycle_data(data):
if not isinstance(data, pd.DataFrame):
raise WrongDataTypeError(
f"Expected DataFrame, got {type(data).__name__}"
)
CLI Exit Code Mapping¶
The CLI maps exceptions to specific exit codes for automation and scripting:
| Exit Code | Category | Exception Type | Description |
|---|---|---|---|
| 0 | Success | — | All files processed successfully |
| 1 | Unexpected | Generic Exception |
Unexpected/unhandled errors |
| 2 | Parse | ParserError |
File parsing failures |
| 3 | Data Quality | BadCellError |
Battery data quality issues |
| 4 | Wrong Data Type | WrongDataTypeError |
Type/format mismatches |
| 13 | Permission | PermissionError |
File access denied |
Multi-File Processing¶
When processing multiple files, the CLI follows these rules:
- At least one success: Exit with code 0 if any file succeeds
- All failures: Exit with the most severe error code encountered
- Error priority: PERMISSION > PARSE > DATA_QUALITY > WRONG_DATA_TYPE > UNEXPECTED
This means if you process 3 files and get: - File 1: Success - File 2: BadCellError (exit code 3) - File 3: ParserError (exit code 2)
The CLI will exit with code 0 (at least one success).
However, if all three fail: - File 1: BadCellError (exit code 3) - File 2: ParserError (exit code 2) - File 3: WrongDataTypeError (exit code 4)
The CLI will exit with code 2 (PARSE has higher priority than DATA_QUALITY and WRONG_DATA_TYPE).
Programmatic API¶
The batteryproc.api module provides exception-free processing via structured results:
from pathlib import Path
from batteryproc.api import process_file
from batteryproc.batteryproc_types import ProcessOptions
# Process a file
result = process_file(Path("data.xlsx"), ProcessOptions())
# Check result
if result.ok:
print(f"Success! Processed {result.path.name}")
print(f"Capacity data: {result.cell.capacity}")
else:
print(f"Failed: {result.status}")
print(f"Error category: {result.error_category}")
print(f"Errors: {result.errors}")
The API never raises sys.exit(). Instead, it returns FileResult objects with:
- ok: Boolean success/failure flag
- status: "success", "failure", or "skipped"
- error_category: One of FileErrorCategory enum values
- errors: List of error messages
- cell: Battery data (if successful)
Implementation Details¶
Exception Flow¶
-
Raise in domain code:
-
Catch in API layer:
The pipeline entry point processing.process_file_pipeline composes
processing.parse_file → processing.analyze_cell → processing.write_artifacts.
Domain errors surface from the parse stage (parse_file raises ParserError,
WrongDataTypeError, or PermissionError); _process_single translates each
raised exception to the matching FileErrorCategory. Non-raising conditions
such as a blocked bad cell are reported through the returned
PipelineOutcome (mapped onto FileResult) rather than as exceptions.
- Map to exit code in CLI:
When to Use Each Exception¶
Use ParserError when: - Reading/parsing the input file fails - File format is invalid or unsupported - Required data structures are missing
Use BadCellError when:
- Battery data fails quality checks
- Capacity is too low for analysis
- Cycle data is incomplete or invalid
- --force-process can override these checks
- batteryproc.formulas.normalize_inputs now surfaces data-quality issues (missing
columns, empty results, trimmed rows) as BadCellError so CLI/API exits with code 3.
- _prepare_normalized_inputs in batteryproc.metrics raises BadCellError when
normalization trims rows and legacy fallbacks are disabled.
Use WrongDataTypeError when:
- Function receives wrong data type
- Data structure doesn't match expectations
- Usually indicates a programming error
- _prepare_normalized_inputs raises WrongDataTypeError for invalid mass/weight
overrides supplied by users or config files.
- configure_cycle_detection validates numeric tuning parameters and raises
WrongDataTypeError for non-numeric or out-of-range values, mapping these
configuration issues to exit code 4.
Use generic exceptions when: - Error is truly unexpected - No specific domain category applies - You want a stack trace for debugging
Testing¶
Exception handling is tested in tests/test_api.py:
def test_process_file_error_classification(mocker):
"""Verify exceptions map to correct error categories."""
# Test ParserError → PARSE
mocker.patch(
"batteryproc.processing.process_file_pipeline",
side_effect=ParserError("test"),
)
result = api.process_file(path, options)
assert result.error_category == FileErrorCategory.PARSE
# Test BadCellError → DATA_QUALITY
mocker.patch(
"batteryproc.processing.process_file_pipeline",
side_effect=BadCellError("test"),
)
result = api.process_file(path, options)
assert result.error_category == FileErrorCategory.DATA_QUALITY
Patch batteryproc.processing.process_file_pipeline (the live per-file entry
point _process_single calls) — or a lower-level target such as
batteryproc.processing.parse_file — rather than any cli helper.
References¶
- Module:
batteryproc/errors.py- Exception definitions - API:
batteryproc/api.py- Exception handling - CLI:
batteryproc/cli.py- Exit code mapping - Types:
batteryproc/batteryproc_types.py- FileErrorCategory enum - Tests:
tests/test_api.py- Exception handling tests