fin-infra 0.1.81__py3-none-any.whl → 0.1.83__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fin_infra/analytics/__init__.py +3 -2
- fin_infra/analytics/add.py +21 -21
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +6 -6
- fin_infra/analytics/projections.py +1 -3
- fin_infra/banking/__init__.py +27 -27
- fin_infra/banking/history.py +4 -5
- fin_infra/banking/utils.py +19 -18
- fin_infra/brokerage/__init__.py +22 -24
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/cashflows/__init__.py +2 -2
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/engine.py +6 -6
- fin_infra/categorization/llm_layer.py +6 -5
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +5 -5
- fin_infra/chat/planning.py +0 -1
- fin_infra/cli/cmds/scaffold_cmds.py +10 -11
- fin_infra/clients/plaid.py +1 -1
- fin_infra/compliance/__init__.py +5 -5
- fin_infra/credit/add.py +6 -7
- fin_infra/credit/experian/auth.py +2 -2
- fin_infra/credit/experian/client.py +1 -1
- fin_infra/credit/experian/provider.py +4 -4
- fin_infra/crypto/__init__.py +7 -9
- fin_infra/documents/add.py +6 -8
- fin_infra/documents/analysis.py +8 -8
- fin_infra/documents/ease.py +14 -14
- fin_infra/documents/ocr.py +7 -7
- fin_infra/documents/storage.py +21 -13
- fin_infra/exceptions.py +0 -1
- fin_infra/goals/__init__.py +8 -8
- fin_infra/goals/add.py +30 -30
- fin_infra/goals/funding.py +1 -1
- fin_infra/goals/management.py +2 -3
- fin_infra/goals/milestones.py +1 -2
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +2 -2
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/providers/base.py +2 -3
- fin_infra/investments/providers/plaid.py +9 -9
- fin_infra/investments/providers/snaptrade.py +10 -10
- fin_infra/markets/__init__.py +1 -1
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/brokerage.py +2 -1
- fin_infra/models/candle.py +1 -0
- fin_infra/models/money.py +1 -0
- fin_infra/models/quotes.py +4 -3
- fin_infra/models/tax.py +2 -1
- fin_infra/models/transactions.py +3 -4
- fin_infra/net_worth/insights.py +0 -1
- fin_infra/normalization/__init__.py +2 -2
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/providers/banking/plaid_client.py +5 -5
- fin_infra/providers/banking/teller_client.py +7 -6
- fin_infra/providers/base.py +1 -1
- fin_infra/providers/brokerage/alpaca.py +3 -3
- fin_infra/providers/market/alphavantage.py +5 -10
- fin_infra/providers/market/ccxt_crypto.py +2 -2
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +5 -5
- fin_infra/providers/tax/__init__.py +1 -1
- fin_infra/providers/tax/irs.py +1 -1
- fin_infra/providers/tax/mock.py +5 -5
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +5 -4
- fin_infra/recurring/detector.py +7 -7
- fin_infra/recurring/detectors_llm.py +6 -6
- fin_infra/recurring/ease.py +2 -4
- fin_infra/recurring/insights.py +13 -13
- fin_infra/recurring/normalizer.py +1 -1
- fin_infra/recurring/normalizers.py +4 -4
- fin_infra/recurring/summary.py +4 -6
- fin_infra/scaffold/budgets.py +6 -6
- fin_infra/scaffold/goals.py +1 -1
- fin_infra/security/__init__.py +8 -8
- fin_infra/security/encryption.py +6 -6
- fin_infra/security/models.py +7 -7
- fin_infra/security/pii_filter.py +6 -6
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +4 -3
- fin_infra/utils/retry.py +1 -1
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/METADATA +1 -1
- fin_infra-0.1.83.dist-info/RECORD +180 -0
- fin_infra-0.1.81.dist-info/RECORD +0 -180
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/entry_points.txt +0 -0
fin_infra/brokerage/__init__.py
CHANGED
|
@@ -22,10 +22,12 @@ from typing import TYPE_CHECKING, Literal
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from fastapi import FastAPI
|
|
24
24
|
|
|
25
|
-
from ..providers.base import BrokerageProvider
|
|
26
|
-
from pydantic import BaseModel, Field
|
|
27
25
|
from decimal import Decimal
|
|
28
26
|
|
|
27
|
+
from pydantic import BaseModel, Field
|
|
28
|
+
|
|
29
|
+
from ..providers.base import BrokerageProvider
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
# Request model for order submission (used by add_brokerage FastAPI routes)
|
|
31
33
|
class OrderRequest(BaseModel):
|
|
@@ -127,7 +129,7 @@ def easy_brokerage(
|
|
|
127
129
|
|
|
128
130
|
|
|
129
131
|
def add_brokerage(
|
|
130
|
-
app:
|
|
132
|
+
app: FastAPI,
|
|
131
133
|
*,
|
|
132
134
|
provider: str | BrokerageProvider | None = None,
|
|
133
135
|
mode: Literal["paper", "live"] = "paper",
|
|
@@ -206,9 +208,9 @@ def add_brokerage(
|
|
|
206
208
|
>>> broker = add_brokerage(app, mode="live")
|
|
207
209
|
>>> # Only use in production with proper safeguards and risk management
|
|
208
210
|
"""
|
|
209
|
-
from svc_infra.api.fastapi.dual.public import public_router
|
|
210
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
211
211
|
from fastapi import HTTPException, Query
|
|
212
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
213
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
212
214
|
|
|
213
215
|
# Initialize provider if string or None
|
|
214
216
|
if isinstance(provider, str):
|
|
@@ -234,7 +236,7 @@ def add_brokerage(
|
|
|
234
236
|
account = brokerage_provider.get_account()
|
|
235
237
|
return account
|
|
236
238
|
except Exception as e:
|
|
237
|
-
raise HTTPException(status_code=500, detail=f"Error fetching account: {
|
|
239
|
+
raise HTTPException(status_code=500, detail=f"Error fetching account: {e!s}")
|
|
238
240
|
|
|
239
241
|
@router.get("/positions")
|
|
240
242
|
async def list_positions():
|
|
@@ -246,7 +248,7 @@ def add_brokerage(
|
|
|
246
248
|
positions = list(brokerage_provider.positions()) # Convert Iterable to list for len()
|
|
247
249
|
return {"positions": positions, "count": len(positions)}
|
|
248
250
|
except Exception as e:
|
|
249
|
-
raise HTTPException(status_code=500, detail=f"Error fetching positions: {
|
|
251
|
+
raise HTTPException(status_code=500, detail=f"Error fetching positions: {e!s}")
|
|
250
252
|
|
|
251
253
|
@router.get("/positions/{symbol}")
|
|
252
254
|
async def get_position(symbol: str):
|
|
@@ -259,9 +261,7 @@ def add_brokerage(
|
|
|
259
261
|
position = brokerage_provider.get_position(symbol)
|
|
260
262
|
return position
|
|
261
263
|
except Exception as e:
|
|
262
|
-
raise HTTPException(
|
|
263
|
-
status_code=404, detail=f"Position not found for {symbol}: {str(e)}"
|
|
264
|
-
)
|
|
264
|
+
raise HTTPException(status_code=404, detail=f"Position not found for {symbol}: {e!s}")
|
|
265
265
|
|
|
266
266
|
@router.delete("/positions/{symbol}")
|
|
267
267
|
async def close_position(symbol: str):
|
|
@@ -274,7 +274,7 @@ def add_brokerage(
|
|
|
274
274
|
order = brokerage_provider.close_position(symbol)
|
|
275
275
|
return {"message": f"Closing position for {symbol}", "order": order}
|
|
276
276
|
except Exception as e:
|
|
277
|
-
raise HTTPException(status_code=400, detail=f"Error closing position: {
|
|
277
|
+
raise HTTPException(status_code=400, detail=f"Error closing position: {e!s}")
|
|
278
278
|
|
|
279
279
|
@router.post("/orders")
|
|
280
280
|
async def submit_order(order_request: OrderRequest):
|
|
@@ -295,7 +295,7 @@ def add_brokerage(
|
|
|
295
295
|
)
|
|
296
296
|
return order
|
|
297
297
|
except Exception as e:
|
|
298
|
-
raise HTTPException(status_code=400, detail=f"Error submitting order: {
|
|
298
|
+
raise HTTPException(status_code=400, detail=f"Error submitting order: {e!s}")
|
|
299
299
|
|
|
300
300
|
@router.get("/orders")
|
|
301
301
|
async def list_orders(
|
|
@@ -312,7 +312,7 @@ def add_brokerage(
|
|
|
312
312
|
orders = brokerage_provider.list_orders(status=status, limit=limit)
|
|
313
313
|
return {"orders": orders, "count": len(orders)}
|
|
314
314
|
except Exception as e:
|
|
315
|
-
raise HTTPException(status_code=500, detail=f"Error fetching orders: {
|
|
315
|
+
raise HTTPException(status_code=500, detail=f"Error fetching orders: {e!s}")
|
|
316
316
|
|
|
317
317
|
@router.get("/orders/{order_id}")
|
|
318
318
|
async def get_order(order_id: str):
|
|
@@ -325,7 +325,7 @@ def add_brokerage(
|
|
|
325
325
|
order = brokerage_provider.get_order(order_id)
|
|
326
326
|
return order
|
|
327
327
|
except Exception as e:
|
|
328
|
-
raise HTTPException(status_code=404, detail=f"Order not found: {
|
|
328
|
+
raise HTTPException(status_code=404, detail=f"Order not found: {e!s}")
|
|
329
329
|
|
|
330
330
|
@router.delete("/orders/{order_id}")
|
|
331
331
|
async def cancel_order(order_id: str):
|
|
@@ -338,7 +338,7 @@ def add_brokerage(
|
|
|
338
338
|
brokerage_provider.cancel_order(order_id)
|
|
339
339
|
return {"message": f"Order {order_id} canceled successfully"}
|
|
340
340
|
except Exception as e:
|
|
341
|
-
raise HTTPException(status_code=400, detail=f"Error canceling order: {
|
|
341
|
+
raise HTTPException(status_code=400, detail=f"Error canceling order: {e!s}")
|
|
342
342
|
|
|
343
343
|
@router.get("/portfolio/history")
|
|
344
344
|
async def get_portfolio_history(
|
|
@@ -355,9 +355,7 @@ def add_brokerage(
|
|
|
355
355
|
history = brokerage_provider.get_portfolio_history(period=period, timeframe=timeframe)
|
|
356
356
|
return history
|
|
357
357
|
except Exception as e:
|
|
358
|
-
raise HTTPException(
|
|
359
|
-
status_code=500, detail=f"Error fetching portfolio history: {str(e)}"
|
|
360
|
-
)
|
|
358
|
+
raise HTTPException(status_code=500, detail=f"Error fetching portfolio history: {e!s}")
|
|
361
359
|
|
|
362
360
|
# Watchlist routes
|
|
363
361
|
@router.post("/watchlists")
|
|
@@ -375,7 +373,7 @@ def add_brokerage(
|
|
|
375
373
|
watchlist = brokerage_provider.create_watchlist(name=name, symbols=symbols)
|
|
376
374
|
return watchlist
|
|
377
375
|
except Exception as e:
|
|
378
|
-
raise HTTPException(status_code=400, detail=f"Error creating watchlist: {
|
|
376
|
+
raise HTTPException(status_code=400, detail=f"Error creating watchlist: {e!s}")
|
|
379
377
|
|
|
380
378
|
@router.get("/watchlists")
|
|
381
379
|
async def list_watchlists():
|
|
@@ -384,7 +382,7 @@ def add_brokerage(
|
|
|
384
382
|
watchlists = brokerage_provider.list_watchlists()
|
|
385
383
|
return {"watchlists": watchlists, "count": len(watchlists)}
|
|
386
384
|
except Exception as e:
|
|
387
|
-
raise HTTPException(status_code=500, detail=f"Error fetching watchlists: {
|
|
385
|
+
raise HTTPException(status_code=500, detail=f"Error fetching watchlists: {e!s}")
|
|
388
386
|
|
|
389
387
|
@router.get("/watchlists/{watchlist_id}")
|
|
390
388
|
async def get_watchlist(watchlist_id: str):
|
|
@@ -397,7 +395,7 @@ def add_brokerage(
|
|
|
397
395
|
watchlist = brokerage_provider.get_watchlist(watchlist_id)
|
|
398
396
|
return watchlist
|
|
399
397
|
except Exception as e:
|
|
400
|
-
raise HTTPException(status_code=404, detail=f"Watchlist not found: {
|
|
398
|
+
raise HTTPException(status_code=404, detail=f"Watchlist not found: {e!s}")
|
|
401
399
|
|
|
402
400
|
@router.delete("/watchlists/{watchlist_id}")
|
|
403
401
|
async def delete_watchlist(watchlist_id: str):
|
|
@@ -410,7 +408,7 @@ def add_brokerage(
|
|
|
410
408
|
brokerage_provider.delete_watchlist(watchlist_id)
|
|
411
409
|
return {"message": f"Watchlist {watchlist_id} deleted successfully"}
|
|
412
410
|
except Exception as e:
|
|
413
|
-
raise HTTPException(status_code=400, detail=f"Error deleting watchlist: {
|
|
411
|
+
raise HTTPException(status_code=400, detail=f"Error deleting watchlist: {e!s}")
|
|
414
412
|
|
|
415
413
|
@router.post("/watchlists/{watchlist_id}/symbols")
|
|
416
414
|
async def add_to_watchlist(
|
|
@@ -426,7 +424,7 @@ def add_brokerage(
|
|
|
426
424
|
watchlist = brokerage_provider.add_to_watchlist(watchlist_id, symbol)
|
|
427
425
|
return watchlist
|
|
428
426
|
except Exception as e:
|
|
429
|
-
raise HTTPException(status_code=400, detail=f"Error adding symbol: {
|
|
427
|
+
raise HTTPException(status_code=400, detail=f"Error adding symbol: {e!s}")
|
|
430
428
|
|
|
431
429
|
@router.delete("/watchlists/{watchlist_id}/symbols/{symbol}")
|
|
432
430
|
async def remove_from_watchlist(watchlist_id: str, symbol: str):
|
|
@@ -440,7 +438,7 @@ def add_brokerage(
|
|
|
440
438
|
watchlist = brokerage_provider.remove_from_watchlist(watchlist_id, symbol)
|
|
441
439
|
return watchlist
|
|
442
440
|
except Exception as e:
|
|
443
|
-
raise HTTPException(status_code=400, detail=f"Error removing symbol: {
|
|
441
|
+
raise HTTPException(status_code=400, detail=f"Error removing symbol: {e!s}")
|
|
444
442
|
|
|
445
443
|
# Mount router
|
|
446
444
|
app.include_router(router, include_in_schema=True)
|
fin_infra/budgets/__init__.py
CHANGED
|
@@ -105,12 +105,12 @@ def __getattr__(name: str):
|
|
|
105
105
|
):
|
|
106
106
|
from fin_infra.budgets.models import ( # noqa: F401
|
|
107
107
|
Budget,
|
|
108
|
-
|
|
109
|
-
BudgetPeriod,
|
|
108
|
+
BudgetAlert,
|
|
110
109
|
BudgetCategory,
|
|
110
|
+
BudgetPeriod,
|
|
111
111
|
BudgetProgress,
|
|
112
|
-
BudgetAlert,
|
|
113
112
|
BudgetTemplate,
|
|
113
|
+
BudgetType,
|
|
114
114
|
)
|
|
115
115
|
|
|
116
116
|
return locals()[name]
|
fin_infra/budgets/add.py
CHANGED
|
@@ -22,7 +22,6 @@ Generic Design:
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
from datetime import datetime
|
|
25
|
-
from typing import Optional
|
|
26
25
|
|
|
27
26
|
from fastapi import FastAPI, HTTPException, Query
|
|
28
27
|
from pydantic import BaseModel, Field
|
|
@@ -42,16 +41,16 @@ class CreateBudgetRequest(BaseModel):
|
|
|
42
41
|
type: BudgetType = Field(..., description="Budget type")
|
|
43
42
|
period: BudgetPeriod = Field(..., description="Budget period")
|
|
44
43
|
categories: dict[str, float] = Field(..., description="Category allocations")
|
|
45
|
-
start_date:
|
|
44
|
+
start_date: datetime | None = Field(None, description="Start date (defaults to now)")
|
|
46
45
|
rollover_enabled: bool = Field(False, description="Enable rollover")
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
class UpdateBudgetRequest(BaseModel):
|
|
50
49
|
"""Request model for updating a budget."""
|
|
51
50
|
|
|
52
|
-
name:
|
|
53
|
-
categories:
|
|
54
|
-
rollover_enabled:
|
|
51
|
+
name: str | None = Field(None, description="Updated budget name")
|
|
52
|
+
categories: dict[str, float] | None = Field(None, description="Updated categories")
|
|
53
|
+
rollover_enabled: bool | None = Field(None, description="Updated rollover setting")
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
class ApplyTemplateRequest(BaseModel):
|
|
@@ -60,14 +59,14 @@ class ApplyTemplateRequest(BaseModel):
|
|
|
60
59
|
user_id: str = Field(..., description="User identifier")
|
|
61
60
|
template_name: str = Field(..., description="Template name (e.g., '50_30_20')")
|
|
62
61
|
total_income: float = Field(..., description="Total income/budget amount", gt=0)
|
|
63
|
-
budget_name:
|
|
64
|
-
start_date:
|
|
62
|
+
budget_name: str | None = Field(None, description="Optional budget name")
|
|
63
|
+
start_date: datetime | None = Field(None, description="Optional start date")
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
def add_budgets(
|
|
68
67
|
app: FastAPI,
|
|
69
|
-
tracker:
|
|
70
|
-
db_url:
|
|
68
|
+
tracker: BudgetTracker | None = None,
|
|
69
|
+
db_url: str | None = None,
|
|
71
70
|
prefix: str = "/budgets",
|
|
72
71
|
) -> BudgetTracker:
|
|
73
72
|
"""Add budget management endpoints to FastAPI app.
|
|
@@ -162,13 +161,13 @@ def add_budgets(
|
|
|
162
161
|
except ValueError as e:
|
|
163
162
|
raise HTTPException(status_code=400, detail=str(e))
|
|
164
163
|
except Exception as e:
|
|
165
|
-
raise HTTPException(status_code=500, detail=f"Failed to create budget: {
|
|
164
|
+
raise HTTPException(status_code=500, detail=f"Failed to create budget: {e!s}")
|
|
166
165
|
|
|
167
166
|
# Endpoint 2: List budgets
|
|
168
167
|
@router.get("", response_model=list[Budget], summary="List Budgets")
|
|
169
168
|
async def list_budgets(
|
|
170
169
|
user_id: str = Query(..., description="User identifier"),
|
|
171
|
-
type:
|
|
170
|
+
type: BudgetType | None = Query(None, description="Filter by budget type"),
|
|
172
171
|
) -> list[Budget]:
|
|
173
172
|
"""
|
|
174
173
|
List budgets for a user.
|
|
@@ -189,7 +188,7 @@ def add_budgets(
|
|
|
189
188
|
budgets = await tracker.get_budgets(user_id=user_id, type=type)
|
|
190
189
|
return budgets
|
|
191
190
|
except Exception as e:
|
|
192
|
-
raise HTTPException(status_code=500, detail=f"Failed to list budgets: {
|
|
191
|
+
raise HTTPException(status_code=500, detail=f"Failed to list budgets: {e!s}")
|
|
193
192
|
|
|
194
193
|
# Endpoint 3: Get single budget
|
|
195
194
|
@router.get("/{budget_id}", response_model=Budget, summary="Get Budget")
|
|
@@ -217,7 +216,7 @@ def add_budgets(
|
|
|
217
216
|
except ValueError as e:
|
|
218
217
|
raise HTTPException(status_code=404, detail=str(e))
|
|
219
218
|
except Exception as e:
|
|
220
|
-
raise HTTPException(status_code=500, detail=f"Failed to get budget: {
|
|
219
|
+
raise HTTPException(status_code=500, detail=f"Failed to get budget: {e!s}")
|
|
221
220
|
|
|
222
221
|
# Endpoint 4: Update budget
|
|
223
222
|
@router.patch("/{budget_id}", response_model=Budget, summary="Update Budget")
|
|
@@ -269,7 +268,7 @@ def add_budgets(
|
|
|
269
268
|
else:
|
|
270
269
|
raise HTTPException(status_code=400, detail=error_msg)
|
|
271
270
|
except Exception as e:
|
|
272
|
-
raise HTTPException(status_code=500, detail=f"Failed to update budget: {
|
|
271
|
+
raise HTTPException(status_code=500, detail=f"Failed to update budget: {e!s}")
|
|
273
272
|
|
|
274
273
|
# Endpoint 5: Delete budget
|
|
275
274
|
@router.delete("/{budget_id}", status_code=204, summary="Delete Budget", response_model=None)
|
|
@@ -296,7 +295,7 @@ def add_budgets(
|
|
|
296
295
|
except ValueError as e:
|
|
297
296
|
raise HTTPException(status_code=404, detail=str(e))
|
|
298
297
|
except Exception as e:
|
|
299
|
-
raise HTTPException(status_code=500, detail=f"Failed to delete budget: {
|
|
298
|
+
raise HTTPException(status_code=500, detail=f"Failed to delete budget: {e!s}")
|
|
300
299
|
|
|
301
300
|
# Endpoint 6: Get budget progress
|
|
302
301
|
@router.get(
|
|
@@ -328,7 +327,7 @@ def add_budgets(
|
|
|
328
327
|
except ValueError as e:
|
|
329
328
|
raise HTTPException(status_code=404, detail=str(e))
|
|
330
329
|
except Exception as e:
|
|
331
|
-
raise HTTPException(status_code=500, detail=f"Failed to get budget progress: {
|
|
330
|
+
raise HTTPException(status_code=500, detail=f"Failed to get budget progress: {e!s}")
|
|
332
331
|
|
|
333
332
|
# Endpoint 7: List templates
|
|
334
333
|
@router.get("/templates/list", response_model=dict, summary="List Budget Templates")
|
|
@@ -402,7 +401,7 @@ def add_budgets(
|
|
|
402
401
|
raise HTTPException(status_code=400, detail=str(e))
|
|
403
402
|
except Exception as e:
|
|
404
403
|
raise HTTPException(
|
|
405
|
-
status_code=500, detail=f"Failed to create budget from template: {
|
|
404
|
+
status_code=500, detail=f"Failed to create budget from template: {e!s}"
|
|
406
405
|
)
|
|
407
406
|
|
|
408
407
|
# Mount router
|
fin_infra/cashflows/__init__.py
CHANGED
|
@@ -29,7 +29,7 @@ import numpy_financial as npf
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from fastapi import FastAPI
|
|
31
31
|
|
|
32
|
-
from .core import
|
|
32
|
+
from .core import irr, npv
|
|
33
33
|
|
|
34
34
|
__all__ = ["npv", "irr", "pmt", "fv", "pv", "add_cashflows"]
|
|
35
35
|
|
|
@@ -173,10 +173,10 @@ def add_cashflows(
|
|
|
173
173
|
- Scoped docs at {prefix}/docs
|
|
174
174
|
"""
|
|
175
175
|
from pydantic import BaseModel, Field
|
|
176
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
176
177
|
|
|
177
178
|
# Import svc-infra public router (no auth - utility calculations)
|
|
178
179
|
from svc_infra.api.fastapi.dual.public import public_router
|
|
179
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
180
180
|
|
|
181
181
|
# Request/Response models
|
|
182
182
|
class NPVRequest(BaseModel):
|
fin_infra/categorization/add.py
CHANGED
|
@@ -6,10 +6,10 @@ Uses svc-infra dual routers for consistent behavior.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import time
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
from fastapi import FastAPI, HTTPException
|
|
12
11
|
|
|
12
|
+
from . import rules
|
|
13
13
|
from .ease import easy_categorization
|
|
14
14
|
from .engine import CategorizationEngine
|
|
15
15
|
from .models import (
|
|
@@ -18,7 +18,6 @@ from .models import (
|
|
|
18
18
|
CategoryStats,
|
|
19
19
|
)
|
|
20
20
|
from .taxonomy import CategoryGroup, count_categories, get_all_categories
|
|
21
|
-
from . import rules
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
def add_categorization(
|
|
@@ -122,7 +121,7 @@ def add_categorization(
|
|
|
122
121
|
raise HTTPException(status_code=400, detail=str(e))
|
|
123
122
|
|
|
124
123
|
@router.get("/categories")
|
|
125
|
-
async def list_categories(group:
|
|
124
|
+
async def list_categories(group: CategoryGroup | None = None):
|
|
126
125
|
"""
|
|
127
126
|
List all available categories.
|
|
128
127
|
|
|
@@ -10,8 +10,8 @@ Hybrid categorization engine (exact → regex → ML → LLM).
|
|
|
10
10
|
Expected overall accuracy: 95-97% (V2 with LLM)
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import time
|
|
14
13
|
import logging
|
|
14
|
+
import time
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Optional
|
|
17
17
|
|
|
@@ -51,7 +51,7 @@ class CategorizationEngine:
|
|
|
51
51
|
enable_ml: bool = False,
|
|
52
52
|
enable_llm: bool = False,
|
|
53
53
|
confidence_threshold: float = 0.6,
|
|
54
|
-
model_path:
|
|
54
|
+
model_path: Path | None = None,
|
|
55
55
|
llm_categorizer: Optional["LLMCategorizer"] = None,
|
|
56
56
|
):
|
|
57
57
|
self.enable_ml = enable_ml
|
|
@@ -81,7 +81,7 @@ class CategorizationEngine:
|
|
|
81
81
|
async def categorize(
|
|
82
82
|
self,
|
|
83
83
|
merchant_name: str,
|
|
84
|
-
user_id:
|
|
84
|
+
user_id: str | None = None,
|
|
85
85
|
include_alternatives: bool = False,
|
|
86
86
|
) -> CategoryPrediction:
|
|
87
87
|
"""
|
|
@@ -195,7 +195,7 @@ class CategorizationEngine:
|
|
|
195
195
|
|
|
196
196
|
def _predict_ml(
|
|
197
197
|
self, merchant_name: str, include_alternatives: bool = False
|
|
198
|
-
) ->
|
|
198
|
+
) -> CategoryPrediction | None:
|
|
199
199
|
"""
|
|
200
200
|
Predict category using ML model.
|
|
201
201
|
|
|
@@ -325,7 +325,7 @@ class CategorizationEngine:
|
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
# Singleton instance (for easy access)
|
|
328
|
-
_default_engine:
|
|
328
|
+
_default_engine: CategorizationEngine | None = None
|
|
329
329
|
|
|
330
330
|
|
|
331
331
|
def get_engine() -> CategorizationEngine:
|
|
@@ -338,7 +338,7 @@ def get_engine() -> CategorizationEngine:
|
|
|
338
338
|
|
|
339
339
|
async def categorize(
|
|
340
340
|
merchant_name: str,
|
|
341
|
-
user_id:
|
|
341
|
+
user_id: str | None = None,
|
|
342
342
|
include_alternatives: bool = False,
|
|
343
343
|
) -> CategoryPrediction:
|
|
344
344
|
"""
|
|
@@ -15,7 +15,8 @@ Expected performance:
|
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import logging
|
|
18
|
-
from typing import Any,
|
|
18
|
+
from typing import Any, cast
|
|
19
|
+
|
|
19
20
|
from pydantic import BaseModel, Field
|
|
20
21
|
|
|
21
22
|
# ai-infra imports
|
|
@@ -157,7 +158,7 @@ class LLMCategorizer:
|
|
|
157
158
|
async def categorize(
|
|
158
159
|
self,
|
|
159
160
|
merchant_name: str,
|
|
160
|
-
user_id:
|
|
161
|
+
user_id: str | None = None,
|
|
161
162
|
) -> CategoryPrediction:
|
|
162
163
|
"""
|
|
163
164
|
Categorize merchant using LLM.
|
|
@@ -209,7 +210,7 @@ class LLMCategorizer:
|
|
|
209
210
|
async def _call_llm(
|
|
210
211
|
self,
|
|
211
212
|
merchant_name: str,
|
|
212
|
-
user_id:
|
|
213
|
+
user_id: str | None = None,
|
|
213
214
|
) -> CategoryPrediction:
|
|
214
215
|
"""Call LLM API with structured output."""
|
|
215
216
|
# Build user message
|
|
@@ -244,7 +245,7 @@ class LLMCategorizer:
|
|
|
244
245
|
f"Must be one of {len(valid_categories)} valid categories."
|
|
245
246
|
)
|
|
246
247
|
|
|
247
|
-
return cast(CategoryPrediction, response)
|
|
248
|
+
return cast("CategoryPrediction", response)
|
|
248
249
|
|
|
249
250
|
def _build_system_prompt(self) -> str:
|
|
250
251
|
"""Build system prompt with few-shot examples (reused across all requests)."""
|
|
@@ -269,7 +270,7 @@ class LLMCategorizer:
|
|
|
269
270
|
def _build_user_message(
|
|
270
271
|
self,
|
|
271
272
|
merchant_name: str,
|
|
272
|
-
user_id:
|
|
273
|
+
user_id: str | None = None,
|
|
273
274
|
) -> str:
|
|
274
275
|
"""Build user message with optional personalization."""
|
|
275
276
|
if self.enable_personalization and user_id:
|
|
@@ -6,12 +6,10 @@ Organized by category for maintainability.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
from .models import CategoryRule
|
|
12
11
|
from .taxonomy import Category
|
|
13
12
|
|
|
14
|
-
|
|
15
13
|
# ===== HELPER FUNCTIONS (defined first) =====
|
|
16
14
|
|
|
17
15
|
|
|
@@ -306,7 +304,7 @@ COMPILED_REGEX_RULES = [
|
|
|
306
304
|
# ===== PUBLIC FUNCTIONS =====
|
|
307
305
|
|
|
308
306
|
|
|
309
|
-
def get_exact_match(merchant: str) ->
|
|
307
|
+
def get_exact_match(merchant: str) -> Category | None:
|
|
310
308
|
"""
|
|
311
309
|
Get category by exact match.
|
|
312
310
|
|
|
@@ -320,7 +318,7 @@ def get_exact_match(merchant: str) -> Optional[Category]:
|
|
|
320
318
|
return EXACT_RULES_NORMALIZED.get(normalized)
|
|
321
319
|
|
|
322
320
|
|
|
323
|
-
def get_regex_match(merchant: str) ->
|
|
321
|
+
def get_regex_match(merchant: str) -> tuple[Category, int] | None:
|
|
324
322
|
"""
|
|
325
323
|
Get category by regex match.
|
|
326
324
|
|
|
@@ -12,7 +12,7 @@ Total: 56 leaf categories
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from enum import Enum
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
from pydantic import BaseModel
|
|
17
17
|
|
|
18
18
|
|
|
@@ -315,7 +315,7 @@ def get_category_group(category: Category) -> CategoryGroup:
|
|
|
315
315
|
return CATEGORY_GROUPS.get(category, CategoryGroup.UNCATEGORIZED)
|
|
316
316
|
|
|
317
317
|
|
|
318
|
-
def get_category_metadata(category: Category) ->
|
|
318
|
+
def get_category_metadata(category: Category) -> CategoryMetadata | None:
|
|
319
319
|
"""Get metadata for a category."""
|
|
320
320
|
return CATEGORY_METADATA.get(category)
|
|
321
321
|
|
fin_infra/chat/__init__.py
CHANGED
|
@@ -33,15 +33,15 @@ from typing import TYPE_CHECKING, Any
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from fastapi import FastAPI
|
|
35
35
|
|
|
36
|
+
from fin_infra.chat.ease import easy_financial_conversation
|
|
36
37
|
from fin_infra.chat.planning import (
|
|
37
|
-
|
|
38
|
-
ConversationResponse,
|
|
38
|
+
SENSITIVE_PATTERNS,
|
|
39
39
|
ConversationContext,
|
|
40
|
+
ConversationResponse,
|
|
40
41
|
Exchange,
|
|
42
|
+
FinancialPlanningConversation,
|
|
41
43
|
is_sensitive_question,
|
|
42
|
-
SENSITIVE_PATTERNS,
|
|
43
44
|
)
|
|
44
|
-
from fin_infra.chat.ease import easy_financial_conversation
|
|
45
45
|
|
|
46
46
|
__all__ = [
|
|
47
47
|
"FinancialPlanningConversation",
|
|
@@ -116,10 +116,10 @@ def add_financial_conversation(
|
|
|
116
116
|
- Logs all LLM calls for compliance (via svc-infra logging)
|
|
117
117
|
"""
|
|
118
118
|
from pydantic import BaseModel, Field
|
|
119
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
119
120
|
|
|
120
121
|
# Import svc-infra user router (requires auth)
|
|
121
122
|
from svc_infra.api.fastapi.dual.protected import user_router
|
|
122
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
123
123
|
|
|
124
124
|
# Auto-create conversation if not provided
|
|
125
125
|
if conversation is None:
|
fin_infra/chat/planning.py
CHANGED
|
@@ -54,7 +54,6 @@ from typing import Any
|
|
|
54
54
|
|
|
55
55
|
from pydantic import BaseModel, Field
|
|
56
56
|
|
|
57
|
-
|
|
58
57
|
# ============================================================================
|
|
59
58
|
# Pydantic Schemas (Structured Output)
|
|
60
59
|
# ============================================================================
|
|
@@ -12,7 +12,6 @@ Usage:
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Optional
|
|
16
15
|
|
|
17
16
|
import click
|
|
18
17
|
import typer
|
|
@@ -54,47 +53,47 @@ def cmd_scaffold(
|
|
|
54
53
|
"--overwrite/--no-overwrite",
|
|
55
54
|
help="Overwrite existing files",
|
|
56
55
|
),
|
|
57
|
-
models_filename:
|
|
56
|
+
models_filename: str | None = typer.Option(
|
|
58
57
|
None,
|
|
59
58
|
"--models-filename",
|
|
60
59
|
help="Custom filename for models (default: {domain}.py)",
|
|
61
60
|
),
|
|
62
|
-
schemas_filename:
|
|
61
|
+
schemas_filename: str | None = typer.Option(
|
|
63
62
|
None,
|
|
64
63
|
"--schemas-filename",
|
|
65
64
|
help="Custom filename for schemas (default: {domain}_schemas.py)",
|
|
66
65
|
),
|
|
67
|
-
repository_filename:
|
|
66
|
+
repository_filename: str | None = typer.Option(
|
|
68
67
|
None,
|
|
69
68
|
"--repository-filename",
|
|
70
69
|
help="Custom filename for repository (default: {domain}_repository.py)",
|
|
71
70
|
),
|
|
72
71
|
) -> None:
|
|
73
72
|
"""Generate SQLAlchemy models, Pydantic schemas, and repository code from templates.
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
The scaffold command generates production-ready persistence layer code that works
|
|
76
75
|
seamlessly with svc-infra's add_sql_resources() for automatic CRUD APIs.
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
Examples:
|
|
79
78
|
# Basic scaffold (models + schemas + repository)
|
|
80
79
|
fin-infra scaffold budgets --dest-dir app/models/
|
|
81
|
-
|
|
80
|
+
|
|
82
81
|
# Financial goals tracking
|
|
83
82
|
fin-infra scaffold goals --dest-dir app/models/goals/
|
|
84
|
-
|
|
83
|
+
|
|
85
84
|
# With multi-tenancy and soft deletes
|
|
86
85
|
fin-infra scaffold budgets --dest-dir app/models/ \
|
|
87
86
|
--include-tenant --include-soft-delete
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# Without repository (use svc-infra SqlRepository directly)
|
|
90
89
|
fin-infra scaffold goals --dest-dir app/models/ \\
|
|
91
90
|
--no-with-repository
|
|
92
|
-
|
|
91
|
+
|
|
93
92
|
# Custom filenames
|
|
94
93
|
fin-infra scaffold budgets --dest-dir app/models/ \\
|
|
95
94
|
--models-filename custom_budget.py \\
|
|
96
95
|
--schemas-filename custom_schemas.py
|
|
97
|
-
|
|
96
|
+
|
|
98
97
|
After scaffolding, integrate with svc-infra:
|
|
99
98
|
1. Run migrations: svc-infra revision -m "add budgets" --autogenerate
|
|
100
99
|
2. Apply: svc-infra upgrade head
|
fin_infra/clients/plaid.py
CHANGED
fin_infra/compliance/__init__.py
CHANGED
|
@@ -20,9 +20,9 @@ Example:
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
|
-
from datetime import datetime
|
|
24
|
-
from typing import Any, TYPE_CHECKING, cast
|
|
25
23
|
from collections.abc import Callable
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from fastapi import FastAPI, Request, Response
|
|
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def log_compliance_event(
|
|
36
|
-
app:
|
|
36
|
+
app: FastAPI,
|
|
37
37
|
event: str,
|
|
38
38
|
context: dict[str, Any] | None = None,
|
|
39
39
|
) -> None:
|
|
@@ -63,7 +63,7 @@ def log_compliance_event(
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def add_compliance_tracking(
|
|
66
|
-
app:
|
|
66
|
+
app: FastAPI,
|
|
67
67
|
*,
|
|
68
68
|
track_banking: bool = True,
|
|
69
69
|
track_credit: bool = True,
|
|
@@ -112,7 +112,7 @@ def add_compliance_tracking(
|
|
|
112
112
|
"""
|
|
113
113
|
|
|
114
114
|
@app.middleware("http")
|
|
115
|
-
async def compliance_tracking_middleware(request:
|
|
115
|
+
async def compliance_tracking_middleware(request: Request, call_next: Callable) -> Response:
|
|
116
116
|
"""Middleware to track compliance events for financial endpoints."""
|
|
117
117
|
path = request.url.path
|
|
118
118
|
method = request.method
|