From 159cba508e749de13bb709201b542f3a8fb052a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elisi=C3=A1rio=20Couto?= Date: Sun, 7 Dec 2025 11:27:55 +0000 Subject: [PATCH] fix: Resolve all lint warnings and type errors across frontend and backend. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend: - Memoize pagination object in TransactionsTable to prevent unnecessary re-renders and fix exhaustive-deps warning - Add optional success and message fields to backup API response types for proper error handling Backend: - Add TypedDict for transaction type configuration to improve type safety in generate_sample_db - Fix unpacking of amount_range with explicit float type hints - Add explicit type hints for descriptions dictionary and specific_descriptions variable - Fix sync endpoint return types: get_sync_status returns SyncStatus and sync_now returns SyncResult - Fix transactions endpoint data type declaration to properly support Union types in PaginatedResponse All checks now pass: - Frontend: npm lint and npm build ✓ - Backend: mypy type checking ✓ - Backend: ruff lint on modified files ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- frontend/src/components/TransactionsTable.tsx | 26 ++++++++------- frontend/src/lib/api.ts | 32 +++++++++++++------ leggen/api/models/common.py | 2 +- leggen/api/routes/accounts.py | 9 +----- leggen/api/routes/backup.py | 12 ++----- leggen/api/routes/sync.py | 6 ++-- leggen/api/routes/transactions.py | 4 +-- leggen/commands/generate_sample_db.py | 18 ++++++++--- tests/conftest.py | 9 +++--- tests/unit/test_api_backup.py | 5 +-- tests/unit/test_api_transactions.py | 1 - 11 files changed, 66 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/TransactionsTable.tsx b/frontend/src/components/TransactionsTable.tsx index 9fd50f4..13fb601 100644 --- a/frontend/src/components/TransactionsTable.tsx +++ b/frontend/src/components/TransactionsTable.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { useReactTable, @@ -126,16 +126,20 @@ export default function TransactionsTable() { }); const transactions = transactionsResponse?.data || []; - const pagination = transactionsResponse - ? { - page: transactionsResponse.page, - total_pages: transactionsResponse.total_pages, - per_page: transactionsResponse.per_page, - total: transactionsResponse.total, - has_next: transactionsResponse.has_next, - has_prev: transactionsResponse.has_prev, - } - : undefined; + const pagination = useMemo( + () => + transactionsResponse + ? { + page: transactionsResponse.page, + total_pages: transactionsResponse.total_pages, + per_page: transactionsResponse.per_page, + total: transactionsResponse.total, + has_next: transactionsResponse.has_next, + has_prev: transactionsResponse.has_prev, + } + : undefined, + [transactionsResponse], + ); // Check if search is currently debouncing const isSearchLoading = filterState.searchTerm !== debouncedSearchTerm; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 3ef6b9e..34669ce 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -288,11 +288,14 @@ export const apiClient = { return response.data; }, - testBackupConnection: async (test: BackupTest): Promise<{ connected?: boolean }> => { - const response = await api.post<{ connected?: boolean }>( - "/backup/test", - test, - ); + testBackupConnection: async ( + test: BackupTest, + ): Promise<{ connected?: boolean; success?: boolean; message?: string }> => { + const response = await api.post<{ + connected?: boolean; + success?: boolean; + message?: string; + }>("/backup/test", test); return response.data; }, @@ -301,11 +304,20 @@ export const apiClient = { return response.data; }, - performBackupOperation: async (operation: BackupOperation): Promise<{ operation: string; completed: boolean }> => { - const response = await api.post<{ operation: string; completed: boolean }>( - "/backup/operation", - operation, - ); + performBackupOperation: async ( + operation: BackupOperation, + ): Promise<{ + operation: string; + completed: boolean; + success?: boolean; + message?: string; + }> => { + const response = await api.post<{ + operation: string; + completed: boolean; + success?: boolean; + message?: string; + }>("/backup/operation", operation); return response.data; }, }; diff --git a/leggen/api/models/common.py b/leggen/api/models/common.py index 7f02ca1..4834310 100644 --- a/leggen/api/models/common.py +++ b/leggen/api/models/common.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Generic, List, TypeVar +from typing import Generic, List, TypeVar from pydantic import BaseModel diff --git a/leggen/api/routes/accounts.py b/leggen/api/routes/accounts.py index e53177e..84dccd9 100644 --- a/leggen/api/routes/accounts.py +++ b/leggen/api/routes/accounts.py @@ -246,11 +246,6 @@ async def get_account_transactions( offset=offset, ) - # Get total count for pagination info - total_transactions = await database_service.get_transaction_count_from_db( - account_id=account_id, - ) - data: Union[List[TransactionSummary], List[Transaction]] if summary_only: @@ -299,9 +294,7 @@ async def get_account_transactions( @router.put("/accounts/{account_id}") -async def update_account_details( - account_id: str, update_data: AccountUpdate -) -> dict: +async def update_account_details(account_id: str, update_data: AccountUpdate) -> dict: """Update account details (currently only display_name)""" try: # Get current account details diff --git a/leggen/api/routes/backup.py b/leggen/api/routes/backup.py index de14e30..3897b9b 100644 --- a/leggen/api/routes/backup.py +++ b/leggen/api/routes/backup.py @@ -129,9 +129,7 @@ async def test_backup_connection(test_request: BackupTest) -> dict: success = await backup_service.test_connection(s3_config) if not success: - raise HTTPException( - status_code=400, detail="S3 connection test failed" - ) + raise HTTPException(status_code=400, detail="S3 connection test failed") return {"connected": True} @@ -193,9 +191,7 @@ async def backup_operation(operation_request: BackupOperation) -> dict: success = await backup_service.backup_database(database_path) if not success: - raise HTTPException( - status_code=500, detail="Database backup failed" - ) + raise HTTPException(status_code=500, detail="Database backup failed") return {"operation": "backup", "completed": True} @@ -213,9 +209,7 @@ async def backup_operation(operation_request: BackupOperation) -> dict: ) if not success: - raise HTTPException( - status_code=500, detail="Database restore failed" - ) + raise HTTPException(status_code=500, detail="Database restore failed") return {"operation": "restore", "completed": True} else: diff --git a/leggen/api/routes/sync.py b/leggen/api/routes/sync.py index 92f5f00..4a505e4 100644 --- a/leggen/api/routes/sync.py +++ b/leggen/api/routes/sync.py @@ -3,7 +3,7 @@ from typing import Optional from fastapi import APIRouter, BackgroundTasks, HTTPException from loguru import logger -from leggen.api.models.sync import SchedulerConfig, SyncRequest +from leggen.api.models.sync import SchedulerConfig, SyncRequest, SyncResult, SyncStatus from leggen.background.scheduler import scheduler from leggen.services.sync_service import SyncService from leggen.utils.config import config @@ -13,7 +13,7 @@ sync_service = SyncService() @router.get("/sync/status") -async def get_sync_status() -> dict: +async def get_sync_status() -> SyncStatus: """Get current sync status""" try: status = await sync_service.get_sync_status() @@ -78,7 +78,7 @@ async def trigger_sync( @router.post("/sync/now") -async def sync_now(sync_request: Optional[SyncRequest] = None) -> dict: +async def sync_now(sync_request: Optional[SyncRequest] = None) -> SyncResult: """Run sync synchronously and return results (slower, for testing)""" try: if sync_request and sync_request.account_ids: diff --git a/leggen/api/routes/transactions.py b/leggen/api/routes/transactions.py index 4a1c1d7..0347576 100644 --- a/leggen/api/routes/transactions.py +++ b/leggen/api/routes/transactions.py @@ -64,11 +64,9 @@ async def get_all_transactions( search=search, ) - data: Union[List[TransactionSummary], List[Transaction]] - if summary_only: # Return simplified transaction summaries - data = [ + data: list[TransactionSummary | Transaction] = [ TransactionSummary( transaction_id=txn["transactionId"], # NEW: stable bank-provided ID internal_transaction_id=txn.get("internalTransactionId"), diff --git a/leggen/commands/generate_sample_db.py b/leggen/commands/generate_sample_db.py index 12b0701..d7aa649 100644 --- a/leggen/commands/generate_sample_db.py +++ b/leggen/commands/generate_sample_db.py @@ -5,11 +5,19 @@ import random import sqlite3 from datetime import datetime, timedelta from pathlib import Path -from typing import Any +from typing import Any, TypedDict import click +class TransactionType(TypedDict): + """Type definition for transaction type configuration.""" + + description: str + amount_range: tuple[float, float] + frequency: float + + class SampleDataGenerator: """Generates realistic sample data for testing Leggen.""" @@ -42,7 +50,7 @@ class SampleDataGenerator: }, ] - self.transaction_types = [ + self.transaction_types: list[TransactionType] = [ { "description": "Grocery Store", "amount_range": (-150, -20), @@ -227,6 +235,8 @@ class SampleDataGenerator: )[0] # Generate transaction amount + min_amount: float + max_amount: float min_amount, max_amount = transaction_type["amount_range"] amount = round(random.uniform(min_amount, max_amount), 2) @@ -245,7 +255,7 @@ class SampleDataGenerator: internal_transaction_id = f"int-txn-{random.randint(100000, 999999)}" # Create realistic descriptions - descriptions = { + descriptions: dict[str, list[str]] = { "Grocery Store": [ "TESCO", "SAINSBURY'S", @@ -273,7 +283,7 @@ class SampleDataGenerator: "Transfer to Savings": ["SAVINGS TRANSFER", "INVESTMENT TRANSFER"], } - specific_descriptions = descriptions.get( + specific_descriptions: list[str] = descriptions.get( transaction_type["description"], [transaction_type["description"]] ) description = random.choice(specific_descriptions) diff --git a/tests/conftest.py b/tests/conftest.py index 94aaccc..a4ab62f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,8 +8,10 @@ from pathlib import Path from unittest.mock import patch import pytest +import tomli_w from fastapi.testclient import TestClient +from leggen.commands.server import create_app from leggen.utils.config import Config # Create test config before any imports that might load it @@ -27,15 +29,12 @@ _config_data = { "scheduler": {"sync": {"enabled": True, "hour": 3, "minute": 0}}, } -import tomli_w with open(_test_config_path, "wb") as f: tomli_w.dump(_config_data, f) # Set environment variables to point to test config BEFORE importing the app os.environ["LEGGEN_CONFIG_FILE"] = str(_test_config_path) -from leggen.commands.server import create_app - def pytest_configure(config): """Pytest hook called before test collection.""" @@ -114,7 +113,9 @@ def mock_auth_token(temp_config_dir): def fastapi_app(mock_db_path): """Create FastAPI test application.""" # Patch the database path for the app - with patch("leggen.utils.paths.path_manager.get_database_path", return_value=mock_db_path): + with patch( + "leggen.utils.paths.path_manager.get_database_path", return_value=mock_db_path + ): app = create_app() yield app diff --git a/tests/unit/test_api_backup.py b/tests/unit/test_api_backup.py index 8f9dfac..7661aca 100644 --- a/tests/unit/test_api_backup.py +++ b/tests/unit/test_api_backup.py @@ -211,10 +211,7 @@ class TestBackupAPI: assert response.status_code == 200 data = response.json() assert len(data) == 2 - assert ( - data[0]["key"] - == "leggen_backups/database_backup_20250101_120000.db" - ) + assert data[0]["key"] == "leggen_backups/database_backup_20250101_120000.db" def test_list_backups_no_config(self, api_client, mock_config): """Test backup listing with no configuration.""" diff --git a/tests/unit/test_api_transactions.py b/tests/unit/test_api_transactions.py index 14840f1..f6e0def 100644 --- a/tests/unit/test_api_transactions.py +++ b/tests/unit/test_api_transactions.py @@ -157,7 +157,6 @@ class TestTransactionsAPI: ) assert response.status_code == 200 - data = response.json() # Verify the database service was called with correct filters mock_get_transactions.assert_called_once_with(