fix(cli): Fix API URL handling for subpaths and improve client robustness.

- Automatically append /api/v1 to base URL if not present
- Fix URL construction to handle subpaths correctly
- Update health check to parse new nested response format
- Refactor bank delete command to use API client instead of direct requests
- Remove redundant /api/v1 prefixes from endpoint calls

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-09-24 23:52:34 +01:00
parent d4edf69f2c
commit ae5d034d4b
2 changed files with 43 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
import os import os
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from urllib.parse import urljoin from urllib.parse import urljoin, urlparse
import requests import requests
@@ -13,11 +13,22 @@ class LeggenAPIClient:
base_url: str base_url: str
def __init__(self, base_url: Optional[str] = None): def __init__(self, base_url: Optional[str] = None):
self.base_url = ( raw_url = (
base_url base_url
or os.environ.get("LEGGEN_API_URL", "http://localhost:8000") or os.environ.get("LEGGEN_API_URL", "http://localhost:8000")
or "http://localhost:8000" or "http://localhost:8000"
) )
# Ensure base_url includes /api/v1 path if not already present
parsed = urlparse(raw_url)
if not parsed.path or parsed.path == "/":
# No path or just root, add /api/v1
self.base_url = f"{raw_url.rstrip('/')}/api/v1"
elif not parsed.path.startswith("/api/v1"):
# Has a path but not /api/v1, add it
self.base_url = f"{raw_url.rstrip('/')}/api/v1"
else:
# Already has /api/v1 path
self.base_url = raw_url.rstrip("/")
self.session = requests.Session() self.session = requests.Session()
self.session.headers.update( self.session.headers.update(
{"Content-Type": "application/json", "Accept": "application/json"} {"Content-Type": "application/json", "Accept": "application/json"}
@@ -25,7 +36,14 @@ class LeggenAPIClient:
def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make HTTP request to the API""" """Make HTTP request to the API"""
url = urljoin(self.base_url, endpoint) # Construct URL by joining base_url with endpoint
# Handle both relative endpoints (starting with /) and paths
if endpoint.startswith("/"):
# Absolute endpoint path - append to base_url
url = f"{self.base_url}{endpoint}"
else:
# Relative endpoint, use urljoin
url = urljoin(f"{self.base_url}/", endpoint)
try: try:
response = self.session.request(method, url, **kwargs) response = self.session.request(method, url, **kwargs)
@@ -52,7 +70,9 @@ class LeggenAPIClient:
"""Check if the leggen server is healthy""" """Check if the leggen server is healthy"""
try: try:
response = self._make_request("GET", "/health") response = self._make_request("GET", "/health")
return response.get("status") == "healthy" # The API now returns nested data structure
data = response.get("data", {})
return data.get("status") == "healthy"
except Exception: except Exception:
return False return False
@@ -60,7 +80,7 @@ class LeggenAPIClient:
def get_institutions(self, country: str = "PT") -> List[Dict[str, Any]]: def get_institutions(self, country: str = "PT") -> List[Dict[str, Any]]:
"""Get bank institutions for a country""" """Get bank institutions for a country"""
response = self._make_request( response = self._make_request(
"GET", "/api/v1/banks/institutions", params={"country": country} "GET", "/banks/institutions", params={"country": country}
) )
return response.get("data", []) return response.get("data", [])
@@ -70,35 +90,35 @@ class LeggenAPIClient:
"""Connect to a bank""" """Connect to a bank"""
response = self._make_request( response = self._make_request(
"POST", "POST",
"/api/v1/banks/connect", "/banks/connect",
json={"institution_id": institution_id, "redirect_url": redirect_url}, json={"institution_id": institution_id, "redirect_url": redirect_url},
) )
return response.get("data", {}) return response.get("data", {})
def get_bank_status(self) -> List[Dict[str, Any]]: def get_bank_status(self) -> List[Dict[str, Any]]:
"""Get bank connection status""" """Get bank connection status"""
response = self._make_request("GET", "/api/v1/banks/status") response = self._make_request("GET", "/banks/status")
return response.get("data", []) return response.get("data", [])
def get_supported_countries(self) -> List[Dict[str, Any]]: def get_supported_countries(self) -> List[Dict[str, Any]]:
"""Get supported countries""" """Get supported countries"""
response = self._make_request("GET", "/api/v1/banks/countries") response = self._make_request("GET", "/banks/countries")
return response.get("data", []) return response.get("data", [])
# Account endpoints # Account endpoints
def get_accounts(self) -> List[Dict[str, Any]]: def get_accounts(self) -> List[Dict[str, Any]]:
"""Get all accounts""" """Get all accounts"""
response = self._make_request("GET", "/api/v1/accounts") response = self._make_request("GET", "/accounts")
return response.get("data", []) return response.get("data", [])
def get_account_details(self, account_id: str) -> Dict[str, Any]: def get_account_details(self, account_id: str) -> Dict[str, Any]:
"""Get account details""" """Get account details"""
response = self._make_request("GET", f"/api/v1/accounts/{account_id}") response = self._make_request("GET", f"/accounts/{account_id}")
return response.get("data", {}) return response.get("data", {})
def get_account_balances(self, account_id: str) -> List[Dict[str, Any]]: def get_account_balances(self, account_id: str) -> List[Dict[str, Any]]:
"""Get account balances""" """Get account balances"""
response = self._make_request("GET", f"/api/v1/accounts/{account_id}/balances") response = self._make_request("GET", f"/accounts/{account_id}/balances")
return response.get("data", []) return response.get("data", [])
def get_account_transactions( def get_account_transactions(
@@ -107,7 +127,7 @@ class LeggenAPIClient:
"""Get account transactions""" """Get account transactions"""
response = self._make_request( response = self._make_request(
"GET", "GET",
f"/api/v1/accounts/{account_id}/transactions", f"/accounts/{account_id}/transactions",
params={"limit": limit, "summary_only": summary_only}, params={"limit": limit, "summary_only": summary_only},
) )
return response.get("data", []) return response.get("data", [])
@@ -120,7 +140,7 @@ class LeggenAPIClient:
params = {"limit": limit, "summary_only": summary_only} params = {"limit": limit, "summary_only": summary_only}
params.update(filters) params.update(filters)
response = self._make_request("GET", "/api/v1/transactions", params=params) response = self._make_request("GET", "/transactions", params=params)
return response.get("data", []) return response.get("data", [])
def get_transaction_stats( def get_transaction_stats(
@@ -131,15 +151,13 @@ class LeggenAPIClient:
if account_id: if account_id:
params["account_id"] = account_id params["account_id"] = account_id
response = self._make_request( response = self._make_request("GET", "/transactions/stats", params=params)
"GET", "/api/v1/transactions/stats", params=params
)
return response.get("data", {}) return response.get("data", {})
# Sync endpoints # Sync endpoints
def get_sync_status(self) -> Dict[str, Any]: def get_sync_status(self) -> Dict[str, Any]:
"""Get sync status""" """Get sync status"""
response = self._make_request("GET", "/api/v1/sync/status") response = self._make_request("GET", "/sync/status")
return response.get("data", {}) return response.get("data", {})
def trigger_sync( def trigger_sync(
@@ -150,7 +168,7 @@ class LeggenAPIClient:
if account_ids: if account_ids:
data["account_ids"] = account_ids data["account_ids"] = account_ids
response = self._make_request("POST", "/api/v1/sync", json=data) response = self._make_request("POST", "/sync", json=data)
return response.get("data", {}) return response.get("data", {})
def sync_now( def sync_now(
@@ -161,12 +179,12 @@ class LeggenAPIClient:
if account_ids: if account_ids:
data["account_ids"] = account_ids data["account_ids"] = account_ids
response = self._make_request("POST", "/api/v1/sync/now", json=data) response = self._make_request("POST", "/sync/now", json=data)
return response.get("data", {}) return response.get("data", {})
def get_scheduler_config(self) -> Dict[str, Any]: def get_scheduler_config(self) -> Dict[str, Any]:
"""Get scheduler configuration""" """Get scheduler configuration"""
response = self._make_request("GET", "/api/v1/sync/scheduler") response = self._make_request("GET", "/sync/scheduler")
return response.get("data", {}) return response.get("data", {})
def update_scheduler_config( def update_scheduler_config(
@@ -185,5 +203,5 @@ class LeggenAPIClient:
if cron: if cron:
data["cron"] = cron data["cron"] = cron
response = self._make_request("PUT", "/api/v1/sync/scheduler", json=data) response = self._make_request("PUT", "/sync/scheduler", json=data)
return response.get("data", {}) return response.get("data", {})

View File

@@ -1,5 +1,6 @@
import click import click
from leggen.api_client import LeggenAPIClient
from leggen.main import cli from leggen.main import cli
from leggen.utils.text import info, success from leggen.utils.text import info, success
@@ -15,12 +16,11 @@ def delete(ctx, requisition_id: str):
Check `leggen status` to get the REQUISITION_ID Check `leggen status` to get the REQUISITION_ID
""" """
import requests api_client = LeggenAPIClient(ctx.obj.get("api_url"))
info(f"Deleting Bank Requisition: {requisition_id}") info(f"Deleting Bank Requisition: {requisition_id}")
api_url = ctx.obj.get("api_url", "http://localhost:8000") # Use API client to make the delete request
res = requests.delete(f"{api_url}/requisitions/{requisition_id}") api_client._make_request("DELETE", f"/requisitions/{requisition_id}")
res.raise_for_status()
success(f"Bank Requisition {requisition_id} deleted") success(f"Bank Requisition {requisition_id} deleted")