From 7b1443de38df16112f366fe250003109e69181ec Mon Sep 17 00:00:00 2001 From: Jason Farrar Date: Wed, 8 Jul 2026 12:22:16 +0100 Subject: [PATCH] fix: guard closeEvent against uninitialized stan_presenter closeEvent can be triggered during initialization if database errors occur (e.g., lock timeout). The event handler tried to access stan_presenter which may not have been initialized yet, causing an AttributeError. Add hasattr() guard to safely check if stan_presenter exists before accessing it. This allows the application to close gracefully even if initialization fails. --- BSP_EXPLORATION_INDEX.md | 269 ++++++++++ BSP_INTEGRATION_SUMMARY.md | 485 ++++++++++++++++++ BSP_KEY_FILES_REFERENCE.md | 372 ++++++++++++++ pyproject.toml | 2 +- src/openstan/data/create_gui_db.py | 33 +- src/openstan/main.py | 6 +- src/openstan/models/statement_result_model.py | 14 +- .../presenters/anonymise_presenter.py | 4 +- .../presenters/statement_result_presenter.py | 31 +- src/openstan/views/debug_info_dialog.py | 42 +- uv.lock | 76 +-- 11 files changed, 1259 insertions(+), 75 deletions(-) create mode 100644 BSP_EXPLORATION_INDEX.md create mode 100644 BSP_INTEGRATION_SUMMARY.md create mode 100644 BSP_KEY_FILES_REFERENCE.md diff --git a/BSP_EXPLORATION_INDEX.md b/BSP_EXPLORATION_INDEX.md new file mode 100644 index 0000000..45f5d51 --- /dev/null +++ b/BSP_EXPLORATION_INDEX.md @@ -0,0 +1,269 @@ +# OpenStan BSP Integration - Exploration Index + +## Overview + +This index provides navigation to comprehensive documentation about how OpenStan integrates with the `bank_statement_parser` (BSP) library. The exploration includes statement import workflows, debugging features, file handling, and export mechanisms. + +## Documentation Files + +### 1. BSP_INTEGRATION_SUMMARY.md (PRIMARY REFERENCE) +**Size:** 17 KB | **Sections:** 12 + +Comprehensive end-to-end overview of the entire BSP integration architecture. + +**Contents:** +- **Section 1:** BSP Integration Points (7 core functions) +- **Section 2:** Import & Processing Workflow (5 phases) +- **Section 3:** Statement Debugging & Analysis UI (DebugInfoDialog, ParquetViewDialog, Project Info) +- **Section 4:** Current File Handling & Export Mechanisms +- **Section 5:** Database Schema (gui.db architecture) +- **Section 6:** Worker Thread Architecture (4 worker classes) +- **Section 7:** Error Handling & Diagnostics +- **Section 8:** State Machine: Queue Lock (prevents concurrent edits) +- **Section 9:** Session Restore Path (app restart with incomplete imports) +- **Section 10:** BSP API Contracts Validated (test_bsp_contract.py) +- **Section 11:** Key Integration Patterns (4 architectural patterns) +- **Section 12:** Next Steps for Enhancement + +**Use this file for:** +- Understanding the complete workflow +- Architecture decisions and rationale +- Error handling strategies +- Future enhancement ideas + +### 2. BSP_KEY_FILES_REFERENCE.md (DETAILED REFERENCE) +**Size:** 14 KB | **Sections:** Organized by functionality + +Detailed file-by-file breakdown with cross-dependencies and data flow diagrams. + +**Contents:** +- **File Structure Overview:** Directory layout with annotations +- **Critical Files by Functionality:** 6 categories (Queue, Results, Debug, Project, Export, DB) +- **Data Flow Diagrams:** 3 text-based flowcharts + - Import Workflow + - Debug Info Access + - Export Flow +- **Cross-File Dependencies:** Import/dependency analysis +- **Key Classes & Types:** All BSP and OpenStan types used +- **Worker Threads:** Details on 5 worker classes +- **Dialogs:** DebugInfoDialog, ParquetViewDialog, etc. +- **Testing Support:** Contract tests and integration tests +- **Configuration & Setup:** Environment variables and BSP config files +- **Performance Notes:** Threading, batch locking, lazy queries, soft-delete +- **Error Recovery:** Session restore, worker cancellation, JSON corruption handling + +**Use this file for:** +- Finding specific files and their purposes +- Understanding dependencies between components +- Tracing data flow through the system +- Performance optimization considerations +- Error recovery procedures + +## Quick Navigation + +### By Topic + +#### Statement Import +→ See **BSP_INTEGRATION_SUMMARY.md § 2** (5 phases) +→ See **BSP_KEY_FILES_REFERENCE.md** → Data Flow Diagrams → Import Workflow + +**Key files:** statement_queue_presenter.py, statement_result_presenter.py, statement_result_model.py + +#### Debug & Analysis +→ See **BSP_INTEGRATION_SUMMARY.md § 3** (UI dialogs) +→ See **BSP_KEY_FILES_REFERENCE.md** → Data Flow Diagrams → Debug Info Access + +**Key files:** debug_info_dialog.py, parquet_view_dialog.py + +#### Exports +→ See **BSP_INTEGRATION_SUMMARY.md § 4** (export mechanisms) +→ See **BSP_KEY_FILES_REFERENCE.md** → Data Flow Diagrams → Export Flow + +**Key files:** export_data_presenter.py, advanced_export_presenter.py, workers.py + +#### Database Architecture +→ See **BSP_INTEGRATION_SUMMARY.md § 5** (dual database model) +→ See **BSP_KEY_FILES_REFERENCE.md** → Performance Notes (soft-delete pattern) + +**Key files:** statement_result_model.py, create_gui_db.py + +#### Worker Threads +→ See **BSP_INTEGRATION_SUMMARY.md § 6** (4 worker classes) +→ See **BSP_KEY_FILES_REFERENCE.md** → Worker Threads + +**Key files:** statement_queue_presenter.py, statement_result_presenter.py, workers.py + +#### Session Restore +→ See **BSP_INTEGRATION_SUMMARY.md § 9** (restart with incomplete imports) +→ See **BSP_KEY_FILES_REFERENCE.md** → Error Recovery + +**Key files:** statement_queue_presenter.py, statement_result_presenter.py + +#### Error Handling +→ See **BSP_INTEGRATION_SUMMARY.md § 7** (exceptions and diagnostics) +→ See **BSP_KEY_FILES_REFERENCE.md** → Error Recovery + +**Key files:** All presenters (error handling in workers) + +#### State Machine +→ See **BSP_INTEGRATION_SUMMARY.md § 8** (queue lock state machine) + +**Key files:** statement_queue_presenter.py + +#### Integration Patterns +→ See **BSP_INTEGRATION_SUMMARY.md § 11** (4 architectural patterns) + +**Key files:** All components + +### By File + +| File | Documentation | Sections | +|------|---------------|----------| +| statement_queue_presenter.py | Summary § 2.1, § 8 | Reference § 1, Data Flows | +| statement_result_presenter.py | Summary § 2.2-2.5, § 3 | Reference § 2, Data Flows | +| statement_result_model.py | Summary § 2.3 | Reference § 2, Serialization | +| debug_info_dialog.py | Summary § 3 | Reference § 3, Data Flows | +| parquet_view_dialog.py | Summary § 3 | Reference § 3 | +| project_presenter.py | Summary § 3 | Reference § 4 | +| export_data_presenter.py | Summary § 4 | Reference § 5, Data Flows | +| advanced_export_presenter.py | Summary § 4 | Reference § 5, Data Flows | +| workers.py | Summary § 6 | Reference § 5.3 | + +### By Concept + +| Concept | Documentation | Key Files | +|---------|---------------|-----------| +| PdfResult handling | Summary § 1, § 2.2 | statement_result_model.py, statement_result_presenter.py | +| Batch locking | Summary § 8 | statement_queue_presenter.py, statement_queue_model.py | +| Soft-delete pattern | Summary § 5, § 2.5 | statement_result_model.py, statement_result_presenter.py | +| JSON serialization | Summary § 2.3 | statement_result_model.py | +| Off-thread workers | Summary § 6 | Multiple presenters | +| Polars lazy queries | Summary § 3, § 4 | project_presenter.py, advanced_export_presenter.py | +| Session restore | Summary § 9 | statement_queue_presenter.py, statement_result_presenter.py | +| Error recovery | Summary § 7 | Multiple files, Summary § 12 | + +## Key Figures & Numbers + +| Item | Count | Reference | +|------|-------|-----------| +| Core BSP functions used | 7 | Summary § 1 | +| Import workflow phases | 5 | Summary § 2 | +| Worker thread classes | 4 | Summary § 6 | +| Result categories | 3 | Summary § 1, § 2.2 | +| GUI DB tables | 7 | Summary § 5 | +| Export types | 2 | Summary § 4 (standard + advanced) | +| Datamart query types | 4 | Summary § 3 | +| Integration patterns | 4 | Summary § 11 | + +## Critical Concepts + +### Three-Step Commit +1. `bsp.update_db()` → persist to project.db datamart +2. `bsp.copy_statements_to_project()` → copy PDF files +3. `bsp.delete_temp_files()` → cleanup + +**See:** Summary § 2.5 + +### Soft-Delete + Hard-Delete +- **Soft delete:** UPDATE deleted=1 (immediate UI update) +- **Hard delete:** DELETE FROM ... (deferred until debug worker finishes) + +**See:** Summary § 5, Reference § Performance Notes + +### Queue Lock State Machine +- **UNLOCKED** → buttons enabled +- **LOCKED** → buttons disabled, import running +- **LOCKED + DONE** → View Results visible, awaiting user action +- **COMMITTED/ABANDONED** → back to UNLOCKED + +**See:** Summary § 8 + +### Batch Metadata Flow +``` +session_id, user_id, batch_id, project_path + → bsp.process_pdf_statement() + → PdfResult + → gui.db statement_result + statement_result_payload + → bsp.update_db() + → project.db datamart +``` + +**See:** Summary § 11 + +## Testing & Validation + +### Contract Tests +File: `tests/test_bsp_contract.py` + +Validates: +- Function signatures (process_pdf_statement, update_db, etc.) +- Result types (Success, Review, Failure) +- Exception types (StatementError, ProjectError, TestGateFailure) +- TestHarness API + +**Run:** `uv run pytest tests/test_bsp_contract.py -v` + +**See:** Summary § 10, Reference § Testing Support + +### Integration Tests +File: `tests/test_integration.py` + +Validates: +- End-to-end import workflows +- Batch processing +- Debug generation +- Export operations + +Uses BSP TestHarness with anonymised PDFs + +**See:** Reference § Testing Support + +## Future Enhancement Ideas + +Based on current architecture, potential enhancements include: + +1. **Statement Export Dialog** — Per-statement CSV/JSON export from debug view +2. **Batch History** — View/retry/export previous batches from gui.db +3. **Anonymisation** — In-app anonymisation (currently opens external dialog) +4. **Advanced Filtering** — Search/filter statements by account, date range, error type +5. **Parquet Data Export** — Export statement_lines/heads to CSV from ParquetViewDialog +6. **Diagnostic Report** — Generate HTML report of batch results +7. **API Mode** — RESTful interface for headless batch processing + +**See:** Summary § 12 + +## Performance Notes + +| Operation | Threading | Details | +|-----------|-----------|---------| +| PDF parsing | Off-thread (SQWorker) | Per-file bsp.process_pdf_statement() | +| Debug generation | Off-thread (DebugWorker) | Per-row bsp.debug_pdf_statement(), cancellable | +| Batch commit | Off-thread (CommitWorker) | 3-step BSP operation with progress bar | +| Data export | Off-thread (ExportWorker) | Generic wrapper for any export function | +| Datamart queries | Off-thread (_DatamartLoadWorker) | Polars lazy API collection | + +**See:** Reference § Performance Notes + +## Error Recovery Procedures + +| Scenario | Recovery | Reference | +|----------|----------|-----------| +| App restart with locked batch | Load results from gui.db | Summary § 9 | +| Worker cancellation | Per-row error handling, skip | Reference § Error Recovery | +| JSON payload corruption | Skip corrupt row, continue | Reference § Error Recovery | +| Missing debug files | Graceful UI handling | Summary § 3 | + +**See:** Summary § 7, § 9; Reference § Error Recovery + +--- + +**Last Updated:** July 7, 2026 + +**Associated Files:** +- BSP_INTEGRATION_SUMMARY.md (17 KB) +- BSP_KEY_FILES_REFERENCE.md (14 KB) +- AGENTS.md (project architecture guide) + +--- + +For questions or clarifications, refer to the comprehensive documentation files listed above. diff --git a/BSP_INTEGRATION_SUMMARY.md b/BSP_INTEGRATION_SUMMARY.md new file mode 100644 index 0000000..890ba23 --- /dev/null +++ b/BSP_INTEGRATION_SUMMARY.md @@ -0,0 +1,485 @@ +# OpenStan: Bank Statement Parser (BSP) Integration Summary + +## Overview + +OpenStan is a Python/PySide6 desktop application for bank statement analysis. It tightly integrates with the `bank_statement_parser` (BSP) library to process PDF bank statements, analyze them, and persist results to a data mart (project.db). + +## 1. BSP Integration Points + +### Core Functions Used +The application calls these BSP functions throughout the import/processing lifecycle: + +| Function | Location | Purpose | +|----------|----------|---------| +| `bsp.process_pdf_statement()` | `statement_queue_presenter.py` (worker thread) | Parse single PDF, return PdfResult | +| `bsp.update_db()` | `statement_result_presenter.py` (commit worker) | Persist parsed statements to project.db datamart | +| `bsp.copy_statements_to_project()` | `statement_result_presenter.py` (commit worker) | Copy statement files to project structure | +| `bsp.delete_temp_files()` | `statement_result_presenter.py` (commit worker) | Clean up temporary working files | +| `bsp.debug_pdf_statement()` | `statement_result_presenter.py` (debug worker) | Generate debug JSON for failed/review statements | +| `bsp.db.DimStatement()`, `bsp.db.DimAccount()`, etc. | `project_presenter.py`, `advanced_export_presenter.py` | Query datamart tables (via Polars API) | +| `bsp.ProjectPaths.resolve()` | Multiple files | Resolve project root path and settings | + +### Result Types Handled +The application processes three result types returned by `bsp.process_pdf_statement()`: + +```python +from bank_statement_parser.modules.statements import PdfResult, Success, Review, Failure +``` + +**PdfResult** structure: +- `result`: Literal["SUCCESS", "REVIEW", "FAILURE"] +- `outcome`: Status indicator +- `batch_lines`: Path to batch processing file +- `checks_and_balances`: Path to validation file (if applicable) +- `payload`: Union[Success, Review, Failure] + +**Success** payload: +- `statement_info`: Account/date/amount data +- `parquet_files`: Paths to SUCCESS parquet files (statement_heads, statement_lines) + +**Review** payload: +- `statement_info`: Same as Success +- `parquet_files`: Same as Success +- `message`: Issue description +- `message_detail`: Extended explanation + +**Failure** payload: +- `message`: Error message +- `error_type`: Category (parsing error, validation failure, etc.) +- `message_detail`: Extended traceback/details + +--- + +## 2. Import & Processing Workflow + +### Phase 1: Queue Management +**Location:** `src/openstan/presenters/statement_queue_presenter.py` + +``` +User adds PDFs to queue + ↓ +Queue rows stored in gui.db (statement_queue table) + ↓ +User clicks "Run Import" + ↓ +SQWorker thread spawned (calls bsp.process_pdf_statement per PDF) + ↓ +Signals emitted: statement_imported(Path, PdfResult, progress%, queue_id) +``` + +**Key Model:** `StatementQueueModel` (QSqlTableModel) +- Manages `statement_queue` table in gui.db +- Rows include: `queue_id`, `path`, `is_folder`, `batch_id`, `session_id`, `project_id` +- Methods: `add_record()`, `delete_records()`, `set_batch_id()` (lock for batch), `clear_batch_id()` (unlock) + +### Phase 2: In-Memory Result Accumulation +**Location:** `src/openstan/models/statement_result_model.py` + +During the import worker loop, each `PdfResult` is: + +1. Converted to a `ResultRow` dataclass: + ```python + @dataclass + class ResultRow: + result_id: str + batch_id: str + queue_id: str + project_id: str + result: str # "SUCCESS" | "REVIEW" | "FAILURE" + file_path: Path + id_account: str | None + statement_date: str | None + payments_in: float | None + payments_out: float | None + error_type: str | None + message: str | None + pdf_result: PdfResult | None # the raw result + ``` + +2. Added to one of three in-memory models: + - `SuccessResultModel` (QStandardItemModel) + - `ReviewResultModel` (QStandardItemModel) + - `FailureResultModel` (QStandardItemModel) + +3. Displayed in the Statement Results View (three tabs) + +**Key Signal:** `StanPresenter.statement_imported` → `StatementResultPresenter.add_result_to_memory()` + +### Phase 3: Persist to GUI DB +**Location:** `src/openstan/presenters/statement_result_presenter.py::persist_batch_to_db()` + +When import finishes, all in-memory rows are written once to gui.db: + +- **statement_result** table: Display columns (file_path, result, account, date, amounts, error_type, message) +- **statement_result_payload** table: JSON serialization of the `PdfResult` object (for REVIEW/SUCCESS rows only) + +**JSON Serialization** (`statement_result_model.py`): +- Custom serializer handles non-JSON types: `Path` → POSIX string, `Decimal` → string, `date` → ISO-8601 +- Includes `_type` discriminator so deserializer knows the payload class (Success/Review/Failure) +- Deserialization gracefully skips corrupt rows + +### Phase 4: Debug Generation (Background) +**Location:** `src/openstan/presenters/statement_result_presenter.py::DebugWorker` + +After persist completes, for each non-success row: +1. Calls `bsp.debug_pdf_statement()` off-thread +2. Returns path to debug JSON file +3. Updates `statement_result.debug_json_path` and `debug_status` in gui.db +4. Updates `DebugInfoDialog` live with progress + +**Cancel Support:** User can cancel debug worker during project switch or app exit + +### Phase 5: Commit to Project DB +**Location:** `src/openstan/presenters/statement_result_presenter.py::CommitWorker` + +Three-step batch operation: + +``` +1. bsp.update_db(processed_pdfs, batch_id, session_id, user_id, path, ...) + → Persists to project.db datamart (statements, accounts, transactions, balances) + +2. bsp.copy_statements_to_project(processed_pdfs, pdfs, project_path) + → Copies PDF files to project structure + +3. bsp.delete_temp_files(processed_pdfs, project_path) + → Cleans up temporary working files +``` + +On success: +- Soft-delete result rows from gui.db (so results panel clears immediately) +- Hard-delete after debug worker finishes +- Clear queue batch lock (unlock for next import) + +--- + +## 3. Statement Debugging & Analysis UI + +### DebugInfoDialog +**Location:** `src/openstan/views/debug_info_dialog.py` + +Modal dialog opened by "View Debug Info" button. Shows all REVIEW and FAILURE rows. + +**Columns:** +- Statement (filename) +- Type (REVIEW | FAILURE) +- Debug Status (pending → done | error) +- Debug JSON button (opens JSON file in browser when done) +- PDF button (opens original PDF) +- Parquet button (REVIEW rows only — opens ParquetViewDialog) +- Anonymise button (if project_paths available) +- Message (error text) + +**Live Updates:** Rows update as DebugWorker completes each entry + +### ParquetViewDialog +**Location:** `src/openstan/views/parquet_view_dialog.py` + +Displays parquet data from REVIEW statements: +- **checks_and_balances** — validation results (single-row table) +- **statement_heads** — statement summary (single-row table) +- **statement_lines** — transactions detail + totals row + +**Features:** +- Columns auto-sized, user-resizable +- ID_* and index columns hidden +- Full value shown on cell hover (tooltip) +- Uses `StanPolarsModel` to display Polars DataFrames +- Handles missing files gracefully + +### Project Info View +**Location:** `src/openstan/presenters/project_presenter.py::get_project_info()` + +When project is selected, queries the datamart (project.db) via BSP's Polars API: + +**Data Displayed:** +- Transaction count +- Statement count +- Account count +- Date range (earliest → latest statement) +- Per-account summary (statements, date range, totals in/out, latest balance) +- Gap report (missing statements between dates) + +Uses Polars lazy queries: `bsp.db.DimStatement()`, `bsp.db.DimAccount()`, `bsp.db.FactTransaction()`, etc. + +--- + +## 4. Current File Handling & Export Mechanisms + +### Statement Queue File Management +**Add Statements:** +- Single files: User selects via file dialog +- Folders: User selects root folder, app discovers PDFs recursively, groups by parent directory +- Drag & drop: PDFs and folders can be dropped onto the queue tree view +- Tree state persisted: Expanded/collapsed folders remembered across refresh + +**Remove Statements:** +- Individual: Click remove button (requires multi-parent FK support) +- All: Clear all items (with confirmation) + +### Debug Output Files +**Generated by BSP:** +- **debug_json_path** → `/log/debug//.json` + - Contains parsed PDF structure, validation failures, error details + - Human-readable JSON format + - Persisted in `statement_result.debug_json_path` in gui.db + +**User Access:** +- Click "Debug JSON" button in DebugInfoDialog → opens in browser +- Click "PDF" button → opens original PDF in system viewer +- Click "View Parquet" (REVIEW only) → opens ParquetViewDialog with parquet tables + +### Export Functions +**Location:** `src/openstan/presenters/export_data_presenter.py` + `advanced_export_presenter.py` + +**Standard Exports** (via BSP default export functions): +- CSV (single file) +- Excel workbook +- JSON file + +**Advanced Exports** (via BSP `export_spec`): +- Custom TOML specifications in `/config/export/` +- Scanned and built into spec button list +- User selects account/statement filters +- Runs `bsp_export_spec()` off-thread + +**Features:** +- Browse custom folder or use BSP defaults +- Exports run on background thread (GUI stays responsive) +- System file manager opens output folder on success +- Error modal on failure (shows traceback) + +--- + +## 5. Database Schema (gui.db) + +OpenStan maintains its own SQLite database (gui.db) separate from the BSP project.db datamart. + +**Key Tables:** + +| Table | Purpose | +|-------|---------| +| `statement_queue` | PDFs queued for import; locked by batch_id during processing | +| `statement_result` | Display columns from parsed statements (file, result type, account, date, amounts) | +| `statement_result_payload` | JSON serialization of PdfResult (for REVIEW/SUCCESS only) | +| `batch` | Metadata: batch_id, session_id, PDF count, error/review counts, processing duration, timestamp | +| `session` | User session info | +| `user` | User credentials | +| `project` | Project names and paths | + +**Soft Delete Pattern:** +- After commit, result rows are marked `deleted=1` (soft delete) +- Results panel clears immediately +- Hard delete (`DELETE FROM statement_result WHERE deleted=1`) happens after debug worker finishes +- Prevents orphaned rows if debug worker is cancelled + +--- + +## 6. Worker Thread Architecture + +### SQWorker (Statement Queue Worker) +**Purpose:** Import PDFs in background + +```python +class SQWorker(QRunnable): + def run(self): + for file in queue: + pdf_result = bsp.process_pdf_statement(pdf, batch_id, ...) + signals.progress.emit(file_path, progress_pc, pdf_result, queue_id) +``` + +**Signals:** +- `progress(Path, int, PdfResult, str)` — emitted per file +- `finished()` — all files processed + +### DebugWorker +**Purpose:** Generate debug JSON files for non-success rows + +```python +class DebugWorker(QRunnable): + def run(self): + for row in non_success_rows: + debug_json = bsp.debug_pdf_statement(pdf, batch_id, ...) + signals.entry_done.emit(result_id, debug_json_path) +``` + +**Features:** +- Cancellable via `threading.Event` +- Graceful error handling per row +- Skips failed entries (doesn't abort entire batch) + +### CommitWorker +**Purpose:** Persist batch to project.db (three-step BSP operation) + +```python +class CommitWorker(QRunnable): + def run(self): + bsp.update_db(processed_pdfs, ...) # Persist to datamart + bsp.copy_statements_to_project(...) # Copy PDFs + bsp.delete_temp_files(...) # Clean up +``` + +**Signals:** +- `step(str)` — step description (for progress bar) +- `finished()` — all three calls succeeded +- `error(str)` — operation failed + +### ExportWorker +**Purpose:** Run BSP export functions off-thread + +```python +class ExportWorker(QRunnable): + def __init__(self, fn: Callable, description: str, output_folder: Path): + self._fn = fn + + def run(self): + try: + self._fn() + signals.finished.emit(description, str(output_folder)) + except Exception: + signals.error.emit(traceback.format_exc()) +``` + +--- + +## 7. Error Handling & Diagnostics + +### Exception Types +Caught from BSP: +- `bsp.StatementError` — Statement-level parsing failures +- `bsp.ProjectError` — Project initialization failures +- `bsp.modules.errors.TestGateFailure` — Validation failures (checks & balances) + +**Handling Pattern:** +```python +try: + result = bsp.process_pdf_statement(pdf, ...) +except sqlite3.OperationalError, bsp.StatementError: + # Datamart not ready or statement parse failed + # Gracefully skip and continue +except Exception as e: + traceback.print_exc(file=sys.stderr) + # Show error dialog to user +``` + +### Debug Output +- **stderr**: Worker exceptions printed for development +- **GUI dialogs**: User-facing errors via `StanErrorMessage` modal +- **Debug JSON**: Machine-readable parse tree + validation details +- **Project log**: `/log/debug//.json` + +--- + +## 8. State Machine: Queue Lock + +The statement queue uses a batch_id-based lock system to prevent user modification during import: + +``` +UNLOCKED (batch_id = NULL): + - Add/Remove/Clear buttons enabled + - Run Import enabled (if queue has rows) + - View Results hidden + +LOCKED (batch_id = ''): + - Modification buttons disabled + - Run Import disabled + - Locked label visible + - View Results hidden (import still running) + +LOCKED + IMPORT DONE: + - Modification buttons disabled + - Run Import disabled + - Locked label visible + - View Results visible (user can view/commit/abandon) + +COMMITTED or ABANDONED: + - Queue unlocked (batch_id cleared) + - Returns to UNLOCKED state +``` + +--- + +## 9. Session Restore Path + +When user switches projects with a locked batch (incomplete import): + +``` +1. StanPresenter.update_current_project_info() called + ↓ +2. StatementQueuePresenter._restore_lock_state() + → Queries gui.db for batch_id + → If found, applies LOCKED state (shows View Results button) + ↓ +3. StatementResultPresenter.load_results_from_db(batch_id) + → Reads statement_result rows for this batch + → Populates in-memory models (without PdfResult payloads) + → Results panel shown with previous import state + ↓ +4. DebugWorker may already be running + → Dialog shows current progress + → Worker can be cancelled +``` + +--- + +## 10. BSP API Contracts Validated + +**Location:** `tests/test_bsp_contract.py` + +Automated tests ensure: +- Function signatures haven't changed (`process_pdf_statement`, `update_db`, etc.) +- Result types exist and have required fields (Success, Review, Failure) +- Exception types are importable and correct +- TestHarness API works as expected + +**Run tests:** +```bash +uv run pytest tests/test_bsp_contract.py -v +``` + +--- + +## 11. Key Integration Patterns + +### Pattern 1: Batch Metadata Flow +``` +session_id, user_id, batch_id, project_path + ↓ +bsp.process_pdf_statement(pdf, batch_id=batch_id, session_id=session_id, ...) + ↓ +Returned in PdfResult, passed to bsp.update_db() + ↓ +Persisted to gui.db + project.db for audit trail +``` + +### Pattern 2: Path Handling +- Input PDFs: User provides absolute paths +- Project paths: Resolved via `bsp.ProjectPaths.resolve()` +- Output files: `/log/debug//`, `/statements/` +- Temp files: Managed by BSP, deleted after commit + +### Pattern 3: Polars Integration +- Datamart tables accessed via lazy Polars API +- Queries run off-thread to prevent UI blocking +- DataFrames converted to `StanPolarsModel` for display +- Column hiding, sorting, filtering handled by view + +### Pattern 4: Soft-Delete + Hard-Delete +- Soft delete: `UPDATE ... SET deleted=1` (UI clears immediately) +- Background work continues (debug worker) +- Hard delete: `DELETE FROM ...` (when background work done) +- Prevents orphaned rows from hung workers + +--- + +## 12. Next Steps for Enhancement + +Based on current architecture, potential enhancements could include: + +1. **Statement Export Dialog** — Per-statement CSV/JSON export from debug view +2. **Batch History** — View/retry/export previous batches from gui.db +3. **Anonymisation** — In-app anonymisation (currently opens external dialog) +4. **Advanced Filtering** — Search/filter statements by account, date range, error type +5. **Parquet Data Export** — Export statement_lines/heads to CSV from ParquetViewDialog +6. **Diagnostic Report** — Generate HTML report of batch results +7. **API Mode** — RESTful interface for headless batch processing + diff --git a/BSP_KEY_FILES_REFERENCE.md b/BSP_KEY_FILES_REFERENCE.md new file mode 100644 index 0000000..e9b91ff --- /dev/null +++ b/BSP_KEY_FILES_REFERENCE.md @@ -0,0 +1,372 @@ +# Key Files Reference for OpenStan BSP Integration + +## File Structure Overview + +``` +src/openstan/ +├── presenters/ +│ ├── statement_queue_presenter.py # Queue management + SQWorker +│ ├── statement_result_presenter.py # Result display + DebugWorker + CommitWorker +│ ├── project_presenter.py # Project info (queries datamart via BSP) +│ ├── export_data_presenter.py # Standard exports (CSV, Excel, JSON) +│ ├── advanced_export_presenter.py # Spec-based exports +│ ├── stan_presenter.py # Main coordinator +│ ├── workers.py # ExportWorker base class +│ └── [other presenters...] +│ +├── models/ +│ ├── statement_queue_model.py # Statement queue DB model (gui.db) +│ ├── statement_result_model.py # Result models + JSON serialization +│ ├── batch_model.py # Batch metadata model +│ └── [other models...] +│ +├── views/ +│ ├── statement_queue_view.py # Queue tree view +│ ├── statement_result_view.py # Result tabs (SUCCESS/REVIEW/FAILURE) +│ ├── debug_info_dialog.py # Debug info modal + file/parquet buttons +│ ├── parquet_view_dialog.py # Parquet data viewer +│ ├── export_data_view.py # Export panel UI +│ ├── advanced_export_view.py # Advanced export panel UI +│ └── [other views...] +│ +└── data/ + └── create_gui_db.py # GUI DB schema initialization +``` + +## Critical Files by Functionality + +### 1. Statement Queue Management +- **statement_queue_presenter.py** (611 lines) + - `SQWorker` — background thread that calls `bsp.process_pdf_statement()` per PDF + - `StatementQueuePresenter` — handles Add/Remove/Clear UI, lock state machine + - `WorkerSignals` — cross-thread signals for progress/completion + +- **statement_queue_model.py** (283 lines) + - `StatementQueueModel` — QSqlTableModel for statement_queue table + - `StatementQueueTreeModel` — tree model for folder/file display + - Queue locking: `set_batch_id()`, `clear_batch_id()`, `get_batch_id()` + +### 2. Statement Result Processing +- **statement_result_presenter.py** (1012 lines) + - `StatementResultPresenter` — result accumulation, persist, debug, commit + - `DebugWorker` — background debug JSON generation via `bsp.debug_pdf_statement()` + - `CommitWorker` — three-step commit: `update_db()` → `copy_statements()` → `delete_temps()` + - `DebugWorkerSignals`, `CommitWorkerSignals` — cross-thread signals + +- **statement_result_model.py** (599 lines) + - `ResultRow` — in-memory data carrier dataclass + - `SuccessResultModel`, `ReviewResultModel`, `FailureResultModel` — QStandardItemModel + - `StatementResultModel` — DB persistence (display columns only) + - `StatementResultPayloadModel` — JSON payload persistence + - `_pdf_result_to_json()`, `_json_to_pdf_result()` — custom serialization + +### 3. Debugging & Analysis UI +- **debug_info_dialog.py** (309 lines) + - `DebugInfoDialog` — modal showing non-success statements + - Columns: filename, type, status, JSON button, PDF button, Parquet button, Anonymise button + - Live updates as DebugWorker progresses + - Handles missing/stale files gracefully + +- **parquet_view_dialog.py** (261 lines) + - `ParquetViewDialog` — displays three parquet files (checks_and_balances, statement_heads, statement_lines) + - `_TooltipPolarsModel` — custom model with cell hover tooltips + - ID/index columns hidden, numeric columns auto-totaled + - Uses `StanPolarsModel` for Polars DataFrame display + +### 4. Project Information +- **project_presenter.py** (435 lines) + - `get_project_info()` — queries project.db datamart via BSP Polars API + - Collects: tx/statement/account counts, date range, per-account summary, gap report + - Uses: `bsp.db.DimStatement()`, `bsp.db.DimAccount()`, `bsp.db.FactBalance()`, `bsp.db.GapReport()` + - Polars lazy queries collected off-thread + +### 5. Export Mechanisms +- **export_data_presenter.py** (342 lines) + - `ExportDataPresenter` — wires standard export buttons (CSV, Excel, JSON) + - Calls BSP default export functions off-thread via `ExportWorker` + - Custom folder selection or BSP defaults + - Pending batch dialog (for incomplete imports) + +- **advanced_export_presenter.py** (395 lines) + - `AdvancedExportPresenter` — custom TOML spec-based exports + - `_DatamartLoadWorker` — loads DimAccount/DimStatement async + - Scans `/config/export/*.toml` for spec buttons + - Calls `bsp_export_spec()` with user-selected filters + +- **workers.py** (56 lines) + - `ExportWorker` — generic background export runner + - `ExportWorkerSignals` — finished(description, folder) | error(traceback) + - Used by both export_data and advanced_export presenters + +### 6. Database Schema +- **create_gui_db.py** + - Initializes gui.db schema (separate from project.db datamart) + - Tables: statement_queue, statement_result, statement_result_payload, batch, session, user, project + +--- + +## Data Flow Diagrams + +### Import Workflow +``` +User selects PDFs + ↓ +StatementQueuePresenter.open_folder_dialog() / open_file_dialog() + ↓ +StatementQueueModel.add_record() → gui.db statement_queue + ↓ +User clicks "Run Import" + ↓ +StatementQueuePresenter.run_import() + → SQWorker thread spawned + → For each PDF: + - bsp.process_pdf_statement(pdf, batch_id, ...) → PdfResult + - signals.progress.emit(Path, progress%, PdfResult, queue_id) + ↓ +StanPresenter.statement_imported() ← receives from SQWorker + ↓ +StatementResultPresenter.add_result_to_memory(ResultRow) + → Routes to SuccessResultModel, ReviewResultModel, or FailureResultModel + → Displayed in StatementResultView (three tabs, one per category) + ↓ +SQWorker finished + ↓ +StanPresenter.on_import_finished() + ↓ +StatementResultPresenter.persist_batch_to_db(batch_id) + → Iterates in-memory models + → StatementResultModel.add_result() → gui.db statement_result + → StatementResultPayloadModel.add_payload() → gui.db payload (JSON serialization) + ↓ +DebugWorker starts (auto) + → For each non-success row: + - bsp.debug_pdf_statement(pdf, batch_id, ...) → debug JSON path + - signals.entry_done.emit(result_id, debug_json_path) + - StatementResultModel.update_debug_info(result_id, status, path) + ↓ +User clicks "Commit Batch" + ↓ +StatementResultPresenter.__on_commit_batch() + → CommitWorker thread spawned + ↓ +CommitWorker.run() + 1. bsp.update_db(processed_pdfs, ...) → persists to project.db datamart + 2. bsp.copy_statements_to_project() → copies PDF files + 3. bsp.delete_temp_files() → cleans temp files + ↓ +CommitWorker finished successfully + ↓ +StatementResultPresenter.__on_commit_finished() + → StatementResultPayloadModel.delete_payloads_for_results() (no longer needed) + → StatementResultModel.soft_delete_batch() (deleted=1) + → StatementResultModel.hard_delete_soft_deleted() (when debug worker finishes) + → StatementQueueModel.clear_batch_id() → unlock queue + → Queue state: UNLOCKED (ready for next import) +``` + +### Debug Info Access +``` +User clicks "View Debug Info" + ↓ +StatementResultPresenter.__on_view_debug_info() + ↓ +DebugInfoDialog initialized with non-success rows + ↓ +Per row, buttons available: + "Debug JSON" → Click → Open debug_json_path in browser (if exists) + "PDF" → Click → QDesktopServices.openUrl(file_path) + "Parquet" (REVIEW only) → Click → ParquetViewDialog(row.pdf_result) + "Anonymise" → Click → AnonymiseDialog(pdf_path) + ↓ +ParquetViewDialog.display(): + 1. Read parquet files: checks_and_balances, statement_heads, statement_lines + 2. Drop ID/index columns (_drop_id_cols) + 3. statement_lines: show totals row + scrollable data table + 4. Display in StanTableView (tooltips on hover) +``` + +### Export Flow +``` +Standard Export (CSV/Excel/JSON): + User clicks button (button_csv, button_excel, button_json) + ↓ + ExportDataPresenter._on_csv() / ._on_excel() / ._on_json() + ↓ + ExportWorker(fn=bsp.export_csv, description="CSV", output_folder) + ↓ + ExportWorker.run() off-thread + ↓ + signals.finished.emit(description, output_folder) + ↓ + QDesktopServices.openUrl(output_folder) + +--- + +Advanced Export (Spec): + User selects account/statement filters + ↓ + User clicks spec button + ↓ + AdvancedExportPresenter._on_spec_button() + ↓ + ExportWorker(fn=partial(bsp_export_spec, ...), description=spec_name, output_folder) + ↓ + ExportWorker.run() off-thread + ↓ + signals.finished.emit(description, output_folder) + ↓ + QDesktopServices.openUrl(output_folder) +``` + +--- + +## Cross-File Dependencies + +### statement_result_presenter.py depends on: +- `statement_result_model.py` — ResultRow, SuccessResultModel, ReviewResultModel, FailureResultModel, StatementResultModel, StatementResultPayloadModel +- `statement_queue_model.py` — StatementQueueModel (for queue unlock after commit) +- `batch_model.py` — BatchModel (for duration) +- `debug_info_dialog.py` — DebugInfoDialog (view debug JSON/PDF/Parquet) +- `bsp` — process_pdf_statement (via worker), update_db, copy_statements_to_project, delete_temp_files, debug_pdf_statement + +### statement_queue_presenter.py depends on: +- `statement_queue_model.py` — StatementQueueModel, StatementQueueTreeModel +- `bsp` — process_pdf_statement (in SQWorker) + +### project_presenter.py depends on: +- `bsp` — db.DimStatement, db.DimAccount, db.FactBalance, db.GapReport + +### debug_info_dialog.py depends on: +- `statement_result_model.py` — ResultRow +- `parquet_view_dialog.py` — ParquetViewDialog (when "View Parquet" clicked) +- `anonymise_dialog.py` — AnonymiseDialog (when "Anonymise" clicked) + +### parquet_view_dialog.py depends on: +- Polars — pl.read_parquet, DataFrame operations +- Components — StanTableView, StanPolarsModel, StanDialog, StanLabel + +--- + +## Key Classes & Types + +### From BSP (imported as `import bank_statement_parser as bsp`) +```python +bsp.PdfResult +bsp.process_pdf_statement() +bsp.update_db() +bsp.copy_statements_to_project() +bsp.delete_temp_files() +bsp.debug_pdf_statement() +bsp.ProjectPaths.resolve() +bsp.db.DimStatement() +bsp.db.DimAccount() +bsp.db.FactTransaction() +bsp.db.FactBalance() +bsp.db.GapReport() +``` + +### From openstan.models.statement_result_model +```python +ResultRow +SuccessResultModel +ReviewResultModel +FailureResultModel +StatementResultModel +StatementResultPayloadModel +_pdf_result_to_json() +_json_to_pdf_result() +``` + +### From openstan.models.statement_queue_model +```python +StatementQueueModel +StatementQueueTreeModel +_safe_hex_id() +``` + +### Worker Threads +```python +SQWorker (statement_queue_presenter.py) +DebugWorker (statement_result_presenter.py) +CommitWorker (statement_result_presenter.py) +ExportWorker (workers.py) +_DatamartLoadWorker (advanced_export_presenter.py) +``` + +### Dialogs +```python +DebugInfoDialog (debug_info_dialog.py) +ParquetViewDialog (parquet_view_dialog.py) +AnonymiseDialog (anonymise_dialog.py, external to this summary) +PendingBatchDialog (pending_batch_dialog.py, for incomplete batch recovery) +``` + +--- + +## Testing Support + +### Contract Tests (test_bsp_contract.py) +- Validates BSP function signatures and result types +- Catches breaking changes early +- Run with: `uv run pytest tests/test_bsp_contract.py -v` + +### Integration Tests (test_integration.py) +- Full end-to-end import workflows +- Uses BSP TestHarness with anonymised PDFs +- Validates batch processing, debug generation, export + +--- + +## Configuration & Setup + +### Environment Variables (set in __main__.py before BSP import) +```python +os.environ["BSP_PROJECT_ROOT"] = +``` + +### BSP Configuration Files +- Located in: `/project/` (bundled in frozen build) +- Copied to: `/project/` on first run +- Contains: TOML configs, templates, data definitions + +--- + +## Performance Notes + +1. **Off-Thread Workers**: All heavy operations run off-thread to keep GUI responsive + - SQWorker: PDF parsing + - DebugWorker: Debug JSON generation + - CommitWorker: DB updates + file copy + - ExportWorker: Export operations + - _DatamartLoadWorker: Polars lazy evaluation + +2. **Batch Locking**: Queue is locked during import to prevent concurrent edits + - Batch_id stamped on all queue rows + - State machine prevents user actions while locked + +3. **Lazy Polars Queries**: Project info queries use Polars lazy API + - Off-thread execution + - Single .collect() call per query + +4. **Soft-Delete Pattern**: Results cleared immediately after commit + - Soft delete: UI updates instantly + - Hard delete: deferred until debug worker finishes + +--- + +## Error Recovery + +1. **Session Restore**: On app restart with locked batch + - `_restore_lock_state()` checks gui.db for active batch_id + - Results loaded from `statement_result` table + - Debug status restored from `debug_json_path` / `debug_status` columns + +2. **Worker Cancellation**: Debug worker can be cancelled + - Threading.Event set on project change or app exit + - Per-row error handling (skip failed entries) + +3. **Payload Deserialization Errors**: JSON corruption handled gracefully + - Per-row try/catch in `load_payloads_for_batch()` + - Corrupt rows logged and skipped + - Session restore continues + diff --git a/pyproject.toml b/pyproject.toml index 2f40436..ba53980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.14" dependencies = [ "pyside6-essentials>=6.7.0", "tomli-w>=1.2.0", - "uk-bank-statement-parser==0.3.6", + "uk-bank-statement-parser==0.3.7", "uk-bank-statement-anonymiser==0.1.7", ] diff --git a/src/openstan/data/create_gui_db.py b/src/openstan/data/create_gui_db.py index af93961..6657e14 100644 --- a/src/openstan/data/create_gui_db.py +++ b/src/openstan/data/create_gui_db.py @@ -98,22 +98,23 @@ ); CREATE TABLE IF NOT EXISTS "statement_result" ( - "result_id" TEXT, - "batch_id" TEXT NOT NULL, - "queue_id" TEXT NOT NULL, - "project_id" TEXT NOT NULL, - "result" TEXT NOT NULL, - "file_path" TEXT NOT NULL, - "id_account" TEXT, - "statement_date" TEXT, - "payments_in" REAL, - "payments_out" REAL, - "error_type" TEXT, - "message" TEXT, - "debug_json_path" TEXT DEFAULT NULL, - "debug_status" TEXT DEFAULT NULL, - "deleted" INTEGER NOT NULL DEFAULT 0, - "created" TEXT NOT NULL DEFAULT (datetime('now')), + "result_id" TEXT, + "batch_id" TEXT NOT NULL, + "queue_id" TEXT NOT NULL, + "project_id" TEXT NOT NULL, + "result" TEXT NOT NULL, + "file_path" TEXT NOT NULL, + "id_account" TEXT, + "statement_date" TEXT, + "payments_in" REAL, + "payments_out" REAL, + "error_type" TEXT, + "message" TEXT, + "debug_json_path" TEXT DEFAULT NULL, + "debug_excel_path" TEXT DEFAULT NULL, + "debug_status" TEXT DEFAULT NULL, + "deleted" INTEGER NOT NULL DEFAULT 0, + "created" TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY("result_id"), FOREIGN KEY("queue_id") REFERENCES "statement_queue"("queue_id"), FOREIGN KEY("project_id") REFERENCES "project"("project_id") diff --git a/src/openstan/main.py b/src/openstan/main.py index 04d1aec..4abc5d0 100644 --- a/src/openstan/main.py +++ b/src/openstan/main.py @@ -615,7 +615,11 @@ def __init__(self, gui_db, sessionID, username) -> None: def closeEvent(self, a0) -> None: # Warn the user if an import is currently in progress - if self.stan_presenter.statement_result_presenter._importing: # noqa: SLF001 + # (guard against closeEvent before stan_presenter is initialized) + if ( + hasattr(self, "stan_presenter") + and self.stan_presenter.statement_result_presenter._importing # noqa: SLF001 + ): warn = StanInfoMessage(self) warn.setText( "An import is currently in progress.\n\n" diff --git a/src/openstan/models/statement_result_model.py b/src/openstan/models/statement_result_model.py index db847e7..1699f2f 100644 --- a/src/openstan/models/statement_result_model.py +++ b/src/openstan/models/statement_result_model.py @@ -193,6 +193,7 @@ class ResultRow: error_type: str | None message: str | None debug_json_path: Path | None = field(default=None) + debug_excel_path: Path | None = field(default=None) debug_status: str | None = field(default=None) pdf_result: "PdfResult | None" = field(default=None) @@ -364,6 +365,7 @@ def add_result( record.setValue("error_type", error_type) record.setValue("message", message) record.setValue("debug_json_path", None) + record.setValue("debug_excel_path", None) record.setValue("debug_status", None) record.setValue("deleted", 0) # QSqlTableModel sets all unset columns to NULL — including 'created' @@ -385,16 +387,20 @@ def update_debug_info( result_id: str, status: str, debug_json_path: Path | None, + debug_excel_path: Path | None = None, ) -> tuple[bool, str]: - """Update debug_status and debug_json_path for a single result row.""" + """Update debug_status, debug_json_path, and debug_excel_path for a single result row.""" query = QSqlQuery(self.database()) query.prepare( "UPDATE statement_result " - "SET debug_status = :status, debug_json_path = :path " + "SET debug_status = :status, debug_json_path = :json_path, debug_excel_path = :excel_path " "WHERE result_id = :result_id" ) query.bindValue(":status", status) - query.bindValue(":path", str(debug_json_path) if debug_json_path else None) + query.bindValue(":json_path", str(debug_json_path) if debug_json_path else None) + query.bindValue( + ":excel_path", str(debug_excel_path) if debug_excel_path else None + ) query.bindValue(":result_id", result_id) if query.exec(): self.select() @@ -464,6 +470,7 @@ def get_rows_for_batch(self, batch_id: str) -> list[ResultRow]: pin = rec.value("payments_in") pout = rec.value("payments_out") djp = rec.value("debug_json_path") + dep = rec.value("debug_excel_path") rows.append( ResultRow( result_id=str(rec.value("result_id")), @@ -479,6 +486,7 @@ def get_rows_for_batch(self, batch_id: str) -> list[ResultRow]: error_type=rec.value("error_type") or None, message=rec.value("message") or None, debug_json_path=Path(str(djp)) if djp else None, + debug_excel_path=Path(str(dep)) if dep else None, debug_status=rec.value("debug_status") or None, ) ) diff --git a/src/openstan/presenters/anonymise_presenter.py b/src/openstan/presenters/anonymise_presenter.py index 8f022df..b31ecaa 100644 --- a/src/openstan/presenters/anonymise_presenter.py +++ b/src/openstan/presenters/anonymise_presenter.py @@ -353,7 +353,7 @@ def _set_input_path(self, path: Path) -> None: @Slot() def _run_anonymisation(self) -> None: """Save configs and kick off background anonymisation. - + Automatically saves the current table state to both TOML files (with 3-retry logic) before starting the anonymisation process. """ @@ -425,7 +425,7 @@ def _on_error(self, message: str) -> None: @Slot(int) def _on_dialog_finished(self, result: int) -> None: """Save configs when dialog is closing. - + Called automatically when the dialog is closed via any method (Close button, X button, accept, or reject). Attempts to save the current table state to both TOML files using 3-retry logic. diff --git a/src/openstan/presenters/statement_result_presenter.py b/src/openstan/presenters/statement_result_presenter.py index ff76f03..2bef255 100644 --- a/src/openstan/presenters/statement_result_presenter.py +++ b/src/openstan/presenters/statement_result_presenter.py @@ -163,8 +163,8 @@ class DebugWorkerSignals(QObject): """Cross-thread signals for DebugWorker.""" # Emitted once per non-success row after debug_pdf_statement completes. - # Carries: (result_id, debug_json_path | None) - entry_done = Signal(str, object) + # Carries: (result_id, debug_json_path | None, debug_excel_path | None) + entry_done = Signal(str, object, object) all_done = Signal() error = Signal(str) @@ -215,6 +215,7 @@ def run(self) -> None: result_id = self._result_ids[i] if i < len(self._result_ids) else "" debug_json_path: Path | None = None + debug_excel_path: Path | None = None try: debug_json_path = bsp.debug_pdf_statement( pdf=row.file_path, @@ -223,15 +224,24 @@ def run(self) -> None: account_key=None, project_path=self._project_path, ) + # Detect Excel file if JSON exists + if debug_json_path is not None: + debug_excel_path = ( + debug_json_path.parent / "debug_dataframes.xlsx" + ) + if not debug_excel_path.exists(): + debug_excel_path = None except Exception: traceback.print_exc(file=sys.stderr) print( f"WARNING: debug_pdf_statement failed for {row.file_path.name}; " - "entry will have no debug JSON.", + "entry will have no debug files.", file=sys.stderr, ) - self.signals.entry_done.emit(result_id, debug_json_path) + self.signals.entry_done.emit( + result_id, debug_json_path, debug_excel_path + ) except Exception: traceback.print_exc(file=sys.stderr) @@ -645,16 +655,23 @@ def __start_debug_worker(self, batch_id: str) -> None: @Slot(str, object) def __on_debug_entry_done( - self, result_id: str, debug_json_path: "Path | None" + self, + result_id: str, + debug_json_path: "Path | None", + debug_excel_path: "Path | None", ) -> None: """Persist debug result for one row and update the open dialog if any.""" status = "done" if debug_json_path is not None else "error" - self.result_model.update_debug_info(result_id, status, debug_json_path) + self.result_model.update_debug_info( + result_id, status, debug_json_path, debug_excel_path + ) self._debug_done_count += 1 if self._debug_dialog is not None: - self._debug_dialog.update_row(result_id, status, debug_json_path) + self._debug_dialog.update_row( + result_id, status, debug_json_path, debug_excel_path + ) self.__update_debug_button_label() diff --git a/src/openstan/views/debug_info_dialog.py b/src/openstan/views/debug_info_dialog.py index a632b90..ed995c0 100644 --- a/src/openstan/views/debug_info_dialog.py +++ b/src/openstan/views/debug_info_dialog.py @@ -2,14 +2,15 @@ Opened by the "View Debug Info" button in the Statement Results panel. Populated live as the background DebugWorker emits ``entry_done`` signals, -and updated on session restore from persisted ``debug_status`` / ``debug_json_path`` -fields in ``statement_result``. +and updated on session restore from persisted ``debug_status`` / ``debug_json_path`` / +``debug_excel_path`` fields in ``statement_result``. Each row shows: - Statement filename - Result type (REVIEW / FAILURE) - Debug status - "Open JSON" button (enabled once debug_json_path is known) + - "Open Excel" button (enabled once debug_excel_path is known) - "Open PDF" button (always enabled — uses the original file_path) - "View Parquet" button (REVIEW rows only — opens ParquetViewDialog) - "Anonymise" button (enabled when project_paths is available) @@ -42,15 +43,17 @@ _COL_TYPE = 1 _COL_STATUS = 2 _COL_JSON = 3 -_COL_PDF = 4 -_COL_PARQUET = 5 -_COL_ANON = 6 -_COL_MESSAGE = 7 +_COL_EXCEL = 4 +_COL_PDF = 5 +_COL_PARQUET = 6 +_COL_ANON = 7 +_COL_MESSAGE = 8 _HEADERS = [ "Statement", "Type", "Debug Status", "Debug JSON", + "Debug Excel", "PDF", "Parquet", "Anonymise", @@ -102,6 +105,7 @@ def __init__( hdr.setSectionResizeMode(_COL_TYPE, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_COL_STATUS, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_COL_JSON, QHeaderView.ResizeMode.ResizeToContents) + hdr.setSectionResizeMode(_COL_EXCEL, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_COL_PDF, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_COL_PARQUET, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_COL_ANON, QHeaderView.ResizeMode.ResizeToContents) @@ -136,8 +140,9 @@ def update_row( result_id: str, status: str, debug_json_path: Path | None, + debug_excel_path: Path | None = None, ) -> None: - """Update the status and JSON link for a single row.""" + """Update the status and debug file links for a single row.""" idx = self._row_index.get(result_id) if idx is None: return @@ -157,6 +162,16 @@ def update_row( json_widget.setEnabled(False) json_widget.setProperty("_path", None) + # Update Excel button + excel_widget = self._table.cellWidget(idx, _COL_EXCEL) + if isinstance(excel_widget, StanButton): + if debug_excel_path is not None and debug_excel_path.exists(): + excel_widget.setEnabled(True) + excel_widget.setProperty("_path", str(debug_excel_path)) + else: + excel_widget.setEnabled(False) + excel_widget.setProperty("_path", None) + self.__update_status_label() def set_all_done(self) -> None: @@ -202,6 +217,19 @@ def __add_row(self, row: "ResultRow") -> None: ) self._table.setCellWidget(table_row, _COL_JSON, json_btn) + # Open Excel button + excel_btn = StanButton("Open Excel", min_width=0) + excel_btn.setEnabled(False) + excel_btn.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + excel_btn.setToolTip("View extracted dataframes in multi-tab Excel file") + if row.debug_excel_path is not None and row.debug_excel_path.exists(): + excel_btn.setEnabled(True) + excel_btn.setProperty("_path", str(row.debug_excel_path)) + excel_btn.clicked.connect( + lambda _checked, btn=excel_btn: self.__open_file(btn.property("_path")) + ) + self._table.setCellWidget(table_row, _COL_EXCEL, excel_btn) + # Open PDF button pdf_btn = StanButton("Open PDF", min_width=0) pdf_btn.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) diff --git a/uv.lock b/uv.lock index 30ceab1..b423095 100644 --- a/uv.lock +++ b/uv.lock @@ -81,37 +81,37 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/56/10a88e00039537d74bd420f0457c52ab8f58a1af56126e3b9f1b1c8c4724/charset_normalizer-3.4.8.tar.gz", hash = "sha256:d9bf144d6faf12c70d58e47f7512992ae2882b820031d6cef68152deb645bf2d", size = 151790, upload-time = "2026-07-06T15:27:58.477Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/bc/0a8540b8cd494951cca1428606373942803f5ffcec40fe798f819c5a8adb/charset_normalizer-3.4.8-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:77e993ecf65f21ab1f82266ff5e84a7de2c879e7d9b8bc006009df83f22a1d5e", size = 316993, upload-time = "2026-07-06T15:26:49.962Z" }, - { url = "https://files.pythonhosted.org/packages/0e/99/a0868f0a1f0a045fd374d1f2cf7042d8ad5d7fb4dd1f4ac7365e319f7e32/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:524939917f17f6de502dfda30b472550965740d7f126659d4c4f8dd1569cce22", size = 215638, upload-time = "2026-07-06T15:26:51.338Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/43c4d09a09b5557cc5fe1d87c9d96f86a3942aec0517d2b5408cef87ca75/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a4508989ba8e2ce43ef989453d18188b261546e8188cbdd4ef451fb9e4c3b467", size = 236456, upload-time = "2026-07-06T15:26:52.531Z" }, - { url = "https://files.pythonhosted.org/packages/e2/67/492ca98b3ab785b736b5da10c1bc233e1c8fec6c0cdb29b482c38bfc52a2/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e44127f7d11eee4548ad2cdf1f4e1b6eaaddd5cb92d15ad65f6ecc9bcf403ab", size = 232253, upload-time = "2026-07-06T15:26:53.838Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/1e6eff58c14f1aace1e26d80defbeaea2d35e075dbe4b611111ee4b47fa8/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb90317359f7e67bb6df615999a95e0980877468e617ddce8b6c2f8e7fe60d95", size = 222886, upload-time = "2026-07-06T15:26:55.009Z" }, - { url = "https://files.pythonhosted.org/packages/40/7a/90056a5326b0c4b9a3f924d337729c344c11542e5bc7191e50410db61587/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:35d9e7a9c960520ae89d1f4e305d1c047a74dea2e0f73a0e84f879356c2e8776", size = 206482, upload-time = "2026-07-06T15:26:56.306Z" }, - { url = "https://files.pythonhosted.org/packages/18/ff/94761d31a33878dbb5008ddbd918615061fcf5c0a612aa3075450e60f628/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e322b054c7ff886f78feab7360736bb45de2e18cf4a0ee84e8fc5a08d53a19", size = 218929, upload-time = "2026-07-06T15:26:57.422Z" }, - { url = "https://files.pythonhosted.org/packages/2b/dc/00b9675acd7c4b926b9102ee3f0d1a570ce943901be73b87485001393fe1/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c0086d97094363556206dc3bcf43f7edcfc043ea7a568a46f45efea74858bd1", size = 218069, upload-time = "2026-07-06T15:26:58.719Z" }, - { url = "https://files.pythonhosted.org/packages/04/11/94ada5a0482ee4bf688d04be4c7d6fd945d37370d04a95671040dfe2b416/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0752c849b51198267df2aba013c4de3a2955bd014a4fd70828809946c1acbc0c", size = 207146, upload-time = "2026-07-06T15:27:00.058Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f7/246bd36762207ab4752cd436b64e5d81a1668b15ddea7b5b2d0e8545e727/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2a4707e09eca11e81ece4fced600c5a0a801f568b962244f6f517bc274745fc9", size = 224896, upload-time = "2026-07-06T15:27:01.599Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f7/3510622d1fbe13b0ebf827c475e40a27e2be427140d792878b63ab6425cc/charset_normalizer-3.4.8-cp314-cp314-win32.whl", hash = "sha256:8ea67f427c073ae3da0923aa55f3715131fa613a61a7f2f8d762bde75eaf00ae", size = 150851, upload-time = "2026-07-06T15:27:02.964Z" }, - { url = "https://files.pythonhosted.org/packages/32/2b/9ce65dd21672b55cf800cca5f4433afa1586fda1d78731067ec9ec544c62/charset_normalizer-3.4.8-cp314-cp314-win_amd64.whl", hash = "sha256:ff71018850863362e5c7533769d0a9f77715c31af1502d523630ce822922f5c9", size = 162549, upload-time = "2026-07-06T15:27:04.249Z" }, - { url = "https://files.pythonhosted.org/packages/2f/34/9a5967eed666a88f31a0866884606d9ec3c2cd6091e2ccd7e0b4c4176c35/charset_normalizer-3.4.8-cp314-cp314-win_arm64.whl", hash = "sha256:44464e66f4da2f21dea7145c7693f9f60717ca4794a954dea5bf8c2c932678bd", size = 153079, upload-time = "2026-07-06T15:27:05.608Z" }, - { url = "https://files.pythonhosted.org/packages/02/4f/aa44cc81d8987f105352c74c0bf919007f8b80e9880d28bcf0393c1a816e/charset_normalizer-3.4.8-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:50a0c2e58ad2c203adb616fef28941b7e13716adbc25e0dfaeec29f5afe6382f", size = 338586, upload-time = "2026-07-06T15:27:06.86Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2b/b0392e2b235c08ff0623d905c2ee8ac820620544043c1ce92ce0b3d64c55/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1e589fdb95c76f08288bbb346230cdd8994db74903db6637b380f7b5fc9336", size = 222764, upload-time = "2026-07-06T15:27:08.23Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a1/7d466879190731f5559662c22232646f2ae2dace2323c3e5aefcf78d458a/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3d7c887444c5a7ef0d68d358d81e758a850bc626f8e639e2ca5667153272b20", size = 241331, upload-time = "2026-07-06T15:27:09.512Z" }, - { url = "https://files.pythonhosted.org/packages/70/17/8b89e797137aa28c8fb0bafbafc243246a7afe21620a13b00e37624ece1d/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:65c389b96c0cfff3a3f0458fa1c7ce554a30e23101a88a49f03997afce6a929f", size = 239323, upload-time = "2026-07-06T15:27:10.86Z" }, - { url = "https://files.pythonhosted.org/packages/d7/98/1c1940730ed22d50983be4e243c722c89d5136d6f073bd840d1128bfddcb/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593403fc47dcdf55e2987b2e3cc2e064127e2b908929f1f18b2e4a4652cbd780", size = 229964, upload-time = "2026-07-06T15:27:12.113Z" }, - { url = "https://files.pythonhosted.org/packages/53/2d/bb8e81b7ff603d3f77e9a8a5d1ad34fcabbf3c54d300c29d99fba581fa23/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:606088e9fa2b7469ab9c42d4da8e05a415622a07714edd2fcd8fed48dda4c853", size = 212405, upload-time = "2026-07-06T15:27:13.447Z" }, - { url = "https://files.pythonhosted.org/packages/ce/1f/e52a3a53b13da591bb8f21d29e63877268eadf20686b7762351d4b89062c/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0317406326fed512f42a1632ad91a96228a7616c06547666a6dd79967f1bd6ca", size = 226918, upload-time = "2026-07-06T15:27:14.89Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f9/32996d79c57189af9722fe618f46d8a86b7be035ca98887b8d0c3821f141/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b67d50ee47e5c57a0064a9cb575b963a7125819dfd1fd094d44d378fff94659b", size = 225113, upload-time = "2026-07-06T15:27:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d2/9248c18e695696513774523a794cfb8b677521ce9ad7554d301cb10a9b20/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79e402b869f270140afa5e2b0e2ac100585358d812fe3dd093d424f7a72964e0", size = 214966, upload-time = "2026-07-06T15:27:17.418Z" }, - { url = "https://files.pythonhosted.org/packages/1e/9d/4b19432d406179a40f924691906ee5b15ac664b408971c973295192444ea/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2970b9f7ab69ec3a0423ec6b6ac718e79fbf4a282c0bc103ef88c1ef50dfa15a", size = 231699, upload-time = "2026-07-06T15:27:19.131Z" }, - { url = "https://files.pythonhosted.org/packages/be/41/bdbdf71e8c3ccff10ef3cc2bb9467a7fdb3dc94b9a406d1a3c44afd39632/charset_normalizer-3.4.8-cp314-cp314t-win32.whl", hash = "sha256:458c2972a78043b7261c9726670029f15f722e70669bcbe961153a01968f589f", size = 155333, upload-time = "2026-07-06T15:27:20.681Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f8/e05c69323bd50091ec39f5f885385b884624b0131a6885a0c83a6217ba7a/charset_normalizer-3.4.8-cp314-cp314t-win_amd64.whl", hash = "sha256:0c926329a1df7cd56d7d8349fe354460d20aefd2e394c9e159e479d018b2b359", size = 167378, upload-time = "2026-07-06T15:27:22.042Z" }, - { url = "https://files.pythonhosted.org/packages/c2/04/cbaf1a2f5e2bbf70760e774380cbf052b10849fc35e770905df31af5cf00/charset_normalizer-3.4.8-cp314-cp314t-win_arm64.whl", hash = "sha256:2232baea80a2b01783679fed4e625ccdb19a974f44c9cf0fba21a777a4c8179c", size = 157782, upload-time = "2026-07-06T15:27:23.312Z" }, - { url = "https://files.pythonhosted.org/packages/23/52/d5bee5b6ea81882d549b566d2545b044bbcbc33fe5fbe001008a7e745a21/charset_normalizer-3.4.8-py3-none-any.whl", hash = "sha256:b7c1fb310df524e01fbe84d43b7f95aa4f808f8eaa0dafc185f64ba395e37d54", size = 64279, upload-time = "2026-07-06T15:27:57.043Z" }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] @@ -249,11 +249,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.6" +version = "3.29.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/c8/35bdf04fb30755e2ed758f877edf3eb4a243c2463d3a258cc28b18b7a6e2/filelock-3.29.6.tar.gz", hash = "sha256:895c532ef3f4ef04972b9446a8c4e2931a5c399ff3c4be4c9369f2639b80f793", size = 70301, upload-time = "2026-07-06T23:08:08.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/49/7467c2946ccd9617f7da38187071bdc45bb9a95df51f4d63d6622432ce4e/filelock-3.29.6-py3-none-any.whl", hash = "sha256:14d5f5597d2e0c4dbd774cfb6d8132da1db44da83732aab679d54f7dcf97ab65", size = 45478, upload-time = "2026-07-06T23:08:07.197Z" }, + { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, ] [[package]] @@ -452,7 +452,7 @@ requires-dist = [ { name = "pyside6-essentials", specifier = ">=6.7.0" }, { name = "tomli-w", specifier = ">=1.2.0" }, { name = "uk-bank-statement-anonymiser", specifier = "==0.1.7" }, - { name = "uk-bank-statement-parser", specifier = "==0.3.6" }, + { name = "uk-bank-statement-parser", specifier = "==0.3.7" }, ] [package.metadata.requires-dev] @@ -908,7 +908,7 @@ wheels = [ [[package]] name = "uk-bank-statement-parser" -version = "0.3.6" +version = "0.3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dacite" }, @@ -916,9 +916,9 @@ dependencies = [ { name = "polars" }, { name = "xlsxwriter" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/f2/ec0ba1e61641d70ccc573a6f2bd7c6286ac926513abb060bfe1f05341e71/uk_bank_statement_parser-0.3.6.tar.gz", hash = "sha256:ba9f92ce9285651171af1df35c0a745e98109e8b3d79c60c5536284c6b810da2", size = 127422, upload-time = "2026-07-01T14:56:28.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/a3/e026fef9f12d4953787ca696cfaad0bff2ac0df544bb166321634c78bc32/uk_bank_statement_parser-0.3.7.tar.gz", hash = "sha256:f76f3bba05c0ec746a106b5c78b4658293566c2aab3a71a20ddb83ad59f53b9b", size = 127415, upload-time = "2026-07-08T10:29:34.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/61/4823bf257aea498ecee22e7668009e6f75357781f35759487c5a313dc7ed/uk_bank_statement_parser-0.3.6-py3-none-any.whl", hash = "sha256:cea4257eca6ff61199f896cf2eea1540a4e47c6354363305a057ef2b63bb8f12", size = 167610, upload-time = "2026-07-01T14:56:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/d9/65/e267eb31829d31d381859c9c065181727e2a36766c7ab779b25f596e29dc/uk_bank_statement_parser-0.3.7-py3-none-any.whl", hash = "sha256:17dd4e07e8a49b8e977b8c1cb90e42c163d6cbeba9abf270400e2b2950f5120a", size = 167632, upload-time = "2026-07-08T10:29:33.207Z" }, ] [[package]]