feat: Add mypy to pre-commit.

This commit is contained in:
Elisiário Couto
2025-09-03 21:40:15 +01:00
committed by Elisiário Couto
parent de3da84dff
commit ec8ef8346a
34 changed files with 226 additions and 242 deletions

0
leggend/__init__.py Normal file
View File

View File

@@ -1,4 +1,3 @@
from datetime import datetime
from typing import Any, Dict, Optional
from pydantic import BaseModel

View File

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

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Optional, Dict, Any
from typing import Optional
from pydantic import BaseModel

View File

@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Optional, List, Union
from fastapi import APIRouter, HTTPException, Query
from loguru import logger
@@ -74,7 +74,9 @@ async def get_all_accounts() -> APIResponse:
except Exception as e:
logger.error(f"Failed to get accounts: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get accounts: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get accounts: {str(e)}"
) from e
@router.get("/accounts/{account_id}", response_model=APIResponse)
@@ -117,7 +119,9 @@ async def get_account_details(account_id: str) -> APIResponse:
except Exception as e:
logger.error(f"Failed to get account details for {account_id}: {e}")
raise HTTPException(status_code=404, detail=f"Account not found: {str(e)}")
raise HTTPException(
status_code=404, detail=f"Account not found: {str(e)}"
) from e
@router.get("/accounts/{account_id}/balances", response_model=APIResponse)
@@ -146,7 +150,9 @@ async def get_account_balances(account_id: str) -> APIResponse:
except Exception as e:
logger.error(f"Failed to get balances for account {account_id}: {e}")
raise HTTPException(status_code=404, detail=f"Failed to get balances: {str(e)}")
raise HTTPException(
status_code=404, detail=f"Failed to get balances: {str(e)}"
) from e
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
@@ -172,11 +178,17 @@ async def get_account_transactions(
# Apply pagination
total_transactions = len(processed_transactions)
paginated_transactions = processed_transactions[offset : offset + limit]
actual_offset = offset or 0
actual_limit = limit or 100
paginated_transactions = processed_transactions[
actual_offset : actual_offset + actual_limit
]
data: Union[List[TransactionSummary], List[Transaction]]
if summary_only:
# Return simplified transaction summaries
summaries = [
data = [
TransactionSummary(
internal_transaction_id=txn["internalTransactionId"],
date=txn["transactionDate"],
@@ -188,10 +200,9 @@ async def get_account_transactions(
)
for txn in paginated_transactions
]
data = summaries
else:
# Return full transaction details
transactions = [
data = [
Transaction(
internal_transaction_id=txn["internalTransactionId"],
institution_id=txn["institutionId"],
@@ -206,16 +217,15 @@ async def get_account_transactions(
)
for txn in paginated_transactions
]
data = transactions
return APIResponse(
success=True,
data=data,
message=f"Retrieved {len(data)} transactions (showing {offset + 1}-{offset + len(data)} of {total_transactions})",
message=f"Retrieved {len(data)} transactions (showing {actual_offset + 1}-{actual_offset + len(data)} of {total_transactions})",
)
except Exception as e:
logger.error(f"Failed to get transactions for account {account_id}: {e}")
raise HTTPException(
status_code=404, detail=f"Failed to get transactions: {str(e)}"
)
) from e

View File

@@ -1,8 +1,7 @@
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query
from loguru import logger
from leggend.api.models.common import APIResponse, ErrorResponse
from leggend.api.models.common import APIResponse
from leggend.api.models.banks import (
BankInstitution,
BankConnectionRequest,
@@ -46,15 +45,16 @@ async def get_bank_institutions(
logger.error(f"Failed to get institutions for {country}: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get institutions: {str(e)}"
)
) from e
@router.post("/banks/connect", response_model=APIResponse)
async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
"""Create a connection to a bank (requisition)"""
try:
redirect_url = request.redirect_url or "http://localhost:8000/"
requisition_data = await gocardless_service.create_requisition(
request.institution_id, request.redirect_url
request.institution_id, redirect_url
)
requisition = BankRequisition(
@@ -69,14 +69,14 @@ async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
return APIResponse(
success=True,
data=requisition,
message=f"Bank connection created. Please visit the link to authorize.",
message="Bank connection created. Please visit the link to authorize.",
)
except Exception as e:
logger.error(f"Failed to connect to bank {request.institution_id}: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to connect to bank: {str(e)}"
)
) from e
@router.get("/banks/status", response_model=APIResponse)
@@ -114,7 +114,7 @@ async def get_bank_connections_status() -> APIResponse:
logger.error(f"Failed to get bank connection status: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get bank status: {str(e)}"
)
) from e
@router.delete("/banks/connections/{requisition_id}", response_model=APIResponse)
@@ -132,7 +132,7 @@ async def delete_bank_connection(requisition_id: str) -> APIResponse:
logger.error(f"Failed to delete bank connection {requisition_id}: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to delete connection: {str(e)}"
)
) from e
@router.get("/banks/countries", response_model=APIResponse)

View File

@@ -1,4 +1,4 @@
from typing import Optional
from typing import Dict, Any
from fastapi import APIRouter, HTTPException
from loguru import logger
@@ -60,7 +60,7 @@ async def get_notification_settings() -> APIResponse:
logger.error(f"Failed to get notification settings: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get notification settings: {str(e)}"
)
) from e
@router.put("/notifications/settings", response_model=APIResponse)
@@ -84,7 +84,7 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
}
# Update filters config
filters_config = {}
filters_config: Dict[str, Any] = {}
if settings.filters.case_insensitive:
filters_config["case-insensitive"] = settings.filters.case_insensitive
if settings.filters.case_sensitive:
@@ -110,7 +110,7 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
logger.error(f"Failed to update notification settings: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to update notification settings: {str(e)}"
)
) from e
@router.post("/notifications/test", response_model=APIResponse)
@@ -137,7 +137,7 @@ async def test_notification(test_request: NotificationTest) -> APIResponse:
logger.error(f"Failed to send test notification: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to send test notification: {str(e)}"
)
) from e
@router.get("/notifications/services", response_model=APIResponse)
@@ -179,7 +179,7 @@ async def get_notification_services() -> APIResponse:
logger.error(f"Failed to get notification services: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get notification services: {str(e)}"
)
) from e
@router.delete("/notifications/settings/{service}", response_model=APIResponse)
@@ -206,4 +206,4 @@ async def delete_notification_service(service: str) -> APIResponse:
logger.error(f"Failed to delete notification service {service}: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to delete notification service: {str(e)}"
)
) from e

