refactor: Remove API response wrapper pattern.

Replace wrapped responses {success, data, message} with direct data returns
following REST best practices. Simplifies 41 endpoints across 7 route files
and updates all 109 tests. Also fixes test config setup to not require
user home directory config file.

🤖 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 00:54:51 +00:00
parent a75365d805
commit fabea404ef
17 changed files with 1171 additions and 835 deletions

View File

@@ -1,29 +1,17 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, Generic, List, TypeVar
from pydantic import BaseModel
class APIResponse(BaseModel):
"""Base API response model"""
success: bool = True
message: Optional[str] = None
data: Optional[Any] = None
T = TypeVar("T")
class ErrorResponse(BaseModel):
"""Error response model"""
success: bool = False
message: str
error_code: Optional[str] = None
details: Optional[Dict[str, Any]] = None
class PaginatedResponse(BaseModel):
class PaginatedResponse(BaseModel, Generic[T]):
"""Paginated response model"""
success: bool = True
data: list
pagination: Dict[str, Any]
message: Optional[str] = None
data: List[T]
total: int
page: int
per_page: int
total_pages: int
has_next: bool
has_prev: bool

View File

@@ -10,15 +10,14 @@ from leggen.api.models.accounts import (
Transaction,
TransactionSummary,
)
from leggen.api.models.common import APIResponse
from leggen.services.database_service import DatabaseService
router = APIRouter()
database_service = DatabaseService()
@router.get("/accounts", response_model=APIResponse)
async def get_all_accounts() -> APIResponse:
@router.get("/accounts")
async def get_all_accounts() -> List[AccountDetails]:
"""Get all connected accounts from database"""
try:
accounts = []
@@ -68,11 +67,7 @@ async def get_all_accounts() -> APIResponse:
)
continue
return APIResponse(
success=True,
data=accounts,
message=f"Retrieved {len(accounts)} accounts from database",
)
return accounts
except Exception as e:
logger.error(f"Failed to get accounts: {e}")
@@ -81,8 +76,8 @@ async def get_all_accounts() -> APIResponse:
) from e
@router.get("/accounts/{account_id}", response_model=APIResponse)
async def get_account_details(account_id: str) -> APIResponse:
@router.get("/accounts/{account_id}")
async def get_account_details(account_id: str) -> AccountDetails:
"""Get details for a specific account from database"""
try:
# Get account details from database
@@ -122,11 +117,7 @@ async def get_account_details(account_id: str) -> APIResponse:
balances=balances,
)
return APIResponse(
success=True,
data=account,
message=f"Account details retrieved from database for {account_id}",
)
return account
except HTTPException:
raise
@@ -137,8 +128,8 @@ async def get_account_details(account_id: str) -> APIResponse:
) from e
@router.get("/accounts/{account_id}/balances", response_model=APIResponse)
async def get_account_balances(account_id: str) -> APIResponse:
@router.get("/accounts/{account_id}/balances")
async def get_account_balances(account_id: str) -> List[AccountBalance]:
"""Get balances for a specific account from database"""
try:
# Get balances from database instead of GoCardless API
@@ -155,11 +146,7 @@ async def get_account_balances(account_id: str) -> APIResponse:
)
)
return APIResponse(
success=True,
data=balances,
message=f"Retrieved {len(balances)} balances for account {account_id}",
)
return balances
except Exception as e:
logger.error(
@@ -170,8 +157,8 @@ async def get_account_balances(account_id: str) -> APIResponse:
) from e
@router.get("/balances", response_model=APIResponse)
async def get_all_balances() -> APIResponse:
@router.get("/balances")
async def get_all_balances() -> List[dict]:
"""Get all balances from all accounts in database"""
try:
# Get all accounts first to iterate through them
@@ -207,11 +194,7 @@ async def get_all_balances() -> APIResponse:
)
continue
return APIResponse(
success=True,
data=all_balances,
message=f"Retrieved {len(all_balances)} balances from {len(db_accounts)} accounts",
)
return all_balances
except Exception as e:
logger.error(f"Failed to get all balances: {e}")
@@ -220,7 +203,7 @@ async def get_all_balances() -> APIResponse:
) from e
@router.get("/balances/history", response_model=APIResponse)
@router.get("/balances/history")
async def get_historical_balances(
days: Optional[int] = Query(
default=365, le=1095, ge=1, description="Number of days of history to retrieve"
@@ -228,7 +211,7 @@ async def get_historical_balances(
account_id: Optional[str] = Query(
default=None, description="Filter by specific account ID"
),
) -> APIResponse:
) -> List[dict]:
"""Get historical balance progression calculated from transaction history"""
try:
# Get historical balances from database
@@ -236,11 +219,7 @@ async def get_historical_balances(
account_id=account_id, days=days or 365
)
return APIResponse(
success=True,
data=historical_balances,
message=f"Retrieved {len(historical_balances)} historical balance points over {days} days",
)
return historical_balances
except Exception as e:
logger.error(f"Failed to get historical balances: {e}")
@@ -249,7 +228,7 @@ async def get_historical_balances(
) from e
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
@router.get("/accounts/{account_id}/transactions")
async def get_account_transactions(
account_id: str,
limit: Optional[int] = Query(default=100, le=500),
@@ -257,7 +236,7 @@ async def get_account_transactions(
summary_only: bool = Query(
default=False, description="Return transaction summaries only"
),
) -> APIResponse:
) -> Union[List[TransactionSummary], List[Transaction]]:
"""Get transactions for a specific account from database"""
try:
# Get transactions from database instead of GoCardless API
@@ -308,12 +287,7 @@ async def get_account_transactions(
for txn in db_transactions
]
actual_offset = offset or 0
return APIResponse(
success=True,
data=data,
message=f"Retrieved {len(data)} transactions (showing {actual_offset + 1}-{actual_offset + len(data)} of {total_transactions})",
)
return data
except Exception as e:
logger.error(
@@ -324,10 +298,10 @@ async def get_account_transactions(
) from e
@router.put("/accounts/{account_id}", response_model=APIResponse)
@router.put("/accounts/{account_id}")
async def update_account_details(
account_id: str, update_data: AccountUpdate
) -> APIResponse:
) -> dict:
"""Update account details (currently only display_name)"""
try:
# Get current account details
@@ -346,11 +320,7 @@ async def update_account_details(
# Persist updated account details
await database_service.persist_account_details(updated_account_data)
return APIResponse(
success=True,
data={"id": account_id, "display_name": update_data.display_name},
message=f"Account {account_id} display name updated successfully",
)
return {"id": account_id, "display_name": update_data.display_name}
except HTTPException:
raise

View File

@@ -9,7 +9,6 @@ from leggen.api.models.backup import (
BackupTest,
S3Config,
)
from leggen.api.models.common import APIResponse
from leggen.models.config import S3BackupConfig
from leggen.services.backup_service import BackupService
from leggen.utils.config import config
@@ -18,8 +17,8 @@ from leggen.utils.paths import path_manager
router = APIRouter()
@router.get("/backup/settings", response_model=APIResponse)
async def get_backup_settings() -> APIResponse:
@router.get("/backup/settings")
async def get_backup_settings() -> BackupSettings:
"""Get current backup settings."""
try:
backup_config = config.backup_config
@@ -41,11 +40,7 @@ async def get_backup_settings() -> APIResponse:
else None,
)
return APIResponse(
success=True,
data=settings,
message="Backup settings retrieved successfully",
)
return settings
except Exception as e:
logger.error(f"Failed to get backup settings: {e}")
@@ -54,8 +49,8 @@ async def get_backup_settings() -> APIResponse:
) from e
@router.put("/backup/settings", response_model=APIResponse)
async def update_backup_settings(settings: BackupSettings) -> APIResponse:
@router.put("/backup/settings")
async def update_backup_settings(settings: BackupSettings) -> dict:
"""Update backup settings."""
try:
# First test the connection if S3 config is provided
@@ -99,11 +94,7 @@ async def update_backup_settings(settings: BackupSettings) -> APIResponse:
if backup_config:
config.update_section("backup", backup_config)
return APIResponse(
success=True,
data={"updated": True},
message="Backup settings updated successfully",
)
return {"updated": True}
except HTTPException:
raise
@@ -114,8 +105,8 @@ async def update_backup_settings(settings: BackupSettings) -> APIResponse:
) from e
@router.post("/backup/test", response_model=APIResponse)
async def test_backup_connection(test_request: BackupTest) -> APIResponse:
@router.post("/backup/test")
async def test_backup_connection(test_request: BackupTest) -> dict:
"""Test backup connection."""
try:
if test_request.service != "s3":
@@ -137,18 +128,13 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
backup_service = BackupService()
success = await backup_service.test_connection(s3_config)
if success:
return APIResponse(
success=True,
data={"connected": True},
message="S3 connection test successful",
)
else:
return APIResponse(
success=False,
message="S3 connection test failed",
if not success:
raise HTTPException(
status_code=400, detail="S3 connection test failed"
)
return {"connected": True}
except HTTPException:
raise
except Exception as e:
@@ -158,18 +144,14 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
) from e
@router.get("/backup/list", response_model=APIResponse)
async def list_backups() -> APIResponse:
@router.get("/backup/list")
async def list_backups() -> list:
"""List available backups."""
try:
backup_config = config.backup_config.get("s3", {})
if not backup_config.get("bucket_name"):
return APIResponse(
success=True,
data=[],
message="No S3 backup configuration found",
)
return []
# Convert config to model
s3_config = S3BackupConfig(**backup_config)
@@ -177,11 +159,7 @@ async def list_backups() -> APIResponse:
backups = await backup_service.list_backups()
return APIResponse(
success=True,
data=backups,
message=f"Found {len(backups)} backups",
)
return backups
except Exception as e:
logger.error(f"Failed to list backups: {e}")
@@ -190,8 +168,8 @@ async def list_backups() -> APIResponse:
) from e
@router.post("/backup/operation", response_model=APIResponse)
async def backup_operation(operation_request: BackupOperation) -> APIResponse:
@router.post("/backup/operation")
async def backup_operation(operation_request: BackupOperation) -> dict:
"""Perform backup operation (backup or restore)."""
try:
backup_config = config.backup_config.get("s3", {})
@@ -214,18 +192,13 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
database_path = path_manager.get_database_path()
success = await backup_service.backup_database(database_path)
if success:
return APIResponse(
success=True,
data={"operation": "backup", "completed": True},
message="Database backup completed successfully",
)
else:
return APIResponse(
success=False,
message="Database backup failed",
if not success:
raise HTTPException(
status_code=500, detail="Database backup failed"
)
return {"operation": "backup", "completed": True}
elif operation_request.operation == "restore":
if not operation_request.backup_key:
raise HTTPException(
@@ -239,17 +212,12 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
operation_request.backup_key, database_path
)
if success:
return APIResponse(
success=True,
data={"operation": "restore", "completed": True},
message="Database restore completed successfully",
)
else:
return APIResponse(
success=False,
message="Database restore failed",
if not success:
raise HTTPException(
status_code=500, detail="Database restore failed"
)
return {"operation": "restore", "completed": True}
else:
raise HTTPException(
status_code=400, detail="Invalid operation. Use 'backup' or 'restore'"

View File

@@ -8,7 +8,6 @@ from leggen.api.models.banks import (
BankInstitution,
BankRequisition,
)
from leggen.api.models.common import APIResponse
from leggen.services.gocardless_service import GoCardlessService
from leggen.utils.gocardless import REQUISITION_STATUS
@@ -16,10 +15,10 @@ router = APIRouter()
gocardless_service = GoCardlessService()
@router.get("/banks/institutions", response_model=APIResponse)
@router.get("/banks/institutions")
async def get_bank_institutions(
country: str = Query(default="PT", description="Country code (e.g., PT, ES, FR)"),
) -> APIResponse:
) -> list[BankInstitution]:
"""Get available bank institutions for a country"""
try:
institutions_response = await gocardless_service.get_institutions(country)
@@ -41,11 +40,7 @@ async def get_bank_institutions(
for inst in institutions_data
]
return APIResponse(
success=True,
data=institutions,
message=f"Found {len(institutions)} institutions for {country}",
)
return institutions
except Exception as e:
logger.error(f"Failed to get institutions for {country}: {e}")
@@ -54,8 +49,8 @@ async def get_bank_institutions(
) from e
@router.post("/banks/connect", response_model=APIResponse)
async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
@router.post("/banks/connect")
async def connect_to_bank(request: BankConnectionRequest) -> BankRequisition:
"""Create a connection to a bank (requisition)"""
try:
redirect_url = request.redirect_url or "http://localhost:8000/"
@@ -72,11 +67,7 @@ async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
accounts=requisition_data.get("accounts", []),
)
return APIResponse(
success=True,
data=requisition,
message="Bank connection created. Please visit the link to authorize.",
)
return requisition
except Exception as e:
logger.error(f"Failed to connect to bank {request.institution_id}: {e}")
@@ -85,8 +76,8 @@ async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
) from e
@router.get("/banks/status", response_model=APIResponse)
async def get_bank_connections_status() -> APIResponse:
@router.get("/banks/status")
async def get_bank_connections_status() -> list[BankConnectionStatus]:
"""Get status of all bank connections"""
try:
requisitions_data = await gocardless_service.get_requisitions()
@@ -110,11 +101,7 @@ async def get_bank_connections_status() -> APIResponse:
)
)
return APIResponse(
success=True,
data=connections,
message=f"Found {len(connections)} bank connections",
)
return connections
except Exception as e:
logger.error(f"Failed to get bank connection status: {e}")
@@ -123,8 +110,8 @@ async def get_bank_connections_status() -> APIResponse:
) from e
@router.delete("/banks/connections/{requisition_id}", response_model=APIResponse)
async def delete_bank_connection(requisition_id: str) -> APIResponse:
@router.delete("/banks/connections/{requisition_id}")
async def delete_bank_connection(requisition_id: str) -> dict:
"""Delete a bank connection"""
try:
# Delete the requisition from GoCardless
@@ -134,10 +121,7 @@ async def delete_bank_connection(requisition_id: str) -> APIResponse:
# We should check if the operation was actually successful
logger.info(f"GoCardless delete response for {requisition_id}: {result}")
return APIResponse(
success=True,
message=f"Bank connection {requisition_id} deleted successfully",
)
return {"deleted": requisition_id}
except httpx.HTTPStatusError as http_err:
logger.error(
@@ -164,8 +148,8 @@ async def delete_bank_connection(requisition_id: str) -> APIResponse:
) from e
@router.get("/banks/countries", response_model=APIResponse)
async def get_supported_countries() -> APIResponse:
@router.get("/banks/countries")
async def get_supported_countries() -> list[dict]:
"""Get list of supported countries"""
countries = [
{"code": "AT", "name": "Austria"},
@@ -201,8 +185,4 @@ async def get_supported_countries() -> APIResponse:
{"code": "GB", "name": "United Kingdom"},
]
return APIResponse(
success=True,
data=countries,
message="Supported countries retrieved successfully",
)
return countries

