fin-infra 0.1.69__py3-none-any.whl → 0.4.0__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.
Files changed (131) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +24 -24
  4. fin_infra/analytics/cash_flow.py +3 -3
  5. fin_infra/analytics/ease.py +19 -20
  6. fin_infra/analytics/models.py +5 -5
  7. fin_infra/analytics/portfolio.py +18 -18
  8. fin_infra/analytics/projections.py +1 -3
  9. fin_infra/analytics/spending.py +4 -5
  10. fin_infra/banking/__init__.py +27 -28
  11. fin_infra/banking/history.py +12 -13
  12. fin_infra/banking/utils.py +27 -26
  13. fin_infra/brokerage/__init__.py +29 -31
  14. fin_infra/budgets/__init__.py +3 -3
  15. fin_infra/budgets/add.py +16 -17
  16. fin_infra/budgets/alerts.py +4 -4
  17. fin_infra/budgets/ease.py +1 -2
  18. fin_infra/budgets/models.py +1 -2
  19. fin_infra/budgets/templates.py +4 -4
  20. fin_infra/budgets/tracker.py +4 -4
  21. fin_infra/cashflows/__init__.py +3 -3
  22. fin_infra/cashflows/core.py +1 -1
  23. fin_infra/categorization/__init__.py +1 -1
  24. fin_infra/categorization/add.py +2 -3
  25. fin_infra/categorization/ease.py +3 -3
  26. fin_infra/categorization/engine.py +18 -15
  27. fin_infra/categorization/llm_layer.py +13 -10
  28. fin_infra/categorization/models.py +3 -4
  29. fin_infra/categorization/rules.py +2 -4
  30. fin_infra/categorization/taxonomy.py +2 -2
  31. fin_infra/chat/__init__.py +6 -6
  32. fin_infra/chat/planning.py +1 -2
  33. fin_infra/cli/cmds/scaffold_cmds.py +16 -17
  34. fin_infra/clients/__init__.py +23 -1
  35. fin_infra/clients/base.py +1 -1
  36. fin_infra/clients/plaid.py +2 -2
  37. fin_infra/compliance/__init__.py +5 -4
  38. fin_infra/credit/add.py +6 -7
  39. fin_infra/credit/experian/auth.py +2 -2
  40. fin_infra/credit/experian/client.py +1 -1
  41. fin_infra/credit/experian/parser.py +5 -5
  42. fin_infra/credit/experian/provider.py +4 -4
  43. fin_infra/crypto/__init__.py +9 -11
  44. fin_infra/crypto/insights.py +4 -3
  45. fin_infra/documents/add.py +6 -8
  46. fin_infra/documents/analysis.py +9 -9
  47. fin_infra/documents/ease.py +14 -14
  48. fin_infra/documents/models.py +5 -6
  49. fin_infra/documents/ocr.py +7 -7
  50. fin_infra/documents/storage.py +21 -13
  51. fin_infra/exceptions.py +0 -1
  52. fin_infra/goals/__init__.py +8 -8
  53. fin_infra/goals/add.py +36 -36
  54. fin_infra/goals/funding.py +4 -6
  55. fin_infra/goals/management.py +5 -6
  56. fin_infra/goals/milestones.py +7 -8
  57. fin_infra/goals/models.py +9 -13
  58. fin_infra/insights/__init__.py +6 -3
  59. fin_infra/insights/aggregator.py +1 -1
  60. fin_infra/investments/__init__.py +3 -3
  61. fin_infra/investments/add.py +23 -23
  62. fin_infra/investments/ease.py +2 -2
  63. fin_infra/investments/models.py +27 -29
  64. fin_infra/investments/providers/base.py +12 -13
  65. fin_infra/investments/providers/plaid.py +52 -26
  66. fin_infra/investments/providers/snaptrade.py +19 -19
  67. fin_infra/investments/scaffold_templates/README.md +17 -17
  68. fin_infra/markets/__init__.py +7 -5
  69. fin_infra/models/__init__.py +10 -10
  70. fin_infra/models/accounts.py +4 -5
  71. fin_infra/models/brokerage.py +2 -1
  72. fin_infra/models/candle.py +1 -0
  73. fin_infra/models/money.py +1 -0
  74. fin_infra/models/quotes.py +4 -3
  75. fin_infra/models/tax.py +2 -1
  76. fin_infra/models/transactions.py +4 -5
  77. fin_infra/net_worth/__init__.py +8 -1
  78. fin_infra/net_worth/aggregator.py +5 -3
  79. fin_infra/net_worth/calculator.py +1 -1
  80. fin_infra/net_worth/insights.py +7 -8
  81. fin_infra/normalization/__init__.py +4 -4
  82. fin_infra/normalization/currency_converter.py +7 -8
  83. fin_infra/normalization/models.py +9 -10
  84. fin_infra/normalization/providers/exchangerate.py +5 -5
  85. fin_infra/normalization/providers/static_mappings.py +1 -1
  86. fin_infra/normalization/symbol_resolver.py +3 -4
  87. fin_infra/obs/classifier.py +3 -3
  88. fin_infra/providers/banking/plaid_client.py +5 -5
  89. fin_infra/providers/banking/teller_client.py +7 -6
  90. fin_infra/providers/base.py +27 -2
  91. fin_infra/providers/brokerage/alpaca.py +4 -4
  92. fin_infra/providers/market/alphavantage.py +6 -11
  93. fin_infra/providers/market/ccxt_crypto.py +19 -3
  94. fin_infra/providers/market/coingecko.py +5 -6
  95. fin_infra/providers/market/yahoo.py +23 -8
  96. fin_infra/providers/tax/__init__.py +1 -1
  97. fin_infra/providers/tax/irs.py +1 -1
  98. fin_infra/providers/tax/mock.py +5 -5
  99. fin_infra/providers/tax/taxbit.py +1 -1
  100. fin_infra/recurring/__init__.py +6 -6
  101. fin_infra/recurring/add.py +6 -5
  102. fin_infra/recurring/detector.py +7 -7
  103. fin_infra/recurring/detectors_llm.py +10 -10
  104. fin_infra/recurring/ease.py +6 -8
  105. fin_infra/recurring/insights.py +25 -24
  106. fin_infra/recurring/normalizer.py +7 -7
  107. fin_infra/recurring/normalizers.py +31 -30
  108. fin_infra/recurring/summary.py +13 -15
  109. fin_infra/scaffold/budgets.py +9 -9
  110. fin_infra/scaffold/goals.py +9 -9
  111. fin_infra/security/__init__.py +8 -8
  112. fin_infra/security/add.py +1 -2
  113. fin_infra/security/audit.py +6 -7
  114. fin_infra/security/encryption.py +6 -6
  115. fin_infra/security/models.py +7 -7
  116. fin_infra/security/pii_filter.py +16 -16
  117. fin_infra/security/token_store.py +2 -3
  118. fin_infra/settings.py +2 -1
  119. fin_infra/tax/__init__.py +1 -1
  120. fin_infra/tax/add.py +5 -4
  121. fin_infra/tax/tlh.py +10 -10
  122. fin_infra/utils/__init__.py +15 -1
  123. fin_infra/utils/deprecation.py +161 -0
  124. fin_infra/utils/http.py +4 -3
  125. fin_infra/utils/retry.py +2 -1
  126. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/METADATA +30 -16
  127. fin_infra-0.4.0.dist-info/RECORD +181 -0
  128. fin_infra-0.1.69.dist-info/RECORD +0 -180
  129. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/LICENSE +0 -0
  130. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/WHEEL +0 -0
  131. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -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, Dict, Optional
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: Dict[str, "OCRResult"] = {}
36
+ _ocr_cache: dict[str, OCRResult] = {}
37
37
 