View File

@@ -3,7 +3,7 @@ from fastapi import APIRouter, HTTPException, BackgroundTasks
from loguru import logger
from leggend.api.models.common import APIResponse
from leggend.api.models.sync import SyncRequest, SyncStatus, SyncResult, SchedulerConfig
from leggend.api.models.sync import SyncRequest, SchedulerConfig
from leggend.services.sync_service import SyncService
from leggend.background.scheduler import scheduler
from leggend.config import config
@@ -31,7 +31,7 @@ async def get_sync_status() -> APIResponse:
logger.error(f"Failed to get sync status: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get sync status: {str(e)}"
)
) from e
@router.post("/sync", response_model=APIResponse)
@@ -78,7 +78,9 @@ async def trigger_sync(
except Exception as e:
logger.error(f"Failed to trigger sync: {e}")
raise HTTPException(status_code=500, detail=f"Failed to trigger sync: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to trigger sync: {str(e)}"
) from e
@router.post("/sync/now", response_model=APIResponse)
@@ -104,7 +106,9 @@ async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
except Exception as e:
logger.error(f"Failed to run sync: {e}")
raise HTTPException(status_code=500, detail=f"Failed to run sync: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to run sync: {str(e)}"
) from e
@router.get("/sync/scheduler", response_model=APIResponse)
@@ -134,7 +138,7 @@ async def get_scheduler_config() -> APIResponse:
logger.error(f"Failed to get scheduler config: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get scheduler config: {str(e)}"
)
) from e
@router.put("/sync/scheduler", response_model=APIResponse)
@@ -152,7 +156,7 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
except Exception as e:
raise HTTPException(
status_code=400, detail=f"Invalid cron expression: {str(e)}"
)
) from e
# Update configuration
schedule_data = scheduler_config.dict(exclude_none=True)
@@ -171,7 +175,7 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
logger.error(f"Failed to update scheduler config: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to update scheduler config: {str(e)}"
)
) from e
@router.post("/sync/scheduler/start", response_model=APIResponse)
@@ -188,7 +192,7 @@ async def start_scheduler() -> APIResponse:
logger.error(f"Failed to start scheduler: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to start scheduler: {str(e)}"
)
) from e
@router.post("/sync/scheduler/stop", response_model=APIResponse)
@@ -205,4 +209,4 @@ async def stop_scheduler() -> APIResponse:
logger.error(f"Failed to stop scheduler: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to stop scheduler: {str(e)}"
)
) from e

