fin-infra 0.1.81__py3-none-any.whl → 0.1.83__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. fin_infra/analytics/__init__.py +3 -2
  2. fin_infra/analytics/add.py +21 -21
  3. fin_infra/analytics/ease.py +19 -20
  4. fin_infra/analytics/portfolio.py +6 -6
  5. fin_infra/analytics/projections.py +1 -3
  6. fin_infra/banking/__init__.py +27 -27
  7. fin_infra/banking/history.py +4 -5
  8. fin_infra/banking/utils.py +19 -18
  9. fin_infra/brokerage/__init__.py +22 -24
  10. fin_infra/budgets/__init__.py +3 -3
  11. fin_infra/budgets/add.py +16 -17
  12. fin_infra/cashflows/__init__.py +2 -2
  13. fin_infra/categorization/add.py +2 -3
  14. fin_infra/categorization/engine.py +6 -6
  15. fin_infra/categorization/llm_layer.py +6 -5
  16. fin_infra/categorization/rules.py +2 -4
  17. fin_infra/categorization/taxonomy.py +2 -2
  18. fin_infra/chat/__init__.py +5 -5
  19. fin_infra/chat/planning.py +0 -1
  20. fin_infra/cli/cmds/scaffold_cmds.py +10 -11
  21. fin_infra/clients/plaid.py +1 -1
  22. fin_infra/compliance/__init__.py +5 -5
  23. fin_infra/credit/add.py +6 -7
  24. fin_infra/credit/experian/auth.py +2 -2
  25. fin_infra/credit/experian/client.py +1 -1
  26. fin_infra/credit/experian/provider.py +4 -4
  27. fin_infra/crypto/__init__.py +7 -9
  28. fin_infra/documents/add.py +6 -8
  29. fin_infra/documents/analysis.py +8 -8
  30. fin_infra/documents/ease.py +14 -14
  31. fin_infra/documents/ocr.py +7 -7
  32. fin_infra/documents/storage.py +21 -13
  33. fin_infra/exceptions.py +0 -1
  34. fin_infra/goals/__init__.py +8 -8
  35. fin_infra/goals/add.py +30 -30
  36. fin_infra/goals/funding.py +1 -1
  37. fin_infra/goals/management.py +2 -3
  38. fin_infra/goals/milestones.py +1 -2
  39. fin_infra/goals/models.py +7 -11
  40. fin_infra/insights/__init__.py +2 -2
  41. fin_infra/insights/aggregator.py +1 -1
  42. fin_infra/investments/__init__.py +1 -1
  43. fin_infra/investments/add.py +23 -23
  44. fin_infra/investments/providers/base.py +2 -3
  45. fin_infra/investments/providers/plaid.py +9 -9
  46. fin_infra/investments/providers/snaptrade.py +10 -10
  47. fin_infra/markets/__init__.py +1 -1
  48. fin_infra/models/__init__.py +10 -10
  49. fin_infra/models/brokerage.py +2 -1
  50. fin_infra/models/candle.py +1 -0
  51. fin_infra/models/money.py +1 -0
  52. fin_infra/models/quotes.py +4 -3
  53. fin_infra/models/tax.py +2 -1
  54. fin_infra/models/transactions.py +3 -4
  55. fin_infra/net_worth/insights.py +0 -1
  56. fin_infra/normalization/__init__.py +2 -2
  57. fin_infra/normalization/providers/exchangerate.py +5 -5
  58. fin_infra/providers/banking/plaid_client.py +5 -5
  59. fin_infra/providers/banking/teller_client.py +7 -6
  60. fin_infra/providers/base.py +1 -1
  61. fin_infra/providers/brokerage/alpaca.py +3 -3
  62. fin_infra/providers/market/alphavantage.py +5 -10
  63. fin_infra/providers/market/ccxt_crypto.py +2 -2
  64. fin_infra/providers/market/coingecko.py +5 -6
  65. fin_infra/providers/market/yahoo.py +5 -5
  66. fin_infra/providers/tax/__init__.py +1 -1
  67. fin_infra/providers/tax/irs.py +1 -1
  68. fin_infra/providers/tax/mock.py +5 -5
  69. fin_infra/providers/tax/taxbit.py +1 -1
  70. fin_infra/recurring/__init__.py +6 -6
  71. fin_infra/recurring/add.py +5 -4
  72. fin_infra/recurring/detector.py +7 -7
  73. fin_infra/recurring/detectors_llm.py +6 -6
  74. fin_infra/recurring/ease.py +2 -4
  75. fin_infra/recurring/insights.py +13 -13
  76. fin_infra/recurring/normalizer.py +1 -1
  77. fin_infra/recurring/normalizers.py +4 -4
  78. fin_infra/recurring/summary.py +4 -6
  79. fin_infra/scaffold/budgets.py +6 -6
  80. fin_infra/scaffold/goals.py +1 -1
  81. fin_infra/security/__init__.py +8 -8
  82. fin_infra/security/encryption.py +6 -6
  83. fin_infra/security/models.py +7 -7
  84. fin_infra/security/pii_filter.py +6 -6
  85. fin_infra/settings.py +2 -1
  86. fin_infra/tax/__init__.py +1 -1
  87. fin_infra/tax/add.py +3 -2
  88. fin_infra/tax/tlh.py +5 -5
  89. fin_infra/utils/http.py +4 -3
  90. fin_infra/utils/retry.py +1 -1
  91. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/METADATA +1 -1
  92. fin_infra-0.1.83.dist-info/RECORD +180 -0
  93. fin_infra-0.1.81.dist-info/RECORD +0 -180
  94. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/LICENSE +0 -0
  95. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/WHEEL +0 -0
  96. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/entry_points.txt +0 -0
