chore: Implement code review suggestions and format code.

This commit is contained in:
Elisiário Couto
2025-09-03 21:11:19 +01:00
committed by Elisiário Couto
parent 47164e8546
commit de3da84dff
42 changed files with 1144 additions and 966 deletions

View File

@@ -11,7 +11,9 @@ class DatabaseService:
self.db_config = config.database_config
self.sqlite_enabled = self.db_config.get("sqlite", True)
async def persist_balance(self, account_id: str, balance_data: Dict[str, Any]) -> None:
async def persist_balance(
self, account_id: str, balance_data: Dict[str, Any]
) -> None:
"""Persist account balance data"""
if not self.sqlite_enabled:
logger.warning("SQLite database disabled, skipping balance persistence")
@@ -19,7 +21,9 @@ class DatabaseService:
await self._persist_balance_sqlite(account_id, balance_data)
async def persist_transactions(self, account_id: str, transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
async def persist_transactions(
self, account_id: str, transactions: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""Persist transactions and return new transactions"""
if not self.sqlite_enabled:
logger.warning("SQLite database disabled, skipping transaction persistence")
@@ -27,32 +31,48 @@ class DatabaseService:
return await self._persist_transactions_sqlite(account_id, transactions)
def process_transactions(self, account_id: str, account_info: Dict[str, Any], transaction_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def process_transactions(
self,
account_id: str,
account_info: Dict[str, Any],
transaction_data: Dict[str, Any],
) -> List[Dict[str, Any]]:
"""Process raw transaction data into standardized format"""
transactions = []
# Process booked transactions
for transaction in transaction_data.get("transactions", {}).get("booked", []):
processed = self._process_single_transaction(account_id, account_info, transaction, "booked")
processed = self._process_single_transaction(
account_id, account_info, transaction, "booked"
)
transactions.append(processed)
# Process pending transactions
# Process pending transactions
for transaction in transaction_data.get("transactions", {}).get("pending", []):
processed = self._process_single_transaction(account_id, account_info, transaction, "pending")
processed = self._process_single_transaction(
account_id, account_info, transaction, "pending"
)
transactions.append(processed)
return transactions
def _process_single_transaction(self, account_id: str, account_info: Dict[str, Any], transaction: Dict[str, Any], status: str) -> Dict[str, Any]:
def _process_single_transaction(
self,
account_id: str,
account_info: Dict[str, Any],
transaction: Dict[str, Any],
status: str,
) -> Dict[str, Any]:
"""Process a single transaction into standardized format"""
# Extract dates
booked_date = transaction.get("bookingDateTime") or transaction.get("bookingDate")
booked_date = transaction.get("bookingDateTime") or transaction.get(
"bookingDate"
)
value_date = transaction.get("valueDateTime") or transaction.get("valueDate")
if booked_date and value_date:
min_date = min(
datetime.fromisoformat(booked_date),
datetime.fromisoformat(value_date)
datetime.fromisoformat(booked_date), datetime.fromisoformat(value_date)
)
else:
min_date = datetime.fromisoformat(booked_date or value_date)
@@ -65,7 +85,7 @@ class DatabaseService:
# Extract description
description = transaction.get(
"remittanceInformationUnstructured",
",".join(transaction.get("remittanceInformationUnstructuredArray", []))
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
)
return {
@@ -81,13 +101,19 @@ class DatabaseService:
"rawTransaction": transaction,
}
async def _persist_balance_sqlite(self, account_id: str, balance_data: Dict[str, Any]) -> None:
async def _persist_balance_sqlite(
self, account_id: str, balance_data: Dict[str, Any]
) -> None:
"""Persist balance to SQLite - placeholder implementation"""
# Would import and use leggen.database.sqlite
logger.info(f"Persisting balance to SQLite for account {account_id}")
async def _persist_transactions_sqlite(self, account_id: str, transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
async def _persist_transactions_sqlite(
self, account_id: str, transactions: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""Persist transactions to SQLite - placeholder implementation"""
# Would import and use leggen.database.sqlite
logger.info(f"Persisting {len(transactions)} transactions to SQLite for account {account_id}")
return transactions # Return new transactions for notifications
logger.info(
f"Persisting {len(transactions)} transactions to SQLite for account {account_id}"
)
return transactions # Return new transactions for notifications

View File

@@ -12,37 +12,36 @@ from leggend.config import config
class GoCardlessService:
def __init__(self):
self.config = config.gocardless_config
self.base_url = self.config.get("url", "https://bankaccountdata.gocardless.com/api/v2")
self.base_url = self.config.get(
"url", "https://bankaccountdata.gocardless.com/api/v2"
)
self._token = None
async def _get_auth_headers(self) -> Dict[str, str]:
"""Get authentication headers for GoCardless API"""
token = await self._get_token()
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
async def _get_token(self) -> str:
"""Get access token for GoCardless API"""
if self._token:
return self._token
# Use ~/.config/leggen for consistency with main config
auth_file = Path.home() / ".config" / "leggen" / "auth.json"
if auth_file.exists():
try:
with open(auth_file, "r") as f:
auth = json.load(f)
if auth.get("access"):
# Try to refresh the token
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{self.base_url}/token/refresh/",
json={"refresh": auth["refresh"]}
json={"refresh": auth["refresh"]},
)
response.raise_for_status()
auth.update(response.json())
@@ -84,7 +83,7 @@ class GoCardlessService:
"""Save authentication data to file"""
auth_file = Path.home() / ".config" / "leggen" / "auth.json"
auth_file.parent.mkdir(parents=True, exist_ok=True)
with open(auth_file, "w") as f:
json.dump(auth_data, f)
@@ -95,22 +94,21 @@ class GoCardlessService:
response = await client.get(
f"{self.base_url}/institutions/",
headers=headers,
params={"country": country}
params={"country": country},
)
response.raise_for_status()
return response.json()
async def create_requisition(self, institution_id: str, redirect_url: str) -> Dict[str, Any]:
async def create_requisition(
self, institution_id: str, redirect_url: str
) -> Dict[str, Any]:
"""Create a bank connection requisition"""
headers = await self._get_auth_headers()
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/requisitions/",
headers=headers,
json={
"institution_id": institution_id,
"redirect": redirect_url
}
json={"institution_id": institution_id, "redirect": redirect_url},
)
response.raise_for_status()
return response.json()
@@ -120,8 +118,7 @@ class GoCardlessService:
headers = await self._get_auth_headers()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/requisitions/",
headers=headers
f"{self.base_url}/requisitions/", headers=headers
)
response.raise_for_status()
return response.json()
@@ -131,8 +128,7 @@ class GoCardlessService:
headers = await self._get_auth_headers()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/accounts/{account_id}/",
headers=headers
f"{self.base_url}/accounts/{account_id}/", headers=headers
)
response.raise_for_status()
return response.json()
@@ -142,8 +138,7 @@ class GoCardlessService:
headers = await self._get_auth_headers()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/accounts/{account_id}/balances/",
headers=headers
f"{self.base_url}/accounts/{account_id}/balances/", headers=headers
)
response.raise_for_status()
return response.json()
@@ -153,8 +148,7 @@ class GoCardlessService:
headers = await self._get_auth_headers()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/accounts/{account_id}/transactions/",
headers=headers
f"{self.base_url}/accounts/{account_id}/transactions/", headers=headers
)
response.raise_for_status()
return response.json()
return response.json()

