fix: Resolve all lint warnings and type errors across frontend and backend.

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 <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-12-07 11:27:55 +00:00
committed by Elisiário Couto
parent 966440006a
commit 159cba508e
11 changed files with 66 additions and 58 deletions

View File

@@ -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;

View File

@@ -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;
},
};

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, Generic, List, TypeVar
from typing import Generic, List, TypeVar
from pydantic import BaseModel

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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

View File

@@ -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."""

View File

@@ -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(