38
38
 
39
39
  async def extract_text(
40
- storage: "StorageBackend",
40
+ storage: StorageBackend,
41
41
  document_id: str,
42
42
  provider: str = "tesseract",
43
43
  force_refresh: bool = False,
44
- ) -> "OCRResult":
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
- ) -> "OCRResult":
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
- ) -> "OCRResult":
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: Optional[str] = None) -> dict[str, str]:
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
 
@@ -36,14 +36,22 @@ Quick Start:
36
36
 
37
37
  from __future__ import annotations
38
38
 
39
- from typing import TYPE_CHECKING, List, Optional
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: "StorageBackend",
75
+ storage: StorageBackend,
68
76
  user_id: str,
69
77
  file: bytes,
70
- document_type: "DocumentType",
78
+ document_type: DocumentType,
71
79
  filename: str,
72
- metadata: Optional[dict] = None,
73
- tax_year: Optional[int] = None,
74
- form_type: Optional[str] = None,
75
- ) -> "FinancialDocument":
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) -> Optional["FinancialDocument"]:
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: "StorageBackend", document_id: str) -> bytes:
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: "StorageBackend", document_id: str) -> bool:
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: Optional["DocumentType"] = None,
242
- tax_year: Optional[int] = None,
249
+ document_type: DocumentType | None = None,
250
+ tax_year: int | None = None,
243
251
  limit: int = 100,
