fin-infra 0.1.62__py3-none-any.whl → 0.1.64__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 (41) hide show
  1. fin_infra/analytics/spending.py +8 -6
  2. fin_infra/banking/__init__.py +7 -4
  3. fin_infra/brokerage/__init__.py +4 -2
  4. fin_infra/categorization/__init__.py +1 -1
  5. fin_infra/categorization/add.py +2 -1
  6. fin_infra/categorization/ease.py +1 -1
  7. fin_infra/categorization/engine.py +1 -1
  8. fin_infra/categorization/llm_layer.py +2 -2
  9. fin_infra/compliance/__init__.py +4 -3
  10. fin_infra/credit/add.py +3 -2
  11. fin_infra/credit/experian/auth.py +3 -2
  12. fin_infra/credit/experian/client.py +2 -2
  13. fin_infra/credit/experian/provider.py +2 -2
  14. fin_infra/goals/add.py +2 -2
  15. fin_infra/goals/management.py +3 -3
  16. fin_infra/goals/milestones.py +2 -2
  17. fin_infra/investments/__init__.py +11 -4
  18. fin_infra/investments/ease.py +11 -7
  19. fin_infra/investments/models.py +1 -1
  20. fin_infra/investments/providers/plaid.py +3 -3
  21. fin_infra/investments/providers/snaptrade.py +2 -2
  22. fin_infra/markets/__init__.py +8 -3
  23. fin_infra/normalization/providers/exchangerate.py +3 -3
  24. fin_infra/providers/banking/plaid_client.py +4 -3
  25. fin_infra/providers/banking/teller_client.py +13 -7
  26. fin_infra/providers/brokerage/alpaca.py +5 -5
  27. fin_infra/providers/market/ccxt_crypto.py +5 -3
  28. fin_infra/providers/tax/mock.py +3 -3
  29. fin_infra/recurring/detectors_llm.py +2 -2
  30. fin_infra/recurring/insights.py +2 -2
  31. fin_infra/recurring/normalizer.py +2 -1
  32. fin_infra/recurring/normalizers.py +2 -2
  33. fin_infra/security/encryption.py +2 -2
  34. fin_infra/security/pii_patterns.py +1 -1
  35. fin_infra/security/token_store.py +3 -1
  36. fin_infra/utils/http.py +3 -2
  37. {fin_infra-0.1.62.dist-info → fin_infra-0.1.64.dist-info}/METADATA +1 -1
  38. {fin_infra-0.1.62.dist-info → fin_infra-0.1.64.dist-info}/RECORD +41 -41
  39. {fin_infra-0.1.62.dist-info → fin_infra-0.1.64.dist-info}/LICENSE +0 -0
  40. {fin_infra-0.1.62.dist-info → fin_infra-0.1.64.dist-info}/WHEEL +0 -0
  41. {fin_infra-0.1.62.dist-info → fin_infra-0.1.64.dist-info}/entry_points.txt +0 -0
@@ -162,12 +162,13 @@ async def analyze_spending(
162
162
  )
163
163
 
164
164
  return SpendingInsight(
165
- top_merchants=top_merchants,
166
- category_breakdown=dict(category_totals),
165
+ # Convert Decimal to float for model compatibility (intentional for Pydantic field types)
166
+ top_merchants=[(m, float(v)) for m, v in top_merchants],
167
+ category_breakdown={k: float(v) for k, v in category_totals.items()},
167
168
  spending_trends=spending_trends,
168
169
  anomalies=anomalies,
169
170
  period_days=days,
170
- total_spending=total_spending,
171
+ total_spending=float(total_spending) if total_spending else 0.0,
171
172
  )
172
173
 
173
174
 
@@ -359,9 +360,10 @@ async def _detect_spending_anomalies(
359
360
  anomalies.append(
360
361
  SpendingAnomaly(
361
362
  category=category,
362
- current_amount=current_amount,
363
- average_amount=average_amount,
364
- deviation_percent=deviation_percent,
363
+ # Convert Decimal to float for model compatibility (intentional for Pydantic field types)
364
+ current_amount=float(current_amount),
365
+ average_amount=float(average_amount),
366
+ deviation_percent=float(deviation_percent),
365
367
  severity=severity,
366
368
  )
367
369
  )
@@ -45,7 +45,7 @@ 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, Optional, cast
49
49
 
50
50
  from pydantic import BaseModel, Field
51
51
 
@@ -199,7 +199,7 @@ def easy_banking(provider: str = "teller", **config) -> BankingProvider:
199
199
  }
200
200
 
201
201
  # Use provider registry to dynamically load and configure provider
202
- return resolve("banking", provider, **config)
202
+ return cast(BankingProvider, resolve("banking", provider, **config))
203
203
 
204
204
 
