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.
Files changed (126) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +30 -32
  4. fin_infra/analytics/cash_flow.py +6 -5
  5. fin_infra/analytics/ease.py +19 -20
  6. fin_infra/analytics/portfolio.py +19 -26
  7. fin_infra/analytics/projections.py +1 -3
  8. fin_infra/analytics/rebalancing.py +2 -4
  9. fin_infra/analytics/savings.py +1 -1
  10. fin_infra/analytics/spending.py +15 -11
  11. fin_infra/banking/__init__.py +33 -31
  12. fin_infra/banking/history.py +11 -12
  13. fin_infra/banking/utils.py +116 -110
  14. fin_infra/brokerage/__init__.py +27 -27
  15. fin_infra/budgets/__init__.py +3 -3
  16. fin_infra/budgets/add.py +16 -17
  17. fin_infra/budgets/alerts.py +3 -3
  18. fin_infra/budgets/tracker.py +4 -5
  19. fin_infra/cashflows/__init__.py +8 -10
  20. fin_infra/cashflows/core.py +1 -1
  21. fin_infra/categorization/__init__.py +1 -1
  22. fin_infra/categorization/add.py +17 -19
  23. fin_infra/categorization/ease.py +3 -4
  24. fin_infra/categorization/engine.py +21 -18
  25. fin_infra/categorization/llm_layer.py +10 -10
  26. fin_infra/categorization/models.py +1 -1
  27. fin_infra/categorization/rules.py +2 -4
  28. fin_infra/categorization/taxonomy.py +2 -2
  29. fin_infra/chat/__init__.py +13 -22
  30. fin_infra/chat/planning.py +57 -1
  31. fin_infra/cli/cmds/scaffold_cmds.py +11 -12
  32. fin_infra/clients/__init__.py +23 -1
  33. fin_infra/clients/base.py +1 -1
  34. fin_infra/clients/plaid.py +2 -2
  35. fin_infra/compliance/__init__.py +7 -6
  36. fin_infra/credit/add.py +7 -7
  37. fin_infra/credit/experian/auth.py +3 -2
  38. fin_infra/credit/experian/client.py +2 -2
  39. fin_infra/credit/experian/provider.py +19 -19
  40. fin_infra/crypto/__init__.py +8 -10
  41. fin_infra/crypto/insights.py +5 -6
  42. fin_infra/documents/add.py +11 -13
  43. fin_infra/documents/analysis.py +9 -9
  44. fin_infra/documents/ease.py +18 -17
  45. fin_infra/documents/models.py +7 -7
  46. fin_infra/documents/ocr.py +8 -8
  47. fin_infra/documents/storage.py +23 -14
  48. fin_infra/exceptions.py +1 -2
  49. fin_infra/goals/__init__.py +8 -8
  50. fin_infra/goals/add.py +36 -36
  51. fin_infra/goals/funding.py +4 -6
  52. fin_infra/goals/management.py +6 -7
  53. fin_infra/goals/milestones.py +2 -3
  54. fin_infra/goals/models.py +7 -11
  55. fin_infra/insights/__init__.py +12 -10
  56. fin_infra/insights/aggregator.py +1 -1
  57. fin_infra/investments/__init__.py +14 -9
  58. fin_infra/investments/add.py +53 -73
  59. fin_infra/investments/ease.py +16 -13
  60. fin_infra/investments/models.py +135 -69
  61. fin_infra/investments/providers/base.py +9 -15
  62. fin_infra/investments/providers/plaid.py +70 -55
  63. fin_infra/investments/providers/snaptrade.py +35 -53
  64. fin_infra/markets/__init__.py +16 -11
  65. fin_infra/models/__init__.py +10 -10
  66. fin_infra/models/accounts.py +2 -1
  67. fin_infra/models/brokerage.py +2 -1
  68. fin_infra/models/candle.py +1 -0
  69. fin_infra/models/money.py +1 -0
  70. fin_infra/models/quotes.py +4 -3
  71. fin_infra/models/tax.py +2 -1
  72. fin_infra/models/transactions.py +4 -4
  73. fin_infra/net_worth/__init__.py +7 -0
  74. fin_infra/net_worth/add.py +8 -5
  75. fin_infra/net_worth/aggregator.py +9 -6
  76. fin_infra/net_worth/calculator.py +8 -6
  77. fin_infra/net_worth/ease.py +36 -15
  78. fin_infra/net_worth/insights.py +4 -5
  79. fin_infra/net_worth/models.py +237 -116
  80. fin_infra/normalization/__init__.py +17 -15
  81. fin_infra/normalization/providers/exchangerate.py +5 -5
  82. fin_infra/obs/classifier.py +3 -3
  83. fin_infra/providers/banking/plaid_client.py +23 -22
  84. fin_infra/providers/banking/teller_client.py +14 -7
  85. fin_infra/providers/base.py +131 -14
  86. fin_infra/providers/brokerage/alpaca.py +7 -7
  87. fin_infra/providers/credit/experian.py +5 -0
  88. fin_infra/providers/market/alphavantage.py +6 -11
  89. fin_infra/providers/market/ccxt_crypto.py +25 -4
  90. fin_infra/providers/market/coingecko.py +5 -6
  91. fin_infra/providers/market/yahoo.py +23 -8
  92. fin_infra/providers/tax/__init__.py +1 -1
  93. fin_infra/providers/tax/irs.py +1 -1
  94. fin_infra/providers/tax/mock.py +8 -8
  95. fin_infra/providers/tax/taxbit.py +1 -1
  96. fin_infra/recurring/__init__.py +6 -6
  97. fin_infra/recurring/add.py +24 -12
  98. fin_infra/recurring/detector.py +8 -8
  99. fin_infra/recurring/detectors_llm.py +14 -13
  100. fin_infra/recurring/ease.py +3 -5
  101. fin_infra/recurring/insights.py +20 -19
  102. fin_infra/recurring/models.py +3 -3
  103. fin_infra/recurring/normalizer.py +3 -2
  104. fin_infra/recurring/normalizers.py +11 -10
  105. fin_infra/recurring/summary.py +13 -15
  106. fin_infra/scaffold/__init__.py +1 -1
  107. fin_infra/scaffold/budgets.py +9 -9
  108. fin_infra/scaffold/goals.py +5 -5
  109. fin_infra/security/__init__.py +8 -8
  110. fin_infra/security/encryption.py +6 -6
  111. fin_infra/security/models.py +7 -7
  112. fin_infra/security/pii_filter.py +6 -6
  113. fin_infra/security/pii_patterns.py +1 -1
  114. fin_infra/security/token_store.py +3 -1
  115. fin_infra/settings.py +2 -1
  116. fin_infra/tax/__init__.py +2 -2
  117. fin_infra/tax/add.py +3 -2
  118. fin_infra/tax/tlh.py +5 -5
  119. fin_infra/utils/http.py +5 -3
  120. fin_infra/utils/retry.py +2 -1
  121. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
  122. fin_infra-0.1.82.dist-info/RECORD +180 -0
  123. fin_infra-0.1.62.dist-info/RECORD +0 -180
  124. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
  125. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
  126. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