View File

@@ -10,7 +10,9 @@ class NotificationService:
self.notifications_config = config.notifications_config
self.filters_config = config.filters_config
async def send_transaction_notifications(self, transactions: List[Dict[str, Any]]) -> None:
async def send_transaction_notifications(
self, transactions: List[Dict[str, Any]]
) -> None:
"""Send notifications for new transactions that match filters"""
if not self.filters_config:
logger.info("No notification filters configured, skipping notifications")
@@ -18,7 +20,7 @@ class NotificationService:
# Filter transactions that match notification criteria
matching_transactions = self._filter_transactions(transactions)
if not matching_transactions:
logger.info("No transactions matched notification filters")
return
@@ -26,7 +28,7 @@ class NotificationService:
# Send to enabled notification services
if self._is_discord_enabled():
await self._send_discord_notifications(matching_transactions)
if self._is_telegram_enabled():
await self._send_telegram_notifications(matching_transactions)
@@ -40,7 +42,9 @@ class NotificationService:
await self._send_telegram_test(message)
return True
else:
logger.error(f"Notification service '{service}' not enabled or not found")
logger.error(
f"Notification service '{service}' not enabled or not found"
)
return False
except Exception as e:
logger.error(f"Failed to send test notification to {service}: {e}")
@@ -50,54 +54,66 @@ class NotificationService:
"""Send notification about account expiry"""
if self._is_discord_enabled():
await self._send_discord_expiry(notification_data)
if self._is_telegram_enabled():
await self._send_telegram_expiry(notification_data)
def _filter_transactions(self, transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _filter_transactions(
self, transactions: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""Filter transactions based on notification criteria"""
matching = []
filters_case_insensitive = self.filters_config.get("case-insensitive", {})
for transaction in transactions:
description = transaction.get("description", "").lower()
# Check case-insensitive filters
for filter_name, filter_value in filters_case_insensitive.items():
if filter_value.lower() in description:
matching.append({
"name": transaction["description"],
"value": transaction["transactionValue"],
"currency": transaction["transactionCurrency"],
"date": transaction["transactionDate"],
})
matching.append(
{
"name": transaction["description"],
"value": transaction["transactionValue"],
"currency": transaction["transactionCurrency"],
"date": transaction["transactionDate"],
}
)
break
return matching
def _is_discord_enabled(self) -> bool:
"""Check if Discord notifications are enabled"""
discord_config = self.notifications_config.get("discord", {})
return bool(discord_config.get("webhook") and discord_config.get("enabled", True))
return bool(
discord_config.get("webhook") and discord_config.get("enabled", True)
)
def _is_telegram_enabled(self) -> bool:
"""Check if Telegram notifications are enabled"""
telegram_config = self.notifications_config.get("telegram", {})
return bool(
telegram_config.get("token") and
telegram_config.get("chat_id") and
telegram_config.get("enabled", True)
telegram_config.get("token")
and telegram_config.get("chat_id")
and telegram_config.get("enabled", True)
)
async def _send_discord_notifications(self, transactions: List[Dict[str, Any]]) -> None:
async def _send_discord_notifications(
self, transactions: List[Dict[str, Any]]
) -> None:
"""Send Discord notifications - placeholder implementation"""
# Would import and use leggen.notifications.discord
logger.info(f"Sending {len(transactions)} transaction notifications to Discord")
async def _send_telegram_notifications(self, transactions: List[Dict[str, Any]]) -> None:
async def _send_telegram_notifications(
self, transactions: List[Dict[str, Any]]
) -> None:
"""Send Telegram notifications - placeholder implementation"""
# Would import and use leggen.notifications.telegram
logger.info(f"Sending {len(transactions)} transaction notifications to Telegram")
logger.info(
f"Sending {len(transactions)} transaction notifications to Telegram"
)
async def _send_discord_test(self, message: str) -> None:
"""Send Discord test notification"""
@@ -113,4 +129,4 @@ class NotificationService:
async def _send_telegram_expiry(self, notification_data: Dict[str, Any]) -> None:
"""Send Telegram expiry notification"""
logger.info(f"Sending Telegram expiry notification: {notification_data}")
logger.info(f"Sending Telegram expiry notification: {notification_data}")

View File

@@ -30,7 +30,7 @@ class SyncService:
start_time = datetime.now()
self._sync_status.is_running = True
self._sync_status.errors = []
accounts_processed = 0
transactions_added = 0
transactions_updated = 0
@@ -39,22 +39,24 @@ class SyncService:
try:
logger.info("Starting sync of all accounts")
# Get all requisitions and accounts
requisitions = await self.gocardless.get_requisitions()
all_accounts = set()
for req in requisitions.get("results", []):
all_accounts.update(req.get("accounts", []))
self._sync_status.total_accounts = len(all_accounts)
# Process each account
for account_id in all_accounts:
try:
# Get account details
account_details = await self.gocardless.get_account_details(account_id)
account_details = await self.gocardless.get_account_details(
account_id
)
# Get and save balances
balances = await self.gocardless.get_account_balances(account_id)
if balances:
@@ -62,7 +64,9 @@ class SyncService:
balances_updated += len(balances.get("balances", []))
# Get and save transactions
transactions = await self.gocardless.get_account_transactions(account_id)
transactions = await self.gocardless.get_account_transactions(
account_id
)
if transactions:
processed_transactions = self.database.process_transactions(
account_id, account_details, transactions
@@ -71,16 +75,18 @@ class SyncService:
account_id, processed_transactions
)
transactions_added += len(new_transactions)
# Send notifications for new transactions
if new_transactions:
await self.notifications.send_transaction_notifications(new_transactions)
await self.notifications.send_transaction_notifications(
new_transactions
)
accounts_processed += 1
self._sync_status.accounts_synced = accounts_processed
logger.info(f"Synced account {account_id} successfully")
except Exception as e:
error_msg = f"Failed to sync account {account_id}: {str(e)}"
errors.append(error_msg)
@@ -88,9 +94,9 @@ class SyncService:
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
self._sync_status.last_sync = end_time
result = SyncResult(
success=len(errors) == 0,
accounts_processed=accounts_processed,
@@ -100,12 +106,14 @@ class SyncService:
duration_seconds=duration,
errors=errors,
started_at=start_time,
completed_at=end_time
completed_at=end_time,
)
logger.info(
f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions"
)
logger.info(f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions")
return result
except Exception as e:
error_msg = f"Sync failed: {str(e)}"
errors.append(error_msg)
@@ -114,7 +122,9 @@ class SyncService:
finally:
self._sync_status.is_running = False
async def sync_specific_accounts(self, account_ids: List[str], force: bool = False) -> SyncResult:
async def sync_specific_accounts(
self, account_ids: List[str], force: bool = False
) -> SyncResult:
"""Sync specific accounts"""
if self._sync_status.is_running and not force:
raise Exception("Sync is already running")
@@ -123,12 +133,12 @@ class SyncService:
# For brevity, implementing a simplified version
start_time = datetime.now()
self._sync_status.is_running = True
try:
# Process only specified accounts
# Implementation would be similar to sync_all_accounts
# but filtered to only the specified account_ids
end_time = datetime.now()
return SyncResult(
success=True,
@@ -139,7 +149,7 @@ class SyncService:
duration_seconds=(end_time - start_time).total_seconds(),
errors=[],
started_at=start_time,
completed_at=end_time
completed_at=end_time,
)
finally:
self._sync_status.is_running = False
self._sync_status.is_running = False