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.
Files changed (108) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +21 -21
  4. fin_infra/analytics/ease.py +19 -20
  5. fin_infra/analytics/portfolio.py +6 -6
  6. fin_infra/analytics/projections.py +1 -3
  7. fin_infra/banking/__init__.py +27 -28
  8. fin_infra/banking/history.py +8 -9
  9. fin_infra/banking/utils.py +27 -26
  10. fin_infra/brokerage/__init__.py +22 -24
  11. fin_infra/budgets/__init__.py +3 -3
  12. fin_infra/budgets/add.py +16 -17
  13. fin_infra/budgets/alerts.py +3 -3
  14. fin_infra/budgets/tracker.py +2 -2
  15. fin_infra/cashflows/__init__.py +3 -3
  16. fin_infra/cashflows/core.py +1 -1
  17. fin_infra/categorization/add.py +2 -3
  18. fin_infra/categorization/engine.py +17 -14
  19. fin_infra/categorization/llm_layer.py +7 -6
  20. fin_infra/categorization/rules.py +2 -4
  21. fin_infra/categorization/taxonomy.py +2 -2
  22. fin_infra/chat/__init__.py +6 -6
  23. fin_infra/chat/planning.py +0 -1
  24. fin_infra/cli/cmds/scaffold_cmds.py +10 -11
  25. fin_infra/clients/__init__.py +23 -1
  26. fin_infra/clients/base.py +1 -1
  27. fin_infra/clients/plaid.py +2 -2
  28. fin_infra/compliance/__init__.py +5 -4
  29. fin_infra/credit/add.py +6 -7
  30. fin_infra/credit/experian/auth.py +2 -2
  31. fin_infra/credit/experian/client.py +1 -1
  32. fin_infra/credit/experian/provider.py +4 -4
  33. fin_infra/crypto/__init__.py +7 -9
  34. fin_infra/crypto/insights.py +4 -3
  35. fin_infra/documents/add.py +6 -8
  36. fin_infra/documents/analysis.py +9 -9
  37. fin_infra/documents/ease.py +14 -14
  38. fin_infra/documents/models.py +4 -4
  39. fin_infra/documents/ocr.py +7 -7
  40. fin_infra/documents/storage.py +21 -13
  41. fin_infra/exceptions.py +0 -1
  42. fin_infra/goals/__init__.py +8 -8
  43. fin_infra/goals/add.py +36 -36
  44. fin_infra/goals/funding.py +4 -6
  45. fin_infra/goals/management.py +2 -3
  46. fin_infra/goals/milestones.py +1 -2
  47. fin_infra/goals/models.py +7 -11
  48. fin_infra/insights/__init__.py +6 -3
  49. fin_infra/insights/aggregator.py +1 -1
  50. fin_infra/investments/__init__.py +1 -1
  51. fin_infra/investments/add.py +23 -23
  52. fin_infra/investments/models.py +5 -5
  53. fin_infra/investments/providers/base.py +8 -9
  54. fin_infra/investments/providers/plaid.py +52 -26
  55. fin_infra/investments/providers/snaptrade.py +19 -19
  56. fin_infra/markets/__init__.py +5 -3
  57. fin_infra/models/__init__.py +10 -10
  58. fin_infra/models/brokerage.py +2 -1
  59. fin_infra/models/candle.py +1 -0
  60. fin_infra/models/money.py +1 -0
  61. fin_infra/models/quotes.py +4 -3
  62. fin_infra/models/tax.py +2 -1
  63. fin_infra/models/transactions.py +3 -4
  64. fin_infra/net_worth/__init__.py +7 -0
  65. fin_infra/net_worth/aggregator.py +4 -2
  66. fin_infra/net_worth/insights.py +0 -1
  67. fin_infra/normalization/__init__.py +2 -2
  68. fin_infra/normalization/providers/exchangerate.py +5 -5
  69. fin_infra/obs/classifier.py +1 -1
  70. fin_infra/providers/banking/plaid_client.py +5 -5
  71. fin_infra/providers/banking/teller_client.py +7 -6
  72. fin_infra/providers/base.py +27 -2
  73. fin_infra/providers/brokerage/alpaca.py +3 -3
  74. fin_infra/providers/market/alphavantage.py +6 -11
  75. fin_infra/providers/market/ccxt_crypto.py +19 -3
  76. fin_infra/providers/market/coingecko.py +5 -6
  77. fin_infra/providers/market/yahoo.py +23 -8
  78. fin_infra/providers/tax/__init__.py +1 -1
  79. fin_infra/providers/tax/irs.py +1 -1
  80. fin_infra/providers/tax/mock.py +5 -5
  81. fin_infra/providers/tax/taxbit.py +1 -1
  82. fin_infra/recurring/__init__.py +6 -6
  83. fin_infra/recurring/add.py +5 -4
  84. fin_infra/recurring/detector.py +7 -7
  85. fin_infra/recurring/detectors_llm.py +6 -6
  86. fin_infra/recurring/ease.py +2 -4
  87. fin_infra/recurring/insights.py +13 -13
  88. fin_infra/recurring/normalizer.py +1 -1
  89. fin_infra/recurring/normalizers.py +4 -4
  90. fin_infra/recurring/summary.py +13 -15
  91. fin_infra/scaffold/budgets.py +9 -9
  92. fin_infra/scaffold/goals.py +5 -5
  93. fin_infra/security/__init__.py +8 -8
  94. fin_infra/security/encryption.py +6 -6
  95. fin_infra/security/models.py +7 -7
  96. fin_infra/security/pii_filter.py +6 -6
  97. fin_infra/settings.py +2 -1
  98. fin_infra/tax/__init__.py +1 -1
  99. fin_infra/tax/add.py +3 -2
  100. fin_infra/tax/tlh.py +5 -5
  101. fin_infra/utils/http.py +4 -3
  102. fin_infra/utils/retry.py +2 -1
  103. {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -8
  104. fin_infra-0.1.82.dist-info/RECORD +180 -0
  105. fin_infra-0.1.69.dist-info/RECORD +0 -180
  106. {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
  107. {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
  108. {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
@@ -36,7 +36,7 @@ Quick Start:
36
36
 
37
37
  from __future__ import annotations
38
38
 
39
- from typing import TYPE_CHECKING, Optional
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: "StorageBackend",
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: "DocumentType",
114
+ document_type: DocumentType,
115
115
  filename: str,
116
- metadata: Optional[dict] = None,
117
- tax_year: Optional[int] = None,
118
- form_type: Optional[str] = None,
119
- ) -> "FinancialDocument":
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: Optional["DocumentType"] = None,
163
- tax_year: Optional[int] = None,
162
+ document_type: DocumentType | None = None,
163
+ tax_year: int | None = None,
164
164
  limit: int = 100,
165
165
  offset: int = 0,
166
- ) -> list["FinancialDocument"]:
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: Optional[str] = None,
210
+ provider: str | None = None,
211
211
  force_refresh: bool = False,
212
- ) -> "OCRResult":
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
- ) -> "DocumentAnalysis":
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: Optional["StorageBackend"] = None,
271
+ storage: StorageBackend | None = None,
272
272
  default_ocr_provider: str = "tesseract",
273
273
  ) -> FinancialDocumentManager:
274
274
  """
@@ -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 Dict, List, Optional
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: Dict[str, str] = Field(
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: List[str] = Field(
184
+ key_findings: list[str] = Field(
185
185
  default_factory=list, description="Important facts extracted from document"
186
186
  )
187
- recommendations: List[str] = Field(
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(
@@ -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
  # ============================================================================
@@ -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:
@@ -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: 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
 
@@ -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")
@@ -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