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
@@ -7,7 +7,7 @@ transactions, accounts, allocation, and securities data.
7
7
  from __future__ import annotations
8
8
 
9
9
  from datetime import date
10
- from typing import TYPE_CHECKING, Optional
10
+ from typing import TYPE_CHECKING
11
11
 
12
12
  from fastapi import HTTPException
13
13
  from pydantic import BaseModel, Field
@@ -24,10 +24,10 @@ except ImportError:
24
24
 
25
25
  from .ease import easy_investments
26
26
  from .models import (
27
+ AssetAllocation,
27
28
  Holding,
28
- InvestmentTransaction,
29
29
  InvestmentAccount,
30
- AssetAllocation,
30
+ InvestmentTransaction,
31
31
  Security,
32
32
  )
33
33
  from .providers.base import InvestmentProvider
@@ -37,55 +37,55 @@ from .providers.base import InvestmentProvider
37
37
  class HoldingsRequest(BaseModel):
38
38
  """Request model for holdings endpoint."""
39
39
 
40
- access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
41
- user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
42
- user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
43
- account_ids: Optional[list[str]] = Field(None, description="Filter by specific account IDs")
40
+ access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
41
+ user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
42
+ user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
43
+ account_ids: list[str] | None = Field(None, description="Filter by specific account IDs")
44
44
 
45
45
 
46
46
  class TransactionsRequest(BaseModel):
47
47
  """Request model for transactions endpoint."""
48
48
 
49
- access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
50
- user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
51
- user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
49
+ access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
50
+ user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
51
+ user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
52
52
  start_date: date = Field(..., description="Start date for transactions (YYYY-MM-DD)")
53
53
  end_date: date = Field(..., description="End date for transactions (YYYY-MM-DD)")
54
- account_ids: Optional[list[str]] = Field(None, description="Filter by specific account IDs")
54
+ account_ids: list[str] | None = Field(None, description="Filter by specific account IDs")
55
55
 
56
56
 
57
57
  class AccountsRequest(BaseModel):
58
58
  """Request model for investment accounts endpoint."""
59
59
 
60
- access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
61
- user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
62
- user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
60
+ access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
61
+ user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
62
+ user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
63
63
 
64
64
 
65
65
  class AllocationRequest(BaseModel):
66
66
  """Request model for asset allocation endpoint."""
67
67
 
68
- access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
69
- user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
70
- user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
71
- account_ids: Optional[list[str]] = Field(None, description="Filter by specific account IDs")
68
+ access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
69
+ user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
70
+ user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
71
+ account_ids: list[str] | None = Field(None, description="Filter by specific account IDs")
72
72
 
73
73
 
74
74
  class SecuritiesRequest(BaseModel):
75
75
  """Request model for securities endpoint."""
76
76
 
77
- access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
78
- user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
79
- user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
77
+ access_token: str | None = Field(None, description="Plaid access token (Plaid only)")
78
+ user_id: str | None = Field(None, description="SnapTrade user ID (SnapTrade only)")
79
+ user_secret: str | None = Field(None, description="SnapTrade user secret (SnapTrade only)")
80
80
  security_ids: list[str] = Field(..., description="List of security IDs to retrieve")
81
81
 
82
82
 
83
83
  def add_investments(
84
84
  app: FastAPI,
85
85
  prefix: str = "/investments",
86
- provider: Optional[InvestmentProvider] = None,
86
+ provider: InvestmentProvider | None = None,
87
87
  include_in_schema: bool = True,
88
- tags: Optional[list[str]] = None,
88
+ tags: list[str] | None = None,
89
89
  ) -> InvestmentProvider:
