fin-infra 0.1.79__py3-none-any.whl → 0.1.81__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 +10 -0
- fin_infra/banking/__init__.py +0 -1
- fin_infra/banking/history.py +5 -5
- fin_infra/banking/utils.py +9 -9
- fin_infra/budgets/alerts.py +3 -3
- fin_infra/budgets/tracker.py +2 -2
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/llm_layer.py +2 -2
- fin_infra/clients/__init__.py +23 -1
- fin_infra/clients/base.py +1 -1
- fin_infra/clients/plaid.py +1 -1
- fin_infra/compliance/__init__.py +2 -1
- fin_infra/documents/analysis.py +2 -2
- fin_infra/documents/models.py +4 -4
- fin_infra/documents/ocr.py +2 -2
- fin_infra/documents/storage.py +2 -2
- fin_infra/goals/add.py +9 -9
- fin_infra/goals/funding.py +3 -5
- fin_infra/investments/models.py +5 -5
- fin_infra/investments/providers/base.py +9 -9
- fin_infra/investments/providers/plaid.py +11 -11
- fin_infra/investments/providers/snaptrade.py +12 -12
- fin_infra/markets/__init__.py +4 -2
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/obs/classifier.py +1 -1
- fin_infra/providers/base.py +27 -2
- fin_infra/providers/market/alphavantage.py +1 -1
- fin_infra/providers/market/yahoo.py +1 -1
- fin_infra/recurring/summary.py +11 -11
- fin_infra/scaffold/budgets.py +4 -4
- fin_infra/scaffold/goals.py +4 -4
- fin_infra/security/encryption.py +4 -4
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.79.dist-info → fin_infra-0.1.81.dist-info}/METADATA +1 -1
- {fin_infra-0.1.79.dist-info → fin_infra-0.1.81.dist-info}/RECORD +38 -38
- {fin_infra-0.1.79.dist-info → fin_infra-0.1.81.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.79.dist-info → fin_infra-0.1.81.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.79.dist-info → fin_infra-0.1.81.dist-info}/entry_points.txt +0 -0
fin_infra/analytics/__init__.py
CHANGED
|
@@ -7,6 +7,16 @@ This module provides comprehensive financial analytics capabilities including:
|
|
|
7
7
|
- Portfolio analytics (returns, allocation, benchmarking)
|
|
8
8
|
- Growth projections (net worth forecasting with scenarios)
|
|
9
9
|
|
|
10
|
+
Feature Status:
|
|
11
|
+
✅ STABLE: Core calculation functions (all analytics work with provided data)
|
|
12
|
+
⚠️ INTEGRATION: Auto-fetching from providers requires setup:
|
|
13
|
+
- Banking provider for transaction data
|
|
14
|
+
- Brokerage provider for investment data
|
|
15
|
+
- Categorization for expense categorization
|
|
16
|
+
|
|
17
|
+
When providers aren't configured, functions accept data directly or return
|
|
18
|
+
sensible placeholder values for testing/development.
|
|
19
|
+
|
|
10
20
|
Serves multiple use cases:
|
|
11
21
|
- Personal finance apps (cash flow, savings tracking)
|
|
12
22
|
- Wealth management platforms (portfolio analytics, projections)
|
fin_infra/banking/__init__.py
CHANGED
|
@@ -174,7 +174,6 @@ def easy_banking(provider: str = "teller", **config) -> BankingProvider:
|
|
|
174
174
|
See Also:
|
|
175
175
|
- add_banking(): For FastAPI integration with routes
|
|
176
176
|
- docs/banking.md: Comprehensive banking integration guide
|
|
177
|
-
- docs/adr/0003-banking-integration.md: Architecture decisions
|
|
178
177
|
"""
|
|
179
178
|
# Auto-detect provider config from environment if not explicitly provided
|
|
180
179
|
# Only auto-detect if no config params were passed
|
fin_infra/banking/history.py
CHANGED
|
@@ -42,7 +42,7 @@ from __future__ import annotations
|
|
|
42
42
|
import logging
|
|
43
43
|
import os
|
|
44
44
|
from datetime import date, datetime, timedelta
|
|
45
|
-
from typing import
|
|
45
|
+
from typing import Optional
|
|
46
46
|
from pydantic import BaseModel, Field, ConfigDict
|
|
47
47
|
|
|
48
48
|
|
|
@@ -59,7 +59,7 @@ _logger = logging.getLogger(__name__)
|
|
|
59
59
|
|
|
60
60
|
# In-memory storage for testing (will be replaced with SQL database in production)
|
|
61
61
|
# ⚠️ WARNING: All data is LOST on restart when using in-memory storage!
|
|
62
|
-
_balance_snapshots:
|
|
62
|
+
_balance_snapshots: list[BalanceSnapshot] = []
|
|
63
63
|
_production_warning_logged = False
|
|
64
64
|
|
|
65
65
|
|
|
@@ -157,7 +157,7 @@ def get_balance_history(
|
|
|
157
157
|
days: int = 90,
|
|
158
158
|
start_date: Optional[date] = None,
|
|
159
159
|
end_date: Optional[date] = None,
|
|
160
|
-
) ->
|
|
160
|
+
) -> list[BalanceSnapshot]:
|
|
161
161
|
"""Get balance history for an account.
|
|
162
162
|
|
|
163
163
|
Retrieves balance snapshots for the specified account within a date range.
|
|
@@ -216,8 +216,8 @@ def get_balance_history(
|
|
|
216
216
|
|
|
217
217
|
def get_balance_snapshots(
|
|
218
218
|
account_id: str,
|
|
219
|
-
dates:
|
|
220
|
-
) ->
|
|
219
|
+
dates: list[date],
|
|
220
|
+
) -> list[BalanceSnapshot]:
|
|
221
221
|
"""Get balance snapshots for specific dates.
|
|
222
222
|
|
|
223
223
|
Args:
|
fin_infra/banking/utils.py
CHANGED
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import re
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Optional, Literal
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field
|
|
14
14
|
|
|
15
15
|
from ..providers.base import BankingProvider
|
|
@@ -179,7 +179,7 @@ def validate_provider_token(provider: str, access_token: str) -> bool:
|
|
|
179
179
|
return validator(access_token)
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def parse_banking_providers(banking_providers:
|
|
182
|
+
def parse_banking_providers(banking_providers: dict[str, Any]) -> BankingConnectionStatus:
|
|
183
183
|
"""
|
|
184
184
|
Parse banking_providers JSON field into structured status.
|
|
185
185
|
|
|
@@ -257,7 +257,7 @@ def parse_banking_providers(banking_providers: Dict[str, Any]) -> BankingConnect
|
|
|
257
257
|
return status
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def sanitize_connection_status(status: BankingConnectionStatus) ->
|
|
260
|
+
def sanitize_connection_status(status: BankingConnectionStatus) -> dict[str, Any]:
|
|
261
261
|
"""
|
|
262
262
|
Sanitize connection status for API responses (removes access tokens).
|
|
263
263
|
|
|
@@ -298,10 +298,10 @@ def sanitize_connection_status(status: BankingConnectionStatus) -> Dict[str, Any
|
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
def mark_connection_unhealthy(
|
|
301
|
-
banking_providers:
|
|
301
|
+
banking_providers: dict[str, Any],
|
|
302
302
|
provider: str,
|
|
303
303
|
error_message: str,
|
|
304
|
-
) ->
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
305
|
"""
|
|
306
306
|
Mark a provider connection as unhealthy (for error handling).
|
|
307
307
|
|
|
@@ -335,9 +335,9 @@ def mark_connection_unhealthy(
|
|
|
335
335
|
|
|
336
336
|
|
|
337
337
|
def mark_connection_healthy(
|
|
338
|
-
banking_providers:
|
|
338
|
+
banking_providers: dict[str, Any],
|
|
339
339
|
provider: str,
|
|
340
|
-
) ->
|
|
340
|
+
) -> dict[str, Any]:
|
|
341
341
|
"""
|
|
342
342
|
Mark a provider connection as healthy (after successful sync).
|
|
343
343
|
|
|
@@ -368,7 +368,7 @@ def mark_connection_healthy(
|
|
|
368
368
|
|
|
369
369
|
|
|
370
370
|
def get_primary_access_token(
|
|
371
|
-
banking_providers:
|
|
371
|
+
banking_providers: dict[str, Any],
|
|
372
372
|
) -> tuple[Optional[str], Optional[str]]:
|
|
373
373
|
"""
|
|
374
374
|
Get the primary access token and provider name.
|
|
@@ -437,7 +437,7 @@ async def test_connection_health(
|
|
|
437
437
|
return False, error_msg
|
|
438
438
|
|
|
439
439
|
|
|
440
|
-
def should_refresh_token(banking_providers:
|
|
440
|
+
def should_refresh_token(banking_providers: dict[str, Any], provider: str) -> bool:
|
|
441
441
|
"""
|
|
442
442
|
Check if a provider token should be refreshed.
|
|
443
443
|
|
fin_infra/budgets/alerts.py
CHANGED
|
@@ -35,7 +35,7 @@ Example:
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
37
|
from datetime import datetime
|
|
38
|
-
from typing import TYPE_CHECKING,
|
|
38
|
+
from typing import TYPE_CHECKING, Optional
|
|
39
39
|
|
|
40
40
|
from fin_infra.budgets.models import (
|
|
41
41
|
AlertSeverity,
|
|
@@ -52,7 +52,7 @@ async def check_budget_alerts(
|
|
|
52
52
|
budget_id: str,
|
|
53
53
|
tracker: BudgetTracker,
|
|
54
54
|
thresholds: Optional[dict[str, float]] = None,
|
|
55
|
-
) ->
|
|
55
|
+
) -> list[BudgetAlert]:
|
|
56
56
|
"""
|
|
57
57
|
Check budget for alerts (overspending, approaching limits, unusual patterns).
|
|
58
58
|
|
|
@@ -111,7 +111,7 @@ async def check_budget_alerts(
|
|
|
111
111
|
# Get budget progress
|
|
112
112
|
progress = await tracker.get_budget_progress(budget_id)
|
|
113
113
|
|
|
114
|
-
alerts:
|
|
114
|
+
alerts: list[BudgetAlert] = []
|
|
115
115
|
|
|
116
116
|
# Check each category for alerts
|
|
117
117
|
for category in progress.categories:
|
fin_infra/budgets/tracker.py
CHANGED
|
@@ -36,7 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
|
|
37
37
|
import uuid
|
|
38
38
|
from datetime import datetime, timedelta
|
|
39
|
-
from typing import TYPE_CHECKING,
|
|
39
|
+
from typing import TYPE_CHECKING, Optional
|
|
40
40
|
|
|
41
41
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
42
42
|
|
|
@@ -206,7 +206,7 @@ class BudgetTracker:
|
|
|
206
206
|
self,
|
|
207
207
|
user_id: str,
|
|
208
208
|
type: Optional[str] = None,
|
|
209
|
-
) ->
|
|
209
|
+
) -> list[Budget]:
|
|
210
210
|
"""
|
|
211
211
|
Get all budgets for a user.
|
|
212
212
|
|
fin_infra/cashflows/core.py
CHANGED
|
@@ -15,7 +15,7 @@ Expected performance:
|
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import logging
|
|
18
|
-
from typing import Any,
|
|
18
|
+
from typing import Any, Optional, cast
|
|
19
19
|
from pydantic import BaseModel, Field
|
|
20
20
|
|
|
21
21
|
# ai-infra imports
|
|
@@ -40,7 +40,7 @@ class CategoryPrediction(BaseModel):
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
# Few-shot examples (20 diverse merchants covering all major categories)
|
|
43
|
-
FEW_SHOT_EXAMPLES:
|
|
43
|
+
FEW_SHOT_EXAMPLES: list[tuple[str, str, str]] = [
|
|
44
44
|
# Food & Dining (5 examples)
|
|
45
45
|
("STARBUCKS #1234", "Coffee Shops", "Popular coffee shop chain"),
|
|
46
46
|
("MCDONALD'S", "Fast Food", "Fast food restaurant"),
|
fin_infra/clients/__init__.py
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
"""DEPRECATED: Use fin_infra.providers instead.
|
|
2
|
+
|
|
3
|
+
This module is deprecated and will be removed in a future version.
|
|
4
|
+
All ABCs have been consolidated into fin_infra.providers.base.
|
|
5
|
+
|
|
6
|
+
Migration:
|
|
7
|
+
# Old (deprecated)
|
|
8
|
+
from fin_infra.clients import BankingClient, MarketDataClient
|
|
9
|
+
|
|
10
|
+
# New
|
|
11
|
+
from fin_infra.providers.base import BankingProvider, MarketDataProvider
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
from .base import BankingClient, CreditClient, MarketDataClient
|
|
17
|
+
|
|
18
|
+
warnings.warn(
|
|
19
|
+
"fin_infra.clients is deprecated. Use fin_infra.providers instead. "
|
|
20
|
+
"This module will be removed in a future version.",
|
|
21
|
+
DeprecationWarning,
|
|
22
|
+
stacklevel=2,
|
|
23
|
+
)
|
|
2
24
|
|
|
3
25
|
__all__ = ["BankingClient", "MarketDataClient", "CreditClient"]
|
fin_infra/clients/base.py
CHANGED
fin_infra/clients/plaid.py
CHANGED
fin_infra/compliance/__init__.py
CHANGED
|
@@ -21,7 +21,8 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
23
|
from datetime import datetime
|
|
24
|
-
from typing import Any,
|
|
24
|
+
from typing import Any, TYPE_CHECKING, cast
|
|
25
|
+
from collections.abc import Callable
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
28
|
from fastapi import FastAPI, Request, Response
|
fin_infra/documents/analysis.py
CHANGED
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import re
|
|
26
26
|
from datetime import datetime
|
|
27
|
-
from typing import TYPE_CHECKING
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
from .models import DocumentAnalysis
|
|
33
33
|
|
|
34
34
|
# In-memory analysis cache (production: use svc-infra cache)
|
|
35
|
-
_analysis_cache:
|
|
35
|
+
_analysis_cache: dict[str, "DocumentAnalysis"] = {}
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
async def analyze_document(
|
fin_infra/documents/models.py
CHANGED
|
@@ -31,7 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
from datetime import datetime
|
|
33
33
|
from enum import Enum
|
|
34
|
-
from typing import
|
|
34
|
+
from typing import Optional
|
|
35
35
|
|
|
36
36
|
from pydantic import BaseModel, ConfigDict, Field
|
|
37
37
|
from svc_infra.documents import Document as BaseDocument
|
|
@@ -145,7 +145,7 @@ class OCRResult(BaseModel):
|
|
|
145
145
|
confidence: float = Field(
|
|
146
146
|
..., description="Overall OCR confidence score (0.0-1.0)", ge=0.0, le=1.0
|
|
147
147
|
)
|
|
148
|
-
fields_extracted:
|
|
148
|
+
fields_extracted: dict[str, str] = Field(
|
|
149
149
|
default_factory=dict,
|
|
150
150
|
description="Structured fields extracted from document (names, amounts, dates)",
|
|
151
151
|
)
|
|
@@ -181,10 +181,10 @@ class DocumentAnalysis(BaseModel):
|
|
|
181
181
|
|
|
182
182
|
document_id: str = Field(..., description="Document that was analyzed")
|
|
183
183
|
summary: str = Field(..., description="High-level document summary")
|
|
184
|
-
key_findings:
|
|
184
|
+
key_findings: list[str] = Field(
|
|
185
185
|
default_factory=list, description="Important facts extracted from document"
|
|
186
186
|
)
|
|
187
|
-
recommendations:
|
|
187
|
+
recommendations: list[str] = Field(
|
|
188
188
|
default_factory=list, description="Action items or suggestions based on document content"
|
|
189
189
|
)
|
|
190
190
|
analysis_date: datetime = Field(
|
fin_infra/documents/ocr.py
CHANGED
|
@@ -25,7 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import re
|
|
27
27
|
from datetime import datetime
|
|
28
|
-
from typing import TYPE_CHECKING,
|
|
28
|
+
from typing import TYPE_CHECKING, Optional
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
|
|
|
33
33
|
from .models import OCRResult
|
|
34
34
|
|
|
35
35
|
# In-memory OCR cache (production: use svc-infra cache)
|
|
36
|
-
_ocr_cache:
|
|
36
|
+
_ocr_cache: dict[str, "OCRResult"] = {}
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
async def extract_text(
|
fin_infra/documents/storage.py
CHANGED
|
@@ -36,7 +36,7 @@ Quick Start:
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
-
from typing import TYPE_CHECKING,
|
|
39
|
+
from typing import TYPE_CHECKING, Optional
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
42
|
from svc_infra.documents import (
|
|
@@ -242,7 +242,7 @@ def list_documents(
|
|
|
242
242
|
tax_year: Optional[int] = None,
|
|
243
243
|
limit: int = 100,
|
|
244
244
|
offset: int = 0,
|
|
245
|
-
) ->
|
|
245
|
+
) -> list["FinancialDocument"]:
|
|
246
246
|
"""
|
|
247
247
|
List user's financial documents with optional filters (delegates to svc-infra).
|
|
248
248
|
|
fin_infra/goals/add.py
CHANGED
|
@@ -29,7 +29,7 @@ add_goals(app)
|
|
|
29
29
|
|
|
30
30
|
import logging
|
|
31
31
|
from datetime import datetime
|
|
32
|
-
from typing import Any,
|
|
32
|
+
from typing import Any, Optional, cast
|
|
33
33
|
|
|
34
34
|
from fastapi import FastAPI, HTTPException, status, Query, Body
|
|
35
35
|
from pydantic import BaseModel, Field
|
|
@@ -89,7 +89,7 @@ class CreateGoalRequest(BaseModel):
|
|
|
89
89
|
description: Optional[str] = Field(None, description="Goal description")
|
|
90
90
|
current_amount: Optional[float] = Field(0.0, ge=0, description="Current amount")
|
|
91
91
|
auto_contribute: Optional[bool] = Field(False, description="Auto-contribute enabled")
|
|
92
|
-
tags: Optional[
|
|
92
|
+
tags: Optional[list[str]] = Field(None, description="Goal tags")
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class UpdateGoalRequest(BaseModel):
|
|
@@ -102,7 +102,7 @@ class UpdateGoalRequest(BaseModel):
|
|
|
102
102
|
current_amount: Optional[float] = Field(None, ge=0)
|
|
103
103
|
status: Optional[GoalStatus] = None
|
|
104
104
|
auto_contribute: Optional[bool] = None
|
|
105
|
-
tags: Optional[
|
|
105
|
+
tags: Optional[list[str]] = None
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
class AddMilestoneRequest(BaseModel):
|
|
@@ -238,14 +238,14 @@ def add_goals(
|
|
|
238
238
|
except ValueError as e:
|
|
239
239
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
240
240
|
|
|
241
|
-
@router.get("", response_model=
|
|
241
|
+
@router.get("", response_model=list[dict])
|
|
242
242
|
async def list_goals_endpoint(
|
|
243
243
|
user_id: Optional[str] = Query(
|
|
244
244
|
None, description="User identifier (optional, returns all if not provided)"
|
|
245
245
|
),
|
|
246
246
|
goal_type: Optional[str] = Query(None, description="Filter by goal type"),
|
|
247
247
|
status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
|
|
248
|
-
) ->
|
|
248
|
+
) -> list[dict]:
|
|
249
249
|
"""
|
|
250
250
|
List all goals for a user with optional filters.
|
|
251
251
|
|
|
@@ -442,8 +442,8 @@ def add_goals(
|
|
|
442
442
|
except ValueError as e:
|
|
443
443
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
444
444
|
|
|
445
|
-
@router.get("/{goal_id}/milestones", response_model=
|
|
446
|
-
async def list_milestones_endpoint(goal_id: str) ->
|
|
445
|
+
@router.get("/{goal_id}/milestones", response_model=list[dict])
|
|
446
|
+
async def list_milestones_endpoint(goal_id: str) -> list[dict]:
|
|
447
447
|
"""
|
|
448
448
|
List all milestones for a goal.
|
|
449
449
|
|
|
@@ -540,8 +540,8 @@ def add_goals(
|
|
|
540
540
|
except ValueError as e:
|
|
541
541
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
542
542
|
|
|
543
|
-
@router.get("/{goal_id}/funding", response_model=
|
|
544
|
-
async def list_funding_sources_endpoint(goal_id: str) ->
|
|
543
|
+
@router.get("/{goal_id}/funding", response_model=list[dict])
|
|
544
|
+
async def list_funding_sources_endpoint(goal_id: str) -> list[dict]:
|
|
545
545
|
"""
|
|
546
546
|
List all funding sources for a goal.
|
|
547
547
|
|
fin_infra/goals/funding.py
CHANGED
|
@@ -22,14 +22,12 @@ Example:
|
|
|
22
22
|
>>> # Raises ValueError if total allocation > 100%
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
from typing import Dict, List
|
|
26
|
-
|
|
27
25
|
from fin_infra.goals.models import FundingSource
|
|
28
26
|
from fin_infra.goals.management import get_goal
|
|
29
27
|
|
|
30
28
|
# In-memory storage for funding allocations
|
|
31
29
|
# Structure: {account_id: {goal_id: allocation_percent}}
|
|
32
|
-
_FUNDING_STORE:
|
|
30
|
+
_FUNDING_STORE: dict[str, dict[str, float]] = {}
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
def link_account_to_goal(
|
|
@@ -108,7 +106,7 @@ def link_account_to_goal(
|
|
|
108
106
|
)
|
|
109
107
|
|
|
110
108
|
|
|
111
|
-
def get_goal_funding_sources(goal_id: str) ->
|
|
109
|
+
def get_goal_funding_sources(goal_id: str) -> list[FundingSource]:
|
|
112
110
|
"""
|
|
113
111
|
Get all accounts funding a specific goal.
|
|
114
112
|
|
|
@@ -154,7 +152,7 @@ def get_goal_funding_sources(goal_id: str) -> List[FundingSource]:
|
|
|
154
152
|
return funding_sources
|
|
155
153
|
|
|
156
154
|
|
|
157
|
-
def get_account_allocations(account_id: str) ->
|
|
155
|
+
def get_account_allocations(account_id: str) -> dict[str, float]:
|
|
158
156
|
"""
|
|
159
157
|
Get all goal allocations for a specific account.
|
|
160
158
|
|
fin_infra/investments/models.py
CHANGED
|
@@ -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,
|
|
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:
|
|
377
|
+
balances: dict[str, Optional[Decimal]] = Field(
|
|
378
378
|
..., description="Current, available, and limit balances"
|
|
379
379
|
)
|
|
380
380
|
|
|
381
381
|
# Holdings
|
|
382
|
-
holdings:
|
|
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:
|
|
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:
|
|
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,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from datetime import date
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Optional
|
|
8
8
|
|
|
9
9
|
# Import will work once models.py is fully implemented in Task 3
|
|
10
10
|
# For now, using TYPE_CHECKING to avoid circular imports
|
|
@@ -30,8 +30,8 @@ class InvestmentProvider(ABC):
|
|
|
30
30
|
|
|
31
31
|
@abstractmethod
|
|
32
32
|
async def get_holdings(
|
|
33
|
-
self, access_token: str, account_ids: Optional[
|
|
34
|
-
) ->
|
|
33
|
+
self, access_token: str, account_ids: Optional[list[str]] = None
|
|
34
|
+
) -> list[Holding]:
|
|
35
35
|
"""Fetch holdings for investment accounts.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
@@ -54,8 +54,8 @@ class InvestmentProvider(ABC):
|
|
|
54
54
|
access_token: str,
|
|
55
55
|
start_date: date,
|
|
56
56
|
end_date: date,
|
|
57
|
-
account_ids: Optional[
|
|
58
|
-
) ->
|
|
57
|
+
account_ids: Optional[list[str]] = None,
|
|
58
|
+
) -> list[InvestmentTransaction]:
|
|
59
59
|
"""Fetch investment transactions within date range.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
@@ -77,7 +77,7 @@ class InvestmentProvider(ABC):
|
|
|
77
77
|
pass
|
|
78
78
|
|
|
79
79
|
@abstractmethod
|
|
80
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
80
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
81
81
|
"""Fetch security details (ticker, name, type, current price).
|
|
82
82
|
|
|
83
83
|
Args:
|
|
@@ -95,7 +95,7 @@ class InvestmentProvider(ABC):
|
|
|
95
95
|
pass
|
|
96
96
|
|
|
97
97
|
@abstractmethod
|
|
98
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
98
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
99
99
|
"""Fetch investment accounts with aggregated holdings.
|
|
100
100
|
|
|
101
101
|
Args:
|
|
@@ -113,7 +113,7 @@ class InvestmentProvider(ABC):
|
|
|
113
113
|
|
|
114
114
|
# Helper methods (concrete - shared across all providers)
|
|
115
115
|
|
|
116
|
-
def calculate_allocation(self, holdings:
|
|
116
|
+
def calculate_allocation(self, holdings: list[Holding]) -> AssetAllocation:
|
|
117
117
|
"""Calculate asset allocation by security type and sector.
|
|
118
118
|
|
|
119
119
|
Groups holdings by security type (equity, bond, ETF, etc.) and calculates
|
|
@@ -183,7 +183,7 @@ class InvestmentProvider(ABC):
|
|
|
183
183
|
cash_percent=cash_percent,
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
def calculate_portfolio_metrics(self, holdings:
|
|
186
|
+
def calculate_portfolio_metrics(self, holdings: list[Holding]) -> dict:
|
|
187
187
|
"""Calculate total value, cost basis, unrealized gain/loss.
|
|
188
188
|
|
|
189
189
|
Aggregates holdings to calculate portfolio-level metrics.
|
|
@@ -10,7 +10,7 @@ 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, Optional, cast
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
16
|
from plaid.api import plaid_api
|
|
@@ -131,8 +131,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
131
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[
|
|
135
|
-
) ->
|
|
134
|
+
self, access_token: str, account_ids: Optional[list[str]] = None
|
|
135
|
+
) -> list[Holding]:
|
|
136
136
|
"""Fetch investment holdings from Plaid.
|
|
137
137
|
|
|
138
138
|
Retrieves holdings with security details, quantity, cost basis, and current value.
|
|
@@ -189,8 +189,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
189
189
|
access_token: str,
|
|
190
190
|
start_date: date,
|
|
191
191
|
end_date: date,
|
|
192
|
-
account_ids: Optional[
|
|
193
|
-
) ->
|
|
192
|
+
account_ids: Optional[list[str]] = None,
|
|
193
|
+
) -> list[InvestmentTransaction]:
|
|
194
194
|
"""Fetch investment transactions from Plaid.
|
|
195
195
|
|
|
196
196
|
Retrieves buy/sell/dividend transactions within the specified date range.
|
|
@@ -252,7 +252,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
252
252
|
except ApiException as e:
|
|
253
253
|
raise self._transform_error(e)
|
|
254
254
|
|
|
255
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
255
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
256
256
|
"""Fetch security details from Plaid holdings.
|
|
257
257
|
|
|
258
258
|
Note: Plaid doesn't have a dedicated securities endpoint.
|
|
@@ -290,7 +290,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
290
290
|
except ApiException as e:
|
|
291
291
|
raise self._transform_error(e)
|
|
292
292
|
|
|
293
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
293
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
294
294
|
"""Fetch investment accounts with aggregated holdings.
|
|
295
295
|
|
|
296
296
|
Returns accounts with total value, cost basis, and unrealized P&L.
|
|
@@ -321,7 +321,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
# Group holdings by account
|
|
324
|
-
accounts_map:
|
|
324
|
+
accounts_map: dict[str, dict[str, Any]] = {}
|
|
325
325
|
for plaid_holding in response.holdings:
|
|
326
326
|
holding_dict = plaid_holding.to_dict()
|
|
327
327
|
account_id = holding_dict["account_id"]
|
|
@@ -373,7 +373,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
373
373
|
|
|
374
374
|
# Helper methods for data transformation
|
|
375
375
|
|
|
376
|
-
def _transform_security(self, plaid_security:
|
|
376
|
+
def _transform_security(self, plaid_security: dict[str, Any]) -> Security:
|
|
377
377
|
"""Transform Plaid security data to Security model."""
|
|
378
378
|
# Handle close_price - Plaid may return None for securities without recent pricing
|
|
379
379
|
close_price_raw = plaid_security.get("close_price")
|
|
@@ -394,7 +394,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
394
394
|
currency=plaid_security.get("iso_currency_code", "USD"),
|
|
395
395
|
)
|
|
396
396
|
|
|
397
|
-
def _transform_holding(self, plaid_holding:
|
|
397
|
+
def _transform_holding(self, plaid_holding: dict[str, Any], security: Security) -> Holding:
|
|
398
398
|
"""Transform Plaid holding data to Holding model."""
|
|
399
399
|
return Holding(
|
|
400
400
|
account_id=plaid_holding["account_id"],
|
|
@@ -410,7 +410,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
410
410
|
)
|
|
411
411
|
|
|
412
412
|
def _transform_transaction(
|
|
413
|
-
self, plaid_transaction:
|
|
413
|
+
self, plaid_transaction: dict[str, Any], security: Security
|
|
414
414
|
) -> InvestmentTransaction:
|
|
415
415
|
"""Transform Plaid investment transaction to InvestmentTransaction model."""
|
|
416
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,
|
|
14
|
+
from typing import Any, Optional, 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) ->
|
|
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[
|
|
119
|
-
) ->
|
|
118
|
+
account_ids: Optional[list[str]] = 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"
|
|
@@ -178,8 +178,8 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
178
178
|
access_token: str,
|
|
179
179
|
start_date: date,
|
|
180
180
|
end_date: date,
|
|
181
|
-
account_ids: Optional[
|
|
182
|
-
) ->
|
|
181
|
+
account_ids: Optional[list[str]] = None,
|
|
182
|
+
) -> list[InvestmentTransaction]:
|
|
183
183
|
"""Fetch investment transactions from SnapTrade.
|
|
184
184
|
|
|
185
185
|
Args:
|
|
@@ -246,7 +246,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
246
246
|
except Exception as e:
|
|
247
247
|
raise ValueError(f"SnapTrade API error: {str(e)}")
|
|
248
248
|
|
|
249
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
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.
|
|
@@ -284,7 +284,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
284
284
|
except Exception as e:
|
|
285
285
|
raise ValueError(f"SnapTrade API error: {str(e)}")
|
|
286
286
|
|
|
287
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
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.
|
|
@@ -358,7 +358,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
358
358
|
except Exception as e:
|
|
359
359
|
raise ValueError(f"SnapTrade API error: {str(e)}")
|
|
360
360
|
|
|
361
|
-
async def list_connections(self, access_token: str) ->
|
|
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.).
|
|
@@ -388,7 +388,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
|
|
|
388
388
|
except Exception as e:
|
|
389
389
|
raise ValueError(f"SnapTrade API error: {str(e)}")
|
|
390
390
|
|
|
391
|
-
def get_brokerage_capabilities(self, brokerage_name: str) ->
|
|
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:
|
|
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:
|
|
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", {})
|
fin_infra/markets/__init__.py
CHANGED
|
@@ -20,7 +20,10 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from fastapi import FastAPI
|
|
21
21
|
|
|
22
22
|
from ..providers.base import MarketDataProvider
|
|
23
|
-
|
|
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(
|
|
@@ -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
|
|
fin_infra/net_worth/__init__.py
CHANGED
|
@@ -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)
|
fin_infra/obs/classifier.py
CHANGED
fin_infra/providers/base.py
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
"""Base provider ABCs for fin-infra.
|
|
2
|
+
|
|
3
|
+
This module defines abstract base classes for all financial data providers.
|
|
4
|
+
These are the canonical ABCs - use these instead of fin_infra.clients.
|
|
5
|
+
|
|
6
|
+
Sync vs Async Pattern:
|
|
7
|
+
Most providers use SYNCHRONOUS methods for simplicity. The exceptions are:
|
|
8
|
+
- InvestmentProvider: Uses async methods (get_holdings, get_investment_accounts)
|
|
9
|
+
|
|
10
|
+
If you need async, wrap sync providers with asyncio.to_thread():
|
|
11
|
+
import asyncio
|
|
12
|
+
result = await asyncio.to_thread(provider.quote, "AAPL")
|
|
13
|
+
|
|
14
|
+
Provider Categories:
|
|
15
|
+
- MarketDataProvider: Stock/equity quotes and historical data
|
|
16
|
+
- CryptoDataProvider: Cryptocurrency market data
|
|
17
|
+
- BankingProvider: Bank account aggregation (Plaid, Teller, MX)
|
|
18
|
+
- BrokerageProvider: Trading operations (Alpaca, Interactive Brokers)
|
|
19
|
+
- CreditProvider: Credit scores and reports
|
|
20
|
+
- TaxProvider: Tax documents and calculations
|
|
21
|
+
- IdentityProvider: Identity verification
|
|
22
|
+
- InvestmentProvider: Investment holdings (async)
|
|
23
|
+
"""
|
|
24
|
+
|
|
1
25
|
from __future__ import annotations
|
|
2
26
|
|
|
3
27
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Any
|
|
28
|
+
from typing import Any
|
|
29
|
+
from collections.abc import Iterable, Sequence
|
|
5
30
|
|
|
6
|
-
from ..models import
|
|
31
|
+
from ..models import Candle, Quote
|
|
7
32
|
|
|
8
33
|
|
|
9
34
|
class MarketDataProvider(ABC):
|
fin_infra/recurring/summary.py
CHANGED
|
@@ -33,7 +33,7 @@ Integration with svc-infra:
|
|
|
33
33
|
|
|
34
34
|
from __future__ import annotations
|
|
35
35
|
|
|
36
|
-
from typing import
|
|
36
|
+
from typing import Optional
|
|
37
37
|
from datetime import datetime
|
|
38
38
|
from collections import defaultdict
|
|
39
39
|
|
|
@@ -121,16 +121,16 @@ class RecurringSummary(BaseModel):
|
|
|
121
121
|
user_id: str = Field(..., description="User identifier")
|
|
122
122
|
total_monthly_cost: float = Field(..., description="Total monthly recurring expenses")
|
|
123
123
|
total_monthly_income: float = Field(0.0, description="Total monthly recurring income")
|
|
124
|
-
subscriptions:
|
|
124
|
+
subscriptions: list[RecurringItem] = Field(
|
|
125
125
|
default_factory=list, description="List of recurring expense items"
|
|
126
126
|
)
|
|
127
|
-
recurring_income:
|
|
127
|
+
recurring_income: list[RecurringItem] = Field(
|
|
128
128
|
default_factory=list, description="List of recurring income items"
|
|
129
129
|
)
|
|
130
|
-
by_category:
|
|
130
|
+
by_category: dict[str, float] = Field(
|
|
131
131
|
default_factory=dict, description="Monthly cost grouped by category"
|
|
132
132
|
)
|
|
133
|
-
cancellation_opportunities:
|
|
133
|
+
cancellation_opportunities: list[CancellationOpportunity] = Field(
|
|
134
134
|
default_factory=list, description="Potential subscriptions to cancel"
|
|
135
135
|
)
|
|
136
136
|
generated_at: str = Field(
|
|
@@ -165,8 +165,8 @@ def _calculate_monthly_cost(amount: float, cadence: str) -> float:
|
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
def _identify_cancellation_opportunities(
|
|
168
|
-
subscriptions:
|
|
169
|
-
) ->
|
|
168
|
+
subscriptions: list[RecurringItem],
|
|
169
|
+
) -> list[CancellationOpportunity]:
|
|
170
170
|
"""Identify potential cancellation opportunities from subscriptions.
|
|
171
171
|
|
|
172
172
|
Looks for:
|
|
@@ -183,7 +183,7 @@ def _identify_cancellation_opportunities(
|
|
|
183
183
|
opportunities = []
|
|
184
184
|
|
|
185
185
|
# Group by category
|
|
186
|
-
by_category:
|
|
186
|
+
by_category: dict[str, list[RecurringItem]] = defaultdict(list)
|
|
187
187
|
for sub in subscriptions:
|
|
188
188
|
by_category[sub.category].append(sub)
|
|
189
189
|
|
|
@@ -253,8 +253,8 @@ def _identify_cancellation_opportunities(
|
|
|
253
253
|
|
|
254
254
|
def get_recurring_summary(
|
|
255
255
|
user_id: str,
|
|
256
|
-
patterns:
|
|
257
|
-
category_map: Optional[
|
|
256
|
+
patterns: list[RecurringPattern],
|
|
257
|
+
category_map: Optional[dict[str, str]] = None,
|
|
258
258
|
) -> RecurringSummary:
|
|
259
259
|
"""Generate a comprehensive recurring transaction summary for a user.
|
|
260
260
|
|
|
@@ -283,7 +283,7 @@ def get_recurring_summary(
|
|
|
283
283
|
"""
|
|
284
284
|
subscriptions = []
|
|
285
285
|
recurring_income = []
|
|
286
|
-
by_category:
|
|
286
|
+
by_category: dict[str, float] = defaultdict(float)
|
|
287
287
|
|
|
288
288
|
for pattern in patterns:
|
|
289
289
|
# Determine amount (use fixed amount or average of range)
|
fin_infra/scaffold/budgets.py
CHANGED
|
@@ -19,7 +19,7 @@ Typical usage:
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Any,
|
|
22
|
+
from typing import Any, Optional
|
|
23
23
|
|
|
24
24
|
# Use svc-infra's scaffold utilities to avoid duplication
|
|
25
25
|
from svc_infra.utils import render_template, write, ensure_init_py
|
|
@@ -34,7 +34,7 @@ def scaffold_budgets_core(
|
|
|
34
34
|
models_filename: Optional[str] = None,
|
|
35
35
|
schemas_filename: Optional[str] = None,
|
|
36
36
|
repository_filename: Optional[str] = None,
|
|
37
|
-
) ->
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
38
|
"""Generate budget persistence code from templates.
|
|
39
39
|
|
|
40
40
|
Args:
|
|
@@ -72,7 +72,7 @@ def scaffold_budgets_core(
|
|
|
72
72
|
subs = _generate_substitutions(include_tenant, include_soft_delete)
|
|
73
73
|
|
|
74
74
|
# Track all file operations
|
|
75
|
-
files:
|
|
75
|
+
files: list[dict[str, Any]] = []
|
|
76
76
|
|
|
77
77
|
# Render and write models
|
|
78
78
|
models_content = render_template("fin_infra.budgets.scaffold_templates", "models.py.tmpl", subs)
|
|
@@ -114,7 +114,7 @@ def scaffold_budgets_core(
|
|
|
114
114
|
def _generate_substitutions(
|
|
115
115
|
include_tenant: bool,
|
|
116
116
|
include_soft_delete: bool,
|
|
117
|
-
) ->
|
|
117
|
+
) -> dict[str, str]:
|
|
118
118
|
"""Generate template variable substitutions for budgets.
|
|
119
119
|
|
|
120
120
|
Args:
|
fin_infra/scaffold/goals.py
CHANGED
|
@@ -17,7 +17,7 @@ Typical usage:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
21
21
|
|
|
22
22
|
from svc_infra.utils import (
|
|
23
23
|
render_template,
|
|
@@ -29,7 +29,7 @@ from svc_infra.utils import (
|
|
|
29
29
|
def _generate_substitutions(
|
|
30
30
|
include_tenant: bool = False,
|
|
31
31
|
include_soft_delete: bool = False,
|
|
32
|
-
) ->
|
|
32
|
+
) -> dict[str, str]:
|
|
33
33
|
"""
|
|
34
34
|
Generate template substitutions for goals domain.
|
|
35
35
|
|
|
@@ -49,7 +49,7 @@ def _generate_substitutions(
|
|
|
49
49
|
Returns:
|
|
50
50
|
Dict mapping variable names to their substitution values
|
|
51
51
|
"""
|
|
52
|
-
subs:
|
|
52
|
+
subs: dict[str, str] = {
|
|
53
53
|
"Entity": "Goal",
|
|
54
54
|
"entity": "goal",
|
|
55
55
|
"table_name": "goals",
|
|
@@ -173,7 +173,7 @@ def scaffold_goals_core(
|
|
|
173
173
|
models_filename: str = "goal.py",
|
|
174
174
|
schemas_filename: str = "goal_schemas.py",
|
|
175
175
|
repository_filename: str = "goal_repository.py",
|
|
176
|
-
) ->
|
|
176
|
+
) -> dict[str, Any]:
|
|
177
177
|
"""
|
|
178
178
|
Scaffold goals domain files: models, schemas, repository (optional), and __init__.py.
|
|
179
179
|
|
fin_infra/security/encryption.py
CHANGED
|
@@ -7,7 +7,7 @@ Encrypt/decrypt financial provider API tokens at rest.
|
|
|
7
7
|
import base64
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, Optional, cast
|
|
11
11
|
|
|
12
12
|
from cryptography.fernet import Fernet, InvalidToken
|
|
13
13
|
|
|
@@ -64,7 +64,7 @@ class ProviderTokenEncryption:
|
|
|
64
64
|
raise ValueError(f"Invalid encryption key: {e}") from e
|
|
65
65
|
|
|
66
66
|
def encrypt(
|
|
67
|
-
self, token: str, context: Optional[
|
|
67
|
+
self, token: str, context: Optional[dict[str, Any]] = None, key_id: Optional[str] = None
|
|
68
68
|
) -> str:
|
|
69
69
|
"""
|
|
70
70
|
Encrypt provider token with optional context.
|
|
@@ -104,7 +104,7 @@ class ProviderTokenEncryption:
|
|
|
104
104
|
def decrypt(
|
|
105
105
|
self,
|
|
106
106
|
encrypted_token: str,
|
|
107
|
-
context: Optional[
|
|
107
|
+
context: Optional[dict[str, Any]] = None,
|
|
108
108
|
verify_context: bool = True,
|
|
109
109
|
) -> str:
|
|
110
110
|
"""
|
|
@@ -154,7 +154,7 @@ class ProviderTokenEncryption:
|
|
|
154
154
|
raise ValueError(f"Decryption failed: {e}") from e
|
|
155
155
|
|
|
156
156
|
def rotate_key(
|
|
157
|
-
self, encrypted_token: str, new_key: bytes, context: Optional[
|
|
157
|
+
self, encrypted_token: str, new_key: bytes, context: Optional[dict[str, Any]] = None
|
|
158
158
|
) -> str:
|
|
159
159
|
"""
|
|
160
160
|
Re-encrypt token with new key (for key rotation).
|
fin_infra/utils/retry.py
CHANGED
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import random
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
6
7
|
|
|
7
8
|
from fin_infra.exceptions import RetryError
|
|
8
9
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fin-infra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.81
|
|
4
4
|
Summary: Financial infrastructure toolkit: banking connections, market data, credit, cashflows, and brokerage integrations
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: finance,banking,plaid,brokerage,markets,credit,tax,cashflow,fintech,infra
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
fin_infra/__init__.py,sha256=7oL-CCsALNifBODAn9LriicaIrzgJkmVPvE-9duP0mw,1574
|
|
2
2
|
fin_infra/__main__.py,sha256=1qNP7j0ffw0wFs1dBwDcJ9TNXlC6FcYuulzoV87pMi8,262
|
|
3
|
-
fin_infra/analytics/__init__.py,sha256=
|
|
3
|
+
fin_infra/analytics/__init__.py,sha256=_qbkbj5cy9uuCp9WdWbIz29XNwvakKf2T2sTRMeUiAs,2326
|
|
4
4
|
fin_infra/analytics/add.py,sha256=YhNxFAH0m-GiLxgNzwZY09wt6kmy4n88Xa9DrYBLB6E,12709
|
|
5
5
|
fin_infra/analytics/cash_flow.py,sha256=lH-J6RIUzcV9Fuy56i12N6vuIdPaGz-rhWquSgjkldU,10300
|
|
6
6
|
fin_infra/analytics/ease.py,sha256=p8WwFP4ivvFsvZzYRCZZVHgnPThiODve9r4-m0xnxQQ,14343
|
|
@@ -11,13 +11,13 @@ fin_infra/analytics/rebalancing.py,sha256=VM8MgoJofmrCXPK1rbmVqWGB4FauNmHCL5HOEb
|
|
|
11
11
|
fin_infra/analytics/savings.py,sha256=n3rGNFP8TU5mW-uz9kOuqX_mDiVnDyAeDN06Q7Abotw,7570
|
|
12
12
|
fin_infra/analytics/scenarios.py,sha256=LE_dZVkbxxAx5sxitGhiOhZfWTlYtVbIvS9pEXkijLc,12246
|
|
13
13
|
fin_infra/analytics/spending.py,sha256=wCa8mhtdMTnI3eG9W28ljCgB_AQRVGxkrRA2ZwLi_RQ,26249
|
|
14
|
-
fin_infra/banking/__init__.py,sha256=
|
|
15
|
-
fin_infra/banking/history.py,sha256=
|
|
16
|
-
fin_infra/banking/utils.py,sha256=
|
|
14
|
+
fin_infra/banking/__init__.py,sha256=5yB_Pi8cnlKzbJu1JiwtaismVFJXrBU7tmThZJ76Pqc,22499
|
|
15
|
+
fin_infra/banking/history.py,sha256=71Q5jHzPCmRdMKQBKPY9KQuis599AAh9DUu0lYo0S5M,10612
|
|
16
|
+
fin_infra/banking/utils.py,sha256=TcNTI2ujqqYGNCwoCBsICnaYwGRs2YRjGzlqVU_iy0M,15335
|
|
17
17
|
fin_infra/brokerage/__init__.py,sha256=8-q1NfKZW9fUQ_2_49Vc19sskZkmHo67TJ5GQDcXqTQ,17259
|
|
18
18
|
fin_infra/budgets/__init__.py,sha256=V6euagDkFHvWyjHeI64fxfddhOHDlIWwOc-PnTyyQb4,3986
|
|
19
19
|
fin_infra/budgets/add.py,sha256=tdfNXD9deEEzy0xCRbTwbx70SX9wiJkWj0Huk_uCjFg,13584
|
|
20
|
-
fin_infra/budgets/alerts.py,sha256=
|
|
20
|
+
fin_infra/budgets/alerts.py,sha256=KlK3r2qPzMw4naFMHsFjqkwKDxdj_2b_y-ofD1nldkg,9714
|
|
21
21
|
fin_infra/budgets/ease.py,sha256=vK5O8rvKzzJ1MUiwi7p9egayDFqyB23hPbbEhb1mhXE,8203
|
|
22
22
|
fin_infra/budgets/models.py,sha256=qd6bcjl2cOtFqRtNe1Xso_05cQlGG-4qhBkiTQKchy0,14335
|
|
23
23
|
fin_infra/budgets/scaffold_templates/README.md,sha256=FBtRSrQSkg7Xp8SPiuSmzGR94I2zzjOckb0_vKtcksY,14084
|
|
@@ -26,14 +26,14 @@ fin_infra/budgets/scaffold_templates/models.py.tmpl,sha256=rpKhXwnx1gQjV_GGVqs8C
|
|
|
26
26
|
fin_infra/budgets/scaffold_templates/repository.py.tmpl,sha256=khFgnQnVNnOo8DWYDmYz58MvdeSJpoS9QvTXcGhsa8g,10023
|
|
27
27
|
fin_infra/budgets/scaffold_templates/schemas.py.tmpl,sha256=x5gSQ7Kiuq08tum5joKmeY0ib2r3ekLKk09dFOl0PS0,5658
|
|
28
28
|
fin_infra/budgets/templates.py,sha256=Sbc7RcHXscq34g4t7J8OXM2Kfkt5DHuvqVnFU0Jiddc,12112
|
|
29
|
-
fin_infra/budgets/tracker.py,sha256=
|
|
29
|
+
fin_infra/budgets/tracker.py,sha256=gF68a6sivdSfVDZanSwdGdhEcg6SEic0K_rO9ed7lvI,16460
|
|
30
30
|
fin_infra/cashflows/__init__.py,sha256=Tdaa83y3sc38ndNJwg9gsKfUQNpIdQQPUS3GPK2ZurY,8517
|
|
31
|
-
fin_infra/cashflows/core.py,sha256=
|
|
31
|
+
fin_infra/cashflows/core.py,sha256=YWvF0DVOfBkvO_MuDODjE-V_g52H2ixFRH_TjMXExDE,541
|
|
32
32
|
fin_infra/categorization/__init__.py,sha256=efLje12AW-ec9Vs5ynb41r4XCIWx5a-Z9WoGb3kQdIE,2030
|
|
33
33
|
fin_infra/categorization/add.py,sha256=JDOvxngh-7oWHTddOyP4GAse9vLuxSTfoIhrDKUHOKg,6278
|
|
34
34
|
fin_infra/categorization/ease.py,sha256=bomEtJAgwk9uiemNt1rk-IsTjJIhyJn0GJ_c58YEmJs,5836
|
|
35
35
|
fin_infra/categorization/engine.py,sha256=vpwxtQGEbjCMyvzB5EQV2etjHHNOu1R05o99mHP_WZY,12132
|
|
36
|
-
fin_infra/categorization/llm_layer.py,sha256=
|
|
36
|
+
fin_infra/categorization/llm_layer.py,sha256=zJ8yHc78Lrc3TjmpA5krFpeZyoSQRh7SttGb_dSRAqA,12686
|
|
37
37
|
fin_infra/categorization/models.py,sha256=-rGXR0RW2EU_FQ7ZfDWBIXxx8QGJDxeBF9zKGYyVgqY,5931
|
|
38
38
|
fin_infra/categorization/rules.py,sha256=m3OogJY0hJe5BrmZqOvOKS2-HRdW4Y5jvvtlPDn9Pn8,12884
|
|
39
39
|
fin_infra/categorization/taxonomy.py,sha256=qsgo7VJkM6GFBBOaTRHWP82vl5SinRKnMsj4ICarEyQ,13281
|
|
@@ -43,10 +43,10 @@ fin_infra/chat/planning.py,sha256=eKUW6VDHJS-xQTks7bgjNQaO32Fr5gA_oP5NLt2y5Zs,19
|
|
|
43
43
|
fin_infra/cli/__init__.py,sha256=7M8gKULnui4__9kXRKRHgETuFwZlacK9xrq5rSZ31CM,376
|
|
44
44
|
fin_infra/cli/cmds/__init__.py,sha256=BvL3wRoUl3cO5wesv1Cqoatup7VeYMhq82tS19iNZHE,136
|
|
45
45
|
fin_infra/cli/cmds/scaffold_cmds.py,sha256=SwNE8AnszmsjyUXDpmn-bauQx8HAQB_fw-6er2QaTCU,7655
|
|
46
|
-
fin_infra/clients/__init__.py,sha256=
|
|
47
|
-
fin_infra/clients/base.py,sha256=
|
|
48
|
-
fin_infra/clients/plaid.py,sha256=
|
|
49
|
-
fin_infra/compliance/__init__.py,sha256=
|
|
46
|
+
fin_infra/clients/__init__.py,sha256=CL_NUNxLAKFvG7wL_F-rAhUucp6pm0sNHVtvCzVvLcw,708
|
|
47
|
+
fin_infra/clients/base.py,sha256=fZebDGIfQQybqeXpFpORlDZ6wSKf33aIAkUUpmyvZrA,979
|
|
48
|
+
fin_infra/clients/plaid.py,sha256=Cj2QNj3xgmPb86wcYg-ZVTPVJ5pXM7qwUgeO8AAWpV4,816
|
|
49
|
+
fin_infra/compliance/__init__.py,sha256=0y3Lx3ai1hhFS2ypvEtBJXEZnEMC0dq30sqHwnqFCeU,5249
|
|
50
50
|
fin_infra/credit/__init__.py,sha256=cwCP_WlrG-0yb_L4zYsuzEsSalcfiCY9ItqXfD7Jx9E,6719
|
|
51
51
|
fin_infra/credit/add.py,sha256=D3btx9pmZ3tF6AYC6P4Y3dYaUuWp7M3FpDrFksxi5uM,8553
|
|
52
52
|
fin_infra/credit/experian/__init__.py,sha256=g3IJGvDOMsnB0er0Uwdvl6hGKKTOazqJxSDnB2oIBm0,761
|
|
@@ -59,15 +59,15 @@ fin_infra/crypto/__init__.py,sha256=p-gEoF59XzyV1RouubW4onl5mE62XvXSAw9isMc48qc,
|
|
|
59
59
|
fin_infra/crypto/insights.py,sha256=cgMsjdrR7vOTKvEDbFCXpS7O7I8dMs1oz6cIbtrJWZI,11393
|
|
60
60
|
fin_infra/documents/__init__.py,sha256=Ub1hbX3PTrBSsBdcbL8PFf6oq8jSH4pYxW45-qOYPqs,1909
|
|
61
61
|
fin_infra/documents/add.py,sha256=dxzhdCsDcVVyTYKrgM30j-Wr0BAG797p4xa4j9UXST8,8118
|
|
62
|
-
fin_infra/documents/analysis.py,sha256=
|
|
62
|
+
fin_infra/documents/analysis.py,sha256=QrRgpqVsV96jOjyJEI05o-hxi-c4AuXi4wLJncDSX68,13986
|
|
63
63
|
fin_infra/documents/ease.py,sha256=YutA7EDYzysH6ppf6iVFhe4v1XOzhmUM3tDwtQPku28,9621
|
|
64
|
-
fin_infra/documents/models.py,sha256=
|
|
65
|
-
fin_infra/documents/ocr.py,sha256=
|
|
66
|
-
fin_infra/documents/storage.py,sha256=
|
|
64
|
+
fin_infra/documents/models.py,sha256=RpazzhE9qBcTRk_NO7Dl_uvcHw9UpGtnpXFcJURMtYc,6865
|
|
65
|
+
fin_infra/documents/ocr.py,sha256=R5v3lwMr-xyAlA1zjTw2MWbT2phawNw7kzbAxrSwYpg,9588
|
|
66
|
+
fin_infra/documents/storage.py,sha256=yu2ZvjJ-iDrt6zhPQ_VZhKZ8Iv99D9X3cUyIb7e7hZ0,10168
|
|
67
67
|
fin_infra/exceptions.py,sha256=va2rZnGhAkAi5LUqk93iGhpfkChUBaBLUNnCx6bykSM,16952
|
|
68
68
|
fin_infra/goals/__init__.py,sha256=Vg8LKLlDoRiWHsJX7wu5Zcc-86NNLpHoLTjYVkGi2c4,2130
|
|
69
|
-
fin_infra/goals/add.py,sha256=
|
|
70
|
-
fin_infra/goals/funding.py,sha256=
|
|
69
|
+
fin_infra/goals/add.py,sha256=46SbPuDuymd3LcDHmQAoQAMthyrKBP-Hv35fXcOccPw,20599
|
|
70
|
+
fin_infra/goals/funding.py,sha256=pJx9W1hpzXwWFUxIXEdsFZt-ytXIo7DK6uRpyO6aO44,9303
|
|
71
71
|
fin_infra/goals/management.py,sha256=Rj-yCcXk4HK5Tg8VauRU3osbK3kRh5O-YR-tupULx8A,33865
|
|
72
72
|
fin_infra/goals/milestones.py,sha256=LEJ9M7yOKJ-8thPuH0byHACabCUA9qW7mMATsPomaJA,9995
|
|
73
73
|
fin_infra/goals/models.py,sha256=DxUrYJqlfKdrmFBucNikLbto3NgxoiJAmsL3v0LR4DQ,10237
|
|
@@ -82,17 +82,17 @@ fin_infra/insights/models.py,sha256=xov_YV8oBLJt3YdyVjbryRfcXqmGeGiPvZsZHSbvtl8,
|
|
|
82
82
|
fin_infra/investments/__init__.py,sha256=o4p_8slq-CzIK0ditVhNfcyoWsDdyFaxRl-IMBHtLNE,6732
|
|
83
83
|
fin_infra/investments/add.py,sha256=Yyh3VIjJ5OBJbvEZQboFDqlYkzrHoEPnWzSY2iBAJiM,17263
|
|
84
84
|
fin_infra/investments/ease.py,sha256=d5ISfxpCius6JM2LZNReztW6-IizaqoxNU4aEbXWA74,9487
|
|
85
|
-
fin_infra/investments/models.py,sha256=
|
|
85
|
+
fin_infra/investments/models.py,sha256=UyOvtU1-uxrgE0zLP5WY3wpn3YOdOv6_b05oGismxNU,18412
|
|
86
86
|
fin_infra/investments/providers/__init__.py,sha256=V1eIzz6EnGJ-pq-9L3S2-evmcExF-YdZfd5P6JMyDtc,383
|
|
87
|
-
fin_infra/investments/providers/base.py,sha256=
|
|
88
|
-
fin_infra/investments/providers/plaid.py,sha256=
|
|
89
|
-
fin_infra/investments/providers/snaptrade.py,sha256=
|
|
87
|
+
fin_infra/investments/providers/base.py,sha256=sQMK5qlIbLqBS6FyTzUWxJy5rPPzAnp5lwbRUM1jHCI,9844
|
|
88
|
+
fin_infra/investments/providers/plaid.py,sha256=Twt2XHxmrXrS-7N3NGoA3yXOXmdTfuuQ2j0Nqn7Kkok,18765
|
|
89
|
+
fin_infra/investments/providers/snaptrade.py,sha256=FNa8c9QI8xBF3NLCg-_3vf0oZZ5o0LiFnj9d3UwNQuk,23278
|
|
90
90
|
fin_infra/investments/scaffold_templates/README.md,sha256=PhgxfMLrro2Jz83b7XEnBi7lexiWKqlMrd2UU2Rbs8A,12149
|
|
91
91
|
fin_infra/investments/scaffold_templates/__init__.py,sha256=iR0oiAzXFYXHBnVJjaEnAzk6omncYOLg0TKMJ7xomBc,82
|
|
92
92
|
fin_infra/investments/scaffold_templates/models.py.tmpl,sha256=5inP5-jw-qEfPYxSN71tn4AojZ9PesOIeuHTw181N-c,5849
|
|
93
93
|
fin_infra/investments/scaffold_templates/repository.py.tmpl,sha256=XwOEpQZfuXut1WLiq-GSSvv0oX0iYCW54eJNL0Cav94,14656
|
|
94
94
|
fin_infra/investments/scaffold_templates/schemas.py.tmpl,sha256=knWmn-Kyr7AdgPD4ZPMb6T49ZuPXeuOMqmjYNyA0CA0,5451
|
|
95
|
-
fin_infra/markets/__init__.py,sha256=
|
|
95
|
+
fin_infra/markets/__init__.py,sha256=_cZGIdaRVZyurVDdqSFpvAgQZbpN2Dq6HvU42xqv66w,9867
|
|
96
96
|
fin_infra/models/__init__.py,sha256=q3SkGzDGFkoAMxwqJw8i4cHWt5NGU5ypjOgntxDGVKo,860
|
|
97
97
|
fin_infra/models/accounts.py,sha256=ExQimE2O5dyugFMW7nCboWbWcaX3Nnl5Gg-B1XLSdIk,1138
|
|
98
98
|
fin_infra/models/brokerage.py,sha256=z6Zyf0N5zmmXtrN2y_4fNmtIP5wNq40H8lrHLBwY7rc,8311
|
|
@@ -102,7 +102,7 @@ fin_infra/models/money.py,sha256=5BX8IQZkrNtjjnGIQAK2tyKnVim0R-yc1F_EBxUhcr0,400
|
|
|
102
102
|
fin_infra/models/quotes.py,sha256=_2cDJS8_RLo4tLpJlqWd32J8uFNP0bbf1V_0u3NuLwo,543
|
|
103
103
|
fin_infra/models/tax.py,sha256=lhNVIW650CdtpfgmSyMMJdojV7QnpHOUFQKiwMLTT4A,15656
|
|
104
104
|
fin_infra/models/transactions.py,sha256=NtIHk3RDM58wYHQiHNOsvU5K6lgpfZodrL7scDRKP6E,865
|
|
105
|
-
fin_infra/net_worth/__init__.py,sha256=
|
|
105
|
+
fin_infra/net_worth/__init__.py,sha256=4LzVM2Y0OkBlZoD5FUAbLwtORp1okYcsYc4zj0P8C7k,3452
|
|
106
106
|
fin_infra/net_worth/add.py,sha256=QWfHIHJs2CV99WRBqjQ2OteiOrn5cR9nurmxTF9v5rg,23191
|
|
107
107
|
fin_infra/net_worth/aggregator.py,sha256=9Kx2vUR71QwqYZdGaCfmYrJ1hNxzd1EEuAdWJoNjqTI,12780
|
|
108
108
|
fin_infra/net_worth/calculator.py,sha256=SQJGJDok5HgvoAhKBxeeqt8vhGMchABU3zPmNRpqNy4,13139
|
|
@@ -123,21 +123,21 @@ fin_infra/normalization/providers/exchangerate.py,sha256=vA1W2yVpCf89kOx6lctbHOQ
|
|
|
123
123
|
fin_infra/normalization/providers/static_mappings.py,sha256=m14VHmTZipbqrgyE0ABToabVx-pDcyB577LNWrACEUM,6809
|
|
124
124
|
fin_infra/normalization/symbol_resolver.py,sha256=M7Li7LFiH4xpvxXcYQlJyk0iqgqpwaj6zQKsTzWZzas,8130
|
|
125
125
|
fin_infra/obs/__init__.py,sha256=kMMVl0fdwtJtZeKiusTuw0iO61Jo9-HNXsLmn3ffLRE,631
|
|
126
|
-
fin_infra/obs/classifier.py,sha256=
|
|
126
|
+
fin_infra/obs/classifier.py,sha256=JasWCqSkYjllJNZ5Gwbrd53ZhLwhYNZ0i2nbTcklEog,5155
|
|
127
127
|
fin_infra/providers/__init__.py,sha256=jxhQm79T6DVXf7Wpy7luL-p50cE_IMUbjt4o3apzJQU,768
|
|
128
128
|
fin_infra/providers/banking/base.py,sha256=KeNU4ur3zLKHVsBF1LQifcs2AKX06IEE-Rx_SetFeAs,102
|
|
129
129
|
fin_infra/providers/banking/plaid_client.py,sha256=LiBIwQLJp-5bA98oEQtTZpkQMC7kwhShq0mPATa9wD8,6520
|
|
130
130
|
fin_infra/providers/banking/teller_client.py,sha256=QmrsBlk3_rHT-pTQPrIAA74kjIjcgdi-gOb8NA3oBO8,10268
|
|
131
|
-
fin_infra/providers/base.py,sha256=
|
|
131
|
+
fin_infra/providers/base.py,sha256=tn153HUMhicv39lsPJ6-0W2Ylg4yYUPiHBltWF7cggE,7722
|
|
132
132
|
fin_infra/providers/brokerage/alpaca.py,sha256=M8Z2i6dY9mgrU4-SDei2nYRKgbxHuPDMHubMb7ZMbEY,9920
|
|
133
133
|
fin_infra/providers/brokerage/base.py,sha256=JJFH0Cqca4Rg4rmxfiwcQt-peRoBf4JpG3g6jx8DVks,106
|
|
134
134
|
fin_infra/providers/credit/experian.py,sha256=r7lpFecgOdNEhb_Lxz2Z-BG8R3p2n0XlqDKL7y8NZ-0,482
|
|
135
135
|
fin_infra/providers/identity/stripe_identity.py,sha256=JQGJRuQdWP5dWDcROgtz1RrmpkytRv95H6Fn-x1kifU,501
|
|
136
|
-
fin_infra/providers/market/alphavantage.py,sha256=
|
|
136
|
+
fin_infra/providers/market/alphavantage.py,sha256=or0YmEBbpa2p4-DUA97JrdaobxUhI3X_AOkfspaPzSY,8972
|
|
137
137
|
fin_infra/providers/market/base.py,sha256=ljBzZTfjYQS9tXahmxFic7JQSZeyoiDMUZ1NY0R7yto,108
|
|
138
138
|
fin_infra/providers/market/ccxt_crypto.py,sha256=AknYS3ZRBOtVxsr1MrI_ECL2vLlTR2hT60SeuAoECXE,1610
|
|
139
139
|
fin_infra/providers/market/coingecko.py,sha256=F1Bwdk28xSsIaFEuT7lhT3F6Vkd0Lp-CMp1rnYiLfaE,2702
|
|
140
|
-
fin_infra/providers/market/yahoo.py,sha256=
|
|
140
|
+
fin_infra/providers/market/yahoo.py,sha256=Ay95DQ2BtYOseWLJ57ozGxcLgf0m73VzIw7CC7l56lw,5425
|
|
141
141
|
fin_infra/providers/registry.py,sha256=yPFmHHaSQERXZTcGkdXAtMU7rL7VwAzW4FOr14o6KS8,8409
|
|
142
142
|
fin_infra/providers/tax/__init__.py,sha256=Tq2gLyTXL_U_ht6r7HXgaDMCAPylgcRD2ZN-COjSSQU,207
|
|
143
143
|
fin_infra/providers/tax/irs.py,sha256=f7l6w0byprBszTlCB4ef60K8GrYV-03Dicl1a1Q2oVk,4701
|
|
@@ -153,14 +153,14 @@ fin_infra/recurring/insights.py,sha256=a4fhASyiNQTdUl2ijaxi2HqhWDhf-aJND2c62d9lg
|
|
|
153
153
|
fin_infra/recurring/models.py,sha256=o0N8G-QhVb4zILEyry6M1VZ7liFJIOHwlejvn6p4K8M,8894
|
|
154
154
|
fin_infra/recurring/normalizer.py,sha256=HZ4N7lXaeOZqphjSbjRmaneONJ5ELOSrqBlM5WM1tww,9758
|
|
155
155
|
fin_infra/recurring/normalizers.py,sha256=37-ER4deeJhywrNVEe9KteecC_eH7xesTl2CWhRbdi0,15928
|
|
156
|
-
fin_infra/recurring/summary.py,sha256=
|
|
156
|
+
fin_infra/recurring/summary.py,sha256=Myb1llV8imKtQ3xYJu_guO2GmrPHHB4PSXjLbktFTQ8,14653
|
|
157
157
|
fin_infra/scaffold/__init__.py,sha256=IfL_CHHMpQB1efqY37BlIu07356tLaeVI2Mv3C0qYDs,827
|
|
158
|
-
fin_infra/scaffold/budgets.py,sha256=
|
|
159
|
-
fin_infra/scaffold/goals.py,sha256=
|
|
158
|
+
fin_infra/scaffold/budgets.py,sha256=l6cdNfc7v8YI6PiWV-pJ69L6PDtcX5qSIgXFtykaRIM,9503
|
|
159
|
+
fin_infra/scaffold/goals.py,sha256=_ymL1HgAF8bvpGSCd_GeoMgVXtog8xCJ8mFIjOn-hQc,9684
|
|
160
160
|
fin_infra/security/__init__.py,sha256=ZXGa7IeoOg50f41KsA7tt9rKTUeg910AagQYXh0MIbs,1363
|
|
161
161
|
fin_infra/security/add.py,sha256=Y_XXNd-FTpSaHmO4xkYvkW4CLlFGCuQWe9gJ7WuwiLY,2746
|
|
162
162
|
fin_infra/security/audit.py,sha256=TekYWCOUT9Sf1sDS2-EEREtW7nhWo3H7iaLVbLPx308,3322
|
|
163
|
-
fin_infra/security/encryption.py,sha256=
|
|
163
|
+
fin_infra/security/encryption.py,sha256=KWgLqt8-mRH2I53WyVAFUx-T-M5F4qX67qneFfxY-_A,6162
|
|
164
164
|
fin_infra/security/models.py,sha256=riQO-083p5rDMRrFxRnc2PTkxkAf-HsSpGvrnzboCNE,1734
|
|
165
165
|
fin_infra/security/pii_filter.py,sha256=lfARBmPRekkyXKJV0tWI_0KVaDsdV61VH-8RHxvbqUs,8307
|
|
166
166
|
fin_infra/security/pii_patterns.py,sha256=SM-o7cL6NdgkOmtBedsN2nJZ5QPbeYehZdYmAujk8Y8,3070
|
|
@@ -171,10 +171,10 @@ fin_infra/tax/add.py,sha256=8INSAv721ir9ICQxQ_oA0hL-Bjg6wLyrtj9tafrcCsA,14552
|
|
|
171
171
|
fin_infra/tax/tlh.py,sha256=6OlZ3Gb13rSFrmW7vPqVTq_NB45D110iHgCwzYp2nTA,21523
|
|
172
172
|
fin_infra/utils/__init__.py,sha256=gKacLSWMAis--pasd8AuVN7ap0e9Z1TjRGur0J23EDo,648
|
|
173
173
|
fin_infra/utils/http.py,sha256=pvcxbNQ9oisoGPkNe3xX9aAgWzEN6mmdtr1w-L02Xj8,629
|
|
174
|
-
fin_infra/utils/retry.py,sha256=
|
|
174
|
+
fin_infra/utils/retry.py,sha256=gC49Kp9Y2219UKIa1rJuxkU0ikRHMN5eotZQityLPIU,1057
|
|
175
175
|
fin_infra/version.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
|
|
176
|
-
fin_infra-0.1.
|
|
177
|
-
fin_infra-0.1.
|
|
178
|
-
fin_infra-0.1.
|
|
179
|
-
fin_infra-0.1.
|
|
180
|
-
fin_infra-0.1.
|
|
176
|
+
fin_infra-0.1.81.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
|
|
177
|
+
fin_infra-0.1.81.dist-info/METADATA,sha256=dJVWcVEVOd45d-Vq_94YO_GjgvyECxmQoWk74V3-aZw,10479
|
|
178
|
+
fin_infra-0.1.81.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
179
|
+
fin_infra-0.1.81.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
|
|
180
|
+
fin_infra-0.1.81.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|