244
252
  offset: int = 0,
245
- ) -> List["FinancialDocument"]:
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
  # =============================================================================
@@ -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, List, Optional, cast
32
+ from typing import Any, cast
33
33
 
34
- from fastapi import FastAPI, HTTPException, status, Query, Body
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: Optional[str]) -> Optional[datetime]:
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: Optional[str] = Field(None, description="Deadline (ISO date)")
89
- description: Optional[str] = Field(None, description="Goal description")
90
- current_amount: Optional[float] = Field(0.0, ge=0, description="Current amount")
91
- auto_contribute: Optional[bool] = Field(False, description="Auto-contribute enabled")
92
- tags: Optional[List[str]] = Field(None, description="Goal 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: Optional[str] = Field(None, min_length=1, max_length=200)
99
- target_amount: Optional[float] = Field(None, gt=0)
100
- deadline: Optional[str] = None
101
- description: Optional[str] = None
102
- current_amount: Optional[float] = Field(None, ge=0)
103
- status: Optional[GoalStatus] = None
104
- auto_contribute: Optional[bool] = None
105
- tags: Optional[List[str]] = None
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: Optional[str] = Field(None, description="Target date (ISO 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=List[dict])
241
+ @router.get("", response_model=list[dict])
242
242
  async def list_goals_endpoint(
243
- user_id: Optional[str] = Query(
243
+ user_id: str | None = Query(
244
244
  None, description="User identifier (optional, returns all if not provided)"
245
245
  ),
246
- goal_type: Optional[str] = Query(None, description="Filter by goal type"),
247
- status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
248
- ) -> List[dict]:
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=List[dict])
446
- async def list_milestones_endpoint(goal_id: str) -> List[dict]:
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=List[dict])
544
- async def list_funding_sources_endpoint(goal_id: str) -> List[dict]:
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
 
@@ -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: Dict[str, Dict[str, float]] = {}
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) -> List[FundingSource]:
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) -> Dict[str, float]:
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
 
@@ -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
  # ============================================================================
@@ -183,7 +182,7 @@ Your response: {
183
182
  "confidence": 0.94
184
183
  }
185
184
 
186
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
185
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
187
186
  Verify calculations independently. For personalized advice, consult a professional."""
188
187
 
189
188
  GOAL_PROGRESS_SYSTEM_PROMPT = """You are a financial advisor reviewing goal progress.