90
90
  """Add investment endpoints to FastAPI application.
91
91
 
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  from datetime import date
21
21
  from decimal import Decimal
22
22
  from enum import Enum
23
- from typing import TYPE_CHECKING, Dict, List, Optional
23
+ from typing import TYPE_CHECKING, Optional
24
24
 
25
25
  from pydantic import BaseModel, ConfigDict, Field, computed_field
26
26
 
@@ -374,12 +374,12 @@ class InvestmentAccount(BaseModel):
374
374
  subtype: Optional[str] = Field(None, description="Account subtype (401k, ira, brokerage)")
375
375
 
376
376
  # Balances
377
- balances: Dict[str, Optional[Decimal]] = Field(
377
+ balances: dict[str, Optional[Decimal]] = Field(
378
378
  ..., description="Current, available, and limit balances"
379
379
  )
380
380
 
381
381
  # Holdings
382
- holdings: List[Holding] = Field(default_factory=list, description="List of holdings in account")
382
+ holdings: list[Holding] = Field(default_factory=list, description="List of holdings in account")
383
383
 
384
384
  if TYPE_CHECKING:
385
385
 
@@ -487,11 +487,11 @@ class AssetAllocation(BaseModel):
487
487
  },
488
488
  )
489
489
 
490
- by_security_type: Dict[SecurityType, float] = Field(
490
+ by_security_type: dict[SecurityType, float] = Field(
491
491
  default_factory=dict,
492
492
  description="Percentage breakdown by security type (equity, bond, etc.)",
493
493
  )
494
- by_sector: Dict[str, float] = Field(
494
+ by_sector: dict[str, float] = Field(
495
495
  default_factory=dict,
496
496
  description="Percentage breakdown by sector (Technology, Healthcare, etc.)",
497
497
  )
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
  from datetime import date
7
- from typing import List, Optional
8
7
 
9
8
  # Import will work once models.py is fully implemented in Task 3
10
9
  # For now, using TYPE_CHECKING to avoid circular imports
@@ -30,8 +29,8 @@ class InvestmentProvider(ABC):
30
29
 
31
30
  @abstractmethod
32
31
  async def get_holdings(
33
- self, access_token: str, account_ids: Optional[List[str]] = None
34
- ) -> List[Holding]:
32
+ self, access_token: str, account_ids: list[str] | None = None
33
+ ) -> list[Holding]:
35
34
  """Fetch holdings for investment accounts.
36
35
 
37
36
  Args:
@@ -54,8 +53,8 @@ class InvestmentProvider(ABC):
54
53
  access_token: str,
55
54
  start_date: date,
56
55
  end_date: date,
57
- account_ids: Optional[List[str]] = None,
58
- ) -> List[InvestmentTransaction]:
56
+ account_ids: list[str] | None = None,
57
+ ) -> list[InvestmentTransaction]:
59
58
  """Fetch investment transactions within date range.
60
59
 
61
60
  Args:
@@ -77,7 +76,7 @@ class InvestmentProvider(ABC):
77
76
  pass
78
77
 
79
78
  @abstractmethod
80
- async def get_securities(self, access_token: str, security_ids: List[str]) -> List[Security]:
79
+ async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
81
80
  """Fetch security details (ticker, name, type, current price).
82
81
 
83
82
  Args:
@@ -95,7 +94,7 @@ class InvestmentProvider(ABC):
95
94
  pass
96
95
 
97
96
  @abstractmethod
98
- async def get_investment_accounts(self, access_token: str) -> List[InvestmentAccount]:
97
+ async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
99
98
  """Fetch investment accounts with aggregated holdings.
100
99
 
101
100
  Args:
@@ -113,7 +112,7 @@ class InvestmentProvider(ABC):
113
112
 
114
113
  # Helper methods (concrete - shared across all providers)
115
114
 
116
- def calculate_allocation(self, holdings: List[Holding]) -> AssetAllocation:
115
+ def calculate_allocation(self, holdings: list[Holding]) -> AssetAllocation:
117
116
  """Calculate asset allocation by security type and sector.
118
117
 
119
118
  Groups holdings by security type (equity, bond, ETF, etc.) and calculates
@@ -183,7 +182,7 @@ class InvestmentProvider(ABC):
183
182
  cash_percent=cash_percent,
184
183
  )
185
184
 
