fin-infra 0.1.62__py3-none-any.whl → 0.1.69__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/add.py +9 -11
- fin_infra/analytics/cash_flow.py +6 -5
- fin_infra/analytics/portfolio.py +13 -20
- fin_infra/analytics/rebalancing.py +2 -4
- fin_infra/analytics/savings.py +1 -1
- fin_infra/analytics/spending.py +15 -11
- fin_infra/banking/__init__.py +8 -5
- fin_infra/banking/history.py +3 -3
- fin_infra/banking/utils.py +93 -88
- fin_infra/brokerage/__init__.py +5 -3
- fin_infra/budgets/tracker.py +2 -3
- fin_infra/cashflows/__init__.py +6 -8
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +15 -16
- fin_infra/categorization/ease.py +3 -4
- fin_infra/categorization/engine.py +4 -4
- fin_infra/categorization/llm_layer.py +5 -6
- fin_infra/categorization/models.py +1 -1
- fin_infra/chat/__init__.py +7 -16
- fin_infra/chat/planning.py +57 -0
- fin_infra/cli/cmds/scaffold_cmds.py +1 -1
- fin_infra/compliance/__init__.py +3 -3
- fin_infra/credit/add.py +3 -2
- fin_infra/credit/experian/auth.py +3 -2
- fin_infra/credit/experian/client.py +2 -2
- fin_infra/credit/experian/provider.py +16 -16
- fin_infra/crypto/__init__.py +1 -1
- fin_infra/crypto/insights.py +1 -3
- fin_infra/documents/add.py +5 -5
- fin_infra/documents/ease.py +4 -3
- fin_infra/documents/models.py +3 -3
- fin_infra/documents/ocr.py +1 -1
- fin_infra/documents/storage.py +2 -1
- fin_infra/exceptions.py +1 -1
- fin_infra/goals/add.py +2 -2
- fin_infra/goals/management.py +6 -6
- fin_infra/goals/milestones.py +2 -2
- fin_infra/insights/__init__.py +7 -8
- fin_infra/investments/__init__.py +13 -8
- fin_infra/investments/add.py +39 -59
- fin_infra/investments/ease.py +16 -13
- fin_infra/investments/models.py +130 -64
- fin_infra/investments/providers/base.py +3 -8
- fin_infra/investments/providers/plaid.py +23 -34
- fin_infra/investments/providers/snaptrade.py +22 -40
- fin_infra/markets/__init__.py +11 -8
- fin_infra/models/accounts.py +2 -1
- fin_infra/models/transactions.py +3 -2
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +5 -4
- fin_infra/net_worth/calculator.py +8 -6
- fin_infra/net_worth/ease.py +36 -15
- fin_infra/net_worth/insights.py +4 -4
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +15 -13
- fin_infra/normalization/providers/exchangerate.py +3 -3
- fin_infra/obs/classifier.py +2 -2
- fin_infra/providers/banking/plaid_client.py +20 -19
- fin_infra/providers/banking/teller_client.py +13 -7
- fin_infra/providers/base.py +105 -13
- fin_infra/providers/brokerage/alpaca.py +7 -7
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/market/ccxt_crypto.py +8 -3
- fin_infra/providers/tax/mock.py +3 -3
- fin_infra/recurring/add.py +20 -9
- fin_infra/recurring/detector.py +1 -1
- fin_infra/recurring/detectors_llm.py +10 -9
- fin_infra/recurring/ease.py +1 -1
- fin_infra/recurring/insights.py +9 -8
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizer.py +3 -2
- fin_infra/recurring/normalizers.py +9 -8
- fin_infra/scaffold/__init__.py +1 -1
- fin_infra/security/encryption.py +2 -2
- fin_infra/security/pii_patterns.py +1 -1
- fin_infra/security/token_store.py +3 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/utils/http.py +3 -2
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/METADATA +1 -2
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/RECORD +83 -83
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/entry_points.txt +0 -0
fin_infra/insights/__init__.py
CHANGED
|
@@ -10,6 +10,11 @@ Aggregates insights from multiple sources:
|
|
|
10
10
|
- Cash flow projections
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from fastapi import FastAPI
|
|
17
|
+
|
|
13
18
|
from .models import Insight, InsightFeed, InsightPriority, InsightCategory
|
|
14
19
|
from .aggregator import aggregate_insights, get_user_insights
|
|
15
20
|
|
|
@@ -25,7 +30,7 @@ __all__ = [
|
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
def add_insights(
|
|
28
|
-
app: "FastAPI",
|
|
33
|
+
app: "FastAPI",
|
|
29
34
|
*,
|
|
30
35
|
prefix: str = "/insights",
|
|
31
36
|
) -> None:
|
|
@@ -71,11 +76,6 @@ def add_insights(
|
|
|
71
76
|
- Real-time aggregation from net worth, budgets, goals, etc.
|
|
72
77
|
- Notification system for critical insights
|
|
73
78
|
"""
|
|
74
|
-
from typing import TYPE_CHECKING
|
|
75
|
-
|
|
76
|
-
if TYPE_CHECKING:
|
|
77
|
-
from fastapi import FastAPI
|
|
78
|
-
|
|
79
79
|
from fastapi import Query
|
|
80
80
|
|
|
81
81
|
# Import svc-infra user router (requires auth)
|
|
@@ -125,5 +125,4 @@ def add_insights(
|
|
|
125
125
|
# Mount router
|
|
126
126
|
app.include_router(router, include_in_schema=True)
|
|
127
127
|
|
|
128
|
-
print(
|
|
129
|
-
|
|
128
|
+
print("✅ Insights feed enabled (unified financial insights)")
|
|
@@ -19,7 +19,7 @@ Example usage:
|
|
|
19
19
|
# Explicit provider
|
|
20
20
|
investments = easy_investments(provider="plaid")
|
|
21
21
|
holdings = await investments.get_holdings(access_token)
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
# Calculate metrics
|
|
24
24
|
allocation = investments.calculate_allocation(holdings)
|
|
25
25
|
metrics = investments.calculate_portfolio_metrics(holdings)
|
|
@@ -51,7 +51,8 @@ from typing import TYPE_CHECKING, Literal
|
|
|
51
51
|
if TYPE_CHECKING:
|
|
52
52
|
from fastapi import FastAPI
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Use the local InvestmentProvider base class (same as providers use)
|
|
55
|
+
from .providers.base import InvestmentProvider
|
|
55
56
|
|
|
56
57
|
# Lazy imports to avoid loading provider SDKs unless needed
|
|
57
58
|
_provider_cache: dict[str, InvestmentProvider] = {}
|
|
@@ -114,6 +115,7 @@ def easy_investments(
|
|
|
114
115
|
return _provider_cache[cache_key]
|
|
115
116
|
|
|
116
117
|
# Lazy import and initialize provider
|
|
118
|
+
instance: InvestmentProvider
|
|
117
119
|
if provider == "plaid":
|
|
118
120
|
from .providers.plaid import PlaidInvestmentProvider
|
|
119
121
|
|
|
@@ -123,9 +125,7 @@ def easy_investments(
|
|
|
123
125
|
|
|
124
126
|
instance = SnapTradeInvestmentProvider(**config)
|
|
125
127
|
else:
|
|
126
|
-
raise ValueError(
|
|
127
|
-
f"Unknown provider: {provider}. Supported: 'plaid', 'snaptrade'"
|
|
128
|
-
)
|
|
128
|
+
raise ValueError(f"Unknown provider: {provider}. Supported: 'plaid', 'snaptrade'")
|
|
129
129
|
|
|
130
130
|
_provider_cache[cache_key] = instance
|
|
131
131
|
return instance
|
|
@@ -172,14 +172,19 @@ def add_investments(
|
|
|
172
172
|
>>> # GET /investments/transactions
|
|
173
173
|
>>> # etc.
|
|
174
174
|
"""
|
|
175
|
-
from .add import add_investments_impl
|
|
175
|
+
from .add import add_investments as add_investments_impl
|
|
176
|
+
from .providers.base import InvestmentProvider as InvestmentProviderBase
|
|
177
|
+
|
|
178
|
+
# Resolve provider from string Literal to actual InvestmentProvider instance
|
|
179
|
+
resolved_provider: InvestmentProviderBase | None = None
|
|
180
|
+
if provider is not None:
|
|
181
|
+
resolved_provider = easy_investments(provider=provider, **provider_config)
|
|
176
182
|
|
|
177
183
|
return add_investments_impl(
|
|
178
184
|
app,
|
|
179
|
-
provider=
|
|
185
|
+
provider=resolved_provider,
|
|
180
186
|
prefix=prefix,
|
|
181
187
|
tags=tags or ["Investments"],
|
|
182
|
-
**provider_config,
|
|
183
188
|
)
|
|
184
189
|
|
|
185
190
|
|
fin_infra/investments/add.py
CHANGED
|
@@ -7,14 +7,13 @@ 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, Optional
|
|
11
11
|
|
|
12
|
-
from fastapi import HTTPException
|
|
12
|
+
from fastapi import HTTPException
|
|
13
13
|
from pydantic import BaseModel, Field
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from fastapi import FastAPI
|
|
17
|
-
from svc_infra.api.fastapi.auth.security import Principal
|
|
18
17
|
|
|
19
18
|
# Import Identity for dependency injection
|
|
20
19
|
try:
|
|
@@ -40,12 +39,8 @@ class HoldingsRequest(BaseModel):
|
|
|
40
39
|
|
|
41
40
|
access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
|
|
42
41
|
user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
43
|
-
user_secret: Optional[str] = Field(
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
account_ids: Optional[list[str]] = Field(
|
|
47
|
-
None, description="Filter by specific account IDs"
|
|
48
|
-
)
|
|
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")
|
|
49
44
|
|
|
50
45
|
|
|
51
46
|
class TransactionsRequest(BaseModel):
|
|
@@ -53,14 +48,10 @@ class TransactionsRequest(BaseModel):
|
|
|
53
48
|
|
|
54
49
|
access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
|
|
55
50
|
user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
56
|
-
user_secret: Optional[str] = Field(
|
|
57
|
-
None, description="SnapTrade user secret (SnapTrade only)"
|
|
58
|
-
)
|
|
51
|
+
user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
59
52
|
start_date: date = Field(..., description="Start date for transactions (YYYY-MM-DD)")
|
|
60
53
|
end_date: date = Field(..., description="End date for transactions (YYYY-MM-DD)")
|
|
61
|
-
account_ids: Optional[list[str]] = Field(
|
|
62
|
-
None, description="Filter by specific account IDs"
|
|
63
|
-
)
|
|
54
|
+
account_ids: Optional[list[str]] = Field(None, description="Filter by specific account IDs")
|
|
64
55
|
|
|
65
56
|
|
|
66
57
|
class AccountsRequest(BaseModel):
|
|
@@ -68,9 +59,7 @@ class AccountsRequest(BaseModel):
|
|
|
68
59
|
|
|
69
60
|
access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
|
|
70
61
|
user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
71
|
-
user_secret: Optional[str] = Field(
|
|
72
|
-
None, description="SnapTrade user secret (SnapTrade only)"
|
|
73
|
-
)
|
|
62
|
+
user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
74
63
|
|
|
75
64
|
|
|
76
65
|
class AllocationRequest(BaseModel):
|
|
@@ -78,12 +67,8 @@ class AllocationRequest(BaseModel):
|
|
|
78
67
|
|
|
79
68
|
access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
|
|
80
69
|
user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
81
|
-
user_secret: Optional[str] = Field(
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
account_ids: Optional[list[str]] = Field(
|
|
85
|
-
None, description="Filter by specific account IDs"
|
|
86
|
-
)
|
|
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")
|
|
87
72
|
|
|
88
73
|
|
|
89
74
|
class SecuritiesRequest(BaseModel):
|
|
@@ -91,9 +76,7 @@ class SecuritiesRequest(BaseModel):
|
|
|
91
76
|
|
|
92
77
|
access_token: Optional[str] = Field(None, description="Plaid access token (Plaid only)")
|
|
93
78
|
user_id: Optional[str] = Field(None, description="SnapTrade user ID (SnapTrade only)")
|
|
94
|
-
user_secret: Optional[str] = Field(
|
|
95
|
-
None, description="SnapTrade user secret (SnapTrade only)"
|
|
96
|
-
)
|
|
79
|
+
user_secret: Optional[str] = Field(None, description="SnapTrade user secret (SnapTrade only)")
|
|
97
80
|
security_ids: list[str] = Field(..., description="List of security IDs to retrieve")
|
|
98
81
|
|
|
99
82
|
|
|
@@ -156,13 +139,13 @@ def add_investments(
|
|
|
156
139
|
- Validates user's JWT/session cookie
|
|
157
140
|
- Ensures user is logged into YOUR application
|
|
158
141
|
- Provides identity.user with authenticated user
|
|
159
|
-
|
|
142
|
+
|
|
160
143
|
2. Provider Access (endpoint logic): Handled by these endpoints
|
|
161
144
|
- Gets Plaid/SnapTrade access token for the provider
|
|
162
145
|
- Auto-resolves from identity.user.banking_providers
|
|
163
146
|
- Can be overridden with explicit token in request body
|
|
164
147
|
- Used to call external provider APIs (Plaid, SnapTrade)
|
|
165
|
-
|
|
148
|
+
|
|
166
149
|
POST requests are used (not GET) because:
|
|
167
150
|
1. Provider credentials should not be in URL query parameters
|
|
168
151
|
2. Request bodies are more suitable for sensitive data
|
|
@@ -174,7 +157,7 @@ def add_investments(
|
|
|
174
157
|
|
|
175
158
|
# 2. Store on app state
|
|
176
159
|
app.state.investment_provider = provider
|
|
177
|
-
|
|
160
|
+
|
|
178
161
|
# 3. Capture provider in local variable for closure
|
|
179
162
|
investment_provider = provider
|
|
180
163
|
|
|
@@ -205,20 +188,19 @@ def add_investments(
|
|
|
205
188
|
access_token = f"{request.user_id}:{request.user_secret}"
|
|
206
189
|
else:
|
|
207
190
|
# Auto-resolve from authenticated user (user_router guarantees identity.user exists)
|
|
208
|
-
banking_providers = getattr(identity.user,
|
|
191
|
+
banking_providers = getattr(identity.user, "banking_providers", {})
|
|
209
192
|
if not banking_providers or "plaid" not in banking_providers:
|
|
210
193
|
raise HTTPException(
|
|
211
194
|
status_code=400,
|
|
212
|
-
detail="No Plaid connection found. Please connect your accounts first."
|
|
195
|
+
detail="No Plaid connection found. Please connect your accounts first.",
|
|
213
196
|
)
|
|
214
|
-
|
|
197
|
+
|
|
215
198
|
access_token = banking_providers["plaid"].get("access_token")
|
|
216
199
|
if not access_token:
|
|
217
200
|
raise HTTPException(
|
|
218
|
-
status_code=400,
|
|
219
|
-
detail="No access token found. Please reconnect your accounts."
|
|
201
|
+
status_code=400, detail="No access token found. Please reconnect your accounts."
|
|
220
202
|
)
|
|
221
|
-
|
|
203
|
+
|
|
222
204
|
# Call provider with resolved token
|
|
223
205
|
try:
|
|
224
206
|
holdings = await investment_provider.get_holdings(
|
|
@@ -237,7 +219,9 @@ def add_investments(
|
|
|
237
219
|
summary="List Transactions",
|
|
238
220
|
description="Fetch investment transactions (buy, sell, dividend, etc.) within date range",
|
|
239
221
|
)
|
|
240
|
-
async def get_transactions(
|
|
222
|
+
async def get_transactions(
|
|
223
|
+
request: TransactionsRequest, identity: Identity
|
|
224
|
+
) -> list[InvestmentTransaction]:
|
|
241
225
|
"""
|
|
242
226
|
Retrieve investment transactions for authenticated user's accounts.
|
|
243
227
|
|
|
@@ -257,18 +241,17 @@ def add_investments(
|
|
|
257
241
|
elif request.user_id and request.user_secret:
|
|
258
242
|
access_token = f"{request.user_id}:{request.user_secret}"
|
|
259
243
|
else:
|
|
260
|
-
banking_providers = getattr(identity.user,
|
|
244
|
+
banking_providers = getattr(identity.user, "banking_providers", {})
|
|
261
245
|
if not banking_providers or "plaid" not in banking_providers:
|
|
262
246
|
raise HTTPException(
|
|
263
247
|
status_code=400,
|
|
264
|
-
detail="No Plaid connection found. Please connect your accounts first."
|
|
248
|
+
detail="No Plaid connection found. Please connect your accounts first.",
|
|
265
249
|
)
|
|
266
|
-
|
|
250
|
+
|
|
267
251
|
access_token = banking_providers["plaid"].get("access_token")
|
|
268
252
|
if not access_token:
|
|
269
253
|
raise HTTPException(
|
|
270
|
-
status_code=400,
|
|
271
|
-
detail="No access token found. Please reconnect your accounts."
|
|
254
|
+
status_code=400, detail="No access token found. Please reconnect your accounts."
|
|
272
255
|
)
|
|
273
256
|
|
|
274
257
|
try:
|
|
@@ -302,18 +285,17 @@ def add_investments(
|
|
|
302
285
|
elif request.user_id and request.user_secret:
|
|
303
286
|
access_token = f"{request.user_id}:{request.user_secret}"
|
|
304
287
|
else:
|
|
305
|
-
banking_providers = getattr(identity.user,
|
|
288
|
+
banking_providers = getattr(identity.user, "banking_providers", {})
|
|
306
289
|
if not banking_providers or "plaid" not in banking_providers:
|
|
307
290
|
raise HTTPException(
|
|
308
291
|
status_code=400,
|
|
309
|
-
detail="No Plaid connection found. Please connect your accounts first."
|
|
292
|
+
detail="No Plaid connection found. Please connect your accounts first.",
|
|
310
293
|
)
|
|
311
|
-
|
|
294
|
+
|
|
312
295
|
access_token = banking_providers["plaid"].get("access_token")
|
|
313
296
|
if not access_token:
|
|
314
297
|
raise HTTPException(
|
|
315
|
-
status_code=400,
|
|
316
|
-
detail="No access token found. Please reconnect your accounts."
|
|
298
|
+
status_code=400, detail="No access token found. Please reconnect your accounts."
|
|
317
299
|
)
|
|
318
300
|
|
|
319
301
|
try:
|
|
@@ -344,20 +326,19 @@ def add_investments(
|
|
|
344
326
|
elif request.user_id and request.user_secret:
|
|
345
327
|
access_token = f"{request.user_id}:{request.user_secret}"
|
|
346
328
|
else:
|
|
347
|
-
banking_providers = getattr(identity.user,
|
|
329
|
+
banking_providers = getattr(identity.user, "banking_providers", {})
|
|
348
330
|
if not banking_providers or "plaid" not in banking_providers:
|
|
349
331
|
raise HTTPException(
|
|
350
332
|
status_code=400,
|
|
351
|
-
detail="No Plaid connection found. Please connect your accounts first."
|
|
333
|
+
detail="No Plaid connection found. Please connect your accounts first.",
|
|
352
334
|
)
|
|
353
|
-
|
|
335
|
+
|
|
354
336
|
access_token = banking_providers["plaid"].get("access_token")
|
|
355
337
|
if not access_token:
|
|
356
338
|
raise HTTPException(
|
|
357
|
-
status_code=400,
|
|
358
|
-
detail="No access token found. Please reconnect your accounts."
|
|
339
|
+
status_code=400, detail="No access token found. Please reconnect your accounts."
|
|
359
340
|
)
|
|
360
|
-
|
|
341
|
+
|
|
361
342
|
# Fetch holdings
|
|
362
343
|
try:
|
|
363
344
|
holdings = await investment_provider.get_holdings(
|
|
@@ -368,7 +349,7 @@ def add_investments(
|
|
|
368
349
|
raise HTTPException(status_code=401, detail=str(e))
|
|
369
350
|
except Exception as e:
|
|
370
351
|
raise HTTPException(status_code=500, detail=f"Failed to fetch allocation: {e}")
|
|
371
|
-
|
|
352
|
+
|
|
372
353
|
# Calculate allocation using base provider helper
|
|
373
354
|
allocation = investment_provider.calculate_allocation(holdings)
|
|
374
355
|
return allocation
|
|
@@ -391,18 +372,17 @@ def add_investments(
|
|
|
391
372
|
elif request.user_id and request.user_secret:
|
|
392
373
|
access_token = f"{request.user_id}:{request.user_secret}"
|
|
393
374
|
else:
|
|
394
|
-
banking_providers = getattr(identity.user,
|
|
375
|
+
banking_providers = getattr(identity.user, "banking_providers", {})
|
|
395
376
|
if not banking_providers or "plaid" not in banking_providers:
|
|
396
377
|
raise HTTPException(
|
|
397
378
|
status_code=400,
|
|
398
|
-
detail="No Plaid connection found. Please connect your accounts first."
|
|
379
|
+
detail="No Plaid connection found. Please connect your accounts first.",
|
|
399
380
|
)
|
|
400
|
-
|
|
381
|
+
|
|
401
382
|
access_token = banking_providers["plaid"].get("access_token")
|
|
402
383
|
if not access_token:
|
|
403
384
|
raise HTTPException(
|
|
404
|
-
status_code=400,
|
|
405
|
-
detail="No access token found. Please reconnect your accounts."
|
|
385
|
+
status_code=400, detail="No access token found. Please reconnect your accounts."
|
|
406
386
|
)
|
|
407
387
|
|
|
408
388
|
try:
|
fin_infra/investments/ease.py
CHANGED
|
@@ -24,13 +24,13 @@ def easy_investments(
|
|
|
24
24
|
|
|
25
25
|
Provider Selection Guide:
|
|
26
26
|
**Most apps should use BOTH providers for complete coverage:**
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
- **Plaid**: Traditional investment accounts (401k, IRA, bank brokerage)
|
|
29
29
|
- Coverage: 15,000+ institutions
|
|
30
30
|
- Best for: Employer retirement accounts, bank-connected investments
|
|
31
31
|
- Data freshness: Daily updates (usually overnight)
|
|
32
32
|
- Authentication: access_token from Plaid Link
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
- **SnapTrade**: Retail brokerage accounts (E*TRADE, Wealthsimple, Robinhood)
|
|
35
35
|
- Coverage: 125M+ accounts, 70+ brokerages
|
|
36
36
|
- Best for: User's EXISTING retail brokerage accounts
|
|
@@ -112,19 +112,18 @@ def easy_investments(
|
|
|
112
112
|
- Most other SnapTrade brokerages support trading operations
|
|
113
113
|
"""
|
|
114
114
|
# Auto-detect provider from environment if not specified
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
detected_provider: str | None = provider
|
|
116
|
+
if detected_provider is None:
|
|
117
|
+
detected_provider = _detect_provider()
|
|
117
118
|
|
|
118
119
|
# Validate provider
|
|
119
|
-
if
|
|
120
|
-
raise ValueError(
|
|
121
|
-
f"Invalid provider: {provider}. Must be 'plaid' or 'snaptrade'."
|
|
122
|
-
)
|
|
120
|
+
if detected_provider not in ("plaid", "snaptrade"):
|
|
121
|
+
raise ValueError(f"Invalid provider: {detected_provider}. Must be 'plaid' or 'snaptrade'.")
|
|
123
122
|
|
|
124
123
|
# Instantiate provider
|
|
125
|
-
if
|
|
124
|
+
if detected_provider == "plaid":
|
|
126
125
|
return _create_plaid_provider(**config)
|
|
127
|
-
elif
|
|
126
|
+
elif detected_provider == "snaptrade":
|
|
128
127
|
return _create_snaptrade_provider(**config)
|
|
129
128
|
|
|
130
129
|
# Should never reach here
|
|
@@ -192,8 +191,7 @@ def _create_plaid_provider(**config: Any) -> InvestmentProvider:
|
|
|
192
191
|
valid_envs = ("sandbox", "development", "production")
|
|
193
192
|
if environment not in valid_envs:
|
|
194
193
|
raise ValueError(
|
|
195
|
-
f"Invalid Plaid environment: {environment}. "
|
|
196
|
-
f"Must be one of: {', '.join(valid_envs)}"
|
|
194
|
+
f"Invalid Plaid environment: {environment}. Must be one of: {', '.join(valid_envs)}"
|
|
197
195
|
)
|
|
198
196
|
|
|
199
197
|
return PlaidInvestmentProvider(
|
|
@@ -233,10 +231,15 @@ def _create_snaptrade_provider(**config: Any) -> InvestmentProvider:
|
|
|
233
231
|
"Example: easy_investments(provider='snaptrade', client_id='...', consumer_key='...')"
|
|
234
232
|
)
|
|
235
233
|
|
|
234
|
+
# Ensure base_url is a string (default is set in SnapTradeInvestmentProvider)
|
|
235
|
+
resolved_base_url: str = (
|
|
236
|
+
base_url if isinstance(base_url, str) else "https://api.snaptrade.com/api/v1"
|
|
237
|
+
)
|
|
238
|
+
|
|
236
239
|
return SnapTradeInvestmentProvider(
|
|
237
240
|
client_id=client_id,
|
|
238
241
|
consumer_key=consumer_key,
|
|
239
|
-
base_url=
|
|
242
|
+
base_url=resolved_base_url,
|
|
240
243
|
)
|
|
241
244
|
|
|
242
245
|
|