fin_infra/credit/add.py CHANGED
@@ -25,15 +25,14 @@ Example:
25
25
  import logging
26
26
  from typing import cast
27
27
 
28
- from fastapi import FastAPI, Depends, HTTPException, status
29
-
30
- from svc_infra.api.fastapi.dual.protected import user_router, RequireUser
28
+ from fastapi import Depends, FastAPI, HTTPException, status
31
29
  from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
30
+ from svc_infra.api.fastapi.dual.protected import RequireUser, user_router
32
31
  from svc_infra.cache import resource
33
32
  from svc_infra.webhooks import add_webhooks
34
33
 
34
+ from fin_infra.models.credit import CreditReport, CreditScore
35
35
  from fin_infra.providers.base import CreditProvider
36
- from fin_infra.models.credit import CreditScore, CreditReport
37
36
 
38
37
  logger = logging.getLogger(__name__)
39
38
 
@@ -156,8 +155,8 @@ def add_credit(
156
155
  if enable_webhooks and hasattr(app.state, "webhooks_outbox"):
157
156
  try:
158
157
  # Get webhook service from app state
159
- from svc_infra.webhooks.service import WebhookService
160
158
  from svc_infra.db.outbox import OutboxStore
159
+ from svc_infra.webhooks.service import WebhookService
161
160
 
162
161
  outbox: OutboxStore = app.state.webhooks_outbox
163
162
  subs = app.state.webhooks_subscriptions
@@ -176,7 +175,7 @@ def add_credit(
176
175
  # Don't fail request if webhook publishing fails
177
176
  logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
178
177
 
179
- return cast(CreditScore, score)
178
+ return cast("CreditScore", score)
180
179
 
181
180
  @router.post("/report", response_model=CreditReport)
182
181
  @credit_resource.cache_read(ttl=cache_ttl, suffix="report")
@@ -220,7 +219,7 @@ def add_credit(
220
219
  detail="Credit bureau service unavailable",
221
220
  )
222
221
 
223
- return cast(CreditReport, report)
222
+ return cast("CreditReport", report)
224
223
 
225
224
  # Mount router with dual routes (with/without trailing slash)
226
225
  app.include_router(router, include_in_schema=True)
@@ -86,7 +86,7 @@ class ExperianAuthManager:
86
86
  >>> headers = {"Authorization": f"Bearer {token}"}
87
87
  """
88
88
  # Call the cached implementation with client_id for cache key
89
- return cast(str, await self._get_token_cached(client_id=self.client_id))
89
+ return cast("str", await self._get_token_cached(client_id=self.client_id))
90
90
 
91
91
  @cache_read(
92
92
  key="oauth_token:experian:{client_id}", # Use client_id for uniqueness
@@ -141,7 +141,7 @@ class ExperianAuthManager:
141
141
 
142
142
  # Parse and return token
143
143
  data = response.json()
144
- return cast(str, data["access_token"])
144
+ return cast("str", data["access_token"])
145
145
 
146
146
  async def invalidate(self) -> None:
147
147
  """Invalidate cached token for THIS client (force refresh on next get_token call).
@@ -155,7 +155,7 @@ class ExperianClient:
155
155
  **kwargs,
156
156
  )
157
157
  response.raise_for_status()
158
- return cast(dict[str, Any], response.json())
158
+ return cast("dict[str, Any]", response.json())
159
159
 
160
160
  except httpx.HTTPStatusError as e:
161
161
  # Parse error response
@@ -30,7 +30,7 @@ Example:
30
30
  """
31
31
 
32
32
  import logging
33
- from datetime import datetime, timezone
33
+ from datetime import UTC, datetime
34
34
  from typing import Literal, cast
35
35
 
36
36
  from fin_infra.credit.experian.auth import ExperianAuthManager
@@ -177,7 +177,7 @@ class ExperianProvider(CreditProvider):
177
177
 
178
178
  # FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
179
179
  # This log must be retained for at least 2 years per FCRA requirements
180
- timestamp = datetime.now(timezone.utc).isoformat()
180
+ timestamp = datetime.now(UTC).isoformat()
181
181
  fcra_audit_logger.info(
182
182
  "FCRA_CREDIT_PULL",
183
183
  extra={
@@ -266,7 +266,7 @@ class ExperianProvider(CreditProvider):
266
266
  # FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
267
267
  # Full credit report pulls have stricter requirements than score-only pulls
268
268
  # This log must be retained for at least 2 years per FCRA requirements
269
- timestamp = datetime.now(timezone.utc).isoformat()
269
+ timestamp = datetime.now(UTC).isoformat()
270
270
  fcra_audit_logger.info(
271
271
  "FCRA_CREDIT_PULL",
272
272
  extra={
@@ -360,4 +360,4 @@ class ExperianProvider(CreditProvider):
360
360
  signature_key=signature_key,
361
361
  )
362
362
 
363
- return cast(str, data.get("subscriptionId", "unknown"))
363
+ return cast("str", data.get("subscriptionId", "unknown"))
@@ -13,7 +13,7 @@ Quick start:
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from datetime import datetime, timezone
16
+ from datetime import UTC, datetime
17
17
  from typing import TYPE_CHECKING, Literal
18
18
 
19
19
  if TYPE_CHECKING:
@@ -74,7 +74,7 @@ def easy_crypto(
74
74
 
75
75
 
76
76
  def add_crypto_data(
77
- app: "FastAPI",
77
+ app: FastAPI,
78
78
  *,
79
79
  provider: str | CryptoDataProvider | None = None,
80
80
  prefix: str = "/crypto",
@@ -131,9 +131,9 @@ def add_crypto_data(
131
131
  >>> add_observability(app)
132
132
  >>> crypto = add_crypto_data(app)
133
133
  """
134
- from svc_infra.api.fastapi.dual.public import public_router
135
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
136
134
  from fastapi import HTTPException, Query
135
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
136
+ from svc_infra.api.fastapi.dual.public import public_router
137
137
 
138
138
  # Initialize provider if string or None
139
139
  if isinstance(provider, str):
@@ -168,11 +168,11 @@ def add_crypto_data(
168
168
  "price": float(ticker.price),
169
169
  "as_of": ticker.as_of.isoformat()
170
170
  if ticker.as_of
171
- else datetime.now(timezone.utc).isoformat(),
171
+ else datetime.now(UTC).isoformat(),
172
172
  }
173
173
  except Exception as e:
174
174
  raise HTTPException(
175
- status_code=400, detail=f"Error fetching ticker for {symbol}: {str(e)}"
175
+ status_code=400, detail=f"Error fetching ticker for {symbol}: {e!s}"
176
176
  )
177
177
 
178
178
  @router.get("/ohlcv/{symbol}")
@@ -216,9 +216,7 @@ def add_crypto_data(
216
216
  ],
217
217
  }