@@ -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, Optional
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: Optional[str] = None
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: "FastAPI",
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: Optional[date] = Query(None, description="Filter by start date (ISO format)"),
354
- end_date: Optional[date] = Query(None, description="Filter by end date (ISO format)"),
355
- merchant: Optional[str] = Query(
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: Optional[str] = Query(
357
+ category: str | None = Query(
359
358
  None, description="Filter by category (comma-separated list for multiple)"
360
359
  ),
361
- min_amount: Optional[float] = Query(
360
+ min_amount: float | None = Query(
362
361
  None, description="Minimum transaction amount (inclusive)"
363
362
  ),
364
- max_amount: Optional[float] = Query(
363
+ max_amount: float | None = Query(
365
364
  None, description="Maximum transaction amount (inclusive)"
366
365
  ),
367
- tags: Optional[str] = Query(None, description="Filter by tags (comma-separated list)"),
368
- account_id: Optional[str] = Query(None, description="Filter by specific account ID"),
369
- is_recurring: Optional[bool] = Query(None, description="Filter by recurring status"),
370
- sort_by: Optional[str] = Query("date", description="Sort field: date, amount, or merchant"),
371
- order: Optional[str] = Query("desc", description="Sort order: asc or desc"),
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=start_date,
403
- end_date=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: Optional[str] = Query(None),
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
- validate_plaid_token,
594
- validate_teller_token,
595
- validate_mx_token,
596
- validate_provider_token,
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
- BankingConnectionInfo,
605
- BankingConnectionStatus,
603
+ test_connection_health,
604
+ validate_mx_token,
605
+ validate_plaid_token,
606
+ validate_provider_token,
607
+ validate_teller_token,
606
608
  )
@@ -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: List[BalanceSnapshot] = []
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: Optional[date] = None,
159
- end_date: Optional[date] = None,
160
- ) -> List[BalanceSnapshot]:
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: List[date],
220
- ) -> List[BalanceSnapshot]:
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: Optional[date] = None,
250
+ before_date: date | None = None,
252
251
  ) -> int:
253
252
  """Delete balance history for an account.
254
253