186
- def calculate_portfolio_metrics(self, holdings: List[Holding]) -> dict:
185
+ def calculate_portfolio_metrics(self, holdings: list[Holding]) -> dict:
187
186
  """Calculate total value, cost basis, unrealized gain/loss.
188
187
 
189
188
  Aggregates holdings to calculate portfolio-level metrics.
@@ -10,21 +10,35 @@ from __future__ import annotations
10
10
 
11
11
  from datetime import date
12
12
  from decimal import Decimal
13
- from typing import Any, Dict, List, Optional, cast
14
-
15
- from plaid.api import plaid_api
16
- from plaid.model.investments_holdings_get_request import InvestmentsHoldingsGetRequest
17
- from plaid.model.investments_transactions_get_request import (
18
- InvestmentsTransactionsGetRequest,
19
- )
20
- from plaid.model.investments_holdings_get_response import InvestmentsHoldingsGetResponse
21
- from plaid.model.investments_transactions_get_response import (
22
- InvestmentsTransactionsGetResponse,
23
- )
24
- from plaid.exceptions import ApiException
25
- import plaid
26
- from plaid.api_client import ApiClient
27
- from plaid.configuration import Configuration
13
+ from typing import Any, cast
14
+
15
+ try:
16
+ import plaid
17
+ from plaid.api import plaid_api
18
+ from plaid.api_client import ApiClient
19
+ from plaid.configuration import Configuration
20
+ from plaid.exceptions import ApiException
21
+ from plaid.model.investments_holdings_get_request import InvestmentsHoldingsGetRequest
22
+ from plaid.model.investments_holdings_get_response import InvestmentsHoldingsGetResponse
23
+ from plaid.model.investments_transactions_get_request import (
24
+ InvestmentsTransactionsGetRequest,
25
+ )
26
+ from plaid.model.investments_transactions_get_response import (
27
+ InvestmentsTransactionsGetResponse,
28
+ )
29
+
30
+ HAS_PLAID = True
31
+ except ImportError: # pragma: no cover
32
+ HAS_PLAID = False
33
+ plaid_api = None
34
+ plaid = None
35
+ ApiClient = None
36
+ Configuration = None
37
+ ApiException = Exception
38
+ InvestmentsHoldingsGetRequest = Any
39
+ InvestmentsTransactionsGetRequest = Any
40
+ InvestmentsHoldingsGetResponse = Any
41
+ InvestmentsTransactionsGetResponse = Any
28
42
 
29
43
  from ..models import (
30
44
  Holding,
@@ -36,6 +50,15 @@ from ..models import (
36
50
  from .base import InvestmentProvider
37
51
 
38
52
 
53
+ def _require_plaid() -> None:
54
+ """Raise ImportError if plaid-python is not installed."""
55
+ if not HAS_PLAID:
56
+ raise ImportError(
57
+ "Plaid support requires the 'plaid-python' package. "
58
+ "Install with: pip install fin-infra[plaid] or pip install fin-infra[banking]"
59
+ )
60
+
61
+
39
62
  class PlaidInvestmentProvider(InvestmentProvider):
40
63
  """Plaid Investment API provider.
41
64
 
@@ -76,7 +99,10 @@ class PlaidInvestmentProvider(InvestmentProvider):
76
99
 
77
100
  Raises:
78
101
  ValueError: If client_id or secret is missing
102
+ ImportError: If plaid-python is not installed
79
103
  """
104
+ _require_plaid()
105
+
80
106
  if not client_id or not secret:
81
107
  raise ValueError("client_id and secret are required for Plaid provider")
82
108
 
@@ -102,11 +128,11 @@ class PlaidInvestmentProvider(InvestmentProvider):
102
128
  "development": plaid.Environment.Sandbox, # Map development to sandbox
103
129
  "production": plaid.Environment.Production,
104
130
  }
105
- return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
131
+ return cast("str", hosts.get(environment.lower(), plaid.Environment.Sandbox))
106
132
 
107
133
  async def get_holdings(
108
- self, access_token: str, account_ids: Optional[List[str]] = None
109
- ) -> List[Holding]:
134
+ self, access_token: str, account_ids: list[str] | None = None
135
+ ) -> list[Holding]:
110
136
  """Fetch investment holdings from Plaid.
111
137
 
112
138
  Retrieves holdings with security details, quantity, cost basis, and current value.
@@ -163,8 +189,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
163
189
  access_token: str,
164
190
  start_date: date,
165
191
  end_date: date,
166
- account_ids: Optional[List[str]] = None,
167
- ) -> List[InvestmentTransaction]:
192
+ account_ids: list[str] | None = None,
193
+ ) -> list[InvestmentTransaction]:
168
194
  """Fetch investment transactions from Plaid.
169
195
 
170
196
  Retrieves buy/sell/dividend transactions within the specified date range.
@@ -226,7 +252,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
226
252
  except ApiException as e:
227
253
  raise self._transform_error(e)
228
254
 
229
- async def get_securities(self, access_token: str, security_ids: List[str]) -> List[Security]:
255
+ async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
230
256
  """Fetch security details from Plaid holdings.
231
257
 
232
258
  Note: Plaid doesn't have a dedicated securities endpoint.
@@ -264,7 +290,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
264
290
  except ApiException as e:
265
291
  raise self._transform_error(e)
266
292
 
