mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 23:12:16 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc3522220a | ||
|
|
1693b3a50d | ||
|
|
460c5af6ea | ||
|
|
5a8614e019 | ||
|
|
ae5d034d4b | ||
|
|
d4edf69f2c |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,4 +1,32 @@
|
|||||||
|
|
||||||
|
## 2025.9.23 (2025/09/24)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **cli:** Fix API URL handling for subpaths and improve client robustness. ([ae5d034d](https://github.com/elisiariocouto/leggen/commit/ae5d034d4b1da785e3dc240c1d60c2cae7de8010))
|
||||||
|
- Correct sync trigger types from manual to scheduled/retry. ([460c5af6](https://github.com/elisiariocouto/leggen/commit/460c5af6ea343ef5685b716413d01d7a30fa9acf))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **frontend:** Add version-based cache invalidation for PWA updates ([d4edf69f](https://github.com/elisiariocouto/leggen/commit/d4edf69f2cea2515a00435ee974116948057148d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025.9.23 (2025/09/24)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **cli:** Fix API URL handling for subpaths and improve client robustness. ([ae5d034d](https://github.com/elisiariocouto/leggen/commit/ae5d034d4b1da785e3dc240c1d60c2cae7de8010))
|
||||||
|
- Correct sync trigger types from manual to scheduled/retry. ([460c5af6](https://github.com/elisiariocouto/leggen/commit/460c5af6ea343ef5685b716413d01d7a30fa9acf))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **frontend:** Add version-based cache invalidation for PWA updates ([d4edf69f](https://github.com/elisiariocouto/leggen/commit/d4edf69f2cea2515a00435ee974116948057148d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 2025.9.22 (2025/09/24)
|
## 2025.9.22 (2025/09/24)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export default function System() {
|
|||||||
: "Sync Failed"}
|
: "Sync Failed"}
|
||||||
</h4>
|
</h4>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{operation.trigger_type}
|
{operation.trigger_type.charAt(0).toUpperCase() + operation.trigger_type.slice(1)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
|
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
|
||||||
@@ -272,7 +272,7 @@ export default function System() {
|
|||||||
: "Sync Failed"}
|
: "Sync Failed"}
|
||||||
</h4>
|
</h4>
|
||||||
<Badge variant="outline" className="text-xs mt-1">
|
<Badge variant="outline" className="text-xs mt-1">
|
||||||
{operation.trigger_type}
|
{operation.trigger_type.charAt(0).toUpperCase() + operation.trigger_type.slice(1)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
interface PWAUpdate {
|
interface PWAUpdate {
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
updateSW: () => Promise<void>;
|
updateSW: () => Promise<void>;
|
||||||
|
forceReload: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePWA(): PWAUpdate {
|
export function usePWA(): PWAUpdate {
|
||||||
@@ -11,6 +12,33 @@ export function usePWA(): PWAUpdate {
|
|||||||
() => async () => {},
|
() => async () => {},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const forceReload = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// Clear all caches
|
||||||
|
if ('caches' in window) {
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
await Promise.all(
|
||||||
|
cacheNames.map(cacheName => caches.delete(cacheName))
|
||||||
|
);
|
||||||
|
console.log("All caches cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister service worker
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
await Promise.all(registrations.map(registration => registration.unregister()));
|
||||||
|
console.log("All service workers unregistered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reload
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during force reload:", error);
|
||||||
|
// Fallback: just reload the page
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if SW registration is available
|
// Check if SW registration is available
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
@@ -37,5 +65,6 @@ export function usePWA(): PWAUpdate {
|
|||||||
return {
|
return {
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
updateSW,
|
updateSW,
|
||||||
|
forceReload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
40
frontend/src/hooks/useVersionCheck.ts
Normal file
40
frontend/src/hooks/useVersionCheck.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { apiClient } from "../lib/api";
|
||||||
|
|
||||||
|
const VERSION_STORAGE_KEY = "leggen_app_version";
|
||||||
|
|
||||||
|
export function useVersionCheck(forceReload: () => Promise<void>) {
|
||||||
|
const {
|
||||||
|
data: healthStatus,
|
||||||
|
isSuccess: healthSuccess,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["health"],
|
||||||
|
queryFn: apiClient.getHealth,
|
||||||
|
refetchInterval: 30000,
|
||||||
|
retry: false,
|
||||||
|
staleTime: 0, // Always consider data stale to ensure fresh version checks
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (healthSuccess && healthStatus?.version) {
|
||||||
|
const currentVersion = healthStatus.version;
|
||||||
|
const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedVersion && storedVersion !== currentVersion) {
|
||||||
|
console.log(`Version mismatch detected: stored=${storedVersion}, current=${currentVersion}`);
|
||||||
|
console.log("Clearing cache and reloading...");
|
||||||
|
|
||||||
|
// Update stored version first
|
||||||
|
localStorage.setItem(VERSION_STORAGE_KEY, currentVersion);
|
||||||
|
|
||||||
|
// Force reload to clear cache
|
||||||
|
forceReload();
|
||||||
|
} else if (!storedVersion) {
|
||||||
|
// First time loading, store the version
|
||||||
|
localStorage.setItem(VERSION_STORAGE_KEY, currentVersion);
|
||||||
|
console.log(`Version stored: ${currentVersion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [healthSuccess, healthStatus?.version, forceReload]);
|
||||||
|
}
|
||||||
@@ -3,10 +3,14 @@ import { AppSidebar } from "../components/AppSidebar";
|
|||||||
import { SiteHeader } from "../components/SiteHeader";
|
import { SiteHeader } from "../components/SiteHeader";
|
||||||
import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts";
|
import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts";
|
||||||
import { usePWA } from "../hooks/usePWA";
|
import { usePWA } from "../hooks/usePWA";
|
||||||
|
import { useVersionCheck } from "../hooks/useVersionCheck";
|
||||||
import { SidebarInset, SidebarProvider } from "../components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "../components/ui/sidebar";
|
||||||
|
|
||||||
function RootLayout() {
|
function RootLayout() {
|
||||||
const { updateAvailable, updateSW } = usePWA();
|
const { updateAvailable, updateSW, forceReload } = usePWA();
|
||||||
|
|
||||||
|
// Check for version mismatches and force reload if needed
|
||||||
|
useVersionCheck(forceReload);
|
||||||
|
|
||||||
const handlePWAInstall = () => {
|
const handlePWAInstall = () => {
|
||||||
console.log("PWA installed successfully");
|
console.log("PWA installed successfully");
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
|
import { tanstackRouter } from "@tanstack/router-vite-plugin";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
TanStackRouterVite(),
|
tanstackRouter(),
|
||||||
react(),
|
react(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class SyncOperation(BaseModel):
|
|||||||
duration_seconds: Optional[float] = None
|
duration_seconds: Optional[float] = None
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
logs: list[str] = []
|
logs: list[str] = []
|
||||||
trigger_type: str = "manual" # manual, scheduled, api
|
trigger_type: str = "manual" # manual, scheduled, retry, api
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
json_encoders = {datetime: lambda v: v.isoformat() if v else None}
|
json_encoders = {datetime: lambda v: v.isoformat() if v else None}
|
||||||
|
|||||||
@@ -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", {})
|
||||||
|
|||||||
@@ -102,12 +102,14 @@ class BackgroundScheduler:
|
|||||||
async def _run_sync(self, retry_count: int = 0):
|
async def _run_sync(self, retry_count: int = 0):
|
||||||
"""Run sync with enhanced error handling and retry logic"""
|
"""Run sync with enhanced error handling and retry logic"""
|
||||||
try:
|
try:
|
||||||
logger.info("Starting scheduled sync job")
|
trigger_type = "retry" if retry_count > 0 else "scheduled"
|
||||||
await self.sync_service.sync_all_accounts()
|
logger.info(f"Starting {trigger_type} sync job")
|
||||||
logger.info("Scheduled sync job completed successfully")
|
await self.sync_service.sync_all_accounts(trigger_type=trigger_type)
|
||||||
|
logger.info(f"{trigger_type.capitalize()} sync job completed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
trigger_type = "retry" if retry_count > 0 else "scheduled"
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Scheduled sync job failed (attempt {retry_count + 1}/{self.max_retries}): {e}"
|
f"{trigger_type.capitalize()} sync job failed (attempt {retry_count + 1}/{self.max_retries}): {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send notification about the failure
|
# Send notification about the failure
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -39,13 +39,11 @@ class Config:
|
|||||||
try:
|
try:
|
||||||
with open(config_path, "rb") as f:
|
with open(config_path, "rb") as f:
|
||||||
raw_config = tomllib.load(f)
|
raw_config = tomllib.load(f)
|
||||||
logger.info(f"Configuration loaded from {config_path}")
|
|
||||||
|
|
||||||
# Validate configuration using Pydantic
|
# Validate configuration using Pydantic
|
||||||
try:
|
try:
|
||||||
self._config_model = ConfigModel(**raw_config)
|
self._config_model = ConfigModel(**raw_config)
|
||||||
self._config = self._config_model.dict(by_alias=True, exclude_none=True)
|
self._config = self._config_model.dict(by_alias=True, exclude_none=True)
|
||||||
logger.info("Configuration validation successful")
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Configuration validation failed: {e}")
|
logger.error(f"Configuration validation failed: {e}")
|
||||||
raise ValueError(f"Invalid configuration: {e}") from e
|
raise ValueError(f"Invalid configuration: {e}") from e
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "leggen"
|
name = "leggen"
|
||||||
version = "2025.9.22"
|
version = "2025.9.23"
|
||||||
description = "An Open Banking CLI"
|
description = "An Open Banking CLI"
|
||||||
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
|
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
|
||||||
requires-python = "~=3.13.0"
|
requires-python = "~=3.13.0"
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ class TestLeggenAPIClient:
|
|||||||
client = LeggenAPIClient("http://localhost:8000")
|
client = LeggenAPIClient("http://localhost:8000")
|
||||||
|
|
||||||
with requests_mock.Mocker() as m:
|
with requests_mock.Mocker() as m:
|
||||||
m.get("http://localhost:8000/health", json={"status": "healthy"})
|
m.get(
|
||||||
|
"http://localhost:8000/api/v1/health",
|
||||||
|
json={"data": {"status": "healthy"}},
|
||||||
|
)
|
||||||
|
|
||||||
result = client.health_check()
|
result = client.health_check()
|
||||||
assert result is True
|
assert result is True
|
||||||
@@ -112,13 +115,13 @@ class TestLeggenAPIClient:
|
|||||||
custom_url = "http://custom-host:9000"
|
custom_url = "http://custom-host:9000"
|
||||||
client = LeggenAPIClient(custom_url)
|
client = LeggenAPIClient(custom_url)
|
||||||
|
|
||||||
assert client.base_url == custom_url
|
assert client.base_url == f"{custom_url}/api/v1"
|
||||||
|
|
||||||
def test_environment_variable_url(self):
|
def test_environment_variable_url(self):
|
||||||
"""Test using environment variable for API URL."""
|
"""Test using environment variable for API URL."""
|
||||||
with patch.dict("os.environ", {"LEGGEN_API_URL": "http://env-host:7000"}):
|
with patch.dict("os.environ", {"LEGGEN_API_URL": "http://env-host:7000"}):
|
||||||
client = LeggenAPIClient()
|
client = LeggenAPIClient()
|
||||||
assert client.base_url == "http://env-host:7000"
|
assert client.base_url == "http://env-host:7000/api/v1"
|
||||||
|
|
||||||
def test_sync_with_options(self):
|
def test_sync_with_options(self):
|
||||||
"""Test sync with various options."""
|
"""Test sync with various options."""
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -220,7 +220,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leggen"
|
name = "leggen"
|
||||||
version = "2025.9.22"
|
version = "2025.9.23"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
|
|||||||
Reference in New Issue
Block a user