218
218
  except Exception as e:
219
- raise HTTPException(
220
- status_code=400, detail=f"Error fetching OHLCV for {symbol}: {str(e)}"
221
- )
219
+ raise HTTPException(status_code=400, detail=f"Error fetching OHLCV for {symbol}: {e!s}")
222
220
 
223
221
  # Mount router
224
222
  app.include_router(router, include_in_schema=True)
@@ -23,23 +23,22 @@ Quick Start:
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- from typing import TYPE_CHECKING, Optional
26
+ from typing import TYPE_CHECKING
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from fastapi import FastAPI
30
-
31
30
  from svc_infra.storage.base import StorageBackend
32
31
 
33
32
  from .ease import FinancialDocumentManager
34
33
 
35
34
 
36
35
  def add_documents(
37
- app: "FastAPI",
38
- storage: Optional["StorageBackend"] = None,
36
+ app: FastAPI,
37
+ storage: StorageBackend | None = None,
39
38
  default_ocr_provider: str = "tesseract",
40
39
  prefix: str = "/documents",
41
- tags: Optional[list[str]] = None,
42
- ) -> "FinancialDocumentManager":
40
+ tags: list[str] | None = None,
41
+ ) -> FinancialDocumentManager:
43
42
  """
44
43
  Add financial document management endpoints to FastAPI app.
45
44
 
@@ -87,7 +86,6 @@ def add_documents(
87
86
  - Stores manager on app.state.financial_documents
88
87
  """