267
- async def get_investment_accounts(self, access_token: str) -> List[InvestmentAccount]:
293
+ async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
268
294
  """Fetch investment accounts with aggregated holdings.
269
295
 
270
296
  Returns accounts with total value, cost basis, and unrealized P&L.
@@ -295,7 +321,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
295
321
  }
296
322
 
297
323
  # Group holdings by account
298
- accounts_map: Dict[str, Dict[str, Any]] = {}
324
+ accounts_map: dict[str, dict[str, Any]] = {}
299
325
  for plaid_holding in response.holdings:
300
326
  holding_dict = plaid_holding.to_dict()
301
327
  account_id = holding_dict["account_id"]
@@ -347,7 +373,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
347
373
 
348
374
  # Helper methods for data transformation
349
375
 
350
- def _transform_security(self, plaid_security: Dict[str, Any]) -> Security:
376
+ def _transform_security(self, plaid_security: dict[str, Any]) -> Security:
351
377
  """Transform Plaid security data to Security model."""
352
378
  # Handle close_price - Plaid may return None for securities without recent pricing
353
379
  close_price_raw = plaid_security.get("close_price")
@@ -368,7 +394,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
368
394
  currency=plaid_security.get("iso_currency_code", "USD"),
369
395
  )
370
396
 
371
- def _transform_holding(self, plaid_holding: Dict[str, Any], security: Security) -> Holding:
397
+ def _transform_holding(self, plaid_holding: dict[str, Any], security: Security) -> Holding:
372
398
  """Transform Plaid holding data to Holding model."""
373
399
  return Holding(
374
400
  account_id=plaid_holding["account_id"],
@@ -384,7 +410,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
384
410
  )
385
411
 
386
412
  def _transform_transaction(
387
- self, plaid_transaction: Dict[str, Any], security: Security
413
+ self, plaid_transaction: dict[str, Any], security: Security
388
414
  ) -> InvestmentTransaction:
389
415
  """Transform Plaid investment transaction to InvestmentTransaction model."""
390
416
  # Map Plaid transaction type to our enum
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  from datetime import date
13
13
  from decimal import Decimal
14
- from typing import Any, Dict, List, Optional, cast
14
+ from typing import Any, cast
15
15
 
16
16
  import httpx
17
17
 
@@ -93,7 +93,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
93
93
  timeout=30.0,
94
94
  )
95
95
 
96
- def _auth_headers(self, user_id: str, user_secret: str) -> Dict[str, str]:
96
+ def _auth_headers(self, user_id: str, user_secret: str) -> dict[str, str]:
97
97
  """Build authentication headers for SnapTrade API requests.
98
98
 
99
99
  SECURITY: User secrets are passed in headers, NOT URL params.
@@ -115,8 +115,8 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
115
115
  async def get_holdings(
116
116
  self,
117
117
  access_token: str,
118
- account_ids: Optional[List[str]] = None,
119
- ) -> List[Holding]:
118
+ account_ids: list[str] | None = None,
119
+ ) -> list[Holding]:
120
120
  """Fetch investment holdings from SnapTrade.
121
121
 
122
122
  Note: SnapTrade uses user_id + user_secret, passed as access_token in format "user_id:user_secret"
@@ -171,15 +171,15 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
171
171
  except httpx.HTTPStatusError as e:
172
172
  raise self._transform_error(e)
173
173
  except Exception as e:
174
- raise ValueError(f"SnapTrade API error: {str(e)}")
174
+ raise ValueError(f"SnapTrade API error: {e!s}")
175
175
 
176
176
  async def get_transactions(
177
177
  self,
178
178
  access_token: str,
179
179
  start_date: date,
180
180
  end_date: date,
181
- account_ids: Optional[List[str]] = None,
182
- ) -> List[InvestmentTransaction]:
181
+ account_ids: list[str] | None = None,
182
+ ) -> list[InvestmentTransaction]:
183
183
  """Fetch investment transactions from SnapTrade.
184
184
 
185
185
  Args:
@@ -244,9 +244,9 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
244
244
  except httpx.HTTPStatusError as e:
245
245
  raise self._transform_error(e)
246
246
  except Exception as e:
247
- raise ValueError(f"SnapTrade API error: {str(e)}")
247
+ raise ValueError(f"SnapTrade API error: {e!s}")
248
248
 
249
- async def get_securities(self, access_token: str, security_ids: List[str]) -> List[Security]:
249
+ async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
250
250
  """Fetch security details from SnapTrade positions.
251
251
 
252
252
  Note: SnapTrade doesn't have a dedicated securities endpoint.
@@ -267,7 +267,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
267
267
  >>> for security in securities:
268
268
  ... print(f"{security.ticker_symbol}: ${security.close_price}")
269
269
  """