205
205
  def add_banking(
@@ -397,10 +397,13 @@ def add_banking(
397
397
  }
398
398
  """
399
399
  # Get all transactions from provider
400
+ # Convert date to ISO string format as expected by BankingProvider.transactions()
401
+ start_date_str: str | None = start_date.isoformat() if start_date else None
402
+ end_date_str: str | None = end_date.isoformat() if end_date else None
400
403
  transactions = banking.transactions(
401
404
  access_token=access_token,
402
- start_date=start_date,
403
- end_date=end_date,
405
+ start_date=start_date_str,
406
+ end_date=end_date_str,
404
407
  )
405
408
 
406
409
  # Apply filters
@@ -212,7 +212,9 @@ def add_brokerage(
212
212
 
213
213
  # Initialize provider if string or None
214
214
  if isinstance(provider, str):
215
- brokerage_provider = easy_brokerage(provider=provider, mode=mode, **config)
215
+ # Cast provider string to Literal type for type checker
216
+ provider_literal: Literal["alpaca"] | None = provider if provider == "alpaca" else None # type: ignore[assignment]
217
+ brokerage_provider = easy_brokerage(provider=provider_literal, mode=mode, **config)
216
218
  elif provider is None:
217
219
  brokerage_provider = easy_brokerage(mode=mode, **config)
218
220
  else:
@@ -241,7 +243,7 @@ def add_brokerage(
241
243
  Returns list of positions with symbol, quantity, P/L, etc.
242
244
  """
243
245
  try:
244
- positions = brokerage_provider.positions()
246
+ positions = list(brokerage_provider.positions()) # Convert Iterable to list for len()
245
247
  return {"positions": positions, "count": len(positions)}
246
248
  except Exception as e:
247
249
  raise HTTPException(status_code=500, detail=f"Error fetching positions: {str(e)}")
@@ -44,7 +44,7 @@ from .taxonomy import Category, CategoryGroup, get_all_categories, get_category_
44
44
  try:
45
45
  from .llm_layer import LLMCategorizer
46
46
  except ImportError:
47
- LLMCategorizer = None
47
+ LLMCategorizer = None # type: ignore[assignment,misc]
48
48
 
49
49
  __all__ = [
50
50
  # Easy setup
@@ -96,7 +96,8 @@ def add_categorization(
96
96
  start_time = time.perf_counter()
97
97
 
98
98
  try:
99
- prediction = engine.categorize(
99
+ # Await the async categorize method
100
+ prediction = await engine.categorize(
100
101
  merchant_name=request.merchant_name,
101
102
  user_id=request.user_id,
102
103
  include_alternatives=request.include_alternatives,
@@ -13,7 +13,7 @@ from .engine import CategorizationEngine
13
13
  try:
14
14
  from .llm_layer import LLMCategorizer
15
15
  except ImportError:
16
- LLMCategorizer = None
16
+ LLMCategorizer = None # type: ignore[assignment,misc]
17
17
 
18
18
 
19
19
  def easy_categorization(
@@ -23,7 +23,7 @@ from .taxonomy import Category
23
23
  try:
24
24
  from .llm_layer import LLMCategorizer
25
25
  except ImportError:
26
- LLMCategorizer = None
26
+ LLMCategorizer = None # type: ignore[assignment,misc]
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
@@ -15,7 +15,7 @@ Expected performance:
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Optional, List, Tuple
18
+ from typing import Optional, List, Tuple, cast
19
19
  from pydantic import BaseModel, Field
20
20
 
21
21
  # ai-infra imports
@@ -245,7 +245,7 @@ class LLMCategorizer:
245
245
  f"Must be one of {len(valid_categories)} valid categories."
246
246
  )
247
247
 
248
- return response
248
+ return cast(CategoryPrediction, response)
249
249
 
250
250
  def _build_system_prompt(self) -> str:
251
251
  """Build system prompt with few-shot examples (reused across all requests)."""
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import logging
23
23
  from datetime import datetime
24
- from typing import Any, Callable, TYPE_CHECKING
24
+ from typing import Any, Callable, TYPE_CHECKING, cast
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from fastapi import FastAPI, Request, Response
@@ -118,7 +118,8 @@ def add_compliance_tracking(
118
118
 
119
119
  # Track only GET requests (data access)
120
120
  if method != "GET":
121
- return await call_next(request)
121
+ from starlette.responses import Response as StarletteResponse
122
+ return cast("Response", await call_next(request))
122
123
 
123
124
  # Determine if path is a compliance-tracked endpoint
124
125
  event = None
@@ -148,7 +149,7 @@ def add_compliance_tracking(
148
149
  if on_event:
149
150
  on_event(event, context)
150
151
 
151
- return response
152
+ return cast("Response", response)
152
153
 
153
154
  logger.info(
154
155
  "Compliance tracking enabled",
fin_infra/credit/add.py CHANGED
@@ -23,6 +23,7 @@ Example:
23
23
  """
24
24
 
25
25
  import logging
26
+ from typing import cast
26
27
 
27
28
  from fastapi import FastAPI, Depends, HTTPException, status
28
29
 
@@ -175,7 +176,7 @@ def add_credit(
175
176
  # Don't fail request if webhook publishing fails
176
177
  logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
177
178
 
178
- return score
179
+ return cast(CreditScore, score)
179
180
 
180
181
  @router.post("/report", response_model=CreditReport)
181
182
  @credit_resource.cache_read(ttl=cache_ttl, suffix="report")
@@ -219,7 +220,7 @@ def add_credit(
219
220
  detail="Credit bureau service unavailable",
220
221
  )
221
222
 
222
- return report
223
+ return cast(CreditReport, report)
223
224
 
224
225
  # Mount router with dual routes (with/without trailing slash)
225
226
  app.include_router(router, include_in_schema=True)
@@ -24,6 +24,7 @@ Example:
24
24
  """
25
25
 
26
26
  import base64
27
+ from typing import cast
27
28
 
28
29
  import httpx
29
30
  from svc_infra.cache import cache_read
@@ -85,7 +86,7 @@ class ExperianAuthManager:
85
86
  >>> headers = {"Authorization": f"Bearer {token}"}
86
87
  """
87
88
  # Call the cached implementation with client_id for cache key
88
- return await self._get_token_cached(client_id=self.client_id)
89
+ return cast(str, await self._get_token_cached(client_id=self.client_id))
89
90
 
90
91
  @cache_read(
91
92
  key="oauth_token:experian:{client_id}", # Use client_id for uniqueness
@@ -140,7 +141,7 @@ class ExperianAuthManager:
140
141
 
141
142
  # Parse and return token
142
143
  data = response.json()
143
- return data["access_token"]
144
+ return cast(str, data["access_token"])
144
145
 
145
146
  async def invalidate(self) -> None:
146
147
  """Invalidate cached token for THIS client (force refresh on next get_token call).
@@ -14,7 +14,7 @@ Example:
14
14
  >>> data = await client.get_credit_score("user123")
15
15
  """
16
16
 
17
- from typing import Any
17
+ from typing import Any, cast
18
18
 
19
19
  import httpx
20
20
  from tenacity import (
@@ -155,7 +155,7 @@ class ExperianClient:
155
155
  **kwargs,
156
156
  )
157
157
  response.raise_for_status()
158
- return response.json()
158
+ return cast(dict[str, Any], response.json())
159
159
 
160
160
  except httpx.HTTPStatusError as e:
161
161
  # Parse error response
@@ -31,7 +31,7 @@ Example:
31
31
 
32
32
  import logging
33
33
  from datetime import datetime, timezone
34
- from typing import Literal
34
+ from typing import Literal, cast
35
35
 
36
36
  from fin_infra.credit.experian.auth import ExperianAuthManager
37
37
  from fin_infra.credit.experian.client import ExperianClient
@@ -360,4 +360,4 @@ class ExperianProvider(CreditProvider):
360
360
  signature_key=signature_key,
361
361
  )
362
362
 
363
- return data.get("subscriptionId", "unknown")
363
+ return cast(str, data.get("subscriptionId", "unknown"))
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 List, Optional
32
+ from typing import Any, List, Optional, cast
33
33
 
34
34
  from fastapi import FastAPI, HTTPException, status, Query, Body
35
35
  from pydantic import BaseModel, Field
@@ -469,7 +469,7 @@ def add_goals(
469
469
  # Get all milestones from the goal (check_milestones only returns newly reached ones)
470
470
  goal = get_goal(goal_id)
471
471
  milestones = goal.get("milestones", [])
472
- return milestones
472
+ return cast(list[dict[Any, Any]], milestones)
473
473
  except KeyError:
474
474
  raise HTTPException(
475
475
  status_code=status.HTTP_404_NOT_FOUND, detail=f"Goal {goal_id} not found"
@@ -41,7 +41,7 @@ Example:
41
41
  """
42
42
 
43
43
  from datetime import datetime
44
- from typing import Any
44
+ from typing import Any, cast
45
45
 
46
46
  from pydantic import BaseModel, Field
47
47
 
@@ -839,7 +839,7 @@ def get_goal(goal_id: str) -> dict[str, Any]:
839
839
  if goal_id not in _GOALS_STORE:
840
840
  raise KeyError(f"Goal not found: {goal_id}")
841
841
 
842
- return _GOALS_STORE[goal_id]
842
+ return cast(dict[str, Any], _GOALS_STORE[goal_id])
843
843
 
844
844
 
845
845
  def update_goal(
@@ -885,7 +885,7 @@ def update_goal(
885
885
 
886
886
  Goal(**goal) # Will raise ValidationError if invalid
887
887
 
888
- return goal
888
+ return cast(dict[str, Any], goal)
889
889
 
890
890
 
891
891
  def delete_goal(goal_id: str) -> None:
@@ -26,7 +26,7 @@ Example:
26
26
  """
27
27
 
28
28
  from datetime import datetime
29
- from typing import Any
29
+ from typing import Any, cast
30
30
 
31
31
  from fin_infra.goals.management import get_goal, update_goal
32
32
  from fin_infra.goals.models import Milestone
@@ -229,7 +229,7 @@ def get_next_milestone(goal_id: str) -> dict[str, Any] | None:
229
229
  # Find first unreached milestone (sorted by amount)
230
230
  for milestone in milestones:
231
231
  if not milestone.get("reached", False):
232
- return milestone
232
+ return cast(dict[str, Any], milestone)
233
233
 
234
234
  return None
235
235
 
@@ -51,7 +51,8 @@ from typing import TYPE_CHECKING, Literal
51
51
  if TYPE_CHECKING:
52
52
  from fastapi import FastAPI
53
53
 
54
- from ..providers.base import InvestmentProvider
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
 
@@ -172,14 +174,19 @@ def add_investments(
172
174
  >>> # GET /investments/transactions
173
175
  >>> # etc.
174
176
  """
175
- from .add import add_investments_impl
177
+ from .add import add_investments as add_investments_impl
178
+ from .providers.base import InvestmentProvider as InvestmentProviderBase
179
+
180
+ # Resolve provider from string Literal to actual InvestmentProvider instance
181
+ resolved_provider: InvestmentProviderBase | None = None
182
+ if provider is not None:
183
+ resolved_provider = easy_investments(provider=provider, **provider_config) # type: ignore[assignment]
176
184
 
177
185
  return add_investments_impl(
178
186
  app,
179
- provider=provider,
187
+ provider=resolved_provider,
180
188
  prefix=prefix,
181
189
  tags=tags or ["Investments"],
182
- **provider_config,
183
190
  )
184
191
 
185
192
 
@@ -112,19 +112,20 @@ 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
- if provider is None:
116
- provider = _detect_provider()
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 provider not in ("plaid", "snaptrade"):
120
+ if detected_provider not in ("plaid", "snaptrade"):
120
121
  raise ValueError(
121
- f"Invalid provider: {provider}. Must be 'plaid' or 'snaptrade'."
122
+ f"Invalid provider: {detected_provider}. Must be 'plaid' or 'snaptrade'."
122
123
  )
123
124
 
124
125
  # Instantiate provider
125
- if provider == "plaid":
126
+ if detected_provider == "plaid":
126
127
  return _create_plaid_provider(**config)
127
- elif provider == "snaptrade":
128
+ elif detected_provider == "snaptrade":
128
129
  return _create_snaptrade_provider(**config)
129
130
 
130
131
  # Should never reach here
@@ -233,10 +234,13 @@ def _create_snaptrade_provider(**config: Any) -> InvestmentProvider:
233
234
  "Example: easy_investments(provider='snaptrade', client_id='...', consumer_key='...')"
234
235
  )
235
236
 
237
+ # Ensure base_url is a string (default is set in SnapTradeInvestmentProvider)
238
+ resolved_base_url: str = base_url if isinstance(base_url, str) else "https://api.snaptrade.com/api/v1"
239
+
236
240
  return SnapTradeInvestmentProvider(
237
241
  client_id=client_id,
238
242
  consumer_key=consumer_key,
239
- base_url=base_url,
243
+ base_url=resolved_base_url,
240
244
  )
241
245
 
242
246
 
@@ -71,7 +71,7 @@ class TransactionType(str, Enum):
71
71
  fee = "fee"
72
72
  tax = "tax"
73
73
  transfer = "transfer"
74
- split = "split"
74
+ split = "split" # type: ignore[assignment] # str.split() name conflict
75
75
  merger = "merger"
76
76
  cancel = "cancel"
77
77
  other = "other"
@@ -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, Dict, List, Optional
13
+ from typing import Any, Dict, List, Optional, cast
14
14
 
15
15
  from plaid.api import plaid_api
16
16
  from plaid.model.investments_holdings_get_request import InvestmentsHoldingsGetRequest
@@ -103,7 +103,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
103
103
  "development": plaid.Environment.Sandbox, # Map development to sandbox
104
104
  "production": plaid.Environment.Production,
105
105
  }
106
- return hosts.get(environment.lower(), plaid.Environment.Sandbox)
106
+ return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
107
107
 
108
108
  async def get_holdings(
109
109
  self, access_token: str, account_ids: Optional[List[str]] = None
@@ -370,7 +370,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
370
370
  isin=plaid_security.get("isin"),
371
371
  sedol=plaid_security.get("sedol"),
372
372
  ticker_symbol=plaid_security.get("ticker_symbol"),
373
- name=plaid_security.get("name"),
373
+ name=plaid_security.get("name") or "Unknown Security",
374
374
  type=self._normalize_security_type(plaid_security.get("type", "other")),
375
375
  sector=plaid_security.get("sector"),
376
376
  close_price=close_price,
@@ -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, Dict, List, Optional
14
+ from typing import Any, Dict, List, Optional, cast
15
15
 
16
16
  import httpx
17
17
 
@@ -393,7 +393,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
393
393
  url = f"{self.base_url}/connections"
394
394
  response = await self.client.get(url, headers=auth_headers)
395
395
  response.raise_for_status()
396
- return await response.json()
396
+ return cast(list[dict[str, Any]], await response.json())
397
397
 
398
398
  except httpx.HTTPStatusError as e:
399
399
  raise self._transform_error(e)
@@ -193,7 +193,11 @@ def add_market_data(
193
193
  if isinstance(provider, MarketDataProvider):
194
194
  market = provider
195
195
  else:
196
- market = easy_market(provider=provider, **config)
196
+ # Cast provider to Literal type for type checker
197
+ provider_literal: Literal["alphavantage", "yahoo"] | None = (
198
+ provider if provider in ("alphavantage", "yahoo", None) else None # type: ignore[assignment]
199
+ )
200
+ market = easy_market(provider=provider_literal, **config)
197
201
 
198
202
  # Create router (public - no auth required)
199
203
  router = public_router(prefix=prefix, tags=["Market Data"])
@@ -223,14 +227,15 @@ def add_market_data(
223
227
  try:
224
228
  candles = market.history(symbol, period=period, interval=interval)
225
229
  # Convert to dicts if they're Pydantic models
226
- candles_list = []
230
+ candles_list: list[dict] = []
227
231
  for candle in candles:
228
232
  if hasattr(candle, "model_dump"):
229
233
  candles_list.append(candle.model_dump())
230
234
  elif hasattr(candle, "dict"):
231
235
  candles_list.append(candle.dict())
232
236
  else:
233
- candles_list.append(candle)
237
+ # Cast to dict for type compatibility
238
+ candles_list.append(dict(candle) if hasattr(candle, "__iter__") else {"data": candle})
234
239
  return {"candles": candles_list}
235
240
  except Exception as e:
236
241
  raise HTTPException(status_code=400, detail=str(e))
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from datetime import date as DateType
5
- from typing import Optional
5
+ from typing import Optional, cast
6
6
 
7
7
  import httpx
8
8
 
@@ -66,10 +66,10 @@ class ExchangeRateClient:
66
66
  raise ExchangeRateAPIError(
67
67
  f"API returned error: {data.get('error-type', 'unknown')}"
68
68
  )
69
- return data["conversion_rates"]
69
+ return cast(dict[str, float], data["conversion_rates"])
70
70
  else:
71
71
  # Free tier response format
72
- return data["rates"]
72
+ return cast(dict[str, float], data["rates"])
73
73
 
74
74
  except httpx.HTTPError as e:
75
75
  raise ExchangeRateAPIError(f"HTTP error fetching rates: {e}")
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import date, datetime, timedelta
4
+ from typing import Any, cast
4
5
 
5
6
  # Plaid SDK v25+ uses new API structure
6
7
  try:
@@ -96,7 +97,7 @@ class PlaidClient(BankingProvider):
96
97
  language="en",
97
98
  )
98
99
  response = self.client.link_token_create(request)
99
- return response["link_token"]
100
+ return cast(str, response["link_token"])
100
101
 
101
102
  def exchange_public_token(self, public_token: str) -> dict:
102
103
  request = ItemPublicTokenExchangeRequest(public_token=public_token)
@@ -146,8 +147,8 @@ class PlaidClient(BankingProvider):
146
147
  # Return all balances
147
148
  return {"balances": [acc.get("balances", {}) for acc in accounts]}
148
149
 
149
- def identity(self, access_token: str) -> dict:
150
+ def identity(self, access_token: str) -> dict[Any, Any]:
150
151
  """Fetch identity/account holder information."""
151
152
  request = IdentityGetRequest(access_token=access_token)
152
153
  response = self.client.identity_get(request)
153
- return response.to_dict()
154
+ return cast(dict[Any, Any], response.to_dict())
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import ssl
26
26
  import httpx
27
- from typing import Any
27
+ from typing import Any, cast
28
28
 
29
29
  from ..base import BankingProvider
30
30
 
@@ -93,7 +93,13 @@ class TellerClient(BankingProvider):
93
93
  ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
94
94
  client_kwargs["verify"] = ssl_context
95
95
 
96
- self.client = httpx.Client(**client_kwargs)
96
+ # Create client with explicit parameters to satisfy type checker
97
+ self.client = httpx.Client(
98
+ base_url=str(client_kwargs["base_url"]),
99
+ timeout=float(client_kwargs["timeout"]), # type: ignore[arg-type]
100
+ headers=client_kwargs["headers"], # type: ignore[arg-type]
101
+ verify=client_kwargs.get("verify", True), # type: ignore[arg-type]
102
+ )
97
103
 
98
104
  def _request(self, method: str, path: str, **kwargs: Any) -> Any:
99
105
  """Make HTTP request to Teller API with error handling.
@@ -139,7 +145,7 @@ class TellerClient(BankingProvider):
139
145
  "products": ["accounts", "transactions", "balances", "identity"],
140
146
  },
141
147
  )
142
- return response.get("enrollment_id", "")
148
+ return cast(str, response.get("enrollment_id", ""))
143
149
 
144
150
  def exchange_public_token(self, public_token: str) -> dict:
145
151
  """Exchange public token for access token.
@@ -186,7 +192,7 @@ class TellerClient(BankingProvider):
186
192
  auth=(access_token, ""),
187
193
  )
188
194
  response.raise_for_status()
189
- return response.json()
195
+ return cast(list[dict[Any, Any]], response.json())
190
196
 
191
197
  def transactions(
192
198
  self,
@@ -229,7 +235,7 @@ class TellerClient(BankingProvider):
229
235
  params=params,
230
236
  )
231
237
  response.raise_for_status()
232
- return response.json()
238
+ return cast(list[dict[Any, Any]], response.json())
233
239
 
234
240
  def balances(self, access_token: str, account_id: str | None = None) -> dict:
235
241
  """Fetch current balances.
@@ -261,7 +267,7 @@ class TellerClient(BankingProvider):
261
267
  )
262
268
 
263
269
  response.raise_for_status()
264
- return response.json()
270
+ return cast(dict[Any, Any], response.json())
265
271
 
266
272
  def identity(self, access_token: str) -> dict:
267
273
  """Fetch identity/account holder information.
@@ -285,7 +291,7 @@ class TellerClient(BankingProvider):
285
291
  auth=(access_token, ""),
286
292
  )
287
293
  response.raise_for_status()
288
- return response.json()
294
+ return cast(dict[Any, Any], response.json())
289
295
 
290
296
  def __del__(self) -> None:
291
297
  """Close HTTP client on cleanup."""
@@ -7,7 +7,7 @@ mode for development and testing. Live trading requires explicit opt-in.
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
- from typing import Literal
10
+ from typing import Any, Literal, cast
11
11
 
12
12
  try:
13
13
  from alpaca_trade_api import REST
@@ -308,14 +308,14 @@ class AlpacaBrokerage(BrokerageProvider):
308
308
  return self._extract_raw(watchlist)
309
309
 
310
310
  @staticmethod
311
- def _extract_raw(obj) -> dict:
311
+ def _extract_raw(obj: Any) -> dict[Any, Any]:
312
312
  """Extract raw dict from Alpaca entity object.
313
313
 
314
314
  Alpaca entities have a _raw attribute with the API response data.
315
315
  """
316
316
  if hasattr(obj, "_raw"):
317
- return obj._raw
317
+ return cast(dict[Any, Any], obj._raw)
318
318
  elif hasattr(obj, "__dict__"):
319
- return obj.__dict__
319
+ return cast(dict[Any, Any], obj.__dict__)
320
320
  else:
321
- return obj
321
+ return cast(dict[Any, Any], obj)
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any, cast
4
+
3
5
  import ccxt
4
6
 
5
7
  from ..base import CryptoDataProvider
@@ -15,14 +17,14 @@ class CCXTCryptoData(CryptoDataProvider):
15
17
  # Defer load_markets to first call to avoid network on construction
16
18
  self._markets_loaded = False
17
19
 
18
- def ticker(self, symbol_pair: str) -> dict:
20
+ def ticker(self, symbol_pair: str) -> dict[Any, Any]:
19
21
  if not self._markets_loaded:
20
22
  self.exchange.load_markets()
21
23
  self._markets_loaded = True
22
- return self.exchange.fetch_ticker(symbol_pair)
24
+ return cast(dict[Any, Any], self.exchange.fetch_ticker(symbol_pair))
23
25
 
24
26
  def ohlcv(self, symbol_pair: str, timeframe: str = "1d", limit: int = 100) -> list[list[float]]:
25
27
  if not self._markets_loaded:
26
28
  self.exchange.load_markets()
27
29
  self._markets_loaded = True
28
- return self.exchange.fetch_ohlcv(symbol_pair, timeframe=timeframe, limit=limit)
30
+ return cast(list[list[float]], self.exchange.fetch_ohlcv(symbol_pair, timeframe=timeframe, limit=limit))
@@ -313,9 +313,9 @@ class MockTaxProvider(TaxProvider):
313
313
  return CryptoTaxReport(
314
314
  user_id=user_id,
315
315
  tax_year=tax_year,
316
- total_gain_loss=short_term + long_term,
317
- short_term_gain_loss=short_term,
318
- long_term_gain_loss=long_term,
316
+ total_gain_loss=Decimal(short_term + long_term),
317
+ short_term_gain_loss=Decimal(short_term),
318
+ long_term_gain_loss=Decimal(long_term),
319
319
  transaction_count=len(crypto_transactions),
320
320
  cost_basis_method=cost_basis_method,
321
321
  transactions=crypto_transactions,
@@ -14,7 +14,7 @@ Only called for ambiguous patterns (20-40% variance, ~10% of patterns).
14
14
  from __future__ import annotations
15
15
 
16
16
  import logging
17
- from typing import Any, Optional
17
+ from typing import Any, Optional, cast
18
18
 
19
19
  from pydantic import BaseModel, ConfigDict, Field
20
20
 
@@ -292,7 +292,7 @@ class VariableDetectorLLM:
292
292
 
293
293
  # Extract structured output
294
294
  if hasattr(response, "structured") and response.structured:
295
- return response.structured
295
+ return cast(VariableRecurringPattern, response.structured)
296
296
  else:
297
297
  raise ValueError(f"LLM returned no structured output for '{merchant_name}'")
298
298
 
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Any, Optional
18
+ from typing import Any, Optional, cast
19
19
 
20
20
  from pydantic import BaseModel, ConfigDict, Field
21
21
 
@@ -383,7 +383,7 @@ class SubscriptionInsightsGenerator:
383
383
 
384
384
  # Extract structured output
385
385
  if hasattr(response, "structured") and response.structured:
386
- return response.structured
386
+ return cast(SubscriptionInsights, response.structured)
387
387
  else:
388
388
  raise ValueError("LLM returned no structured output for insights")
389
389
 
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import re
13
13
  from functools import lru_cache
14
+ from typing import cast
14
15
 
15
16
  try:
16
17
  from rapidfuzz import fuzz, process
@@ -165,7 +166,7 @@ class FuzzyMatcher:
165
166
  norm2 = normalize_merchant(name2)
166
167
 
167
168
  similarity = fuzz.token_sort_ratio(norm1, norm2)
168
- return similarity >= self.similarity_threshold
169
+ return cast(bool, similarity >= self.similarity_threshold)
169
170
 
170
171
  def group_merchants(self, merchants: list[str]) -> dict[str, list[str]]:
171
172
  """
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import hashlib
18
18
  import logging
19
- from typing import Any, Optional
19
+ from typing import Any, Optional, cast
20
20
 
21
21
  from pydantic import BaseModel, ConfigDict, Field
22
22
 
@@ -354,7 +354,7 @@ class MerchantNormalizer:
354
354
 
355
355
  # Extract structured output
356
356
  if hasattr(response, "structured") and response.structured:
357
- return response.structured
357
+ return cast(MerchantNormalized, response.structured)
358
358
  else:
359
359
  raise ValueError(f"LLM returned no structured output for '{merchant_name}'")
360
360
 
@@ -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, Dict, Optional
10
+ from typing import Any, Dict, Optional, cast
11
11
 
12
12
  from cryptography.fernet import Fernet, InvalidToken
13
13
 
@@ -144,7 +144,7 @@ class ProviderTokenEncryption:
144
144
  "Token may have been tampered with or used for wrong user/provider."
145
145
  )
146
146
 
147
- return data["token"]
147
+ return cast(str, data["token"])
148
148
 
149
149
  except InvalidToken as e:
150
150
  raise ValueError(
@@ -67,7 +67,7 @@ def luhn_checksum(card_number: str) -> bool:
67
67
  True if valid, False otherwise
68
68
  """
69
69
 
70
- def digits_of(n):
70
+ def digits_of(n: int | str) -> list[int]:
71
71
  return [int(d) for d in str(n)]
72
72
 
73
73
  digits = digits_of(card_number)
@@ -162,7 +162,9 @@ async def get_provider_token(
162
162
 
163
163
  # Decrypt token
164
164
  context = {"user_id": user_id, "provider": provider}
165
- token = encryption.decrypt(token_obj.encrypted_token, context=context)
165
+ # Cast to str since SQLAlchemy Column[str] needs explicit conversion for type checker
166
+ encrypted_token_str: str = str(token_obj.encrypted_token)
167
+ token = encryption.decrypt(encrypted_token_str, context=context)
166
168
 
167
169
  # Update last_used_at
168
170
  update_stmt = (
fin_infra/utils/http.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import httpx
4
+ from typing import Any, cast
4
5
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
5
6
 
6
7
  _DEFAULT_TIMEOUT = httpx.Timeout(20.0)
@@ -12,8 +13,8 @@ _DEFAULT_TIMEOUT = httpx.Timeout(20.0)
12
13
  retry=retry_if_exception_type(httpx.HTTPError),
13
14
  reraise=True,
14
15
  )
15
- async def aget_json(url: str, **kwargs) -> dict:
16
+ async def aget_json(url: str, **kwargs) -> dict[Any, Any]:
16
17
  async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
17
18
  r = await client.get(url, **kwargs)
18
19
  r.raise_for_status()
19
- return r.json()
20
+ return cast(dict[Any, Any], r.json())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.1.62
3
+ Version: 0.1.64
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
@@ -10,11 +10,11 @@ fin_infra/analytics/projections.py,sha256=7cuG6w1KXq8sd3UNufu5aOcxG5n-foswrHqrgW
10
10
  fin_infra/analytics/rebalancing.py,sha256=K3S7KQiIU2LwyAwWN9VrSly4AOl24vN9tz_JX7I9FJ8,14642
11
11
  fin_infra/analytics/savings.py,sha256=tavIRZtu9FjCm-DeWg5f060GcsdgD-cl-vgKOnieOUw,7574
12
12
  fin_infra/analytics/scenarios.py,sha256=LE_dZVkbxxAx5sxitGhiOhZfWTlYtVbIvS9pEXkijLc,12246
13
- fin_infra/analytics/spending.py,sha256=zXTcYAj_fWQtzOHgSN4P0dSIm80Q5eke6T3LbWltjyU,25882
14
- fin_infra/banking/__init__.py,sha256=wva1SEyrH2po79YycQ_00ZyC2tVeuO3uYcyvudOW484,22267
13
+ fin_infra/analytics/spending.py,sha256=SxfsBPdLoHrFKcx56Da63j0YEV_zxYxaV2lEIQIaMQk,26197
14
+ fin_infra/banking/__init__.py,sha256=-Pn8YxpE_aOefFGL4bBmOogywNWShEV271CKxlrvTIM,22556
15
15
  fin_infra/banking/history.py,sha256=1ufAwkTnXr-QJetFzJl4xA2e3dqd1-TkT8pf46MNfho,10630
16
16
  fin_infra/banking/utils.py,sha256=B2ebnTeUz-56l8XMBWnf2txFOr0bXIo3cKPio7_bhc4,15711
17
- fin_infra/brokerage/__init__.py,sha256=RB0wbVlxM9PCbWUezzjrOf19JucVDpCvNlT62LoMzho,17023
17
+ fin_infra/brokerage/__init__.py,sha256=2Qmiubu9kxouidrCcIHOMvYGHEgVDD5y2S2XRBAWng8,17263
18
18
  fin_infra/budgets/__init__.py,sha256=V6euagDkFHvWyjHeI64fxfddhOHDlIWwOc-PnTyyQb4,3986
19
19
  fin_infra/budgets/add.py,sha256=tdfNXD9deEEzy0xCRbTwbx70SX9wiJkWj0Huk_uCjFg,13584
20
20
  fin_infra/budgets/alerts.py,sha256=i0lQa3mLWuLQorWL-77VKhXQG2F_0U1cpdZnK3g1y3M,9720
@@ -29,11 +29,11 @@ fin_infra/budgets/templates.py,sha256=Sbc7RcHXscq34g4t7J8OXM2Kfkt5DHuvqVnFU0Jidd
29
29
  fin_infra/budgets/tracker.py,sha256=U8C7k2VV8bOjtjPIWR8qXktsBNSyBAUnE9o2mjEs1MU,16490
30
30
  fin_infra/cashflows/__init__.py,sha256=OEleZSwEHffxTvz0J52qqlkBwnu4BHbQUW0vKwzsWAs,8579
31
31
  fin_infra/cashflows/core.py,sha256=Or0hPqCvY_ypV0YiMXh-mle6xWK0tE8WuPPAqHGUp8E,532
32
- fin_infra/categorization/__init__.py,sha256=7551OjE668A_Bhm07QSTBkm4PD3uCOEwdz05KnIlr2A,1997
33
- fin_infra/categorization/add.py,sha256=jbxM51MyIFsAcleCMzP1I5jYV9EsKALzBCnuzKk76sc,6328
34
- fin_infra/categorization/ease.py,sha256=NudJBqFByS0YONPn_4O_Q7QYIiVCCgNbAhn-ugJpa0Y,5826
35
- fin_infra/categorization/engine.py,sha256=VxVuLym_RkKK0xpZrfLKuksFVoURmXICgdik7KpxXMs,12075
36
- fin_infra/categorization/llm_layer.py,sha256=JkzTTdlUWtiCBzKgNAGWvhR7Qt4-UVc12itD2BlxwlQ,12695
32
+ fin_infra/categorization/__init__.py,sha256=efLje12AW-ec9Vs5ynb41r4XCIWx5a-Z9WoGb3kQdIE,2030
33
+ fin_infra/categorization/add.py,sha256=rsFPNWx-c8bXHiizA_MHMjsn6YiCPUBVU2kwA_8yoeM,6382
34
+ fin_infra/categorization/ease.py,sha256=oIcPVuxXOPaAhSe_OcfO4eumCg9WpfIZUdg-k-Xx800,5859
35
+ fin_infra/categorization/engine.py,sha256=ZJm-V6I1okDSFQA34GFZCOTsLCztNtmHlbm2-r51mwQ,12108
36
+ fin_infra/categorization/llm_layer.py,sha256=0Y71o7jzE_Xs2wl_x7COM37PeP8NuTiaKiXzNCVm2sE,12727
37
37
  fin_infra/categorization/models.py,sha256=O8ceQOM0ljRh0jkmnjV7CK5Jyq1DI3lG07UTeeMheNg,5931
38
38
  fin_infra/categorization/rules.py,sha256=m3OogJY0hJe5BrmZqOvOKS2-HRdW4Y5jvvtlPDn9Pn8,12884
39
39
  fin_infra/categorization/taxonomy.py,sha256=qsgo7VJkM6GFBBOaTRHWP82vl5SinRKnMsj4ICarEyQ,13281
@@ -46,14 +46,14 @@ fin_infra/cli/cmds/scaffold_cmds.py,sha256=HZrnJ6NgTBYbt6LuJeoi7JKJgWE_umX9v7zjt
46
46
  fin_infra/clients/__init__.py,sha256=EiKkAW8WY5dWtu7DDmpf3DBNcjJArxcQnE_wTAtVRho,129
47
47
  fin_infra/clients/base.py,sha256=K5nI4GJzT36oUUYGynV3b4eywJdTTa5EX26QK7XdTcc,970
48
48
  fin_infra/clients/plaid.py,sha256=jwcLdQe0G7afLO5JH5jsBosE4iz_cFsb04I8_hCbIb0,807
49
- fin_infra/compliance/__init__.py,sha256=03eXxRDFeQnEaz_W8MVYYDv_ni7Urue2StLf02jH624,5180
49
+ fin_infra/compliance/__init__.py,sha256=6agXxEA5GApSvYgFWNWJDcytGv-pxu0uxx8AG9qL_n0,5296
50
50
  fin_infra/credit/__init__.py,sha256=cwCP_WlrG-0yb_L4zYsuzEsSalcfiCY9ItqXfD7Jx9E,6719
51
- fin_infra/credit/add.py,sha256=etRbqw15vzUQfvnMTmznZlLiKy2GVEe8ok08Ea3pjdE,8490
51
+ fin_infra/credit/add.py,sha256=D3btx9pmZ3tF6AYC6P4Y3dYaUuWp7M3FpDrFksxi5uM,8553
52
52
  fin_infra/credit/experian/__init__.py,sha256=g3IJGvDOMsnB0er0Uwdvl6hGKKTOazqJxSDnB2oIBm0,761
53
- fin_infra/credit/experian/auth.py,sha256=SHi3YNPFwEAS_SraAiAK7V-DEokgaq-7-eqkkBrcgMo,5562
54
- fin_infra/credit/experian/client.py,sha256=crIO37qBoC4wGWH4X_-2cSosf7hX6kfVDQU1NTH58HE,8615
53
+ fin_infra/credit/experian/auth.py,sha256=e9AF-HCNga-561END4kOYtANUvPKLfK1HUoFA-jpBys,5608
54
+ fin_infra/credit/experian/client.py,sha256=p_NUMNyEpXEAoXTQo91je12bysn_EswbJxJOzGIwsO0,8643
55
55
  fin_infra/credit/experian/parser.py,sha256=7ptdLyTWWqHWqCo1CXn6L7XaIn9ZRRuOaATbFmMZZ64,7489
56
- fin_infra/credit/experian/provider.py,sha256=iG2cyftdc7c2pvKWVfeNd3vF_ylNayyhgyUG7Jnl1VI,13766
56
+ fin_infra/credit/experian/provider.py,sha256=l3NW6dppgxeUkrThftH-IB43bwuZGhNcW0jVBGF8XGY,13783
57
57
  fin_infra/credit/mock.py,sha256=xKWZk3fhuIYRfiZkNc9fbHUNViNKjmOLSj0MTI1f4ik,5356
58
58
  fin_infra/crypto/__init__.py,sha256=HpplYEY8GiBz55ehYRDQxs8SWJIW1smBs9eFOKt_nzI,8318
59
59
  fin_infra/crypto/insights.py,sha256=u5gzoLtVPuWbQcFYX-TW-bJtILqB_AvCIxXZ9hd8oQg,11625
@@ -66,10 +66,10 @@ fin_infra/documents/ocr.py,sha256=cuXzrx6k3GIhiaB4-OMPyroB6GBdXuvXP7LAcs0ZV5o,95
66
66
  fin_infra/documents/storage.py,sha256=GS_GtUXLMIYqe2yHb9IaQFloRER3xeQ8fla_loozP68,10177
67
67
  fin_infra/exceptions.py,sha256=woCazH0RxnGcrmsSA3NMZF4Ygr2dtI4tfzKNiFZ10AA,16953
68
68
  fin_infra/goals/__init__.py,sha256=Vg8LKLlDoRiWHsJX7wu5Zcc-86NNLpHoLTjYVkGi2c4,2130
69
- fin_infra/goals/add.py,sha256=cNf0H7EzssMeCYHBWQPW4lHoz1uUWhGMVUUqGMKhNtk,20566
69
+ fin_infra/goals/add.py,sha256=vhExYtXrIId4ZMBr3WH9iLsCT3gAvuAxeHHE8xNTT0U,20605
70
70
  fin_infra/goals/funding.py,sha256=6wn25N0VTYfKLzZWhEn0xdC0ft49qdElkQFc9IwmdPk,9334
71
- fin_infra/goals/management.py,sha256=_DA4lHvcNJVCKLusSU1tIbPc_2Ya8KqXY_67ku6Asws,33815
72
- fin_infra/goals/milestones.py,sha256=MTh3iyJSkDjLNYE1RtmyY4MuxqaGTgohTGU0NAbJaV0,9967
71
+ fin_infra/goals/management.py,sha256=72nYJjbzPNMU9TjmVZnL9wrwXCl9MlmGDfxQiVVBIqc,33865
72
+ fin_infra/goals/milestones.py,sha256=LEJ9M7yOKJ-8thPuH0byHACabCUA9qW7mMATsPomaJA,9995
73
73
  fin_infra/goals/models.py,sha256=DxUrYJqlfKdrmFBucNikLbto3NgxoiJAmsL3v0LR4DQ,10237
74
74
  fin_infra/goals/scaffold_templates/README.md,sha256=CoE_3I2K32orOFH6CvfVBaJBTGDYIESd5-48V7vU1FI,9974
75
75
  fin_infra/goals/scaffold_templates/__init__.py,sha256=rLFam-mRsj8LvJu5kRBEIJtw9rFUof7KApgD2IRE56c,107
@@ -79,20 +79,20 @@ fin_infra/goals/scaffold_templates/schemas.py.tmpl,sha256=M1hS1pK9UDXcNqPW-NGu98
79
79
  fin_infra/insights/__init__.py,sha256=crIXNlztTCcYHNcEVMo8FwCTCUBwIK2wovb4HahzRYw,3988
80
80
  fin_infra/insights/aggregator.py,sha256=XG32mN5w5Nc4AZllmfl1esL4q44mFAf0Fvj9mWev_zk,10249
81
81
  fin_infra/insights/models.py,sha256=xov_YV8oBLJt3YdyVjbryRfcXqmGeGiPvZsZHSbvtl8,3202
82
- fin_infra/investments/__init__.py,sha256=UiWvTdKH7V9aaqZLunPT1_QGfXBAZbPk_w4QmeLWLqo,6324
82
+ fin_infra/investments/__init__.py,sha256=mDHYMLSo14KByrpW9HuxIDFBdfEESZ6tFKC09Vr_aIE,6786
83
83
  fin_infra/investments/add.py,sha256=3cbjXbWoTuDglwk9U48X6768Etv1XLTWysdDPgsn7Yg,17658
84
- fin_infra/investments/ease.py,sha256=ocs7xvnZ1u8riFjH9KHi1yFEUF0lfuEcd-QMpsuiOu8,9229
85
- fin_infra/investments/models.py,sha256=8GQuq-aGww2tzze-VrW71dBNYN918_TmtkwycCVa434,15975
84
+ fin_infra/investments/ease.py,sha256=7oyMcTVmnc8pn3lqriLRruBLEznI_grpDkH7hfXmGhE,9527
85
+ fin_infra/investments/models.py,sha256=KwGLw8jdgX5tw4zZjBWBEKNqKsMo3hJnmsx5hIV-rQU,16032
86
86
  fin_infra/investments/providers/__init__.py,sha256=V1eIzz6EnGJ-pq-9L3S2-evmcExF-YdZfd5P6JMyDtc,383
87
87
  fin_infra/investments/providers/base.py,sha256=KaJdIdeWi2WaWAogcFZw7jcqQ_IzMZw6misBNk-n6bE,9890
88
- fin_infra/investments/providers/plaid.py,sha256=OKzLNPAcBiZlqBRCOeaBogB5fvHdvlRpPuGHKvRGS2E,18069
89
- fin_infra/investments/providers/snaptrade.py,sha256=IUgXoI7UCBXOAxn2J62cOiQSW02sujqEk47nb638264,23508
88
+ fin_infra/investments/providers/plaid.py,sha256=2WqXwPVckIc56dx9BleC10z-WHDt7y1dO1xhsI_8kT4,18108
89
+ fin_infra/investments/providers/snaptrade.py,sha256=ILpup62u5zCOgnQ_4RF1_m2BY7qowyINB-FUgq-jUrI,23542
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=mStcYiA4dq2yHEyStZyOLd-KkW-Jf657l8NSLLa_MU8,9512
95
+ fin_infra/markets/__init__.py,sha256=S6vaHI5T3NnP_Kwf5bym7P0lFUhewqmXKh6sEPbaBRs,9892
96
96
  fin_infra/models/__init__.py,sha256=q3SkGzDGFkoAMxwqJw8i4cHWt5NGU5ypjOgntxDGVKo,860
97
97
  fin_infra/models/accounts.py,sha256=PeobjGg6WM70OvOTe0JIo0zo7tBM0PDAcyClQT-Jo4o,1141
98
98
  fin_infra/models/brokerage.py,sha256=z6Zyf0N5zmmXtrN2y_4fNmtIP5wNq40H8lrHLBwY7rc,8311
@@ -119,40 +119,40 @@ fin_infra/normalization/__init__.py,sha256=-7EP_lTExQpoCtgsx1wD3j8aMH9y3SlFgHke3
119
119
  fin_infra/normalization/currency_converter.py,sha256=uuu8ASa5ppEniWLEVEpiDxXjZzln9nopWrhrATcD6Z4,7058
120
120
  fin_infra/normalization/models.py,sha256=gNC9chpbQPRN58V2j__VEPVNReO1N8jH_AHObwGPWu0,1928
121
121
  fin_infra/normalization/providers/__init__.py,sha256=LFU1tB2hVO42Yrkw-IDpPexD4mIlxob9lRrJEeGYqpE,559
122
- fin_infra/normalization/providers/exchangerate.py,sha256=zOTcDYjKDeGpBjplnSB7XVQo_Zt6y0EdSIbzdziLkUs,6298
122
+ fin_infra/normalization/providers/exchangerate.py,sha256=vA1W2yVpCf89kOx6lctbHOQbR96ByhvxH8SeyYSV94c,6352
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
126
  fin_infra/obs/classifier.py,sha256=qZHgUV6J2sXdOhHCPOxmonyvE4V1vY-A5MDwFpzk2lk,5136
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
- fin_infra/providers/banking/plaid_client.py,sha256=t4pW6vkecx1AgkKDXUSiileAxA6pu6dA4L6c8zSff0k,6545
130
- fin_infra/providers/banking/teller_client.py,sha256=r4apwTNt8FJ2Rn2bC97orzaVYFkE0yMXXl--H5rtph0,9800
129
+ fin_infra/providers/banking/plaid_client.py,sha256=21m6ZkovwXuUuj0-dgQVDLxSfxZVjhuXj8di_-q3jGc,6617
130
+ fin_infra/providers/banking/teller_client.py,sha256=QmrsBlk3_rHT-pTQPrIAA74kjIjcgdi-gOb8NA3oBO8,10268
131
131
  fin_infra/providers/base.py,sha256=oLzdExPGE7yg-URtin3vGTQ8hEzG7UnTmDGDWJB5oL0,4273
132
- fin_infra/providers/brokerage/alpaca.py,sha256=c6Qapf271jpTe1seuFGIbBTDaQkMpSsOH1hE4f_h3Ms,9846
132
+ fin_infra/providers/brokerage/alpaca.py,sha256=wRVfVmExiYXCk1pLRmHSrfo91714JIm3rrD0djrNfT8,9938
133
133
  fin_infra/providers/brokerage/base.py,sha256=JJFH0Cqca4Rg4rmxfiwcQt-peRoBf4JpG3g6jx8DVks,106
134
134
  fin_infra/providers/credit/experian.py,sha256=hNEVqmCaPT72NHV3Nw3sKOYPX0kIsl819ucqUc-7z2k,341
135
135
  fin_infra/providers/identity/stripe_identity.py,sha256=JQGJRuQdWP5dWDcROgtz1RrmpkytRv95H6Fn-x1kifU,501
136
136
  fin_infra/providers/market/alphavantage.py,sha256=srZdkf-frBuKyPTdWasMmVrpnh76BEBDXa-nsYtLzNc,8963
137
137
  fin_infra/providers/market/base.py,sha256=ljBzZTfjYQS9tXahmxFic7JQSZeyoiDMUZ1NY0R7yto,108
138
- fin_infra/providers/market/ccxt_crypto.py,sha256=sqWu-718mGi7gUTIZKX4huJlMNLEIhpApIRFTBP915g,1054
138
+ fin_infra/providers/market/ccxt_crypto.py,sha256=52WdAx106deCFqLvzhOzrToVH2xkNXGf0i5kymVIjEA,1141
139
139
  fin_infra/providers/market/coingecko.py,sha256=F1Bwdk28xSsIaFEuT7lhT3F6Vkd0Lp-CMp1rnYiLfaE,2702
140
140
  fin_infra/providers/market/yahoo.py,sha256=FNhqkCFC0In-Z3zpzmuknEORHLRK5Evk2KSk0yysKjg,4954
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
144
- fin_infra/providers/tax/mock.py,sha256=35QulDz-fmgXyibPt1cpMhL0WgGWeziOwHnlEd1QRd0,14415
144
+ fin_infra/providers/tax/mock.py,sha256=AxI3RmEn3exdQeeUNkQYqZ-war5PS--WnLGXfRRee8o,14442
145
145
  fin_infra/providers/tax/taxbit.py,sha256=DEA7vgQPYMjz4ZdC0DpY7112FLZJ2kvwgAbDZnpHFy0,4271
146
146
  fin_infra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
147
  fin_infra/recurring/__init__.py,sha256=ihMPywft8pGqzMu6EXxbQCU7ByoMl_dvad00gWV1mnk,2308
148
148
  fin_infra/recurring/add.py,sha256=bOvpRbtMjWYqNpq8dTR6aCNsR0iTRMyXWGZyMWQZk8A,18573
149
149
  fin_infra/recurring/detector.py,sha256=1e6PRoBAT2NxoGAgcVHAWwpPtznkJMaYSrJtvSq0YqM,20154
150
- fin_infra/recurring/detectors_llm.py,sha256=e7hLHD4AVobPAOS5SUGBy2QHulVv4VAvZ4Si4hiHCSo,11466
150
+ fin_infra/recurring/detectors_llm.py,sha256=LGNj_uMK8bQGtKnmDsjWDr11WJM331lvfEZ4ZdBD67c,11504
151
151
  fin_infra/recurring/ease.py,sha256=OrpxGHi8kt6LkMmww5l0Xy2pU-5hP_dR4IgdOiaIRaU,11179
152
- fin_infra/recurring/insights.py,sha256=wVf3Fi6qsdiJ_l1ntNGZicahSWnGDn86f7SaWJSk0NA,15852
152
+ fin_infra/recurring/insights.py,sha256=J_Gvbv9-pkb0IcjNJYSitejNVQPnKJ1N5L1hTkzbnGA,15886
153
153
  fin_infra/recurring/models.py,sha256=N4G_LM0xZr3ptHtlqOmcsw3AL2v9g7IX92SmBljkNek,8894
154
- fin_infra/recurring/normalizer.py,sha256=LIZU90BTshsFvswu5pjLPIvIoWbUTXxzTWBOrmzJ6pI,9725
155
- fin_infra/recurring/normalizers.py,sha256=0LJ3B30LWlI9crIvwnxWxDW6CYJV5uGKCU7FGcN3x8o,15896
154
+ fin_infra/recurring/normalizer.py,sha256=Rc1ntIDGir6X-I5lgv49kdLry_zHGJ8cys_Jf3F6Lhk,9761
155
+ fin_infra/recurring/normalizers.py,sha256=9kGsbNuxGb4xYUwMqjJZ84m924jT6jWZPQUSXGkqkNU,15928
156
156
  fin_infra/recurring/summary.py,sha256=1Wte58ZZkEFulkb-nnpwfC5h7C_JrqByy47itdVdWwc,14665
157
157
  fin_infra/scaffold/__init__.py,sha256=OyD8ZtIC4eNTHqD16rbpT8KU0TpZUI6VV4xne4vpaHg,831
158
158
  fin_infra/scaffold/budgets.py,sha256=XXOLlEcyBXVwdbJB__qObRXJ0oe1okwDT_-5tG8c9Yk,9515
@@ -160,21 +160,21 @@ fin_infra/scaffold/goals.py,sha256=uVYzbbfbXGrf8qeGvq8mtY6o_YIk17aZ0DfSGQx6Y58,9
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=BzNH4L72E-lBgTTeaHADgaftO4rsjNKTmVcixOR4xUE,6151
163
+ fin_infra/security/encryption.py,sha256=z1k5LFkuuMCjAUnBzBCOviyi0F1R_vabdHhdJJdbPx4,6168
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
- fin_infra/security/pii_patterns.py,sha256=hsW-2RwA8XW3wprsvzkqcWK9uX_HrdLH53g7OUKiwvM,3046
167
- fin_infra/security/token_store.py,sha256=UucTXfgRbdbogahS_2q5CPSb7dFyctN9D3m-ecJkqX4,5929
166
+ fin_infra/security/pii_patterns.py,sha256=SM-o7cL6NdgkOmtBedsN2nJZ5QPbeYehZdYmAujk8Y8,3070
167
+ fin_infra/security/token_store.py,sha256=qgxhBKwhtVpchyHv30mM-cttuGZlzZvZLC4Oa-gTTeg,6075
168
168
  fin_infra/settings.py,sha256=xitpBQJmuvSy9prQhvXOW1scbwB1KAyGD8XqYgU_hQU,1388
169
169
  fin_infra/tax/__init__.py,sha256=NXUjV-k-rw4774pookY3UOwEXYRQauJze6Yift5RjW0,6107
170
170
  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
- fin_infra/utils/http.py,sha256=wgXo5amXyzAX49v_lRUvp4Xxq8nodX32CMJyWl6u89I,568
173
+ fin_infra/utils/http.py,sha256=pvcxbNQ9oisoGPkNe3xX9aAgWzEN6mmdtr1w-L02Xj8,629
174
174
  fin_infra/utils/retry.py,sha256=ISBrup5XCuXqHZh9kjTGvGQYcuyYyqZE4u26wW7r3CM,1030
175
175
  fin_infra/version.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
176
- fin_infra-0.1.62.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
- fin_infra-0.1.62.dist-info/METADATA,sha256=OeV__Ai7lp82Wes9tvQAL5TE2TRmZcDBJicDqMhskJc,10218
178
- fin_infra-0.1.62.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
- fin_infra-0.1.62.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
- fin_infra-0.1.62.dist-info/RECORD,,
176
+ fin_infra-0.1.64.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
+ fin_infra-0.1.64.dist-info/METADATA,sha256=e3BndWoe-yekpgp-__h85zwt-_f5PMLj3inQ4Y-oS_8,10218
178
+ fin_infra-0.1.64.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
+ fin_infra-0.1.64.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
+ fin_infra-0.1.64.dist-info/RECORD,,