mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-23 21:59:22 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user