270
- user_id, user_secret = self._parse_access_token(access_token)
270
+ _user_id, _user_secret = self._parse_access_token(access_token)
271
271
 
272
272
  try:
273
273
  # Get all holdings to extract securities
@@ -282,9 +282,9 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
282
282
  return list(securities_map.values())
283
283
 
284
284
  except Exception as e:
285
- raise ValueError(f"SnapTrade API error: {str(e)}")
285
+ raise ValueError(f"SnapTrade API error: {e!s}")
286
286
 
287
- async def get_investment_accounts(self, access_token: str) -> List[InvestmentAccount]:
287
+ async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
288
288
  """Fetch investment accounts with aggregated holdings.
289
289
 
290
290
  Returns accounts with total value, cost basis, and unrealized P&L.
@@ -356,9 +356,9 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
356
356
  except httpx.HTTPStatusError as e:
357
357
  raise self._transform_error(e)
358
358
  except Exception as e:
359
- raise ValueError(f"SnapTrade API error: {str(e)}")
359
+ raise ValueError(f"SnapTrade API error: {e!s}")
360
360
 
361
- async def list_connections(self, access_token: str) -> List[Dict[str, Any]]:
361
+ async def list_connections(self, access_token: str) -> list[dict[str, Any]]:
362
362
  """List brokerage connections for a user.
363
363
 
364
364
  Returns which brokerages the user has connected (E*TRADE, Robinhood, etc.).
@@ -381,14 +381,14 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
381
381
  url = f"{self.base_url}/connections"
382
382
  response = await self.client.get(url, headers=auth_headers)
383
383
  response.raise_for_status()
384
- return cast(list[dict[str, Any]], await response.json())
384
+ return cast("list[dict[str, Any]]", await response.json())
385
385
 
386
386
  except httpx.HTTPStatusError as e:
387
387
  raise self._transform_error(e)
388
388
  except Exception as e:
389
- raise ValueError(f"SnapTrade API error: {str(e)}")
389
+ raise ValueError(f"SnapTrade API error: {e!s}")
390
390
 
391
- def get_brokerage_capabilities(self, brokerage_name: str) -> Dict[str, Any]:
391
+ def get_brokerage_capabilities(self, brokerage_name: str) -> dict[str, Any]:
392
392
  """Get capabilities for a specific brokerage.
393
393
 
394
394
  Important: Robinhood is READ-ONLY (no trading support).
@@ -480,7 +480,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
480
480
  except ValueError:
481
481
  raise ValueError("Invalid access_token format. Expected 'user_id:user_secret'")
482
482
 
483
- def _transform_holding(self, snaptrade_position: Dict[str, Any], account_id: str) -> Holding:
483
+ def _transform_holding(self, snaptrade_position: dict[str, Any], account_id: str) -> Holding:
484
484
  """Transform SnapTrade position data to Holding model."""
485
485
  symbol_data = snaptrade_position.get("symbol", {})
486
486
 
@@ -510,7 +510,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
510
510
  )
511
511
 
512
512
  def _transform_transaction(
513
- self, snaptrade_tx: Dict[str, Any], account_id: str
513
+ self, snaptrade_tx: dict[str, Any], account_id: str
514
514
  ) -> InvestmentTransaction:
515
515
  """Transform SnapTrade transaction to InvestmentTransaction model."""
516
516
  symbol_data = snaptrade_tx.get("symbol", {})
@@ -20,7 +20,10 @@ if TYPE_CHECKING:
20
20
  from fastapi import FastAPI
21
21
 
22
22
  from ..providers.base import MarketDataProvider
23
- from ..clients.base import MarketDataClient
23
+
24
+ # Deprecated: MarketDataClient alias for backward compatibility
25
+ # Use MarketDataProvider instead
26
+ MarketDataClient = MarketDataProvider # type: ignore[misc]
24
27
 
25
28
 
26
29
  def easy_market(
@@ -98,7 +101,7 @@ def easy_market(
98
101
 
99
102
 
100
103
  def add_market_data(
101
- app: "FastAPI",
104
+ app: FastAPI,
102
105
  *,
103
106
  provider: str | MarketDataProvider | None = None,
104
107
  prefix: str = "/market",
@@ -178,7 +181,6 @@ def add_market_data(
178
181
  See Also:
179
182
  - easy_market(): For standalone provider usage without FastAPI
180
183
  - docs/market-data.md: API documentation and examples
181
- - docs/adr/0004-market-data-integration.md: Architecture decisions
182
184
  """
