fin-infra 0.1.62__py3-none-any.whl → 0.1.82__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/__init__.py +53 -3
- fin_infra/analytics/__init__.py +13 -2
- fin_infra/analytics/add.py +30 -32
- fin_infra/analytics/cash_flow.py +6 -5
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +19 -26
- fin_infra/analytics/projections.py +1 -3
- fin_infra/analytics/rebalancing.py +2 -4
- fin_infra/analytics/savings.py +1 -1
- fin_infra/analytics/spending.py +15 -11
- fin_infra/banking/__init__.py +33 -31
- fin_infra/banking/history.py +11 -12
- fin_infra/banking/utils.py +116 -110
- fin_infra/brokerage/__init__.py +27 -27
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/budgets/alerts.py +3 -3
- fin_infra/budgets/tracker.py +4 -5
- fin_infra/cashflows/__init__.py +8 -10
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +17 -19
- fin_infra/categorization/ease.py +3 -4
- fin_infra/categorization/engine.py +21 -18
- fin_infra/categorization/llm_layer.py +10 -10
- fin_infra/categorization/models.py +1 -1
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +13 -22
- fin_infra/chat/planning.py +57 -1
- fin_infra/cli/cmds/scaffold_cmds.py +11 -12
- fin_infra/clients/__init__.py +23 -1
- fin_infra/clients/base.py +1 -1
- fin_infra/clients/plaid.py +2 -2
- fin_infra/compliance/__init__.py +7 -6
- fin_infra/credit/add.py +7 -7
- fin_infra/credit/experian/auth.py +3 -2
- fin_infra/credit/experian/client.py +2 -2
- fin_infra/credit/experian/provider.py +19 -19
- fin_infra/crypto/__init__.py +8 -10
- fin_infra/crypto/insights.py +5 -6
- fin_infra/documents/add.py +11 -13
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +18 -17
- fin_infra/documents/models.py +7 -7
- fin_infra/documents/ocr.py +8 -8
- fin_infra/documents/storage.py +23 -14
- fin_infra/exceptions.py +1 -2
- fin_infra/goals/__init__.py +8 -8
- fin_infra/goals/add.py +36 -36
- fin_infra/goals/funding.py +4 -6
- fin_infra/goals/management.py +6 -7
- fin_infra/goals/milestones.py +2 -3
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +12 -10
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +14 -9
- fin_infra/investments/add.py +53 -73
- fin_infra/investments/ease.py +16 -13
- fin_infra/investments/models.py +135 -69
- fin_infra/investments/providers/base.py +9 -15
- fin_infra/investments/providers/plaid.py +70 -55
- fin_infra/investments/providers/snaptrade.py +35 -53
- fin_infra/markets/__init__.py +16 -11
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/accounts.py +2 -1
- 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 +4 -4
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +9 -6
- fin_infra/net_worth/calculator.py +8 -6
- fin_infra/net_worth/ease.py +36 -15
- fin_infra/net_worth/insights.py +4 -5
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +17 -15
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/obs/classifier.py +3 -3
- fin_infra/providers/banking/plaid_client.py +23 -22
- fin_infra/providers/banking/teller_client.py +14 -7
- fin_infra/providers/base.py +131 -14
- fin_infra/providers/brokerage/alpaca.py +7 -7
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +25 -4
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +23 -8
- fin_infra/providers/tax/__init__.py +1 -1
- fin_infra/providers/tax/irs.py +1 -1
- fin_infra/providers/tax/mock.py +8 -8
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +24 -12
- fin_infra/recurring/detector.py +8 -8
- fin_infra/recurring/detectors_llm.py +14 -13
- fin_infra/recurring/ease.py +3 -5
- fin_infra/recurring/insights.py +20 -19
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizer.py +3 -2
- fin_infra/recurring/normalizers.py +11 -10
- fin_infra/recurring/summary.py +13 -15
- fin_infra/scaffold/__init__.py +1 -1
- fin_infra/scaffold/budgets.py +9 -9
- fin_infra/scaffold/goals.py +5 -5
- 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/security/pii_patterns.py +1 -1
- fin_infra/security/token_store.py +3 -1
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +2 -2
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +5 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.62.dist-info/RECORD +0 -180
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
fin_infra/documents/storage.py
CHANGED
|
@@ -36,16 +36,25 @@ Quick Start:
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
-
from typing import TYPE_CHECKING
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
42
|
from svc_infra.documents import (
|
|
43
43
|
delete_document as base_delete_document,
|
|
44
|
+
)
|
|
45
|
+
from svc_infra.documents import (
|
|
44
46
|
download_document as base_download_document,
|
|
47
|
+
)
|
|
48
|
+
from svc_infra.documents import (
|
|
45
49
|
get_document as base_get_document,
|
|
50
|
+
)
|
|
51
|
+
from svc_infra.documents import (
|
|
46
52
|
list_documents as base_list_documents,
|
|
53
|
+
)
|
|
54
|
+
from svc_infra.documents import (
|
|
47
55
|
upload_document as base_upload_document,
|
|
48
56
|
)
|
|
57
|
+
|
|
49
58
|
HAS_SVC_INFRA_DOCUMENTS = True
|
|
50
59
|
except ImportError:
|
|
51
60
|
# Fallback for older svc-infra versions - use legacy implementation
|
|
@@ -63,15 +72,15 @@ if TYPE_CHECKING:
|
|
|
63
72
|
|
|
64
73
|
|
|
65
74
|
async def upload_document(
|
|
66
|
-
storage:
|
|
75
|
+
storage: StorageBackend,
|
|
67
76
|
user_id: str,
|
|
68
77
|
file: bytes,
|
|
69
|
-
document_type:
|
|
78
|
+
document_type: DocumentType,
|
|
70
79
|
filename: str,
|
|
71
|
-
metadata:
|
|
72
|
-
tax_year:
|
|
73
|
-
form_type:
|
|
74
|
-
) ->
|
|
80
|
+
metadata: dict | None = None,
|
|
81
|
+
tax_year: int | None = None,
|
|
82
|
+
form_type: str | None = None,
|
|
83
|
+
) -> FinancialDocument:
|
|
75
84
|
"""
|
|
76
85
|
Upload a financial document (delegates to svc-infra, adds financial fields).
|
|
77
86
|
|
|
@@ -139,7 +148,7 @@ async def upload_document(
|
|
|
139
148
|
return financial_doc
|
|
140
149
|
|
|
141
150
|
|
|
142
|
-
def get_document(document_id: str) ->
|
|
151
|
+
def get_document(document_id: str) -> FinancialDocument | None:
|
|
143
152
|
"""
|
|
144
153
|
Get financial document metadata by ID (delegates to svc-infra).
|
|
145
154
|
|
|
@@ -153,7 +162,7 @@ def get_document(document_id: str) -> Optional["FinancialDocument"]:
|
|
|
153
162
|
>>> doc = get_document("doc_abc123")
|
|
154
163
|
>>> if doc:
|
|
155
164
|
... print(doc.filename, doc.type, doc.tax_year)
|
|
156
|
-
|
|
165
|
+
|
|
157
166
|
Notes:
|
|
158
167
|
- Delegates to svc-infra.documents.get_document
|
|
159
168
|
- Converts base Document to FinancialDocument
|
|
@@ -186,7 +195,7 @@ def get_document(document_id: str) -> Optional["FinancialDocument"]:
|
|
|
186
195
|
return financial_doc
|
|
187
196
|
|
|
188
197
|
|
|
189
|
-
async def download_document(storage:
|
|
198
|
+
async def download_document(storage: StorageBackend, document_id: str) -> bytes:
|
|
190
199
|
"""
|
|
191
200
|
Download a financial document by ID (delegates to svc-infra).
|
|
192
201
|
|
|
@@ -212,7 +221,7 @@ async def download_document(storage: "StorageBackend", document_id: str) -> byte
|
|
|
212
221
|
return await base_download_document(storage=storage, document_id=document_id)
|
|
213
222
|
|
|
214
223
|
|
|
215
|
-
async def delete_document(storage:
|
|
224
|
+
async def delete_document(storage: StorageBackend, document_id: str) -> bool:
|
|
216
225
|
"""
|
|
217
226
|
Delete a financial document and its metadata (delegates to svc-infra).
|
|
218
227
|
|
|
@@ -237,11 +246,11 @@ async def delete_document(storage: "StorageBackend", document_id: str) -> bool:
|
|
|
237
246
|
|
|
238
247
|
def list_documents(
|
|
239
248
|
user_id: str,
|
|
240
|
-
document_type:
|
|
241
|
-
tax_year:
|
|
249
|
+
document_type: DocumentType | None = None,
|
|
250
|
+
tax_year: int | None = None,
|
|
242
251
|
limit: int = 100,
|
|
243
252
|
offset: int = 0,
|
|
244
|
-
) ->
|
|
253
|
+
) -> list[FinancialDocument]:
|
|
245
254
|
"""
|
|
246
255
|
List user's financial documents with optional filters (delegates to svc-infra).
|
|
247
256
|
|
fin_infra/exceptions.py
CHANGED
|
@@ -6,7 +6,7 @@ This module provides a consistent exception hierarchy across all fin-infra compo
|
|
|
6
6
|
- Validation errors (data validation, compliance)
|
|
7
7
|
- Calculation errors (financial calculations)
|
|
8
8
|
|
|
9
|
-
All exceptions inherit from FinInfraError, allowing users to catch all library
|
|
9
|
+
All exceptions inherit from FinInfraError, allowing users to catch all library
|
|
10
10
|
errors with a single except clause.
|
|
11
11
|
|
|
12
12
|
Example:
|
|
@@ -23,7 +23,6 @@ from __future__ import annotations
|
|
|
23
23
|
import logging
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
# =============================================================================
|
|
28
27
|
# Logging Helper
|
|
29
28
|
# =============================================================================
|
fin_infra/goals/__init__.py
CHANGED
|
@@ -5,6 +5,14 @@ Provides comprehensive goal management with milestone tracking,
|
|
|
5
5
|
funding allocation, and progress monitoring.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from fin_infra.goals.add import add_goals
|
|
9
|
+
from fin_infra.goals.funding import (
|
|
10
|
+
get_account_allocations,
|
|
11
|
+
get_goal_funding_sources,
|
|
12
|
+
link_account_to_goal,
|
|
13
|
+
remove_account_from_goal,
|
|
14
|
+
update_account_allocation,
|
|
15
|
+
)
|
|
8
16
|
from fin_infra.goals.management import (
|
|
9
17
|
FinancialGoalTracker,
|
|
10
18
|
GoalProgressReport,
|
|
@@ -28,14 +36,6 @@ from fin_infra.goals.milestones import (
|
|
|
28
36
|
get_next_milestone,
|
|
29
37
|
trigger_milestone_notification,
|
|
30
38
|
)
|
|
31
|
-
from fin_infra.goals.funding import (
|
|
32
|
-
link_account_to_goal,
|
|
33
|
-
get_goal_funding_sources,
|
|
34
|
-
get_account_allocations,
|
|
35
|
-
update_account_allocation,
|
|
36
|
-
remove_account_from_goal,
|
|
37
|
-
)
|
|
38
|
-
from fin_infra.goals.add import add_goals
|
|
39
39
|
from fin_infra.goals.models import (
|
|
40
40
|
FundingSource,
|
|
41
41
|
Goal,
|
fin_infra/goals/add.py
CHANGED
|
@@ -29,30 +29,30 @@ add_goals(app)
|
|
|
29
29
|
|
|
30
30
|
import logging
|
|
31
31
|
from datetime import datetime
|
|
32
|
-
from typing import
|
|
32
|
+
from typing import Any, cast
|
|
33
33
|
|
|
34
|
-
from fastapi import FastAPI, HTTPException,
|
|
34
|
+
from fastapi import Body, FastAPI, HTTPException, Query, status
|
|
35
35
|
from pydantic import BaseModel, Field
|
|
36
36
|
|
|
37
|
+
from fin_infra.goals.funding import (
|
|
38
|
+
get_goal_funding_sources,
|
|
39
|
+
link_account_to_goal,
|
|
40
|
+
remove_account_from_goal,
|
|
41
|
+
update_account_allocation,
|
|
42
|
+
)
|
|
37
43
|
from fin_infra.goals.management import (
|
|
38
44
|
create_goal,
|
|
39
|
-
list_goals,
|
|
40
|
-
get_goal,
|
|
41
|
-
update_goal,
|
|
42
45
|
delete_goal,
|
|
46
|
+
get_goal,
|
|
43
47
|
get_goal_progress,
|
|
48
|
+
list_goals,
|
|
49
|
+
update_goal,
|
|
44
50
|
)
|
|
45
51
|
from fin_infra.goals.milestones import (
|
|
46
52
|
add_milestone,
|
|
47
53
|
check_milestones,
|
|
48
54
|
get_milestone_progress,
|
|
49
55
|
)
|
|
50
|
-
from fin_infra.goals.funding import (
|
|
51
|
-
link_account_to_goal,
|
|
52
|
-
get_goal_funding_sources,
|
|
53
|
-
update_account_allocation,
|
|
54
|
-
remove_account_from_goal,
|
|
55
|
-
)
|
|
56
56
|
from fin_infra.goals.models import GoalStatus
|
|
57
57
|
|
|
58
58
|
logger = logging.getLogger(__name__)
|
|
@@ -63,7 +63,7 @@ logger = logging.getLogger(__name__)
|
|
|
63
63
|
# ============================================================================
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def parse_iso_date(date_str:
|
|
66
|
+
def parse_iso_date(date_str: str | None) -> datetime | None:
|
|
67
67
|
"""Parse ISO date string to datetime object."""
|
|
68
68
|
if date_str is None:
|
|
69
69
|
return None
|
|
@@ -85,24 +85,24 @@ class CreateGoalRequest(BaseModel):
|
|
|
85
85
|
name: str = Field(..., min_length=1, max_length=200, description="Goal name")
|
|
86
86
|
goal_type: str = Field(..., description="Goal type (savings, debt, investment, etc.)")
|
|
87
87
|
target_amount: float = Field(..., gt=0, description="Target amount")
|
|
88
|
-
deadline:
|
|
89
|
-
description:
|
|
90
|
-
current_amount:
|
|
91
|
-
auto_contribute:
|
|
92
|
-
tags:
|
|
88
|
+
deadline: str | None = Field(None, description="Deadline (ISO date)")
|
|
89
|
+
description: str | None = Field(None, description="Goal description")
|
|
90
|
+
current_amount: float | None = Field(0.0, ge=0, description="Current amount")
|
|
91
|
+
auto_contribute: bool | None = Field(False, description="Auto-contribute enabled")
|
|
92
|
+
tags: list[str] | None = Field(None, description="Goal tags")
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class UpdateGoalRequest(BaseModel):
|
|
96
96
|
"""Request body for updating a goal."""
|
|
97
97
|
|
|
98
|
-
name:
|
|
99
|
-
target_amount:
|
|
100
|
-
deadline:
|
|
101
|
-
description:
|
|
102
|
-
current_amount:
|
|
103
|
-
status:
|
|
104
|
-
auto_contribute:
|
|
105
|
-
tags:
|
|
98
|
+
name: str | None = Field(None, min_length=1, max_length=200)
|
|
99
|
+
target_amount: float | None = Field(None, gt=0)
|
|
100
|
+
deadline: str | None = None
|
|
101
|
+
description: str | None = None
|
|
102
|
+
current_amount: float | None = Field(None, ge=0)
|
|
103
|
+
status: GoalStatus | None = None
|
|
104
|
+
auto_contribute: bool | None = None
|
|
105
|
+
tags: list[str] | None = None
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
class AddMilestoneRequest(BaseModel):
|
|
@@ -110,7 +110,7 @@ class AddMilestoneRequest(BaseModel):
|
|
|
110
110
|
|
|
111
111
|
amount: float = Field(..., gt=0, description="Milestone amount")
|
|
112
112
|
description: str = Field(..., min_length=1, description="Milestone description")
|
|
113
|
-
target_date:
|
|
113
|
+
target_date: str | None = Field(None, description="Target date (ISO date)")
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
class LinkAccountRequest(BaseModel):
|
|
@@ -238,14 +238,14 @@ def add_goals(
|
|
|
238
238
|
except ValueError as e:
|
|
239
239
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
240
240
|
|
|
241
|
-
@router.get("", response_model=
|
|
241
|
+
@router.get("", response_model=list[dict])
|
|
242
242
|
async def list_goals_endpoint(
|
|
243
|
-
user_id:
|
|
243
|
+
user_id: str | None = Query(
|
|
244
244
|
None, description="User identifier (optional, returns all if not provided)"
|
|
245
245
|
),
|
|
246
|
-
goal_type:
|
|
247
|
-
status_filter:
|
|
248
|
-
) ->
|
|
246
|
+
goal_type: str | None = Query(None, description="Filter by goal type"),
|
|
247
|
+
status_filter: str | None = Query(None, alias="status", description="Filter by status"),
|
|
248
|
+
) -> list[dict]:
|
|
249
249
|
"""
|
|
250
250
|
List all goals for a user with optional filters.
|
|
251
251
|
|
|
@@ -442,8 +442,8 @@ def add_goals(
|
|
|
442
442
|
except ValueError as e:
|
|
443
443
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
444
444
|
|
|
445
|
-
@router.get("/{goal_id}/milestones", response_model=
|
|
446
|
-
async def list_milestones_endpoint(goal_id: str) ->
|
|
445
|
+
@router.get("/{goal_id}/milestones", response_model=list[dict])
|
|
446
|
+
async def list_milestones_endpoint(goal_id: str) -> list[dict]:
|
|
447
447
|
"""
|
|
448
448
|
List all milestones for a goal.
|
|
449
449
|
|
|
@@ -469,7 +469,7 @@ def add_goals(
|
|
|
469
469
|
# Get all milestones from the goal (check_milestones only returns newly reached ones)
|
|
470
470
|
goal = get_goal(goal_id)
|
|
471
471
|
milestones = goal.get("milestones", [])
|
|
472
|
-
return milestones
|
|
472
|
+
return cast("list[dict[Any, Any]]", milestones)
|
|
473
473
|
except KeyError:
|
|
474
474
|
raise HTTPException(
|
|
475
475
|
status_code=status.HTTP_404_NOT_FOUND, detail=f"Goal {goal_id} not found"
|
|
@@ -540,8 +540,8 @@ def add_goals(
|
|
|
540
540
|
except ValueError as e:
|
|
541
541
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
542
542
|
|
|
543
|
-
@router.get("/{goal_id}/funding", response_model=
|
|
544
|
-
async def list_funding_sources_endpoint(goal_id: str) ->
|
|
543
|
+
@router.get("/{goal_id}/funding", response_model=list[dict])
|
|
544
|
+
async def list_funding_sources_endpoint(goal_id: str) -> list[dict]:
|
|
545
545
|
"""
|
|
546
546
|
List all funding sources for a goal.
|
|
547
547
|
|
fin_infra/goals/funding.py
CHANGED
|
@@ -22,14 +22,12 @@ Example:
|
|
|
22
22
|
>>> # Raises ValueError if total allocation > 100%
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
from typing import Dict, List
|
|
26
|
-
|
|
27
|
-
from fin_infra.goals.models import FundingSource
|
|
28
25
|
from fin_infra.goals.management import get_goal
|
|
26
|
+
from fin_infra.goals.models import FundingSource
|
|
29
27
|
|
|
30
28
|
# In-memory storage for funding allocations
|
|
31
29
|
# Structure: {account_id: {goal_id: allocation_percent}}
|
|
32
|
-
_FUNDING_STORE:
|
|
30
|
+
_FUNDING_STORE: dict[str, dict[str, float]] = {}
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
def link_account_to_goal(
|
|
@@ -108,7 +106,7 @@ def link_account_to_goal(
|
|
|
108
106
|
)
|
|
109
107
|
|
|
110
108
|
|
|
111
|
-
def get_goal_funding_sources(goal_id: str) ->
|
|
109
|
+
def get_goal_funding_sources(goal_id: str) -> list[FundingSource]:
|
|
112
110
|
"""
|
|
113
111
|
Get all accounts funding a specific goal.
|
|
114
112
|
|
|
@@ -154,7 +152,7 @@ def get_goal_funding_sources(goal_id: str) -> List[FundingSource]:
|
|
|
154
152
|
return funding_sources
|
|
155
153
|
|
|
156
154
|
|
|
157
|
-
def get_account_allocations(account_id: str) ->
|
|
155
|
+
def get_account_allocations(account_id: str) -> dict[str, float]:
|
|
158
156
|
"""
|
|
159
157
|
Get all goal allocations for a specific account.
|
|
160
158
|
|
fin_infra/goals/management.py
CHANGED
|
@@ -41,11 +41,10 @@ Example:
|
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
43
|
from datetime import datetime
|
|
44
|
-
from typing import Any
|
|
44
|
+
from typing import Any, cast
|
|
45
45
|
|
|
46
46
|
from pydantic import BaseModel, Field
|
|
47
47
|
|
|
48
|
-
|
|
49
48
|
# ============================================================================
|
|
50
49
|
# Pydantic Schemas (Structured Output)
|
|
51
50
|
# ============================================================================
|
|
@@ -597,10 +596,10 @@ Goal type: {goal_type}
|
|
|
597
596
|
Goal data: {goal}
|
|
598
597
|
|
|
599
598
|
CALCULATED VALUES (use these exactly, don't recalculate):
|
|
600
|
-
- Feasibility: {calc[
|
|
601
|
-
- Required monthly: ${calc[
|
|
599
|
+
- Feasibility: {calc["feasibility"]}
|
|
600
|
+
- Required monthly: ${calc["required_monthly"]:,.0f}
|
|
602
601
|
- Projected completion: {projected_date}
|
|
603
|
-
- Current progress: {calc[
|
|
602
|
+
- Current progress: {calc["current_progress"]:.1%}
|
|
604
603
|
|
|
605
604
|
Provide context and advice around these calculations. Suggest 2-3 alternative paths and 3-5 specific recommendations."""
|
|
606
605
|
|
|
@@ -839,7 +838,7 @@ def get_goal(goal_id: str) -> dict[str, Any]:
|
|
|
839
838
|
if goal_id not in _GOALS_STORE:
|
|
840
839
|
raise KeyError(f"Goal not found: {goal_id}")
|
|
841
840
|
|
|
842
|
-
return _GOALS_STORE[goal_id]
|
|
841
|
+
return cast("dict[str, Any]", _GOALS_STORE[goal_id])
|
|
843
842
|
|
|
844
843
|
|
|
845
844
|
def update_goal(
|
|
@@ -885,7 +884,7 @@ def update_goal(
|
|
|
885
884
|
|
|
886
885
|
Goal(**goal) # Will raise ValidationError if invalid
|
|
887
886
|
|
|
888
|
-
return goal
|
|
887
|
+
return cast("dict[str, Any]", goal)
|
|
889
888
|
|
|
890
889
|
|
|
891
890
|
def delete_goal(goal_id: str) -> None:
|
fin_infra/goals/milestones.py
CHANGED
|
@@ -26,12 +26,11 @@ Example:
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
from datetime import datetime
|
|
29
|
-
from typing import Any
|
|
29
|
+
from typing import Any, cast
|
|
30
30
|
|
|
31
31
|
from fin_infra.goals.management import get_goal, update_goal
|
|
32
32
|
from fin_infra.goals.models import Milestone
|
|
33
33
|
|
|
34
|
-
|
|
35
34
|
# ============================================================================
|
|
36
35
|
# Milestone Management
|
|
37
36
|
# ============================================================================
|
|
@@ -229,7 +228,7 @@ def get_next_milestone(goal_id: str) -> dict[str, Any] | None:
|
|
|
229
228
|
# Find first unreached milestone (sorted by amount)
|
|
230
229
|
for milestone in milestones:
|
|
231
230
|
if not milestone.get("reached", False):
|
|
232
|
-
return milestone
|
|
231
|
+
return cast("dict[str, Any]", milestone)
|
|
233
232
|
|
|
234
233
|
return None
|
|
235
234
|
|
fin_infra/goals/models.py
CHANGED
|
@@ -11,11 +11,9 @@ Provides comprehensive data models for:
|
|
|
11
11
|
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from enum import Enum
|
|
14
|
-
from typing import Optional
|
|
15
14
|
|
|
16
15
|
from pydantic import BaseModel, Field, field_validator
|
|
17
16
|
|
|
18
|
-
|
|
19
17
|
# ============================================================================
|
|
20
18
|
# Enums
|
|
21
19
|
# ============================================================================
|
|
@@ -69,14 +67,14 @@ class Milestone(BaseModel):
|
|
|
69
67
|
"""
|
|
70
68
|
|
|
71
69
|
amount: float = Field(..., description="Milestone target amount", gt=0)
|
|
72
|
-
target_date:
|
|
70
|
+
target_date: datetime | None = Field(
|
|
73
71
|
None, description="Target date to reach milestone (optional)"
|
|
74
72
|
)
|
|
75
73
|
description: str = Field(
|
|
76
74
|
..., description="Milestone description (e.g., '25% to emergency fund')", max_length=200
|
|
77
75
|
)
|
|
78
76
|
reached: bool = Field(default=False, description="Whether milestone has been reached")
|
|
79
|
-
reached_date:
|
|
77
|
+
reached_date: datetime | None = Field(
|
|
80
78
|
None, description="Date milestone was reached (if reached=True)"
|
|
81
79
|
)
|
|
82
80
|
|
|
@@ -107,7 +105,7 @@ class FundingSource(BaseModel):
|
|
|
107
105
|
ge=0.0,
|
|
108
106
|
le=100.0,
|
|
109
107
|
)
|
|
110
|
-
account_name:
|
|
108
|
+
account_name: str | None = Field(
|
|
111
109
|
None, description="Human-readable account name (e.g., 'Chase Savings')"
|
|
112
110
|
)
|
|
113
111
|
|
|
@@ -154,9 +152,7 @@ class Goal(BaseModel):
|
|
|
154
152
|
id: str = Field(..., description="Unique goal identifier")
|
|
155
153
|
user_id: str = Field(..., description="User who owns this goal")
|
|
156
154
|
name: str = Field(..., description="Goal name", max_length=200)
|
|
157
|
-
description:
|
|
158
|
-
None, description="Detailed goal description", max_length=1000
|
|
159
|
-
)
|
|
155
|
+
description: str | None = Field(None, description="Detailed goal description", max_length=1000)
|
|
160
156
|
|
|
161
157
|
# Goal type and status
|
|
162
158
|
type: GoalType = Field(..., description="Goal type")
|
|
@@ -165,7 +161,7 @@ class Goal(BaseModel):
|
|
|
165
161
|
# Financial targets
|
|
166
162
|
target_amount: float = Field(..., description="Target amount to achieve", gt=0)
|
|
167
163
|
current_amount: float = Field(default=0.0, description="Current progress toward target", ge=0.0)
|
|
168
|
-
deadline:
|
|
164
|
+
deadline: datetime | None = Field(None, description="Target completion date")
|
|
169
165
|
|
|
170
166
|
# Milestone tracking
|
|
171
167
|
milestones: list[Milestone] = Field(default_factory=list, description="Progress milestones")
|
|
@@ -190,7 +186,7 @@ class Goal(BaseModel):
|
|
|
190
186
|
updated_at: datetime = Field(
|
|
191
187
|
default_factory=datetime.utcnow, description="Last update timestamp"
|
|
192
188
|
)
|
|
193
|
-
completed_at:
|
|
189
|
+
completed_at: datetime | None = Field(
|
|
194
190
|
None, description="Completion timestamp (if status=COMPLETED)"
|
|
195
191
|
)
|
|
196
192
|
|
|
@@ -264,7 +260,7 @@ class GoalProgress(BaseModel):
|
|
|
264
260
|
)
|
|
265
261
|
|
|
266
262
|
# Projections
|
|
267
|
-
projected_completion_date:
|
|
263
|
+
projected_completion_date: datetime | None = Field(
|
|
268
264
|
None, description="Projected completion date at current pace"
|
|
269
265
|
)
|
|
270
266
|
on_track: bool = Field(..., description="Whether on track to meet deadline")
|
fin_infra/insights/__init__.py
CHANGED
|
@@ -10,8 +10,16 @@ Aggregates insights from multiple sources:
|
|
|
10
10
|
- Cash flow projections
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import logging
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
|
|
14
19
|
from .aggregator import aggregate_insights, get_user_insights
|
|
20
|
+
from .models import Insight, InsightCategory, InsightFeed, InsightPriority
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
15
23
|
|
|
16
24
|
__all__ = [
|
|
17
25
|
"Insight",
|
|
@@ -25,7 +33,7 @@ __all__ = [
|
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
def add_insights(
|
|
28
|
-
app: "FastAPI",
|
|
36
|
+
app: "FastAPI",
|
|
29
37
|
*,
|
|
30
38
|
prefix: str = "/insights",
|
|
31
39
|
) -> None:
|
|
@@ -71,16 +79,11 @@ def add_insights(
|
|
|
71
79
|
- Real-time aggregation from net worth, budgets, goals, etc.
|
|
72
80
|
- Notification system for critical insights
|
|
73
81
|
"""
|
|
74
|
-
from typing import TYPE_CHECKING
|
|
75
|
-
|
|
76
|
-
if TYPE_CHECKING:
|
|
77
|
-
from fastapi import FastAPI
|
|
78
|
-
|
|
79
82
|
from fastapi import Query
|
|
83
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
80
84
|
|
|
81
85
|
# Import svc-infra user router (requires auth)
|
|
82
86
|
from svc_infra.api.fastapi.dual.protected import user_router
|
|
83
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
84
87
|
|
|
85
88
|
# Create router
|
|
86
89
|
router = user_router(prefix=prefix, tags=["Insights"])
|
|
@@ -125,5 +128,4 @@ def add_insights(
|
|
|
125
128
|
# Mount router
|
|
126
129
|
app.include_router(router, include_in_schema=True)
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
logger.info("Insights feed enabled")
|
fin_infra/insights/aggregator.py
CHANGED
|
@@ -224,7 +224,7 @@ def _generate_recurring_insights(user_id: str, patterns: list[RecurringPattern])
|
|
|
224
224
|
high_cost = [
|
|
225
225
|
p
|
|
226
226
|
for p in patterns
|
|
227
|
-
if p.amount is not None and p.amount > 50 or (p.amount_range and p.amount_range[1] > 50)
|
|
227
|
+
if (p.amount is not None and p.amount > 50) or (p.amount_range and p.amount_range[1] > 50)
|
|
228
228
|
]
|
|
229
229
|
if high_cost:
|
|
230
230
|
total = Decimal("0")
|
|
@@ -19,7 +19,7 @@ Example usage:
|
|
|
19
19
|
# Explicit provider
|
|
20
20
|
investments = easy_investments(provider="plaid")
|
|
21
21
|
holdings = await investments.get_holdings(access_token)
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
# Calculate metrics
|
|
24
24
|
allocation = investments.calculate_allocation(holdings)
|
|
25
25
|
metrics = investments.calculate_portfolio_metrics(holdings)
|
|
@@ -51,7 +51,8 @@ from typing import TYPE_CHECKING, Literal
|
|
|
51
51
|
if TYPE_CHECKING:
|
|
52
52
|
from fastapi import FastAPI
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Use the local InvestmentProvider base class (same as providers use)
|
|
55
|
+
from .providers.base import InvestmentProvider
|
|
55
56
|
|
|
56
57
|
# Lazy imports to avoid loading provider SDKs unless needed
|
|
57
58
|
_provider_cache: dict[str, InvestmentProvider] = {}
|
|
@@ -109,11 +110,12 @@ def easy_investments(
|
|
|
109
110
|
provider = "plaid" # Default to Plaid
|
|
110
111
|
|
|
111
112
|
# Check cache
|
|
112
|
-
cache_key = f"{provider}:{
|
|
113
|
+
cache_key = f"{provider}:{sorted(config.items())!s}"
|
|
113
114
|
if cache_key in _provider_cache:
|
|
114
115
|
return _provider_cache[cache_key]
|
|
115
116
|
|
|
116
117
|
# Lazy import and initialize provider
|
|
118
|
+
instance: InvestmentProvider
|
|
117
119
|
if provider == "plaid":
|
|
118
120
|
from .providers.plaid import PlaidInvestmentProvider
|
|
119
121
|
|
|
@@ -123,9 +125,7 @@ def easy_investments(
|
|
|
123
125
|
|
|
124
126
|
instance = SnapTradeInvestmentProvider(**config)
|
|
125
127
|
else:
|
|
126
|
-
raise ValueError(
|
|
127
|
-
f"Unknown provider: {provider}. Supported: 'plaid', 'snaptrade'"
|
|
128
|
-
)
|
|
128
|
+
raise ValueError(f"Unknown provider: {provider}. Supported: 'plaid', 'snaptrade'")
|
|
129
129
|
|
|
130
130
|
_provider_cache[cache_key] = instance
|
|
131
131
|
return instance
|
|
@@ -172,14 +172,19 @@ def add_investments(
|
|
|
172
172
|
>>> # GET /investments/transactions
|
|
173
173
|
>>> # etc.
|
|
174
174
|
"""
|
|
175
|
-
from .add import add_investments_impl
|
|
175
|
+
from .add import add_investments as add_investments_impl
|
|
176
|
+
from .providers.base import InvestmentProvider as InvestmentProviderBase
|
|
177
|
+
|
|
178
|
+
# Resolve provider from string Literal to actual InvestmentProvider instance
|
|
179
|
+
resolved_provider: InvestmentProviderBase | None = None
|
|
180
|
+
if provider is not None:
|
|
181
|
+
resolved_provider = easy_investments(provider=provider, **provider_config)
|
|
176
182
|
|
|
177
183
|
return add_investments_impl(
|
|
178
184
|
app,
|
|
179
|
-
provider=
|
|
185
|
+
provider=resolved_provider,
|
|
180
186
|
prefix=prefix,
|
|
181
187
|
tags=tags or ["Investments"],
|
|
182
|
-
**provider_config,
|
|
183
188
|
)
|
|
184
189
|
|
|
185
190
|
|