fin-infra 0.1.69__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 +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 -28
- fin_infra/banking/history.py +8 -9
- fin_infra/banking/utils.py +27 -26
- fin_infra/brokerage/__init__.py +22 -24
- 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 +2 -2
- fin_infra/cashflows/__init__.py +3 -3
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/engine.py +17 -14
- fin_infra/categorization/llm_layer.py +7 -6
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +6 -6
- fin_infra/chat/planning.py +0 -1
- fin_infra/cli/cmds/scaffold_cmds.py +10 -11
- 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 +5 -4
- 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/crypto/insights.py +4 -3
- fin_infra/documents/add.py +6 -8
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +14 -14
- fin_infra/documents/models.py +4 -4
- 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 +36 -36
- fin_infra/goals/funding.py +4 -6
- 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 +6 -3
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/models.py +5 -5
- fin_infra/investments/providers/base.py +8 -9
- fin_infra/investments/providers/plaid.py +52 -26
- fin_infra/investments/providers/snaptrade.py +19 -19
- fin_infra/markets/__init__.py +5 -3
- 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/__init__.py +7 -0
- fin_infra/net_worth/aggregator.py +4 -2
- 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/obs/classifier.py +1 -1
- fin_infra/providers/banking/plaid_client.py +5 -5
- fin_infra/providers/banking/teller_client.py +7 -6
- fin_infra/providers/base.py +27 -2
- fin_infra/providers/brokerage/alpaca.py +3 -3
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +19 -3
- 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 +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 +13 -15
- 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/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 +2 -1
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -8
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.69.dist-info/RECORD +0 -180
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
fin_infra/documents/ease.py
CHANGED
|
@@ -36,7 +36,7 @@ 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 DocumentManager as BaseDocumentManager
|
|
@@ -94,7 +94,7 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
94
94
|
|
|
95
95
|
def __init__(
|
|
96
96
|
self,
|
|
97
|
-
storage:
|
|
97
|
+
storage: StorageBackend,
|
|
98
98
|
default_ocr_provider: str = "tesseract",
|
|
99
99
|
):
|
|
100
100
|
"""
|
|
@@ -111,12 +111,12 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
111
111
|
self,
|
|
112
112
|
user_id: str,
|
|
113
113
|
file: bytes,
|
|
114
|
-
document_type:
|
|
114
|
+
document_type: DocumentType,
|
|
115
115
|
filename: str,
|
|
116
|
-
metadata:
|
|
117
|
-
tax_year:
|
|
118
|
-
form_type:
|
|
119
|
-
) ->
|
|
116
|
+
metadata: dict | None = None,
|
|
117
|
+
tax_year: int | None = None,
|
|
118
|
+
form_type: str | None = None,
|
|
119
|
+
) -> FinancialDocument:
|
|
120
120
|
"""
|
|
121
121
|
Upload a financial document with financial-specific fields.
|
|
122
122
|
|
|
@@ -159,11 +159,11 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
159
159
|
def list_financial(
|
|
160
160
|
self,
|
|
161
161
|
user_id: str,
|
|
162
|
-
document_type:
|
|
163
|
-
tax_year:
|
|
162
|
+
document_type: DocumentType | None = None,
|
|
163
|
+
tax_year: int | None = None,
|
|
164
164
|
limit: int = 100,
|
|
165
165
|
offset: int = 0,
|
|
166
|
-
) -> list[
|
|
166
|
+
) -> list[FinancialDocument]:
|
|
167
167
|
"""
|
|
168
168
|
List user's financial documents with filters.
|
|
169
169
|
|
|
@@ -207,9 +207,9 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
207
207
|
async def extract_text(
|
|
208
208
|
self,
|
|
209
209
|
document_id: str,
|
|
210
|
-
provider:
|
|
210
|
+
provider: str | None = None,
|
|
211
211
|
force_refresh: bool = False,
|
|
212
|
-
) ->
|
|
212
|
+
) -> OCRResult:
|
|
213
213
|
"""
|
|
214
214
|
Extract text from document using OCR (financial extension).
|
|
215
215
|
|
|
@@ -239,7 +239,7 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
239
239
|
self,
|
|
240
240
|
document_id: str,
|
|
241
241
|
force_refresh: bool = False,
|
|
242
|
-
) ->
|
|
242
|
+
) -> DocumentAnalysis:
|
|
243
243
|
"""
|
|
244
244
|
Analyze document using AI (financial extension).
|
|
245
245
|
|
|
@@ -268,7 +268,7 @@ DocumentManager = FinancialDocumentManager
|
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
def easy_documents(
|
|
271
|
-
storage:
|
|
271
|
+
storage: StorageBackend | None = None,
|
|
272
272
|
default_ocr_provider: str = "tesseract",
|
|
273
273
|
) -> FinancialDocumentManager:
|
|
274
274
|
"""
|
fin_infra/documents/models.py
CHANGED
|
@@ -31,7 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
from datetime import datetime
|
|
33
33
|
from enum import Enum
|
|
34
|
-
from typing import
|
|
34
|
+
from typing import Optional
|
|
35
35
|
|
|
36
36
|
from pydantic import BaseModel, ConfigDict, Field
|
|
37
37
|
from svc_infra.documents import Document as BaseDocument
|
|
@@ -145,7 +145,7 @@ class OCRResult(BaseModel):
|
|
|
145
145
|
confidence: float = Field(
|
|
146
146
|
..., description="Overall OCR confidence score (0.0-1.0)", ge=0.0, le=1.0
|
|
147
147
|
)
|
|
148
|
-
fields_extracted:
|
|
148
|
+
fields_extracted: dict[str, str] = Field(
|
|
149
149
|
default_factory=dict,
|
|
150
150
|
description="Structured fields extracted from document (names, amounts, dates)",
|
|
151
151
|
)
|
|
@@ -181,10 +181,10 @@ class DocumentAnalysis(BaseModel):
|
|
|
181
181
|
|
|
182
182
|
document_id: str = Field(..., description="Document that was analyzed")
|
|
183
183
|
summary: str = Field(..., description="High-level document summary")
|
|
184
|
-
key_findings:
|
|
184
|
+
key_findings: list[str] = Field(
|
|
185
185
|
default_factory=list, description="Important facts extracted from document"
|
|
186
186
|
)
|
|
187
|
-
recommendations:
|
|
187
|
+
recommendations: list[str] = Field(
|
|
188
188
|
default_factory=list, description="Action items or suggestions based on document content"
|
|
189
189
|
)
|
|
190
190
|
analysis_date: datetime = Field(
|
fin_infra/documents/ocr.py
CHANGED
|
@@ -25,7 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import re
|
|
27
27
|
from datetime import datetime
|
|
28
|
-
from typing import TYPE_CHECKING
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -33,15 +33,15 @@ if TYPE_CHECKING:
|
|
|
33
33
|
from .models import OCRResult
|
|
34
34
|
|
|
35
35
|
# In-memory OCR cache (production: use svc-infra cache)
|
|
36
|
-
_ocr_cache:
|
|
36
|
+
_ocr_cache: dict[str, OCRResult] = {}
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
async def extract_text(
|
|
40
|
-
storage:
|
|
40
|
+
storage: StorageBackend,
|
|
41
41
|
document_id: str,
|
|
42
42
|
provider: str = "tesseract",
|
|
43
43
|
force_refresh: bool = False,
|
|
44
|
-
) ->
|
|
44
|
+
) -> OCRResult:
|
|
45
45
|
"""
|
|
46
46
|
Extract text from a document using OCR (uses svc-infra storage).
|
|
47
47
|
|
|
@@ -106,7 +106,7 @@ async def extract_text(
|
|
|
106
106
|
|
|
107
107
|
def _extract_with_tesseract(
|
|
108
108
|
file_content: bytes, filename: str, metadata: dict, document_id: str
|
|
109
|
-
) ->
|
|
109
|
+
) -> OCRResult:
|
|
110
110
|
"""
|
|
111
111
|
Extract text using Tesseract OCR (simulated).
|
|
112
112
|
|
|
@@ -158,7 +158,7 @@ def _extract_with_tesseract(
|
|
|
158
158
|
|
|
159
159
|
def _extract_with_textract(
|
|
160
160
|
file_content: bytes, filename: str, metadata: dict, document_id: str
|
|
161
|
-
) ->
|
|
161
|
+
) -> OCRResult:
|
|
162
162
|
"""
|
|
163
163
|
Extract text using AWS Textract (simulated).
|
|
164
164
|
|
|
@@ -207,7 +207,7 @@ def _extract_with_textract(
|
|
|
207
207
|
)
|
|
208
208
|
|
|
209
209
|
|
|
210
|
-
def _parse_tax_form(text: str, form_type:
|
|
210
|
+
def _parse_tax_form(text: str, form_type: str | None = None) -> dict[str, str]:
|
|
211
211
|
"""
|
|
212
212
|
Parse tax form text into structured fields.
|
|
213
213
|
|
fin_infra/documents/storage.py
CHANGED
|
@@ -36,14 +36,22 @@ 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
|
)
|
|
49
57
|
|
|
@@ -64,15 +72,15 @@ if TYPE_CHECKING:
|
|
|
64
72
|
|
|
65
73
|
|
|
66
74
|
async def upload_document(
|
|
67
|
-
storage:
|
|
75
|
+
storage: StorageBackend,
|
|
68
76
|
user_id: str,
|
|
69
77
|
file: bytes,
|
|
70
|
-
document_type:
|
|
78
|
+
document_type: DocumentType,
|
|
71
79
|
filename: str,
|
|
72
|
-
metadata:
|
|
73
|
-
tax_year:
|
|
74
|
-
form_type:
|
|
75
|
-
) ->
|
|
80
|
+
metadata: dict | None = None,
|
|
81
|
+
tax_year: int | None = None,
|
|
82
|
+
form_type: str | None = None,
|
|
83
|
+
) -> FinancialDocument:
|
|
76
84
|
"""
|
|
77
85
|
Upload a financial document (delegates to svc-infra, adds financial fields).
|
|
78
86
|
|
|
@@ -140,7 +148,7 @@ async def upload_document(
|
|
|
140
148
|
return financial_doc
|
|
141
149
|
|
|
142
150
|
|
|
143
|
-
def get_document(document_id: str) ->
|
|
151
|
+
def get_document(document_id: str) -> FinancialDocument | None:
|
|
144
152
|
"""
|
|
145
153
|
Get financial document metadata by ID (delegates to svc-infra).
|
|
146
154
|
|
|
@@ -187,7 +195,7 @@ def get_document(document_id: str) -> Optional["FinancialDocument"]:
|
|
|
187
195
|
return financial_doc
|
|
188
196
|
|
|
189
197
|
|
|
190
|
-
async def download_document(storage:
|
|
198
|
+
async def download_document(storage: StorageBackend, document_id: str) -> bytes:
|
|
191
199
|
"""
|
|
192
200
|
Download a financial document by ID (delegates to svc-infra).
|
|
193
201
|
|
|
@@ -213,7 +221,7 @@ async def download_document(storage: "StorageBackend", document_id: str) -> byte
|
|
|
213
221
|
return await base_download_document(storage=storage, document_id=document_id)
|
|
214
222
|
|
|
215
223
|
|
|
216
|
-
async def delete_document(storage:
|
|
224
|
+
async def delete_document(storage: StorageBackend, document_id: str) -> bool:
|
|
217
225
|
"""
|
|
218
226
|
Delete a financial document and its metadata (delegates to svc-infra).
|
|
219
227
|
|
|
@@ -238,11 +246,11 @@ async def delete_document(storage: "StorageBackend", document_id: str) -> bool:
|
|
|
238
246
|
|
|
239
247
|
def list_documents(
|
|
240
248
|
user_id: str,
|
|
241
|
-
document_type:
|
|
242
|
-
tax_year:
|
|
249
|
+
document_type: DocumentType | None = None,
|
|
250
|
+
tax_year: int | None = None,
|
|
243
251
|
limit: int = 100,
|
|
244
252
|
offset: int = 0,
|
|
245
|
-
) ->
|
|
253
|
+
) -> list[FinancialDocument]:
|
|
246
254
|
"""
|
|
247
255
|
List user's financial documents with optional filters (delegates to svc-infra).
|
|
248
256
|
|
fin_infra/exceptions.py
CHANGED
|
@@ -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 Any,
|
|
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 cast(list[dict[Any, Any]], 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
|
@@ -45,7 +45,6 @@ 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
|
# ============================================================================
|
|
@@ -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 cast(dict[str, Any], _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 cast(dict[str, Any], 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
|
@@ -31,7 +31,6 @@ from typing import Any, cast
|
|
|
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 cast(dict[str, Any], 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,13 +10,16 @@ Aggregates insights from multiple sources:
|
|
|
10
10
|
- Cash flow projections
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
from typing import TYPE_CHECKING
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from fastapi import FastAPI
|
|
17
18
|
|
|
18
|
-
from .models import Insight, InsightFeed, InsightPriority, InsightCategory
|
|
19
19
|
from .aggregator import aggregate_insights, get_user_insights
|
|
20
|
+
from .models import Insight, InsightCategory, InsightFeed, InsightPriority
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
__all__ = [
|
|
22
25
|
"Insight",
|
|
@@ -77,10 +80,10 @@ def add_insights(
|
|
|
77
80
|
- Notification system for critical insights
|
|
78
81
|
"""
|
|
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,4 +128,4 @@ def add_insights(
|
|
|
125
128
|
# Mount router
|
|
126
129
|
app.include_router(router, include_in_schema=True)
|
|
127
130
|
|
|
128
|
-
|
|
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")
|
|
@@ -110,7 +110,7 @@ def easy_investments(
|
|
|
110
110
|
provider = "plaid" # Default to Plaid
|
|
111
111
|
|
|
112
112
|
# Check cache
|
|
113
|
-
cache_key = f"{provider}:{
|
|
113
|
+
cache_key = f"{provider}:{sorted(config.items())!s}"
|
|
114
114
|
if cache_key in _provider_cache:
|
|
115
115
|
return _provider_cache[cache_key]
|
|
116
116
|
|