View File

@@ -3,7 +3,6 @@ from typing import Any, Dict
from fastapi import APIRouter, HTTPException
from loguru import logger
from leggen.api.models.common import APIResponse
from leggen.api.models.notifications import (
DiscordConfig,
NotificationFilters,
@@ -18,8 +17,8 @@ router = APIRouter()
notification_service = NotificationService()
@router.get("/notifications/settings", response_model=APIResponse)
async def get_notification_settings() -> APIResponse:
@router.get("/notifications/settings")
async def get_notification_settings() -> NotificationSettings:
"""Get current notification settings"""
try:
notifications_config = config.notifications_config
@@ -49,11 +48,7 @@ async def get_notification_settings() -> APIResponse:
),
)
return APIResponse(
success=True,
data=settings,
message="Notification settings retrieved successfully",
)
return settings
except Exception as e:
logger.error(f"Failed to get notification settings: {e}")
@@ -62,8 +57,8 @@ async def get_notification_settings() -> APIResponse:
) from e
@router.put("/notifications/settings", response_model=APIResponse)
async def update_notification_settings(settings: NotificationSettings) -> APIResponse:
@router.put("/notifications/settings")
async def update_notification_settings(settings: NotificationSettings) -> dict:
"""Update notification settings"""
try:
# Update notifications config
@@ -95,11 +90,7 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
if filters_config:
config.update_section("filters", filters_config)
return APIResponse(
success=True,
data={"updated": True},
message="Notification settings updated successfully",
)
return {"updated": True}
except Exception as e:
logger.error(f"Failed to update notification settings: {e}")
@@ -108,26 +99,24 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
) from e
@router.post("/notifications/test", response_model=APIResponse)
async def test_notification(test_request: NotificationTest) -> APIResponse:
@router.post("/notifications/test")
async def test_notification(test_request: NotificationTest) -> dict:
"""Send a test notification"""
try:
success = await notification_service.send_test_notification(
test_request.service, test_request.message
)
if success:
return APIResponse(
success=True,
data={"sent": True},
message=f"Test notification sent to {test_request.service} successfully",
)
else:
return APIResponse(
success=False,
message=f"Failed to send test notification to {test_request.service}",
if not success:
raise HTTPException(
status_code=400,
detail=f"Failed to send test notification to {test_request.service}",
)
return {"sent": True}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to send test notification: {e}")
raise HTTPException(
@@ -135,8 +124,8 @@ async def test_notification(test_request: NotificationTest) -> APIResponse:
) from e
@router.get("/notifications/services", response_model=APIResponse)
async def get_notification_services() -> APIResponse:
@router.get("/notifications/services")
async def get_notification_services() -> dict:
"""Get available notification services and their status"""
try:
notifications_config = config.notifications_config
@@ -164,11 +153,7 @@ async def get_notification_services() -> APIResponse:
},
}
return APIResponse(
success=True,
data=services,
message="Notification services status retrieved successfully",
)
return services
except Exception as e:
logger.error(f"Failed to get notification services: {e}")
@@ -177,8 +162,8 @@ async def get_notification_services() -> APIResponse:
) from e
@router.delete("/notifications/settings/{service}", response_model=APIResponse)
async def delete_notification_service(service: str) -> APIResponse:
@router.delete("/notifications/settings/{service}")
async def delete_notification_service(service: str) -> dict:
"""Delete/disable a notification service"""
try:
if service not in ["discord", "telegram"]:
@@ -191,12 +176,10 @@ async def delete_notification_service(service: str) -> APIResponse:
del notifications_config[service]
config.update_section("notifications", notifications_config)
return APIResponse(
success=True,
data={"deleted": service},
message=f"{service.capitalize()} notification service deleted successfully",
)
return {"deleted": service}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete notification service {service}: {e}")
raise HTTPException(

View File

@@ -3,7 +3,6 @@ from typing import Optional
from fastapi import APIRouter, BackgroundTasks, HTTPException
from loguru import logger
from leggen.api.models.common import APIResponse
from leggen.api.models.sync import SchedulerConfig, SyncRequest
from leggen.background.scheduler import scheduler
from leggen.services.sync_service import SyncService
@@ -13,8 +12,8 @@ router = APIRouter()
sync_service = SyncService()
@router.get("/sync/status", response_model=APIResponse)
async def get_sync_status() -> APIResponse:
@router.get("/sync/status")
async def get_sync_status() -> dict:
"""Get current sync status"""
try:
status = await sync_service.get_sync_status()
@@ -24,9 +23,7 @@ async def get_sync_status() -> APIResponse:
if next_sync_time:
status.next_sync = next_sync_time
return APIResponse(
success=True, data=status, message="Sync status retrieved successfully"
)
return status
except Exception as e:
logger.error(f"Failed to get sync status: {e}")
@@ -35,18 +32,18 @@ async def get_sync_status() -> APIResponse:
) from e
@router.post("/sync", response_model=APIResponse)
@router.post("/sync")
async def trigger_sync(
background_tasks: BackgroundTasks, sync_request: Optional[SyncRequest] = None
) -> APIResponse:
) -> dict:
"""Trigger a manual sync operation"""
try:
# Check if sync is already running
status = await sync_service.get_sync_status()
if status.is_running and not (sync_request and sync_request.force):
return APIResponse(
success=False,
message="Sync is already running. Use 'force: true' to override.",
raise HTTPException(
status_code=409,
detail="Sync is already running. Use 'force: true' to override.",
)
# Determine what to sync
@@ -58,9 +55,6 @@ async def trigger_sync(
sync_request.force if sync_request else False,
"api", # trigger_type
)
message = (
f"Started sync for {len(sync_request.account_ids)} specific accounts"
)
else:
# Sync all accounts in background
background_tasks.add_task(
@@ -68,17 +62,14 @@ async def trigger_sync(
sync_request.force if sync_request else False,
"api", # trigger_type
)
message = "Started sync for all accounts"
return APIResponse(
success=True,
data={
"sync_started": True,
"force": sync_request.force if sync_request else False,
},
message=message,
)
return {
"sync_started": True,
"force": sync_request.force if sync_request else False,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to trigger sync: {e}")
raise HTTPException(
@@ -86,8 +77,8 @@ async def trigger_sync(
) from e
@router.post("/sync/now", response_model=APIResponse)
async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
@router.post("/sync/now")
async def sync_now(sync_request: Optional[SyncRequest] = None) -> dict:
"""Run sync synchronously and return results (slower, for testing)"""
try:
if sync_request and sync_request.account_ids:
@@ -99,13 +90,7 @@ async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
sync_request.force if sync_request else False, "api"
)
return APIResponse(
success=result.success,
data=result,
message="Sync completed"
if result.success
else f"Sync failed with {len(result.errors)} errors",
)
return result
except Exception as e:
logger.error(f"Failed to run sync: {e}")
@@ -114,8 +99,8 @@ async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
) from e
@router.get("/sync/scheduler", response_model=APIResponse)
async def get_scheduler_config() -> APIResponse:
@router.get("/sync/scheduler")
async def get_scheduler_config() -> dict:
"""Get current scheduler configuration"""
try:
scheduler_config = config.scheduler_config
@@ -131,11 +116,7 @@ async def get_scheduler_config() -> APIResponse:
else False,
}
return APIResponse(
success=True,
data=response_data,
message="Scheduler configuration retrieved successfully",
)
return response_data
except Exception as e:
logger.error(f"Failed to get scheduler config: {e}")
@@ -144,8 +125,8 @@ async def get_scheduler_config() -> APIResponse:
) from e
@router.put("/sync/scheduler", response_model=APIResponse)
async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIResponse:
@router.put("/sync/scheduler")
async def update_scheduler_config(scheduler_config: SchedulerConfig) -> dict:
"""Update scheduler configuration"""
try:
# Validate cron expression if provided
@@ -168,12 +149,10 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
# Reschedule the job
scheduler.reschedule_sync(schedule_data)
return APIResponse(
success=True,
data=schedule_data,
message="Scheduler configuration updated successfully",
)
return schedule_data
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update scheduler config: {e}")
raise HTTPException(
@@ -181,15 +160,15 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
) from e
@router.post("/sync/scheduler/start", response_model=APIResponse)
async def start_scheduler() -> APIResponse:
@router.post("/sync/scheduler/start")
async def start_scheduler() -> dict:
"""Start the background scheduler"""
try:
if not scheduler.scheduler.running:
scheduler.start()
return APIResponse(success=True, message="Scheduler started successfully")
return {"started": True}
else:
return APIResponse(success=True, message="Scheduler is already running")
return {"started": False, "message": "Scheduler is already running"}
except Exception as e:
logger.error(f"Failed to start scheduler: {e}")
@@ -198,15 +177,15 @@ async def start_scheduler() -> APIResponse:
) from e
@router.post("/sync/scheduler/stop", response_model=APIResponse)
async def stop_scheduler() -> APIResponse:
@router.post("/sync/scheduler/stop")
async def stop_scheduler() -> dict:
"""Stop the background scheduler"""
try:
if scheduler.scheduler.running:
scheduler.shutdown()
return APIResponse(success=True, message="Scheduler stopped successfully")
return {"stopped": True}
else:
return APIResponse(success=True, message="Scheduler is already stopped")
return {"stopped": False, "message": "Scheduler is already stopped"}
except Exception as e:
logger.error(f"Failed to stop scheduler: {e}")
@@ -215,19 +194,15 @@ async def stop_scheduler() -> APIResponse:
) from e
@router.get("/sync/operations", response_model=APIResponse)
async def get_sync_operations(limit: int = 50, offset: int = 0) -> APIResponse:
@router.get("/sync/operations")
async def get_sync_operations(limit: int = 50, offset: int = 0) -> dict:
"""Get sync operations history"""
try:
operations = await sync_service.database.get_sync_operations(
limit=limit, offset=offset
)
return APIResponse(
success=True,
data={"operations": operations, "count": len(operations)},
message="Sync operations retrieved successfully",
)
return {"operations": operations, "count": len(operations)}
except Exception as e:
logger.error(f"Failed to get sync operations: {e}")

