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
@@ -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")
@@ -16,8 +16,8 @@ from typing import TYPE_CHECKING
16
16
  if TYPE_CHECKING:
17
17
  from fastapi import FastAPI
18
18
 
19
- from .models import Insight, InsightFeed, InsightPriority, InsightCategory
20
19
  from .aggregator import aggregate_insights, get_user_insights
20
+ from .models import Insight, InsightCategory, InsightFeed, InsightPriority
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
@@ -80,10 +80,10 @@ def add_insights(
80
80
  - Notification system for critical insights
81
81
  """
82
82
  from fastapi import Query
83
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
83
84
 
84
85
  # Import svc-infra user router (requires auth)
85
86
  from svc_infra.api.fastapi.dual.protected import user_router
86
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
87
87
 
88
88
  # Create router
89
89
  router = user_router(prefix=prefix, tags=["Insights"])
@@ -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
 
@@ -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
 
@@ -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 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,7 +29,7 @@ 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
32
+ self, access_token: str, account_ids: list[str] | None = None
34
33
  ) -> list[Holding]:
35
34
  """Fetch holdings for investment accounts.
36
35
 
@@ -54,7 +53,7 @@ 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,
56
+ account_ids: list[str] | None = None,
58
57
  ) -> list[InvestmentTransaction]:
59
58
  """Fetch investment transactions within date range.
60
59
 
@@ -10,22 +10,22 @@ from __future__ import annotations
10
10
 
11
11
  from datetime import date
12
12
  from decimal import Decimal
13
- from typing import Any, Optional, cast
13
+ from typing import Any, cast
14
14
 
15
15
  try:
16
+ import plaid
16
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
17
21
  from plaid.model.investments_holdings_get_request import InvestmentsHoldingsGetRequest
22
+ from plaid.model.investments_holdings_get_response import InvestmentsHoldingsGetResponse
18
23
  from plaid.model.investments_transactions_get_request import (
19
24
  InvestmentsTransactionsGetRequest,
20
25
  )
21
- from plaid.model.investments_holdings_get_response import InvestmentsHoldingsGetResponse
22
26
  from plaid.model.investments_transactions_get_response import (
23
27
  InvestmentsTransactionsGetResponse,
24
28
  )
25
- from plaid.exceptions import ApiException
26
- import plaid
27
- from plaid.api_client import ApiClient
28
- from plaid.configuration import Configuration
29
29
 
30
30
  HAS_PLAID = True
31
31
  except ImportError: # pragma: no cover
@@ -128,10 +128,10 @@ class PlaidInvestmentProvider(InvestmentProvider):
128
128
  "development": plaid.Environment.Sandbox, # Map development to sandbox
129
129
  "production": plaid.Environment.Production,
130
130
  }
131
- return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
131
+ return cast("str", hosts.get(environment.lower(), plaid.Environment.Sandbox))
132
132
 
133
133
  async def get_holdings(
134
- self, access_token: str, account_ids: Optional[list[str]] = None
134
+ self, access_token: str, account_ids: list[str] | None = None
135
135
  ) -> list[Holding]:
136
136
  """Fetch investment holdings from Plaid.
137
137
 
@@ -189,7 +189,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
189
189
  access_token: str,
190
190
  start_date: date,
191
191
  end_date: date,
192
- account_ids: Optional[list[str]] = None,
192
+ account_ids: list[str] | None = None,
193
193
  ) -> list[InvestmentTransaction]:
194
194
  """Fetch investment transactions from Plaid.
195
195
 
@@ -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, Optional, cast
14
+ from typing import Any, cast
15
15
 
16
16
  import httpx
17
17
 
@@ -115,7 +115,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
115
115
  async def get_holdings(
116
116
  self,
117
117
  access_token: str,
118
- account_ids: Optional[list[str]] = None,
118
+ account_ids: list[str] | None = None,
119
119
  ) -> list[Holding]:
120
120
  """Fetch investment holdings from SnapTrade.
121
121
 
@@ -171,14 +171,14 @@ 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,
181
+ account_ids: list[str] | None = None,
182
182
  ) -> list[InvestmentTransaction]:
183
183
  """Fetch investment transactions from SnapTrade.
184
184
 
@@ -244,7 +244,7 @@ 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
249
  async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
250
250
  """Fetch security details from SnapTrade positions.
@@ -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,7 +282,7 @@ 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
287
  async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
288
288
  """Fetch investment accounts with aggregated holdings.
@@ -356,7 +356,7 @@ 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
361
  async def list_connections(self, access_token: str) -> list[dict[str, Any]]:
362
362
  """List brokerage connections for a user.
@@ -381,12 +381,12 @@ 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
391
  def get_brokerage_capabilities(self, brokerage_name: str) -> dict[str, Any]:
