fin-infra 0.1.62__py3-none-any.whl → 0.1.82__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fin_infra/__init__.py +53 -3
- fin_infra/analytics/__init__.py +13 -2
- fin_infra/analytics/add.py +30 -32
- fin_infra/analytics/cash_flow.py +6 -5
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +19 -26
- fin_infra/analytics/projections.py +1 -3
- 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 +33 -31
- fin_infra/banking/history.py +11 -12
- fin_infra/banking/utils.py +116 -110
- fin_infra/brokerage/__init__.py +27 -27
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/budgets/alerts.py +3 -3
- fin_infra/budgets/tracker.py +4 -5
- fin_infra/cashflows/__init__.py +8 -10
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +17 -19
- fin_infra/categorization/ease.py +3 -4
- fin_infra/categorization/engine.py +21 -18
- fin_infra/categorization/llm_layer.py +10 -10
- fin_infra/categorization/models.py +1 -1
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +13 -22
- fin_infra/chat/planning.py +57 -1
- fin_infra/cli/cmds/scaffold_cmds.py +11 -12
- fin_infra/clients/__init__.py +23 -1
- fin_infra/clients/base.py +1 -1
- fin_infra/clients/plaid.py +2 -2
- fin_infra/compliance/__init__.py +7 -6
- fin_infra/credit/add.py +7 -7
- fin_infra/credit/experian/auth.py +3 -2
- fin_infra/credit/experian/client.py +2 -2
- fin_infra/credit/experian/provider.py +19 -19
- fin_infra/crypto/__init__.py +8 -10
- fin_infra/crypto/insights.py +5 -6
- fin_infra/documents/add.py +11 -13
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +18 -17
- fin_infra/documents/models.py +7 -7
- fin_infra/documents/ocr.py +8 -8
- fin_infra/documents/storage.py +23 -14
- fin_infra/exceptions.py +1 -2
- fin_infra/goals/__init__.py +8 -8
- fin_infra/goals/add.py +36 -36
- fin_infra/goals/funding.py +4 -6
- fin_infra/goals/management.py +6 -7
- fin_infra/goals/milestones.py +2 -3
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +12 -10
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +14 -9
- fin_infra/investments/add.py +53 -73
- fin_infra/investments/ease.py +16 -13
- fin_infra/investments/models.py +135 -69
- fin_infra/investments/providers/base.py +9 -15
- fin_infra/investments/providers/plaid.py +70 -55
- fin_infra/investments/providers/snaptrade.py +35 -53
- fin_infra/markets/__init__.py +16 -11
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/accounts.py +2 -1
- 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 +4 -4
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +9 -6
- fin_infra/net_worth/calculator.py +8 -6
- fin_infra/net_worth/ease.py +36 -15
- fin_infra/net_worth/insights.py +4 -5
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +17 -15
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/obs/classifier.py +3 -3
- fin_infra/providers/banking/plaid_client.py +23 -22
- fin_infra/providers/banking/teller_client.py +14 -7
- fin_infra/providers/base.py +131 -14
- fin_infra/providers/brokerage/alpaca.py +7 -7
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +25 -4
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +23 -8
- fin_infra/providers/tax/__init__.py +1 -1
- fin_infra/providers/tax/irs.py +1 -1
- fin_infra/providers/tax/mock.py +8 -8
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +24 -12
- fin_infra/recurring/detector.py +8 -8
- fin_infra/recurring/detectors_llm.py +14 -13
- fin_infra/recurring/ease.py +3 -5
- fin_infra/recurring/insights.py +20 -19
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizer.py +3 -2
- fin_infra/recurring/normalizers.py +11 -10
- fin_infra/recurring/summary.py +13 -15
- fin_infra/scaffold/__init__.py +1 -1
- fin_infra/scaffold/budgets.py +9 -9
- fin_infra/scaffold/goals.py +5 -5
- 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/security/pii_patterns.py +1 -1
- fin_infra/security/token_store.py +3 -1
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +2 -2
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +5 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.62.dist-info/RECORD +0 -180
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
fin_infra/banking/__init__.py
CHANGED
|
@@ -45,12 +45,12 @@ from __future__ import annotations
|
|
|
45
45
|
|
|
46
46
|
import os
|
|
47
47
|
from datetime import date
|
|
48
|
-
from typing import TYPE_CHECKING,
|
|
48
|
+
from typing import TYPE_CHECKING, cast
|
|
49
49
|
|
|
50
50
|
from pydantic import BaseModel, Field
|
|
51
51
|
|
|
52
|
-
from ..providers.registry import resolve
|
|
53
52
|
from ..providers.base import BankingProvider
|
|
53
|
+
from ..providers.registry import resolve
|
|
54
54
|
|
|
55
55
|
if TYPE_CHECKING:
|
|
56
56
|
from fastapi import FastAPI
|
|
@@ -99,7 +99,7 @@ class ExchangeTokenResponse(BaseModel):
|
|
|
99
99
|
"""Response model for token exchange."""
|
|
100
100
|
|
|
101
101
|
access_token: str
|
|
102
|
-
item_id:
|
|
102
|
+
item_id: str | None = None
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
class BalanceHistoryStats(BaseModel):
|
|
@@ -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
|
|
@@ -199,11 +198,11 @@ def easy_banking(provider: str = "teller", **config) -> BankingProvider:
|
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
# Use provider registry to dynamically load and configure provider
|
|
202
|
-
return resolve("banking", provider, **config)
|
|
201
|
+
return cast("BankingProvider", resolve("banking", provider, **config))
|
|
203
202
|
|
|
204
203
|
|
|
205
204
|
def add_banking(
|
|
206
|
-
app:
|
|
205
|
+
app: FastAPI,
|
|
207
206
|
*,
|
|
208
207
|
provider: str | BankingProvider | None = None,
|
|
209
208
|
prefix: str = "/banking",
|
|
@@ -350,25 +349,25 @@ def add_banking(
|
|
|
350
349
|
@router.get("/transactions")
|
|
351
350
|
async def get_transactions(
|
|
352
351
|
access_token: str = Depends(get_access_token),
|
|
353
|
-
start_date:
|
|
354
|
-
end_date:
|
|
355
|
-
merchant:
|
|
352
|
+
start_date: date | None = Query(None, description="Filter by start date (ISO format)"),
|
|
353
|
+
end_date: date | None = Query(None, description="Filter by end date (ISO format)"),
|
|
354
|
+
merchant: str | None = Query(
|
|
356
355
|
None, description="Filter by merchant name (partial match, case-insensitive)"
|
|
357
356
|
),
|
|
358
|
-
category:
|
|
357
|
+
category: str | None = Query(
|
|
359
358
|
None, description="Filter by category (comma-separated list for multiple)"
|
|
360
359
|
),
|
|
361
|
-
min_amount:
|
|
360
|
+
min_amount: float | None = Query(
|
|
362
361
|
None, description="Minimum transaction amount (inclusive)"
|
|
363
362
|
),
|
|
364
|
-
max_amount:
|
|
363
|
+
max_amount: float | None = Query(
|
|
365
364
|
None, description="Maximum transaction amount (inclusive)"
|
|
366
365
|
),
|
|
367
|
-
tags:
|
|
368
|
-
account_id:
|
|
369
|
-
is_recurring:
|
|
370
|
-
sort_by:
|
|
371
|
-
order:
|
|
366
|
+
tags: str | None = Query(None, description="Filter by tags (comma-separated list)"),
|
|
367
|
+
account_id: str | None = Query(None, description="Filter by specific account ID"),
|
|
368
|
+
is_recurring: bool | None = Query(None, description="Filter by recurring status"),
|
|
369
|
+
sort_by: str | None = Query("date", description="Sort field: date, amount, or merchant"),
|
|
370
|
+
order: str | None = Query("desc", description="Sort order: asc or desc"),
|
|
372
371
|
page: int = Query(1, ge=1, description="Page number (starts at 1)"),
|
|
373
372
|
per_page: int = Query(50, ge=1, le=200, description="Items per page (max 200)"),
|
|
374
373
|
):
|
|
@@ -397,10 +396,13 @@ def add_banking(
|
|
|
397
396
|
}
|
|
398
397
|
"""
|
|
399
398
|
# Get all transactions from provider
|
|
399
|
+
# Convert date to ISO string format as expected by BankingProvider.transactions()
|
|
400
|
+
start_date_str: str | None = start_date.isoformat() if start_date else None
|
|
401
|
+
end_date_str: str | None = end_date.isoformat() if end_date else None
|
|
400
402
|
transactions = banking.transactions(
|
|
401
403
|
access_token=access_token,
|
|
402
|
-
start_date=
|
|
403
|
-
end_date=
|
|
404
|
+
start_date=start_date_str,
|
|
405
|
+
end_date=end_date_str,
|
|
404
406
|
)
|
|
405
407
|
|
|
406
408
|
# Apply filters
|
|
@@ -473,7 +475,7 @@ def add_banking(
|
|
|
473
475
|
@router.get("/balances")
|
|
474
476
|
async def get_balances(
|
|
475
477
|
access_token: str = Depends(get_access_token),
|
|
476
|
-
account_id:
|
|
478
|
+
account_id: str | None = Query(None),
|
|
477
479
|
):
|
|
478
480
|
"""Get current balances."""
|
|
479
481
|
balances = banking.balances(
|
|
@@ -589,18 +591,18 @@ def add_banking(
|
|
|
589
591
|
|
|
590
592
|
|
|
591
593
|
# Import utilities at end to avoid circular imports
|
|
592
|
-
from .utils import (
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
594
|
+
from .utils import ( # noqa: E402
|
|
595
|
+
BankingConnectionInfo,
|
|
596
|
+
BankingConnectionStatus,
|
|
597
|
+
get_primary_access_token,
|
|
598
|
+
mark_connection_healthy,
|
|
599
|
+
mark_connection_unhealthy,
|
|
597
600
|
parse_banking_providers,
|
|
598
601
|
sanitize_connection_status,
|
|
599
|
-
mark_connection_unhealthy,
|
|
600
|
-
mark_connection_healthy,
|
|
601
|
-
get_primary_access_token,
|
|
602
|
-
test_connection_health,
|
|
603
602
|
should_refresh_token,
|
|
604
|
-
|
|
605
|
-
|
|
603
|
+
test_connection_health,
|
|
604
|
+
validate_mx_token,
|
|
605
|
+
validate_plaid_token,
|
|
606
|
+
validate_provider_token,
|
|
607
|
+
validate_teller_token,
|
|
606
608
|
)
|
fin_infra/banking/history.py
CHANGED
|
@@ -42,9 +42,8 @@ from __future__ import annotations
|
|
|
42
42
|
import logging
|
|
43
43
|
import os
|
|
44
44
|
from datetime import date, datetime, timedelta
|
|
45
|
-
from typing import List, Optional
|
|
46
|
-
from pydantic import BaseModel, Field, ConfigDict
|
|
47
45
|
|
|
46
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
48
47
|
|
|
49
48
|
__all__ = [
|
|
50
49
|
"BalanceSnapshot",
|
|
@@ -59,7 +58,7 @@ _logger = logging.getLogger(__name__)
|
|
|
59
58
|
|
|
60
59
|
# In-memory storage for testing (will be replaced with SQL database in production)
|
|
61
60
|
# ⚠️ WARNING: All data is LOST on restart when using in-memory storage!
|
|
62
|
-
_balance_snapshots:
|
|
61
|
+
_balance_snapshots: list[BalanceSnapshot] = []
|
|
63
62
|
_production_warning_logged = False
|
|
64
63
|
|
|
65
64
|
|
|
@@ -68,10 +67,10 @@ def _check_in_memory_warning() -> None:
|
|
|
68
67
|
global _production_warning_logged
|
|
69
68
|
if _production_warning_logged:
|
|
70
69
|
return
|
|
71
|
-
|
|
70
|
+
|
|
72
71
|
env = os.getenv("ENV", "development").lower()
|
|
73
72
|
storage_backend = os.getenv("FIN_INFRA_STORAGE_BACKEND", "memory").lower()
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
if env in ("production", "staging") and storage_backend == "memory":
|
|
76
75
|
_logger.warning(
|
|
77
76
|
"⚠️ CRITICAL: Balance history using IN-MEMORY storage in %s environment! "
|
|
@@ -135,7 +134,7 @@ def record_balance_snapshot(
|
|
|
135
134
|
"""
|
|
136
135
|
# Check if in-memory storage is being used in production
|
|
137
136
|
_check_in_memory_warning()
|
|
138
|
-
|
|
137
|
+
|
|
139
138
|
snapshot = BalanceSnapshot(
|
|
140
139
|
account_id=account_id,
|
|
141
140
|
balance=balance,
|
|
@@ -155,9 +154,9 @@ def record_balance_snapshot(
|
|
|
155
154
|
def get_balance_history(
|
|
156
155
|
account_id: str,
|
|
157
156
|
days: int = 90,
|
|
158
|
-
start_date:
|
|
159
|
-
end_date:
|
|
160
|
-
) ->
|
|
157
|
+
start_date: date | None = None,
|
|
158
|
+
end_date: date | None = None,
|
|
159
|
+
) -> list[BalanceSnapshot]:
|
|
161
160
|
"""Get balance history for an account.
|
|
162
161
|
|
|
163
162
|
Retrieves balance snapshots for the specified account within a date range.
|
|
@@ -216,8 +215,8 @@ def get_balance_history(
|
|
|
216
215
|
|
|
217
216
|
def get_balance_snapshots(
|
|
218
217
|
account_id: str,
|
|
219
|
-
dates:
|
|
220
|
-
) ->
|
|
218
|
+
dates: list[date],
|
|
219
|
+
) -> list[BalanceSnapshot]:
|
|
221
220
|
"""Get balance snapshots for specific dates.
|
|
222
221
|
|
|
223
222
|
Args:
|
|
@@ -248,7 +247,7 @@ def get_balance_snapshots(
|
|
|
248
247
|
|
|
249
248
|
def delete_balance_history(
|
|
250
249
|
account_id: str,
|
|
251
|
-
before_date:
|
|
250
|
+
before_date: date | None = None,
|
|
252
251
|
) -> int:
|
|
253
252
|
"""Delete balance history for an account.
|
|
254
253
|
|