@@ -238,7 +237,7 @@ Your response: {
238
237
  "projected_completion_date": "2029-06-01",
239
238
  "variance_from_target_days": -365,
240
239
  "course_corrections": [
241
- "⚠️ 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
240
+ "[!] 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
242
241
  "Emergency: reduce expenses by $500/month (cancel subscriptions, cut entertainment)",
243
242
  "Contact debt counselor for consolidation or negotiation options",
244
243
  "Consider side income: gig work, selling unused items ($500/month target)",
@@ -247,7 +246,7 @@ Your response: {
247
246
  "confidence": 0.95
248
247
  }
249
248
 
250
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
249
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
251
250
  Verify calculations independently. For personalized advice, consult a professional."""
252
251
 
253
252
 
@@ -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:
@@ -22,7 +22,7 @@ Example:
22
22
  # Check which milestones have been reached
23
23
  reached = check_milestones("goal_123")
24
24
  for m in reached:
25
- print(f"🎉 Milestone reached: {m['description']}")
25
+ print(f" Milestone reached: {m['description']}")
26
26
  """
27
27
 
28
28
  from datetime import datetime
@@ -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
  # ============================================================================
@@ -125,7 +124,7 @@ def check_milestones(goal_id: str) -> list[dict[str, Any]]:
125
124
 
126
125
  reached = check_milestones("goal_123")
127
126
  if reached:
128
- print(f"🎉 {len(reached)} milestones reached!")
127
+ print(f" {len(reached)} milestones reached!")
129
128
  for m in reached:
130
129
  print(f" - {m['description']}: ${m['amount']:,.0f}")
131
130
 
@@ -185,17 +184,17 @@ def get_celebration_message(milestone: dict[str, Any]) -> str:
185
184
 
186
185
  Example:
187
186
  message = get_celebration_message(milestone)
188
- # "🎉 Milestone reached! You've hit $25,000 - 25% to target!"
187
+ # " Milestone reached! You've hit $25,000 - 25% to target!"
189
188
  """
190
189
  amount = milestone["amount"]
191
190
  description = milestone["description"]
192
191
 
193
192
  messages = [
194
- f"🎉 Milestone reached! You've hit ${amount:,.0f} - {description}!",
193
+ f" Milestone reached! You've hit ${amount:,.0f} - {description}!",
195
194
  f"🎊 Congratulations! ${amount:,.0f} milestone achieved - {description}",
196
195
  f"🌟 Great progress! You reached ${amount:,.0f} - {description}",
197
- f"💪 Keep going! ${amount:,.0f} milestone completed - {description}",
198
- f"🚀 Amazing! You hit ${amount:,.0f} - {description}",
196
+ f" Keep going! ${amount:,.0f} milestone completed - {description}",
197
+ f" Amazing! You hit ${amount:,.0f} - {description}",
199
198
  ]
200
199
 
201
200
  # Use amount to pick consistent message for same milestone
@@ -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: Optional[datetime] = Field(
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: Optional[datetime] = Field(
77
+ reached_date: datetime | None = Field(
80
78
  None, description="Date milestone was reached (if reached=True)"
81
79
  )
82
80
 
@@ -95,8 +93,8 @@ class FundingSource(BaseModel):
95
93
 
96
94
  Supports split allocation:
97
95
  - Multiple accounts can fund one goal (e.g., savings + checking)
98
- - One account can fund multiple goals (e.g., savings emergency + vacation)
99
- - Allocation percentages must sum to 100% per account
96
+ - One account can fund multiple goals (e.g., savings -> emergency + vacation)
97
+ - Allocation percentages must sum to <=100% per account
100
98
  """
101
99
 
102
100
  goal_id: str = Field(..., description="Goal identifier")
@@ -107,7 +105,7 @@ class FundingSource(BaseModel):
107
105
  ge=0.0,
108
106
  le=100.0,
109
107
  )
110
- account_name: Optional[str] = Field(
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: Optional[str] = Field(
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: Optional[datetime] = Field(None, description="Target completion date")
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: Optional[datetime] = Field(
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: Optional[datetime] = Field(
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")
@@ -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
- print("Insights feed enabled (unified financial insights)")
131
+ logger.info("Insights feed enabled")
@@ -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")
@@ -74,8 +74,8 @@ def easy_investments(
74
74
  InvestmentProvider instance for fetching holdings, transactions, securities.
75
75
 
76
76
  Environment detection order:
77
- 1. If PLAID_CLIENT_ID set Plaid
78
- 2. If SNAPTRADE_CLIENT_ID set SnapTrade
77
+ 1. If PLAID_CLIENT_ID set -> Plaid
78
+ 2. If SNAPTRADE_CLIENT_ID set -> SnapTrade
79
79
  3. Default: Plaid (most common)
80
80
 
81
81
  Examples:
@@ -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}:{str(sorted(config.items()))}"
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