View File

@@ -5,14 +5,14 @@ from fastapi import APIRouter, HTTPException, Query
from loguru import logger
from leggen.api.models.accounts import Transaction, TransactionSummary
from leggen.api.models.common import APIResponse, PaginatedResponse
from leggen.api.models.common import PaginatedResponse
from leggen.services.database_service import DatabaseService
router = APIRouter()
database_service = DatabaseService()
@router.get("/transactions", response_model=PaginatedResponse)
@router.get("/transactions")
async def get_all_transactions(
page: int = Query(default=1, ge=1, description="Page number (1-based)"),
per_page: int = Query(default=50, le=500, description="Items per page"),
@@ -35,7 +35,7 @@ async def get_all_transactions(
default=None, description="Search in transaction descriptions"
),
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
) -> PaginatedResponse:
) -> PaginatedResponse[Union[TransactionSummary, Transaction]]:
"""Get all transactions from database with filtering options"""
try:
# Calculate offset from page and per_page
@@ -103,16 +103,13 @@ async def get_all_transactions(
total_pages = (total_transactions + per_page - 1) // per_page
return PaginatedResponse(
success=True,
data=data,
pagination={
"total": total_transactions,
"page": page,
"per_page": per_page,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1,
},
total=total_transactions,
page=page,
per_page=per_page,
total_pages=total_pages,
has_next=page < total_pages,
has_prev=page > 1,
)
except Exception as e:
@@ -122,11 +119,11 @@ async def get_all_transactions(
) from e
@router.get("/transactions/stats", response_model=APIResponse)
@router.get("/transactions/stats")
async def get_transaction_stats(
days: int = Query(default=30, description="Number of days to include in stats"),
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
) -> APIResponse:
) -> dict:
"""Get transaction statistics for the last N days from database"""
try:
# Date range for stats
@@ -192,11 +189,7 @@ async def get_transaction_stats(
"accounts_included": unique_accounts,
}
return APIResponse(
success=True,
data=stats,
message=f"Transaction statistics for last {days} days",
)
return stats
except Exception as e:
logger.error(f"Failed to get transaction stats from database: {e}")
@@ -205,11 +198,11 @@ async def get_transaction_stats(
) from e
@router.get("/transactions/analytics", response_model=APIResponse)
@router.get("/transactions/analytics")
async def get_transactions_for_analytics(
days: int = Query(default=365, description="Number of days to include"),
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
) -> APIResponse:
) -> List[dict]:
"""Get all transactions for analytics (no pagination) for the last N days"""
try:
# Date range for analytics
@@ -242,11 +235,7 @@ async def get_transactions_for_analytics(
for txn in transactions
]
return APIResponse(
success=True,
data=transaction_summaries,
message=f"Retrieved {len(transaction_summaries)} transactions for analytics",
)
return transaction_summaries
except Exception as e:
logger.error(f"Failed to get transactions for analytics: {e}")
@@ -255,11 +244,11 @@ async def get_transactions_for_analytics(
) from e
@router.get("/transactions/monthly-stats", response_model=APIResponse)
@router.get("/transactions/monthly-stats")
async def get_monthly_transaction_stats(
days: int = Query(default=365, description="Number of days to include"),
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
) -> APIResponse:
) -> List[dict]:
"""Get monthly transaction statistics aggregated by the database"""
try:
# Date range for monthly stats
@@ -277,11 +266,7 @@ async def get_monthly_transaction_stats(
date_to=date_to,
)
return APIResponse(
success=True,
data=monthly_stats,
message=f"Retrieved monthly stats for last {days} days",
)
return monthly_stats
except Exception as e:
logger.error(f"Failed to get monthly transaction stats: {e}")