183
185
  from fastapi import HTTPException, Query
184
186
 
@@ -1,21 +1,21 @@
1
1
  from .accounts import Account, AccountType
2
- from .transactions import Transaction
3
- from .quotes import Quote
4
- from .money import Money
5
- from .candle import Candle
6
- from .brokerage import Order, Position, PortfolioHistory
7
2
  from .brokerage import Account as BrokerageAccount # Avoid name conflict
3
+ from .brokerage import Order, PortfolioHistory, Position
4
+ from .candle import Candle
5
+ from .money import Money
6
+ from .quotes import Quote
8
7
  from .tax import (
8
+ CryptoTaxReport,
9
+ CryptoTransaction,
9
10
  TaxDocument,
10
- TaxFormW2,
11
- TaxForm1099INT,
12
- TaxForm1099DIV,
13
11
  TaxForm1099B,
12
+ TaxForm1099DIV,
13
+ TaxForm1099INT,
14
14
  TaxForm1099MISC,
15
- CryptoTransaction,
16
- CryptoTaxReport,
15
+ TaxFormW2,
17
16
  TaxLiability,
18
17
  )
18
+ from .transactions import Transaction
19
19
 
20
20
  __all__ = [
21
21
  "Account",
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from decimal import Decimal
6
5
  from datetime import datetime
6
+ from decimal import Decimal
7
7
  from typing import Literal
8
+
8
9
  from pydantic import BaseModel, Field
9
10
 
10
11
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from decimal import Decimal
4
+
4
5
  from pydantic import BaseModel, field_validator
5
6
 
6
7
 
fin_infra/models/money.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from decimal import Decimal
4
+
4
5
  from pydantic import BaseModel, field_validator
5
6
 
6
7
 
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import datetime, timezone
3
+ from datetime import UTC, datetime
4
4
  from decimal import Decimal
5
+
5
6
  from pydantic import BaseModel, field_validator
6
7
 
7
8
 
@@ -16,5 +17,5 @@ class Quote(BaseModel):
16
17
  def _ensure_tzaware(cls, v: datetime) -> datetime:
17
18
  # Normalize to timezone-aware (UTC) for consistency
18
19
  if v.tzinfo is None:
19
- return v.replace(tzinfo=timezone.utc)
20
- return v.astimezone(timezone.utc)
20
+ return v.replace(tzinfo=UTC)
21
+ return v.astimezone(UTC)
fin_infra/models/tax.py CHANGED
@@ -37,7 +37,8 @@ Example:
37
37
 
38
38
  from datetime import date, datetime
39
39
  from decimal import Decimal
40
- from pydantic import BaseModel, Field, ConfigDict
40
+
41
+ from pydantic import BaseModel, ConfigDict, Field
41
42
 
42
43
 
43
44
  class TaxDocument(BaseModel):
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import date # noqa: F401 - used in type annotation
3
+ from datetime import date
4
4
  from decimal import Decimal
5
- from typing import Optional
6
5
 
7
6
  from pydantic import BaseModel, field_validator
8
7
 
@@ -19,8 +18,8 @@ class Transaction(BaseModel):
19
18
  date: date
20
19
  amount: Decimal
21
20
  currency: str = "USD"
22
- description: Optional[str] = None
23
- category: Optional[str] = None
21
+ description: str | None = None
22
+ category: str | None = None
24
23
 
25
24
  @field_validator("amount", mode="before")
26
25
  @classmethod
@@ -4,6 +4,13 @@ Net Worth Tracking Module
4
4
  Calculates net worth by aggregating balances from multiple financial providers
5
5
  (banking, brokerage, crypto) with historical snapshots and change detection.
6
6
 
7
+ **Feature Status**:
8
+ ✅ STABLE: Core calculation (works with provided data)
9
+ ✅ STABLE: Banking integration (Plaid, Teller)
10
+ ⚠️ INTEGRATION: Brokerage integration (requires provider setup)
11
+ ⚠️ INTEGRATION: Crypto integration (requires provider setup)
12
+ ⚠️ INTEGRATION: Currency conversion (pass exchange_rate manually)
13
+
7
14
  **Key Features**:
8
15
  - Multi-provider aggregation (banking + brokerage + crypto)
9
16
  - Currency normalization (all currencies → USD)