fin-infra 0.1.63__py3-none-any.whl → 0.1.65__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 (33) hide show
  1. fin_infra/analytics/spending.py +9 -7
  2. fin_infra/banking/__init__.py +5 -2
  3. fin_infra/brokerage/__init__.py +4 -2
  4. fin_infra/categorization/__init__.py +1 -1
  5. fin_infra/categorization/add.py +15 -16
  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/chat/__init__.py +1 -10
  10. fin_infra/chat/ease.py +1 -1
  11. fin_infra/chat/planning.py +57 -0
  12. fin_infra/crypto/insights.py +1 -1
  13. fin_infra/investments/__init__.py +11 -4
  14. fin_infra/investments/ease.py +11 -7
  15. fin_infra/investments/models.py +1 -1
  16. fin_infra/investments/providers/plaid.py +1 -1
  17. fin_infra/markets/__init__.py +8 -3
  18. fin_infra/net_worth/ease.py +34 -13
  19. fin_infra/normalization/__init__.py +6 -6
  20. fin_infra/providers/banking/teller_client.py +7 -1
  21. fin_infra/providers/base.py +98 -1
  22. fin_infra/providers/credit/experian.py +5 -0
  23. fin_infra/providers/tax/mock.py +3 -3
  24. fin_infra/recurring/add.py +10 -1
  25. fin_infra/recurring/detectors_llm.py +1 -1
  26. fin_infra/recurring/insights.py +1 -1
  27. fin_infra/recurring/normalizers.py +1 -1
  28. fin_infra/security/token_store.py +3 -1
  29. {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/METADATA +1 -1
  30. {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/RECORD +33 -33
  31. {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/LICENSE +0 -0
  32. {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/WHEEL +0 -0
  33. {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.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
  )
@@ -468,7 +470,7 @@ async def generate_spending_insights(
468
470
 
469
471
  # Try to import ai-infra LLM (optional dependency)
470
472
  try:
471
- from ai_infra.llm import LLM
473
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
472
474
  except ImportError:
473
475
  # Graceful degradation: return rule-based insights
474
476
  return _generate_rule_based_insights(spending_insight, user_context)
@@ -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,
@@ -135,21 +136,19 @@ def add_categorization(
135
136
  categories = get_all_categories()
136
137
 
137
138
  # Return category metadata
138
- return [
139
- {
140
- "name": cat.value,
141
- "group": get_category_metadata(cat).group.value
142
- if get_category_metadata(cat)
143
- else None,
144
- "display_name": get_category_metadata(cat).display_name
145
- if get_category_metadata(cat)
146
- else cat.value,
147
- "description": get_category_metadata(cat).description
148
- if get_category_metadata(cat)
149
- else None,
150
- }
151
- for cat in categories
152
- ]
139
+ result = []
140
+ for cat in categories:
141
+ meta = get_category_metadata(cat)
142
+ result.append(
143
+ {
144
+ "name": cat.value,
145
+ "group": meta.group.value if meta else None,
146
+ "display_name": meta.display_name if meta else cat.value,
147
+ "description": meta.description if meta else None,
148
+ }
149
+ )
150
+
151
+ return result
153
152
 
154
153
  @router.get("/stats", response_model=CategoryStats)
155
154
  async def get_stats():
@@ -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
 
@@ -20,8 +20,8 @@ from pydantic import BaseModel, Field
20
20
 
21
21
  # ai-infra imports
22
22
  try:
23
- from ai_infra.llm import LLM
24
- from ai_infra.llm.providers import Providers
23
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
24
+ from ai_infra.llm.providers import Providers # type: ignore[attr-defined]
25
25
  except ImportError:
26
26
  raise ImportError("ai-infra not installed. Install with: pip install ai-infra")
27
27
 
@@ -149,15 +149,6 @@ def add_financial_conversation(
149
149
  # TODO: Get user_id from svc-infra auth context
150
150
  user_id = "demo_user" # Placeholder
151
151
 
152
- # Check for sensitive content
153
- if is_sensitive_question(request.question):
154
- return ConversationResponse(
155
- answer="I cannot process requests containing sensitive information like SSNs, passwords, or account numbers. Please rephrase your question without this information.",
156
- follow_up_questions=[],
157
- conversation_id=f"{user_id}_denied",
158
- disclaimer="This is an automated safety response.",
159
- )
160
-
161
152
  # Ask conversation
162
153
  response = await conversation.ask(
163
154
  user_id=user_id,
@@ -173,7 +164,7 @@ def add_financial_conversation(
173
164
  # TODO: Get user_id from svc-infra auth context
174
165
  user_id = "demo_user"
175
166
  context = await conversation._get_context(user_id)
176
- return context.exchanges if context else []
167
+ return context.previous_exchanges if context else []
177
168
 
178
169
  @router.delete("/history")
179
170
  async def clear_history():
fin_infra/chat/ease.py CHANGED
@@ -69,7 +69,7 @@ def easy_financial_conversation(
69
69
  # Auto-create LLM if not provided
70
70
  if llm is None:
71
71
  try:
72
- from ai_infra.llm import LLM
72
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
73
73
 
74
74
  llm = LLM()
75
75
  except ImportError:
@@ -338,8 +338,65 @@ class FinancialPlanningConversation:
338
338
  # Save updated context (24h TTL)
339
339
  await self._save_context(context)
340
340
 
341
+ # Track latest session id for convenience endpoints (history/clear).
342
+ # Best-effort: failures here must not break the chat response.
343
+ try:
344
+ await self.cache.set(
345
+ self._latest_session_key(user_id),
346
+ context.session_id,
347
+ ttl=86400,
348
+ )
349
+ except Exception:
350
+ pass
351
+
341
352
  return response
342
353
 
354
+ # ---------------------------------------------------------------------
355
+ # Backward-compatible context helpers
356
+ # ---------------------------------------------------------------------
357
+
358
+ def _latest_session_key(self, user_id: str) -> str:
359
+ return f"fin_infra:conversation_latest_session:{user_id}"
360
+
361
+ async def _get_latest_session_id(self, user_id: str) -> str | None:
362
+ try:
363
+ value = await self.cache.get(self._latest_session_key(user_id))
364
+ except Exception:
365
+ return None
366
+
367
+ if value is None:
368
+ return None
369
+ if isinstance(value, bytes):
370
+ try:
371
+ return value.decode("utf-8")
372
+ except Exception:
373
+ return None
374
+ if isinstance(value, str):
375
+ return value
376
+ return str(value)
377
+
378
+ async def _get_context(
379
+ self, user_id: str, session_id: str | None = None
380
+ ) -> ConversationContext | None:
381
+ if session_id is None:
382
+ session_id = await self._get_latest_session_id(user_id)
383
+ if session_id is None:
384
+ return None
385
+
386
+ return await self._load_context(user_id=user_id, session_id=session_id)
387
+
388
+ async def _clear_context(self, user_id: str, session_id: str | None = None) -> None:
389
+ if session_id is None:
390
+ session_id = await self._get_latest_session_id(user_id)
391
+
392
+ if session_id is not None:
393
+ await self.clear_session(user_id=user_id, session_id=session_id)
394
+
395
+ try:
396
+ await self.cache.delete(self._latest_session_key(user_id))
397
+ except Exception:
398
+ pass
399
+
343
400
  async def _load_context(
344
401
  self,
345
402
  user_id: str,
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING
15
15
  from pydantic import BaseModel, Field
16
16
 
17
17
  if TYPE_CHECKING:
18
- from ai_infra.llm import LLM
18
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
19
19
 
20
20
 
21
21
  class CryptoInsight(BaseModel):
@@ -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"
@@ -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,
@@ -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))
@@ -121,6 +121,15 @@ class NetWorthTracker:
121
121
  self.goal_tracker = goal_tracker
122
122
  self.conversation = conversation
123
123
 
124
+ # Configuration set by easy_net_worth(); declared here for type checkers.
125
+ self.snapshot_schedule: str = "daily"
126
+ self.change_threshold_percent: float = 5.0
127
+ self.change_threshold_amount: float = 10000.0
128
+ self.enable_llm: bool = False
129
+ self.llm_provider: str | None = None
130
+ self.llm_model: str | None = None
131
+ self.config: dict[str, Any] = {}
132
+
124
133
  async def calculate_net_worth(
125
134
  self,
126
135
  user_id: str,
@@ -368,12 +377,20 @@ def easy_net_worth(
368
377
 
369
378
  if enable_llm:
370
379
  try:
371
- from ai_infra.llm import LLM
380
+ from ai_infra.llm.llm import LLM # type: ignore[attr-defined]
372
381
  except ImportError:
373
382
  raise ImportError(
374
383
  "LLM features require ai-infra package. " "Install with: pip install ai-infra"
375
384
  )
376
385
 
386
+ cache = None
387
+ try:
388
+ from svc_infra.cache import get_cache
389
+
390
+ cache = get_cache()
391
+ except Exception:
392
+ cache = None
393
+
377
394
  # Determine default model
378
395
  default_models = {
379
396
  "google": "gemini-2.0-flash-exp",
@@ -416,18 +433,22 @@ def easy_net_worth(
416
433
  # goals.management not yet implemented, skip
417
434
  pass
418
435
 
419
- try:
420
- from fin_infra.conversation import FinancialPlanningConversation
421
-
422
- conversation = FinancialPlanningConversation(
423
- llm=llm,
424
- cache=cache, # Required for context storage
425
- provider=llm_provider,
426
- model_name=model_name,
427
- )
428
- except ImportError:
429
- # conversation module not yet implemented, skip
430
- pass
436
+ if cache is not None:
437
+ try:
438
+ from fin_infra.conversation import FinancialPlanningConversation
439
+
440
+ conversation = FinancialPlanningConversation(
441
+ llm=llm,
442
+ cache=cache, # Required for context storage
443
+ provider=llm_provider,
444
+ model_name=model_name,
445
+ )
446
+ except ImportError:
447
+ # conversation module not yet implemented, skip
448
+ pass
449
+ except Exception:
450
+ # Cache not configured or other runtime issue; skip optional conversation wiring.
451
+ pass
431
452
 
432
453
  # Create tracker
433
454
  tracker = NetWorthTracker(
@@ -150,14 +150,14 @@ def add_normalization(
150
150
  ):
151
151
  """Convert amount between currencies."""
152
152
  try:
153
- result = await converter.convert(amount, from_currency, to_currency)
153
+ result = await converter.convert_with_details(amount, from_currency, to_currency)
154
154
  return {
155
- "amount": amount,
156
- "from_currency": from_currency,
157
- "to_currency": to_currency,
158
- "result": result.amount,
155
+ "amount": result.amount,
156
+ "from_currency": result.from_currency,
157
+ "to_currency": result.to_currency,
158
+ "result": result.converted,
159
159
  "rate": result.rate,
160
- "timestamp": result.timestamp.isoformat(),
160
+ "timestamp": result.date.isoformat() if result.date else None,
161
161
  }
162
162
  except CurrencyNotSupportedError as e:
163
163
  raise HTTPException(status_code=400, detail=str(e))
@@ -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.
@@ -67,7 +67,15 @@ class BankingProvider(ABC):
67
67
  class BrokerageProvider(ABC):
68
68
  @abstractmethod
69
69
  def submit_order(
70
- self, symbol: str, qty: float, side: str, type_: str, time_in_force: str
70
+ self,
71
+ symbol: str,
72
+ qty: float,
73
+ side: str,
74
+ type_: str,
75
+ time_in_force: str,
76
+ limit_price: float | None = None,
77
+ stop_price: float | None = None,
78
+ client_order_id: str | None = None,
71
79
  ) -> dict:
72
80
  pass
73
81
 
@@ -75,6 +83,71 @@ class BrokerageProvider(ABC):
75
83
  def positions(self) -> Iterable[dict]:
76
84
  pass
77
85
 
86
+ @abstractmethod
87
+ def get_account(self) -> dict:
88
+ """Get trading account information."""
89
+ pass
90
+
91
+ @abstractmethod
92
+ def get_position(self, symbol: str) -> dict:
93
+ """Get position for a specific symbol."""
94
+ pass
95
+
96
+ @abstractmethod
97
+ def close_position(self, symbol: str) -> dict:
98
+ """Close a position (market sell/cover)."""
99
+ pass
100
+
101
+ @abstractmethod
102
+ def list_orders(self, status: str = "open", limit: int = 50) -> list[dict]:
103
+ """List orders."""
104
+ pass
105
+
106
+ @abstractmethod
107
+ def get_order(self, order_id: str) -> dict:
108
+ """Get order by ID."""
109
+ pass
110
+
111
+ @abstractmethod
112
+ def cancel_order(self, order_id: str) -> None:
113
+ """Cancel an order."""
114
+ pass
115
+
116
+ @abstractmethod
117
+ def get_portfolio_history(self, period: str = "1M", timeframe: str = "1D") -> dict:
118
+ """Get portfolio value history."""
119
+ pass
120
+
121
+ @abstractmethod
122
+ def create_watchlist(self, name: str, symbols: list[str] | None = None) -> dict:
123
+ """Create a new watchlist."""
124
+ pass
125
+
126
+ @abstractmethod
127
+ def list_watchlists(self) -> list[dict]:
128
+ """List all watchlists."""
129
+ pass
130
+
131
+ @abstractmethod
132
+ def get_watchlist(self, watchlist_id: str) -> dict:
133
+ """Get a watchlist by ID."""
134
+ pass
135
+
136
+ @abstractmethod
137
+ def delete_watchlist(self, watchlist_id: str) -> None:
138
+ """Delete a watchlist."""
139
+ pass
140
+
141
+ @abstractmethod
142
+ def add_to_watchlist(self, watchlist_id: str, symbol: str) -> dict:
143
+ """Add a symbol to a watchlist."""
144
+ pass
145
+
146
+ @abstractmethod
147
+ def remove_from_watchlist(self, watchlist_id: str, symbol: str) -> dict:
148
+ """Remove a symbol from a watchlist."""
149
+ pass
150
+
78
151
 
79
152
  class IdentityProvider(ABC):
80
153
  @abstractmethod
@@ -91,6 +164,11 @@ class CreditProvider(ABC):
91
164
  def get_credit_score(self, user_id: str, **kwargs) -> dict | None:
92
165
  pass
93
166
 
167
+ @abstractmethod
168
+ def get_credit_report(self, user_id: str, **kwargs) -> dict | None:
169
+ """Retrieve full credit report for a user."""
170
+ pass
171
+
94
172
 
95
173
  class TaxProvider(ABC):
96
174
  """Provider for tax data and document retrieval."""
@@ -100,6 +178,11 @@ class TaxProvider(ABC):
100
178
  """Retrieve tax forms for a user and tax year."""
101
179
  pass
102
180
 
181
+ @abstractmethod
182
+ def get_tax_documents(self, user_id: str, tax_year: int, **kwargs) -> list[dict]:
183
+ """Retrieve tax documents for a user and tax year."""
184
+ pass
185
+
103
186
  @abstractmethod
104
187
  def get_tax_document(self, document_id: str, **kwargs) -> dict:
105
188
  """Retrieve a specific tax document by ID."""
@@ -110,6 +193,20 @@ class TaxProvider(ABC):
110
193
  """Calculate capital gains from crypto transactions."""
111
194
  pass
112
195
 
196
+ @abstractmethod
197
+ def calculate_tax_liability(
198
+ self,
199
+ user_id: str,
200
+ income: float,
201
+ deductions: float,
202
+ filing_status: str,
203
+ tax_year: int,
204
+ state: str | None = None,
205
+ **kwargs,
206
+ ) -> dict:
207
+ """Calculate estimated tax liability."""
208
+ pass
209
+
113
210
 
114
211
  class InvestmentProvider(ABC):
115
212
  """Provider for investment holdings and portfolio data (Plaid, SnapTrade).
@@ -11,3 +11,8 @@ class ExperianCredit(CreditProvider):
11
11
  self, user_id: str, **kwargs
12
12
  ) -> dict | None: # pragma: no cover - placeholder
13
13
  return None
14
+
15
+ def get_credit_report(
16
+ self, user_id: str, **kwargs
17
+ ) -> dict | None: # pragma: no cover - placeholder
18
+ return None
@@ -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,
@@ -403,7 +403,16 @@ def add_recurring_detection(
403
403
 
404
404
  # Generate insights with LLM
405
405
  # TODO: Pass user_id for better caching (currently uses subscriptions hash)
406
- insights = await detector.insights_generator.generate(subscriptions)
406
+ insights_generator = detector.insights_generator
407
+ if insights_generator is None:
408
+ from fastapi import HTTPException
409
+
410
+ raise HTTPException(
411
+ status_code=500,
412
+ detail="Subscription insights generator not configured (enable_llm=True required).",
413
+ )
414
+
415
+ insights = await insights_generator.generate(subscriptions)
407
416
 
408
417
  return insights
409
418
  else:
@@ -20,7 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field
20
20
 
21
21
  # Lazy import for optional dependency (ai-infra)
22
22
  try:
23
- from ai_infra.llm import LLM
23
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
24
24
  except ImportError:
25
25
  LLM = None
26
26
 
@@ -21,7 +21,7 @@ from pydantic import BaseModel, ConfigDict, Field
21
21
 
22
22
  # Lazy import for optional dependency (ai-infra)
23
23
  try:
24
- from ai_infra.llm import LLM
24
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
25
25
  except ImportError:
26
26
  LLM = None
27
27
 
@@ -22,7 +22,7 @@ from pydantic import BaseModel, ConfigDict, Field
22
22
 
23
23
  # Lazy import for optional dependency (ai-infra)
24
24
  try:
25
- from ai_infra.llm import LLM
25
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
26
26
  except ImportError:
27
27
  LLM = None
28
28
 
@@ -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 = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.1.63
3
+ Version: 0.1.65
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=IoVLc3FhfE_XWMj9Vt4_gpALStnxu7_8xLw1VBpTSxs,22296
13
+ fin_infra/analytics/spending.py,sha256=-11Pm2E-UjuLfYWLD8wQFf5Xyk_0es1xmiedTsBj-Bc,26227
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,17 +29,17 @@ 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=0Y71o7jzE_Xs2wl_x7COM37PeP8NuTiaKiXzNCVm2sE,12727
32
+ fin_infra/categorization/__init__.py,sha256=efLje12AW-ec9Vs5ynb41r4XCIWx5a-Z9WoGb3kQdIE,2030
33
+ fin_infra/categorization/add.py,sha256=JDOvxngh-7oWHTddOyP4GAse9vLuxSTfoIhrDKUHOKg,6278
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=sOR3gtyPR4y58-5a_XW7U4GMwPsvBwaL_mtmaExC_zk,12787
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
40
- fin_infra/chat/__init__.py,sha256=_4yQ7jRgrOgQAeiplrOnPJnTk9Ojc4Mdxg9thHhI_MQ,6837
41
- fin_infra/chat/ease.py,sha256=8T0BQUkWQVpaTooD5-ZtinackkciqGargXnzWzayj3M,3113
42
- fin_infra/chat/planning.py,sha256=gmVo7t-KLohEaI3r8rJ_-6EFtMnpEmlfl8JhZLAHx94,17936
40
+ fin_infra/chat/__init__.py,sha256=NsYyRxZGwUFvYEmoLfuTaAfBWn34KOKqeRi6_hSVgGE,6356
41
+ fin_infra/chat/ease.py,sha256=b99CtKSe9UJqXPLqlqoUI0mI6bmxHVlbpdFq4K1oCGg,3143
42
+ fin_infra/chat/planning.py,sha256=eKUW6VDHJS-xQTks7bgjNQaO32Fr5gA_oP5NLt2y5Zs,19916
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=HZrnJ6NgTBYbt6LuJeoi7JKJgWE_umX9v7zjtwYfP-g,7659
@@ -56,7 +56,7 @@ fin_infra/credit/experian/parser.py,sha256=7ptdLyTWWqHWqCo1CXn6L7XaIn9ZRRuOaATbF
56
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
- fin_infra/crypto/insights.py,sha256=u5gzoLtVPuWbQcFYX-TW-bJtILqB_AvCIxXZ9hd8oQg,11625
59
+ fin_infra/crypto/insights.py,sha256=2Q0QnpOC-nXJa8pYZmjNre6csz2peGbb5chN5-cipWI,11655
60
60
  fin_infra/documents/__init__.py,sha256=Ub1hbX3PTrBSsBdcbL8PFf6oq8jSH4pYxW45-qOYPqs,1909
61
61
  fin_infra/documents/add.py,sha256=ztSFCY42hfLLgUbXefxvf_AAbzaxJ6xpEZJpcHE8g7c,8133
62
62
  fin_infra/documents/analysis.py,sha256=zY5OQEIlq3JLNND_cg2KheFdryUmIecPOR2lR6oKhPw,13992
@@ -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=z_f4NbJDhi_vcLDA_SR2yuEaRrjRxbRirlKYH6ofDAk,18086
88
+ fin_infra/investments/providers/plaid.py,sha256=2WqXwPVckIc56dx9BleC10z-WHDt7y1dO1xhsI_8kT4,18108
89
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
@@ -106,7 +106,7 @@ fin_infra/net_worth/__init__.py,sha256=EjEuHNg8gEfFwbfko1-o5j-gSUZ2FcO9h7l05C-zA
106
106
  fin_infra/net_worth/add.py,sha256=5xYy2L5hEEPiQNF79i-ArWVztLXk2XM97DoZYNWGAz8,23100
107
107
  fin_infra/net_worth/aggregator.py,sha256=grif-N8qk77L_JQ4IlcOJaKKP1qpxel0lIV_ll3HgjI,12646
108
108
  fin_infra/net_worth/calculator.py,sha256=JERDtZyFurw5x2NYqfHvJzv6qigamI3AFfR-wesTj_E,13133
109
- fin_infra/net_worth/ease.py,sha256=XvOaowgj64JLKOtEq-B8MIc3fgrysL4vG8m1e5j-ukY,15094
109
+ fin_infra/net_worth/ease.py,sha256=n1YI5rEmh48homMJvJMETCbcnICVE5myOnd1OZDJgiY,15920
110
110
  fin_infra/net_worth/goals.py,sha256=BJGxdsMjvgQDELFEJo-ai3DvsAzUNXvzMXkwovHr8yQ,1238
111
111
  fin_infra/net_worth/insights.py,sha256=vVK4BtfHNJGb1wyk9XD0fLpoadATTdorF8OxHOgD9b0,25222
112
112
  fin_infra/net_worth/models.py,sha256=A5idGtMEQy1J4jaLMq9ZZslmvOhxfkW7cjLKW23AMQo,22403
@@ -115,7 +115,7 @@ fin_infra/net_worth/scaffold_templates/__init__.py,sha256=OKeMCC_JNw6m8rBWr_wesO
115
115
  fin_infra/net_worth/scaffold_templates/models.py.tmpl,sha256=9BKsoD08RZbSdOm0wFTbx5OzKfAEtuA1NcWyS1Aywx4,5934
116
116
  fin_infra/net_worth/scaffold_templates/repository.py.tmpl,sha256=DSErnNxeAe4pWeefARRK3bU0hHltqdIFffENfVwdd7c,12798
117
117
  fin_infra/net_worth/scaffold_templates/schemas.py.tmpl,sha256=VkFsxyZx4DFDhXDhn-7KT0IgrXCvgaS5ZdWbjyezWj0,4709
118
- fin_infra/normalization/__init__.py,sha256=-7EP_lTExQpoCtgsx1wD3j8aMH9y3SlFgHke3mWCQI8,6195
118
+ fin_infra/normalization/__init__.py,sha256=foCru-Nf9M1zP1jdrT0oNazzmp6AWiaDbUDoiyJefvA,6252
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
@@ -127,11 +127,11 @@ fin_infra/obs/classifier.py,sha256=qZHgUV6J2sXdOhHCPOxmonyvE4V1vY-A5MDwFpzk2lk,5
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=21m6ZkovwXuUuj0-dgQVDLxSfxZVjhuXj8di_-q3jGc,6617
130
- fin_infra/providers/banking/teller_client.py,sha256=pdb5JK8hdYAMFVCXBFsxY12aDxhL8afLK4NeZ4KbsvA,9917
131
- fin_infra/providers/base.py,sha256=oLzdExPGE7yg-URtin3vGTQ8hEzG7UnTmDGDWJB5oL0,4273
130
+ fin_infra/providers/banking/teller_client.py,sha256=QmrsBlk3_rHT-pTQPrIAA74kjIjcgdi-gOb8NA3oBO8,10268
131
+ fin_infra/providers/base.py,sha256=zQBiIPYrWELl65bvekAsl7WnuUYHAkM96cYmKQYTb3U,6857
132
132
  fin_infra/providers/brokerage/alpaca.py,sha256=wRVfVmExiYXCk1pLRmHSrfo91714JIm3rrD0djrNfT8,9938
133
133
  fin_infra/providers/brokerage/base.py,sha256=JJFH0Cqca4Rg4rmxfiwcQt-peRoBf4JpG3g6jx8DVks,106
134
- fin_infra/providers/credit/experian.py,sha256=hNEVqmCaPT72NHV3Nw3sKOYPX0kIsl819ucqUc-7z2k,341
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
136
  fin_infra/providers/market/alphavantage.py,sha256=srZdkf-frBuKyPTdWasMmVrpnh76BEBDXa-nsYtLzNc,8963
137
137
  fin_infra/providers/market/base.py,sha256=ljBzZTfjYQS9tXahmxFic7JQSZeyoiDMUZ1NY0R7yto,108
@@ -141,18 +141,18 @@ fin_infra/providers/market/yahoo.py,sha256=FNhqkCFC0In-Z3zpzmuknEORHLRK5Evk2KSk0
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
- fin_infra/recurring/add.py,sha256=bOvpRbtMjWYqNpq8dTR6aCNsR0iTRMyXWGZyMWQZk8A,18573
148
+ fin_infra/recurring/add.py,sha256=bPAVHknGoTLqk3T4vGZw1ROiFGHvASSakafGGmMlL9k,18917
149
149
  fin_infra/recurring/detector.py,sha256=1e6PRoBAT2NxoGAgcVHAWwpPtznkJMaYSrJtvSq0YqM,20154
150
- fin_infra/recurring/detectors_llm.py,sha256=LGNj_uMK8bQGtKnmDsjWDr11WJM331lvfEZ4ZdBD67c,11504
150
+ fin_infra/recurring/detectors_llm.py,sha256=jCW7xXaFRISnsCYDgvyiovXThNceVK_ypWYEawUfE78,11534
151
151
  fin_infra/recurring/ease.py,sha256=OrpxGHi8kt6LkMmww5l0Xy2pU-5hP_dR4IgdOiaIRaU,11179
152
- fin_infra/recurring/insights.py,sha256=J_Gvbv9-pkb0IcjNJYSitejNVQPnKJ1N5L1hTkzbnGA,15886
152
+ fin_infra/recurring/insights.py,sha256=bpZGPZiCgrPK0Nr24QS3jc1F9sCldm6Jd5t-i6wbu7M,15916
153
153
  fin_infra/recurring/models.py,sha256=N4G_LM0xZr3ptHtlqOmcsw3AL2v9g7IX92SmBljkNek,8894
154
154
  fin_infra/recurring/normalizer.py,sha256=Rc1ntIDGir6X-I5lgv49kdLry_zHGJ8cys_Jf3F6Lhk,9761
155
- fin_infra/recurring/normalizers.py,sha256=9kGsbNuxGb4xYUwMqjJZ84m924jT6jWZPQUSXGkqkNU,15928
155
+ fin_infra/recurring/normalizers.py,sha256=9by3lF7EVkCWy_-rLiEKpgw-qJWzEiDOKUMscFx8hmI,15958
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
@@ -164,7 +164,7 @@ fin_infra/security/encryption.py,sha256=z1k5LFkuuMCjAUnBzBCOviyi0F1R_vabdHhdJJdb
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
167
- fin_infra/security/token_store.py,sha256=UucTXfgRbdbogahS_2q5CPSb7dFyctN9D3m-ecJkqX4,5929
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
@@ -173,8 +173,8 @@ fin_infra/utils/__init__.py,sha256=gKacLSWMAis--pasd8AuVN7ap0e9Z1TjRGur0J23EDo,6
173
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.63.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
- fin_infra-0.1.63.dist-info/METADATA,sha256=CYK6i2jvcZZ-Y7eKY4eYnuUCiMSwwV2UZWe2QKtARAo,10218
178
- fin_infra-0.1.63.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
- fin_infra-0.1.63.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
- fin_infra-0.1.63.dist-info/RECORD,,
176
+ fin_infra-0.1.65.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
+ fin_infra-0.1.65.dist-info/METADATA,sha256=qOIl3r1vojckM7SamBAKGx-kTPqhqfp5Pji27lxINsk,10218
178
+ fin_infra-0.1.65.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
+ fin_infra-0.1.65.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
+ fin_infra-0.1.65.dist-info/RECORD,,