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.
- fin_infra/analytics/__init__.py +3 -2
- fin_infra/analytics/add.py +21 -21
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +6 -6
- fin_infra/analytics/projections.py +1 -3
- fin_infra/banking/__init__.py +27 -27
- fin_infra/banking/history.py +4 -5
- fin_infra/banking/utils.py +19 -18
- fin_infra/brokerage/__init__.py +22 -24
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/cashflows/__init__.py +2 -2
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/engine.py +6 -6
- fin_infra/categorization/llm_layer.py +6 -5
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +5 -5
- fin_infra/chat/planning.py +0 -1
- fin_infra/cli/cmds/scaffold_cmds.py +10 -11
- fin_infra/clients/plaid.py +1 -1
- fin_infra/compliance/__init__.py +5 -5
- fin_infra/credit/add.py +6 -7
- fin_infra/credit/experian/auth.py +2 -2
- fin_infra/credit/experian/client.py +1 -1
- fin_infra/credit/experian/provider.py +4 -4
- fin_infra/crypto/__init__.py +7 -9
- fin_infra/documents/add.py +6 -8
- fin_infra/documents/analysis.py +8 -8
- fin_infra/documents/ease.py +14 -14
- fin_infra/documents/ocr.py +7 -7
- fin_infra/documents/storage.py +21 -13
- fin_infra/exceptions.py +0 -1
- fin_infra/goals/__init__.py +8 -8
- fin_infra/goals/add.py +30 -30
- fin_infra/goals/funding.py +1 -1
- fin_infra/goals/management.py +2 -3
- fin_infra/goals/milestones.py +1 -2
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +2 -2
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/providers/base.py +2 -3
- fin_infra/investments/providers/plaid.py +9 -9
- fin_infra/investments/providers/snaptrade.py +10 -10
- fin_infra/markets/__init__.py +1 -1
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/brokerage.py +2 -1
- fin_infra/models/candle.py +1 -0
- fin_infra/models/money.py +1 -0
- fin_infra/models/quotes.py +4 -3
- fin_infra/models/tax.py +2 -1
- fin_infra/models/transactions.py +3 -4
- fin_infra/net_worth/insights.py +0 -1
- fin_infra/normalization/__init__.py +2 -2
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/providers/banking/plaid_client.py +5 -5
- fin_infra/providers/banking/teller_client.py +7 -6
- fin_infra/providers/base.py +1 -1
- fin_infra/providers/brokerage/alpaca.py +3 -3
- fin_infra/providers/market/alphavantage.py +5 -10
- fin_infra/providers/market/ccxt_crypto.py +2 -2
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +5 -5
- fin_infra/providers/tax/__init__.py +1 -1
- fin_infra/providers/tax/irs.py +1 -1
- fin_infra/providers/tax/mock.py +5 -5
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +5 -4
- fin_infra/recurring/detector.py +7 -7
- fin_infra/recurring/detectors_llm.py +6 -6
- fin_infra/recurring/ease.py +2 -4
- fin_infra/recurring/insights.py +13 -13
- fin_infra/recurring/normalizer.py +1 -1
- fin_infra/recurring/normalizers.py +4 -4
- fin_infra/recurring/summary.py +4 -6
- fin_infra/scaffold/budgets.py +6 -6
- fin_infra/scaffold/goals.py +1 -1
- fin_infra/security/__init__.py +8 -8
- fin_infra/security/encryption.py +6 -6
- fin_infra/security/models.py +7 -7
- fin_infra/security/pii_filter.py +6 -6
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +4 -3
- fin_infra/utils/retry.py +1 -1
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/METADATA +1 -1
- fin_infra-0.1.83.dist-info/RECORD +180 -0
- fin_infra-0.1.81.dist-info/RECORD +0 -180
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/entry_points.txt +0 -0
fin_infra/goals/management.py
CHANGED
|
@@ -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:
|
fin_infra/goals/milestones.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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")
|
fin_infra/insights/__init__.py
CHANGED
|
@@ -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"])
|
fin_infra/insights/aggregator.py
CHANGED
|
@@ -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}:{
|
|
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
|
|
fin_infra/investments/add.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
41
|
-
user_id:
|
|
42
|
-
user_secret:
|
|
43
|
-
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:
|
|
50
|
-
user_id:
|
|
51
|
-
user_secret:
|
|
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:
|
|
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:
|
|
61
|
-
user_id:
|
|
62
|
-
user_secret:
|
|
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:
|
|
69
|
-
user_id:
|
|
70
|
-
user_secret:
|
|
71
|
-
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:
|
|
78
|
-
user_id:
|
|
79
|
-
user_secret:
|
|
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:
|
|
86
|
+
provider: InvestmentProvider | None = None,
|
|
87
87
|
include_in_schema: bool = True,
|
|
88
|
-
tags:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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: {
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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.
|
fin_infra/markets/__init__.py
CHANGED
fin_infra/models/__init__.py
CHANGED
|
@@ -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
|
-
|
|
16
|
-
CryptoTaxReport,
|
|
15
|
+
TaxFormW2,
|
|
17
16
|
TaxLiability,
|
|
18
17
|
)
|
|
18
|
+
from .transactions import Transaction
|
|
19
19
|
|
|
20
20
|
__all__ = [
|
|
21
21
|
"Account",
|
fin_infra/models/brokerage.py
CHANGED
fin_infra/models/candle.py
CHANGED
fin_infra/models/money.py
CHANGED
fin_infra/models/quotes.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import
|
|
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=
|
|
20
|
-
return v.astimezone(
|
|
20
|
+
return v.replace(tzinfo=UTC)
|
|
21
|
+
return v.astimezone(UTC)
|
fin_infra/models/tax.py
CHANGED
fin_infra/models/transactions.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import date
|
|
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:
|
|
23
|
-
category:
|
|
21
|
+
description: str | None = None
|
|
22
|
+
category: str | None = None
|
|
24
23
|
|
|
25
24
|
@field_validator("amount", mode="before")
|
|
26
25
|
@classmethod
|
fin_infra/net_worth/insights.py
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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:
|
|
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())
|