add bankclient
This commit is contained in:
+5
-2
@@ -1,6 +1,9 @@
|
||||
# Enable Banking Credentials
|
||||
EB_CLIENT_ID=your_client_id
|
||||
EB_CLIENT_SECRET=your_client_secret
|
||||
EB_APP_ID=your_app_id # This should be the Application ID provided by Enable Banking when you register your application.
|
||||
EB_PEM_FILE=your_app_secret # This should be the path to the PEM file containing your private key for authentication with Enable Banking.
|
||||
EB_BANK_NAME=your_bank_name # e.g. "Deutsche Bank", "N26", "Revolut", etc.
|
||||
EB_COUNTRY_CODE=your_country_code # e.g. DE for Germany, US for United States, etc.
|
||||
EB_API_BASE_URL=https://api.enablebanking.com # Base URL for Enable Banking API
|
||||
|
||||
# Actual Budget Configuration
|
||||
ACTUAL_SERVER_URL=https://your-actual-server.com
|
||||
|
||||
@@ -32,7 +32,7 @@ Automate your financial tracking without sacrificing privacy. Actualize pulls yo
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
pip install actualpy enablebanking_sdk python-dotenv
|
||||
pip install actualpy enablebanking_sdk python-dotenv fastapi "uvicorn[standard]"
|
||||
```
|
||||
|
||||
3. **Configure environment variables:**
|
||||
@@ -60,7 +60,13 @@ Automate your financial tracking without sacrificing privacy. Actualize pulls yo
|
||||
2. **Sync:**
|
||||
Run the sync script to pull transactions and push them to Actual.
|
||||
```bash
|
||||
python sync.py
|
||||
python sync.py
|
||||
```
|
||||
|
||||
3. **API Mode:**
|
||||
Start the FastAPI server.
|
||||
```bash
|
||||
uvicorn src.main:app --reload
|
||||
```
|
||||
|
||||
## 📦 Tech Stack
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
EMFD4S9908gAmV1/qNe4pZKB6c26xmh0
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,3 +1,6 @@
|
||||
actualpy
|
||||
enablebanking_sdk
|
||||
python-dotenv
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
PyJWT
|
||||
|
||||
+2539
File diff suppressed because it is too large
Load Diff
+5
-14
@@ -1,17 +1,8 @@
|
||||
import enablebanking
|
||||
from .config import Config
|
||||
from src.client.httpclient import BankClient as HttpBankClient
|
||||
|
||||
class BankClient:
|
||||
class BankClient(HttpBankClient):
|
||||
def __init__(self):
|
||||
self.api_client = enablebanking.ApiClient(
|
||||
client_id=Config.EB_CLIENT_ID,
|
||||
client_secret=Config.EB_CLIENT_SECRET
|
||||
)
|
||||
|
||||
def authenticate(self):
|
||||
# Placeholder for bank authentication logic
|
||||
pass
|
||||
|
||||
def fetch_transactions(self, account_id, start_date):
|
||||
# Placeholder for fetching transactions from bank
|
||||
pass
|
||||
super().__init__()
|
||||
def get_application_details(self):
|
||||
return super().get_application_details()
|
||||
@@ -0,0 +1,151 @@
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import requests
|
||||
import jwt as pyjwt
|
||||
from src.config import Config
|
||||
|
||||
class ActualClient:
|
||||
def __init__(self):
|
||||
self.base_url = Config.ACTUAL_SERVER_URL
|
||||
self.password = Config.ACTUAL_PASSWORD
|
||||
self.budget_id = Config.ACTUAL_BUDGET_ID
|
||||
self.encryption_password = Config.ACTUAL_ENCRYPTION_PASSWORD
|
||||
|
||||
def push_transactions(self, transactions):
|
||||
# TODO: Implement actualpy integration
|
||||
pass
|
||||
|
||||
class BankClient:
|
||||
def __init__(self):
|
||||
self.app_id = Config.EB_APP_ID
|
||||
self.pem_file = Config.EB_PEM_FILE
|
||||
self.bank_name = Config.EB_BANK_NAME
|
||||
self.country_code = Config.EB_COUNTRY_CODE
|
||||
self.api_base_url = Config.EB_API_BASE_URL
|
||||
self._app_details = None
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
"""Generates a fresh JWT token for Enable Banking API."""
|
||||
iat = int(datetime.now().timestamp())
|
||||
payload = {
|
||||
"iss": "enablebanking.com",
|
||||
"aud": "api.enablebanking.com",
|
||||
"iat": iat,
|
||||
"exp": iat + 3600,
|
||||
}
|
||||
with open(self.pem_file, "rb") as f:
|
||||
private_key = f.read()
|
||||
|
||||
return pyjwt.encode(
|
||||
payload,
|
||||
private_key,
|
||||
algorithm="RS256",
|
||||
headers={"kid": self.app_id},
|
||||
)
|
||||
|
||||
def _get_headers(self):
|
||||
return {"Authorization": f"Bearer {self.token}"}
|
||||
|
||||
def get_application_details(self):
|
||||
"""Fetches and caches application details."""
|
||||
if self._app_details:
|
||||
return self._app_details
|
||||
|
||||
r = requests.get(f"{self.api_base_url}/application", headers=self._get_headers())
|
||||
r.raise_for_status()
|
||||
self._app_details = r.json()
|
||||
return self._app_details
|
||||
|
||||
def get_auth_url(self, redirect_url=None, state=None):
|
||||
"""Initiates bank authentication and returns the redirect URL."""
|
||||
if not redirect_url:
|
||||
app = self.get_application_details()
|
||||
redirect_url = app["redirect_urls"][0]
|
||||
|
||||
body = {
|
||||
"access": {
|
||||
"valid_until": (datetime.now(timezone.utc) + timedelta(days=10)).isoformat()
|
||||
},
|
||||
"aspsp": {"name": self.bank_name, "country": self.country_code},
|
||||
"state": state or str(uuid.uuid4()),
|
||||
"redirect_url": redirect_url,
|
||||
"psu_type": "personal",
|
||||
}
|
||||
|
||||
r = requests.post(f"{self.api_base_url}/auth", json=body, headers=self._get_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()["url"]
|
||||
|
||||
def create_session(self, redirected_url_or_code):
|
||||
"""Exchanges the redirect URL or code for a user session."""
|
||||
code = redirected_url_or_code
|
||||
if "code=" in redirected_url_or_code:
|
||||
code = parse_qs(urlparse(redirected_url_or_code).query)["code"][0]
|
||||
|
||||
r = requests.post(
|
||||
f"{self.api_base_url}/sessions",
|
||||
json={"code": code},
|
||||
headers=self._get_headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def get_accounts(self, session_id):
|
||||
"""Lists accounts associated with a session."""
|
||||
r = requests.get(
|
||||
f"{self.api_base_url}/sessions/{session_id}/accounts",
|
||||
headers=self._get_headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json().get("accounts", [])
|
||||
|
||||
def get_balances(self, account_id):
|
||||
"""Retrieves balances for a specific account."""
|
||||
r = requests.get(
|
||||
f"{self.api_base_url}/accounts/{account_id}/balances",
|
||||
headers=self._get_headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def fetch_transactions(self, account_id, date_from=None, date_to=None):
|
||||
"""
|
||||
Fetches transactions for a specific account with pagination support.
|
||||
:param account_id: The unique identifier of the account.
|
||||
:param date_from: ISO date string (YYYY-MM-DD). Defaults to 90 days ago.
|
||||
:param date_to: ISO date string (YYYY-MM-DD). Defaults to today.
|
||||
"""
|
||||
if not date_from:
|
||||
date_from = (datetime.now(timezone.utc) - timedelta(days=90)).date().isoformat()
|
||||
|
||||
query = {"date_from": date_from}
|
||||
if date_to:
|
||||
query["date_to"] = date_to
|
||||
|
||||
all_transactions = []
|
||||
continuation_key = None
|
||||
|
||||
while True:
|
||||
params = query.copy()
|
||||
if continuation_key:
|
||||
params["continuation_key"] = continuation_key
|
||||
|
||||
r = requests.get(
|
||||
f"{self.api_base_url}/accounts/{account_id}/transactions",
|
||||
params=params,
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
all_transactions.extend(data.get("transactions", []))
|
||||
|
||||
continuation_key = data.get("continuation_key")
|
||||
if not continuation_key:
|
||||
break
|
||||
|
||||
return all_transactions
|
||||
+8
-3
@@ -4,10 +4,15 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
class Config:
|
||||
EB_CLIENT_ID = os.getenv("EB_CLIENT_ID")
|
||||
EB_CLIENT_SECRET = os.getenv("EB_CLIENT_SECRET")
|
||||
|
||||
EB_APP_ID = os.getenv("EB_APP_ID")
|
||||
EB_PEM_FILE = os.getenv("EB_PEM_FILE")
|
||||
EB_BANK_NAME = os.getenv("EB_BANK_NAME")
|
||||
EB_COUNTRY_CODE = os.getenv("EB_COUNTRY_CODE")
|
||||
EB_API_BASE_URL = os.getenv("EB_API_BASE_URL")
|
||||
|
||||
ACTUAL_SERVER_URL = os.getenv("ACTUAL_SERVER_URL")
|
||||
ACTUAL_PASSWORD = os.getenv("ACTUAL_PASSWORD")
|
||||
ACTUAL_BUDGET_ID = os.getenv("ACTUAL_BUDGET_ID")
|
||||
ACTUAL_ENCRYPTION_PASSWORD = os.getenv("ACTUAL_ENCRYPTION_PASSWORD")
|
||||
SERVER_PUBLIC_URL = os.getenv("SERVER_PUBLIC_URL")
|
||||
SERVER_PORT = int(os.getenv("SERVER_PORT", 8000))
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
from fastapi import FastAPI
|
||||
from src import client
|
||||
from .bank_client import BankClient
|
||||
from .actual_client import ActualClient
|
||||
|
||||
app = FastAPI(title="Actualize API")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Actualize API is running"}
|
||||
|
||||
@app.post("/sync")
|
||||
async def trigger_sync():
|
||||
# Placeholder for sync logic
|
||||
return {"status": "Sync initiated"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.get("/bank/application")
|
||||
async def get_bank_application_details():
|
||||
bank_client = BankClient()
|
||||
details = bank_client.get_application_details()
|
||||
return details
|
||||
|
||||
@app.get("/bank/auth")
|
||||
async def get_bank_auth():
|
||||
"""Returns a login URL for the bank."""
|
||||
bank_client = BankClient()
|
||||
auth_url = bank_client.get_auth_url()
|
||||
return {"auth_url": auth_url}
|
||||
|
||||
@app.get("/bank/callback")
|
||||
async def bank_callback(code: str):
|
||||
"""Exchanges the 'code' from the bank redirect for a session_id."""
|
||||
bank_client = BankClient()
|
||||
session = bank_client.create_session(code)
|
||||
return session
|
||||
|
||||
@app.post("/bank/transactions")
|
||||
async def sync_bank_transactions(session_id: str):
|
||||
bank_client = BankClient()
|
||||
accounts = bank_client.get_accounts(session_id=session_id)
|
||||
|
||||
if not accounts:
|
||||
return {"transactions": [], "message": "No accounts found for this session"}
|
||||
|
||||
# Use 'uid' as per Enable Banking API and fetch via the instance
|
||||
account_uid = accounts[0]["uid"]
|
||||
transactions = bank_client.fetch_transactions(
|
||||
account_id=account_uid,
|
||||
date_from="2024-01-01"
|
||||
)
|
||||
|
||||
return {"transactions": transactions}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user