import json from datetime import datetime, timezone from urllib.parse import urljoin from urllib.request import Request, urlopen from overview.brokerage_types import ( BrokerageAccountPayload, BrokerageAssetPayload, BrokerageHoldingPayload, BrokerageSnapshot, ) from .base import BaseBrokerageAdapter, BrokerageAdapterError class GenericRestBrokerageAdapter(BaseBrokerageAdapter): provider = "generic-rest" def fetch_snapshot(self) -> BrokerageSnapshot: accounts = self._fetch_collection( self.connection.get_setting("account_endpoint", "/accounts") ) assets = self._fetch_collection( self.connection.get_setting("asset_endpoint", "/assets") ) holdings = self._fetch_collection( self.connection.get_setting("holding_endpoint", "/holdings") ) return BrokerageSnapshot( accounts=[self._build_account(item) for item in accounts], assets=[self._build_asset(item) for item in assets], holdings=[self._build_holding(item) for item in holdings], ) def _fetch_collection(self, endpoint: str) -> list[dict]: url = urljoin(self.connection.base_url, endpoint) request = Request(url, headers=self._headers()) try: with urlopen(request, timeout=30) as response: payload = json.load(response) except Exception as exc: raise BrokerageAdapterError( f"Unable to fetch brokerage data from {url}: {exc}" ) from exc if isinstance(payload, dict): for key in ("results", "data", "items"): value = payload.get(key) if isinstance(value, list): return value if not isinstance(payload, list): raise BrokerageAdapterError( f"Expected a list payload from {url}, received {type(payload).__name__}" ) return payload def _headers(self) -> dict[str, str]: headers = {"Accept": "application/json"} token = self.connection.get_credential("access_token") api_key = self.connection.get_credential("api_key") api_key_header = self.connection.get_setting("api_key_header", "X-API-Key") if token: headers["Authorization"] = f"Bearer {token}" if api_key: headers[api_key_header] = api_key extra_headers = self.connection.get_setting("headers", {}) if isinstance(extra_headers, dict): headers.update({str(key): str(value) for key, value in extra_headers.items()}) return headers def _build_account(self, item: dict) -> BrokerageAccountPayload: account_id_field = self.connection.get_setting("account_id_field", "id") account_name_field = self.connection.get_setting("account_name_field", "name") account_number_field = self.connection.get_setting("account_number_field", "number") account_balance_field = self.connection.get_setting("account_balance_field", "balance") return BrokerageAccountPayload( external_id=str(item[account_id_field]), name=item[account_name_field], number=item.get(account_number_field, ""), balance=int(item.get(account_balance_field, 0)), ) def _build_asset(self, item: dict) -> BrokerageAssetPayload: asset_id_field = self.connection.get_setting("asset_id_field", "id") asset_ticker_field = self.connection.get_setting("asset_ticker_field", "ticker") asset_price_field = self.connection.get_setting("asset_price_field", "price") return BrokerageAssetPayload( external_id=str(item[asset_id_field]), ticker=item[asset_ticker_field], price=float(item.get(asset_price_field, 0)), ) def _build_holding(self, item: dict) -> BrokerageHoldingPayload: holding_id_field = self.connection.get_setting("holding_id_field", "id") holding_account_field = self.connection.get_setting("holding_account_field", "account_id") holding_asset_field = self.connection.get_setting("holding_asset_field", "asset_id") holding_ticker_field = self.connection.get_setting("holding_ticker_field", "ticker") holding_amount_field = self.connection.get_setting("holding_amount_field", "amount") holding_cost_basis_field = self.connection.get_setting( "holding_cost_basis_field", "cost_basis" ) holding_purchase_date_field = self.connection.get_setting( "holding_purchase_date_field", "purchase_date" ) purchase_date = item.get(holding_purchase_date_field) if purchase_date: purchase_date = datetime.fromisoformat( str(purchase_date).replace("Z", "+00:00") ) else: purchase_date = datetime.now(timezone.utc) return BrokerageHoldingPayload( external_id=str(item[holding_id_field]), account_external_id=str(item[holding_account_field]), asset_external_id=str(item[holding_asset_field]), ticker=item[holding_ticker_field], amount=float(item.get(holding_amount_field, 0)), cost_basis=float(item.get(holding_cost_basis_field, 0)), purchase_date=purchase_date, )