View File

@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Optional, List, Union
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, Query
from loguru import logger
@@ -120,7 +120,13 @@ async def get_all_transactions(
# Apply pagination
total_transactions = len(filtered_transactions)
paginated_transactions = filtered_transactions[offset : offset + limit]
actual_offset = offset or 0
actual_limit = limit or 100
paginated_transactions = filtered_transactions[
actual_offset : actual_offset + actual_limit
]
data: Union[List[TransactionSummary], List[Transaction]]
if summary_only:
# Return simplified transaction summaries
@@ -157,14 +163,14 @@ async def get_all_transactions(
return APIResponse(
success=True,
data=data,
message=f"Retrieved {len(data)} transactions (showing {offset + 1}-{offset + len(data)} of {total_transactions})",
message=f"Retrieved {len(data)} transactions (showing {actual_offset + 1}-{actual_offset + len(data)} of {total_transactions})",
)
except Exception as e:
logger.error(f"Failed to get transactions: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get transactions: {str(e)}"
)
) from e
@router.get("/transactions/stats", response_model=APIResponse)
@@ -270,4 +276,4 @@ async def get_transaction_stats(
logger.error(f"Failed to get transaction stats: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get transaction stats: {str(e)}"
)
) from e

View File

@@ -17,7 +17,7 @@ class Config:
cls._instance = super().__new__(cls)
return cls._instance
def load_config(self, config_path: str = None) -> Dict[str, Any]:
def load_config(self, config_path: Optional[str] = None) -> Dict[str, Any]:
if self._config is not None:
return self._config
@@ -43,7 +43,9 @@ class Config:
return self._config
def save_config(
self, config_data: Dict[str, Any] = None, config_path: str = None
self,
config_data: Optional[Dict[str, Any]] = None,
config_path: Optional[str] = None,
) -> None:
"""Save configuration to TOML file"""
if config_data is None:
@@ -55,6 +57,11 @@ class Config:
str(Path.home() / ".config" / "leggen" / "config.toml"),
)
if config_path is None:
raise ValueError("No config path specified")
if config_data is None:
raise ValueError("No config data to save")
# Ensure directory exists
Path(config_path).parent.mkdir(parents=True, exist_ok=True)
@@ -75,6 +82,9 @@ class Config:
if self._config is None:
self.load_config()
if self._config is None:
raise RuntimeError("Failed to load config")
if section not in self._config:
self._config[section] = {}
@@ -86,6 +96,9 @@ class Config:
if self._config is None:
self.load_config()
if self._config is None:
raise RuntimeError("Failed to load config")
self._config[section] = data
self.save_config()
@@ -93,6 +106,8 @@ class Config:
def config(self) -> Dict[str, Any]:
if self._config is None:
self.load_config()
if self._config is None:
raise RuntimeError("Failed to load config")
return self._config
@property

View File

@@ -1,4 +1,3 @@
import asyncio
from contextlib import asynccontextmanager
from importlib import metadata

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any
from loguru import logger
@@ -75,7 +75,10 @@ class DatabaseService:
datetime.fromisoformat(booked_date), datetime.fromisoformat(value_date)
)
else:
min_date = datetime.fromisoformat(booked_date or value_date)
date_str = booked_date or value_date
if not date_str:
raise ValueError("No valid date found in transaction")
min_date = datetime.fromisoformat(date_str)
# Extract amount and currency
transaction_amount = transaction.get("transactionAmount", {})

View File

@@ -1,8 +1,7 @@
import asyncio
import json
import httpx
from pathlib import Path
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List
from loguru import logger

View File

@@ -69,7 +69,7 @@ class NotificationService:
description = transaction.get("description", "").lower()
# Check case-insensitive filters
for filter_name, filter_value in filters_case_insensitive.items():
for _filter_name, filter_value in filters_case_insensitive.items():
if filter_value.lower() in description:
matching.append(
{

View File

@@ -1,10 +1,8 @@
import asyncio
from datetime import datetime
from typing import List, Dict, Any
from typing import List
from loguru import logger
from leggend.config import config
from leggend.api.models.sync import SyncResult, SyncStatus
from leggend.services.gocardless_service import GoCardlessService
from leggend.services.database_service import DatabaseService