392
392
  """Get capabilities for a specific brokerage.
@@ -101,7 +101,7 @@ def easy_market(
101
101
 
102
102
 
103
103
  def add_market_data(
104
- app: "FastAPI",
104
+ app: FastAPI,
105
105
  *,
106
106
  provider: str | MarketDataProvider | None = None,
107
107
  prefix: str = "/market",
@@ -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
@@ -31,7 +31,6 @@ from typing import Any
31
31
 
32
32
  from pydantic import BaseModel, Field
33
33
 
34
-
35
34
  # ============================================================================
36
35
  # Pydantic Schemas (Structured Output)
37
36
  # ============================================================================
@@ -116,11 +116,11 @@ def add_normalization(
116
116
  - Scoped docs at {prefix}/docs for standalone documentation
117
117
  """
118
118
  # Import FastAPI dependencies
119
- from fastapi import Query, HTTPException
119
+ from fastapi import HTTPException, Query
120
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
120
121
 
121
122
  # Import svc-infra public router (no auth - utility endpoints)
122
123
  from svc_infra.api.fastapi.dual.public import public_router
123
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
124
124
 
125
125
  # Get normalization services
126
126
  resolver, converter = easy_normalization(api_key=api_key)
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from datetime import date as DateType
5
- from typing import Optional, cast
5
+ from typing import cast
6
6
 
7
7
  import httpx
8
8
 
@@ -19,7 +19,7 @@ __all__ = [
19
19
  class ExchangeRateClient:
20
20
  """Client for exchangerate-api.io API."""
21
21
 
22
- def __init__(self, api_key: Optional[str] = None):
22
+ def __init__(self, api_key: str | None = None):
23
23
  """
24
24
  Initialize exchange rate client.
25
25
 
@@ -66,10 +66,10 @@ class ExchangeRateClient:
66
66
  raise ExchangeRateAPIError(
67
67
  f"API returned error: {data.get('error-type', 'unknown')}"
68
68
  )
69
- return cast(dict[str, float], data["conversion_rates"])
69
+ return cast("dict[str, float]", data["conversion_rates"])
70
70
  else:
71
71
  # Free tier response format
72
- return cast(dict[str, float], data["rates"])
72
+ return cast("dict[str, float]", data["rates"])
73
73
 
74
74
  except httpx.HTTPError as e:
75
75
  raise ExchangeRateAPIError(f"HTTP error fetching rates: {e}")
@@ -77,7 +77,7 @@ class ExchangeRateClient:
77
77
  raise ExchangeRateAPIError(f"Invalid API response: {e}")
78
78
 
79
79
  async def get_rate(
80
- self, from_currency: str, to_currency: str, date: Optional[DateType] = None
80
+ self, from_currency: str, to_currency: str, date: DateType | None = None
81
81
  ) -> ExchangeRate:
82
82
  """
83
83
  Get exchange rate between two currencies.
@@ -7,15 +7,15 @@ from typing import Any, cast
7
7
  try:
8
8
  import plaid
9
9
  from plaid.api import plaid_api
10
+ from plaid.model.accounts_balance_get_request import AccountsBalanceGetRequest
11
+ from plaid.model.accounts_get_request import AccountsGetRequest
10
12
  from plaid.model.country_code import CountryCode
13
+ from plaid.model.identity_get_request import IdentityGetRequest
11
14
  from plaid.model.item_public_token_exchange_request import ItemPublicTokenExchangeRequest
12
15
  from plaid.model.link_token_create_request import LinkTokenCreateRequest
13
16
  from plaid.model.link_token_create_request_user import LinkTokenCreateRequestUser
14
17
  from plaid.model.products import Products
15
18
  from plaid.model.transactions_get_request import TransactionsGetRequest
16
- from plaid.model.accounts_get_request import AccountsGetRequest
17
- from plaid.model.accounts_balance_get_request import AccountsBalanceGetRequest
18
- from plaid.model.identity_get_request import IdentityGetRequest
19
19
 
20
20
  PLAID_AVAILABLE = True
21
21
  except Exception: # pragma: no cover - dynamic import guard
@@ -97,7 +97,7 @@ class PlaidClient(BankingProvider):
97
97
  language="en",
98
98
  )
99
99
  response = self.client.link_token_create(request)
100
- return cast(str, response["link_token"])
100
+ return cast("str", response["link_token"])
101
101
 
102
102
  def exchange_public_token(self, public_token: str) -> dict:
103
103
  request = ItemPublicTokenExchangeRequest(public_token=public_token)
@@ -151,4 +151,4 @@ class PlaidClient(BankingProvider):
151
151
  """Fetch identity/account holder information."""
152
152
  request = IdentityGetRequest(access_token=access_token)
153
153
  response = self.client.identity_get(request)
154
- return cast(dict[Any, Any], response.to_dict())
154
+ return cast("dict[Any, Any]", response.to_dict())