89
88
  from fastapi import HTTPException
90
-
91
89
  from svc_infra.api.fastapi.dual.protected import user_router
92
90
 
93
91
  # Import svc-infra base function to mount base endpoints (with fallback)
@@ -128,7 +126,7 @@ def add_documents(
128
126
  @router.post("/{document_id}/ocr", response_model=OCRResult)
129
127
  async def extract_text_ocr(
130
128
  document_id: str,
131
- provider: Optional[str] = None,
129
+ provider: str | None = None,
132
130
  force_refresh: bool = False,
133
131
  ) -> OCRResult:
134
132
  """
@@ -32,14 +32,14 @@ if TYPE_CHECKING:
32
32
  from .models import DocumentAnalysis
33
33
 
34
34
  # In-memory analysis cache (production: use svc-infra cache)
35
- _analysis_cache: dict[str, "DocumentAnalysis"] = {}
35
+ _analysis_cache: dict[str, DocumentAnalysis] = {}
36
36
 
37
37
 
38
38
  async def analyze_document(
39
- storage: "StorageBackend",
39
+ storage: StorageBackend,
40
40
  document_id: str,
41
41
  force_refresh: bool = False,
42
- ) -> "DocumentAnalysis":
42
+ ) -> DocumentAnalysis:
43
43
  """
44
44
  Analyze a document using AI to extract insights and recommendations.
45
45
 
@@ -165,7 +165,7 @@ Important: This analysis is not a substitute for professional financial advice.
165
165
  return prompt
166
166
 
167
167
 
168
- def _validate_analysis(analysis: "DocumentAnalysis") -> bool:
168
+ def _validate_analysis(analysis: DocumentAnalysis) -> bool:
169
169
  """
170
170
  Validate LLM analysis output.
171
171
 
@@ -201,7 +201,7 @@ def _validate_analysis(analysis: "DocumentAnalysis") -> bool:
201
201
  return True
202
202
 
203
203
 
204
- def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> "DocumentAnalysis":
204
+ def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
205
205
  """
206
206
  Specialized analysis for tax documents.
207
207
 
@@ -301,7 +301,7 @@ def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> "D
301
301
  )
302
302
 
303
303
 
304
- def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) -> "DocumentAnalysis":
304
+ def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
305
305
  """
306
306
  Specialized analysis for bank statements.
307
307
 
@@ -352,7 +352,7 @@ def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) ->
352
352
  )
353
353
 
354
354
 
355
- def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> "DocumentAnalysis":
355
+ def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
356
356
  """
357
357
  Specialized analysis for receipts.
358
358
 
@@ -394,7 +394,7 @@ def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> "Docume
394
394
 
395
395
  def _analyze_generic_document(
396
396
  ocr_text: str, document_type: str, metadata: dict, document_id: str
397
- ) -> "DocumentAnalysis":
397
+ ) -> DocumentAnalysis:
398
398
  """
399
399
  Generic analysis for other document types.
400
400
 
@@ -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
  """
@@ -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, 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, 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, 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):
@@ -240,11 +240,11 @@ def add_goals(
240
240
 
241
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"),
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
248
  ) -> list[dict]:
249
249
  """
250
250
  List all goals for a user with optional filters.
@@ -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"
@@ -22,8 +22,8 @@ Example:
22
22
  >>> # Raises ValueError if total allocation > 100%
23
23
  """
24
24
 
25
- from fin_infra.goals.models import FundingSource
26
25
  from fin_infra.goals.management import get_goal
26
+ from fin_infra.goals.models import FundingSource
27
27
 
28
28
  # In-memory storage for funding allocations
29
29
  # Structure: {account_id: {goal_id: allocation_percent}}