fin-infra 0.1.61__py3-none-any.whl → 0.1.63__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 (37) hide show
  1. fin_infra/banking/__init__.py +2 -2
  2. fin_infra/budgets/__init__.py +4 -4
  3. fin_infra/categorization/llm_layer.py +2 -2
  4. fin_infra/compliance/__init__.py +4 -3
  5. fin_infra/credit/add.py +3 -2
  6. fin_infra/credit/experian/auth.py +3 -2
  7. fin_infra/credit/experian/client.py +2 -2
  8. fin_infra/credit/experian/provider.py +2 -2
  9. fin_infra/crypto/insights.py +1 -1
  10. fin_infra/goals/add.py +2 -2
  11. fin_infra/goals/management.py +3 -3
  12. fin_infra/goals/milestones.py +2 -2
  13. fin_infra/investments/models.py +6 -6
  14. fin_infra/investments/providers/plaid.py +2 -2
  15. fin_infra/investments/providers/snaptrade.py +2 -2
  16. fin_infra/net_worth/models.py +14 -14
  17. fin_infra/normalization/providers/exchangerate.py +3 -3
  18. fin_infra/obs/classifier.py +4 -2
  19. fin_infra/providers/banking/plaid_client.py +4 -3
  20. fin_infra/providers/banking/teller_client.py +6 -6
  21. fin_infra/providers/brokerage/alpaca.py +6 -6
  22. fin_infra/providers/market/ccxt_crypto.py +5 -3
  23. fin_infra/recurring/detectors_llm.py +3 -3
  24. fin_infra/recurring/insights.py +3 -3
  25. fin_infra/recurring/normalizer.py +2 -1
  26. fin_infra/recurring/normalizers.py +3 -3
  27. fin_infra/security/encryption.py +2 -2
  28. fin_infra/security/pii_patterns.py +1 -1
  29. fin_infra/security/token_store.py +6 -2
  30. fin_infra/tax/add.py +1 -1
  31. fin_infra/utils/http.py +3 -2
  32. fin_infra/utils/retry.py +1 -1
  33. {fin_infra-0.1.61.dist-info → fin_infra-0.1.63.dist-info}/METADATA +1 -1
  34. {fin_infra-0.1.61.dist-info → fin_infra-0.1.63.dist-info}/RECORD +37 -37
  35. {fin_infra-0.1.61.dist-info → fin_infra-0.1.63.dist-info}/LICENSE +0 -0
  36. {fin_infra-0.1.61.dist-info → fin_infra-0.1.63.dist-info}/WHEEL +0 -0
  37. {fin_infra-0.1.61.dist-info → fin_infra-0.1.63.dist-info}/entry_points.txt +0 -0
@@ -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(
@@ -87,11 +87,11 @@ __all__ = [
87
87
  def __getattr__(name: str):
88
88
  """Lazy import for budgets module components."""
89
89
  if name == "easy_budgets":
90
- from fin_infra.budgets.ease import easy_budgets # type: ignore[attr-defined]
90
+ from fin_infra.budgets.ease import easy_budgets
91
91
 
92
92
  return easy_budgets
93
93
  elif name == "add_budgets":
94
- from fin_infra.budgets.add import add_budgets # type: ignore[attr-defined]
94
+ from fin_infra.budgets.add import add_budgets
95
95
 
96
96
  return add_budgets
97
97
  elif name in (
@@ -119,11 +119,11 @@ def __getattr__(name: str):
119
119
 
120
120
  return BudgetTracker
121
121
  elif name == "check_budget_alerts":
122
- from fin_infra.budgets.alerts import check_budget_alerts # type: ignore[attr-defined]
122
+ from fin_infra.budgets.alerts import check_budget_alerts
123
123
 
124
124
  return check_budget_alerts
125
125
  elif name == "apply_template":
126
- from fin_infra.budgets.templates import apply_template # type: ignore[attr-defined]
126
+ from fin_infra.budgets.templates import apply_template
127
127
 
128
128
  return apply_template
129
129
 
@@ -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"))
@@ -260,7 +260,7 @@ Provide your insight:"""
260
260
  # Use natural language conversation (no output_schema)
261
261
  # Note: In tests, achat is mocked with messages= parameter
262
262
  # In production, this should use user_msg, provider, model_name parameters
263
- response = await llm.achat( # type: ignore[call-arg]
263
+ response = await llm.achat(
264
264
  messages=[{"role": "user", "content": prompt}],
265
265
  )
266
266
 
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
 
@@ -201,7 +201,7 @@ class Holding(BaseModel):
201
201
  unofficial_currency_code: Optional[str] = Field(None, description="For crypto/alt currencies")
202
202
  as_of_date: Optional[date] = Field(None, description="Date of pricing data")
203
203
 
204
- @computed_field # type: ignore[misc]
204
+ @computed_field
205
205
  @property
206
206
  def unrealized_gain_loss(self) -> Optional[Decimal]:
207
207
  """Calculate unrealized gain/loss (current value - cost basis)."""
@@ -209,7 +209,7 @@ class Holding(BaseModel):
209
209
  return None
210
210
  return self.institution_value - self.cost_basis
211
211
 
212
- @computed_field # type: ignore[misc]
212
+ @computed_field
213
213
  @property
214
214
  def unrealized_gain_loss_percent(self) -> Optional[Decimal]:
215
215
  """Calculate unrealized gain/loss percentage."""
@@ -350,7 +350,7 @@ class InvestmentAccount(BaseModel):
350
350
  # Holdings
351
351
  holdings: List[Holding] = Field(default_factory=list, description="List of holdings in account")
352
352
 
353
- @computed_field # type: ignore[misc]
353
+ @computed_field
354
354
  @property
355
355
  def total_value(self) -> Decimal:
356
356
  """Calculate total account value (sum of holdings + cash)."""
@@ -358,20 +358,20 @@ class InvestmentAccount(BaseModel):
358
358
  cash_balance = self.balances.get("current") or Decimal(0)
359
359
  return holdings_value + cash_balance
360
360
 
361
- @computed_field # type: ignore[misc]
361
+ @computed_field
362
362
  @property
363
363
  def total_cost_basis(self) -> Decimal:
364
364
  """Calculate total cost basis (sum of cost_basis across holdings)."""
365
365
  return sum(h.cost_basis for h in self.holdings if h.cost_basis is not None)
366
366
 
367
- @computed_field # type: ignore[misc]
367
+ @computed_field
368
368
  @property
369
369
  def total_unrealized_gain_loss(self) -> Decimal:
370
370
  """Calculate total unrealized P&L (value - cost_basis)."""
371
371
  holdings_value = sum(h.institution_value for h in self.holdings)
372
372
  return holdings_value - self.total_cost_basis
373
373
 
374
- @computed_field # type: ignore[misc]
374
+ @computed_field
375
375
  @property
376
376
  def total_unrealized_gain_loss_percent(self) -> Optional[Decimal]:
377
377
  """Calculate total unrealized P&L percentage."""
@@ -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
@@ -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)
@@ -207,7 +207,7 @@ class AssetAllocation(BaseModel):
207
207
  vehicles: float = Field(0.0, ge=0, description="Vehicle value")
208
208
  other_assets: float = Field(0.0, ge=0, description="Other asset value")
209
209
 
210
- @computed_field # type: ignore[misc]
210
+ @computed_field
211
211
  @property
212
212
  def total_assets(self) -> float:
213
213
  """Sum of all asset categories."""
@@ -220,37 +220,37 @@ class AssetAllocation(BaseModel):
220
220
  + self.other_assets
221
221
  )
222
222
 
223
- @computed_field # type: ignore[misc]
223
+ @computed_field
224
224
  @property
225
225
  def cash_percentage(self) -> float:
226
226
  """Cash as percentage of total assets."""
227
227
  return (self.cash / self.total_assets * 100) if self.total_assets > 0 else 0.0
228
228
 
229
- @computed_field # type: ignore[misc]
229
+ @computed_field
230
230
  @property
231
231
  def investments_percentage(self) -> float:
232
232
  """Investments as percentage of total assets."""
233
233
  return (self.investments / self.total_assets * 100) if self.total_assets > 0 else 0.0
234
234
 
235
- @computed_field # type: ignore[misc]
235
+ @computed_field
236
236
  @property
237
237
  def crypto_percentage(self) -> float:
238
238
  """Crypto as percentage of total assets."""
239
239
  return (self.crypto / self.total_assets * 100) if self.total_assets > 0 else 0.0
240
240
 
241
- @computed_field # type: ignore[misc]
241
+ @computed_field
242
242
  @property
243
243
  def real_estate_percentage(self) -> float:
244
244
  """Real estate as percentage of total assets."""
245
245
  return (self.real_estate / self.total_assets * 100) if self.total_assets > 0 else 0.0
246
246
 
247
- @computed_field # type: ignore[misc]
247
+ @computed_field
248
248
  @property
249
249
  def vehicles_percentage(self) -> float:
250
250
  """Vehicles as percentage of total assets."""
251
251
  return (self.vehicles / self.total_assets * 100) if self.total_assets > 0 else 0.0
252
252
 
253
- @computed_field # type: ignore[misc]
253
+ @computed_field
254
254
  @property
255
255
  def other_percentage(self) -> float:
256
256
  """Other assets as percentage of total assets."""
@@ -288,7 +288,7 @@ class LiabilityBreakdown(BaseModel):
288
288
  personal_loans: float = Field(0.0, ge=0, description="Personal loan balance")
289
289
  lines_of_credit: float = Field(0.0, ge=0, description="Line of credit balance")
290
290
 
291
- @computed_field # type: ignore[misc]
291
+ @computed_field
292
292
  @property
293
293
  def total_liabilities(self) -> float:
294
294
  """Sum of all liability categories."""
@@ -301,7 +301,7 @@ class LiabilityBreakdown(BaseModel):
301
301
  + self.lines_of_credit
302
302
  )
303
303
 
304
- @computed_field # type: ignore[misc]
304
+ @computed_field
305
305
  @property
306
306
  def credit_cards_percentage(self) -> float:
307
307
  """Credit cards as percentage of total liabilities."""
@@ -311,7 +311,7 @@ class LiabilityBreakdown(BaseModel):
311
311
  else 0.0
312
312
  )
313
313
 
314
- @computed_field # type: ignore[misc]
314
+ @computed_field
315
315
  @property
316
316
  def mortgages_percentage(self) -> float:
317
317
  """Mortgages as percentage of total liabilities."""
@@ -319,7 +319,7 @@ class LiabilityBreakdown(BaseModel):
319
319
  (self.mortgages / self.total_liabilities * 100) if self.total_liabilities > 0 else 0.0
320
320
  )
321
321
 
322
- @computed_field # type: ignore[misc]
322
+ @computed_field
323
323
  @property
324
324
  def auto_loans_percentage(self) -> float:
325
325
  """Auto loans as percentage of total liabilities."""
@@ -327,7 +327,7 @@ class LiabilityBreakdown(BaseModel):
327
327
  (self.auto_loans / self.total_liabilities * 100) if self.total_liabilities > 0 else 0.0
328
328
  )
329
329
 
330
- @computed_field # type: ignore[misc]
330
+ @computed_field
331
331
  @property
332
332
  def student_loans_percentage(self) -> float:
333
333
  """Student loans as percentage of total liabilities."""
@@ -337,7 +337,7 @@ class LiabilityBreakdown(BaseModel):
337
337
  else 0.0
338
338
  )
339
339
 
340
- @computed_field # type: ignore[misc]
340
+ @computed_field
341
341
  @property
342
342
  def personal_loans_percentage(self) -> float:
343
343
  """Personal loans as percentage of total liabilities."""
@@ -347,7 +347,7 @@ class LiabilityBreakdown(BaseModel):
347
347
  else 0.0
348
348
  )
349
349
 
350
- @computed_field # type: ignore[misc]
350
+ @computed_field
351
351
  @property
352
352
  def lines_of_credit_percentage(self) -> float:
353
353
  """Lines of credit as percentage of total liabilities."""
@@ -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}")
@@ -37,6 +37,8 @@ Usage:
37
37
 
38
38
  from __future__ import annotations
39
39
 
40
+ from typing import Callable
41
+
40
42
  # Financial capability prefix patterns (extensible)
41
43
  FINANCIAL_ROUTE_PREFIXES = (
42
44
  "/banking",
@@ -110,9 +112,9 @@ def financial_route_classifier(route_path: str, method: str) -> str:
110
112
 
111
113
 
112
114
  def compose_classifiers(
113
- *classifiers: callable,
115
+ *classifiers: Callable[[str], str],
114
116
  default: str = "public",
115
- ) -> callable:
117
+ ) -> Callable[[str], str]:
116
118
  """
117
119
  Compose multiple route classifiers with fallback logic.
118
120
 
@@ -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
 
@@ -139,7 +139,7 @@ class TellerClient(BankingProvider):
139
139
  "products": ["accounts", "transactions", "balances", "identity"],
140
140
  },
141
141
  )
142
- return response.get("enrollment_id", "")
142
+ return cast(str, response.get("enrollment_id", ""))
143
143
 
144
144
  def exchange_public_token(self, public_token: str) -> dict:
145
145
  """Exchange public token for access token.
@@ -186,7 +186,7 @@ class TellerClient(BankingProvider):
186
186
  auth=(access_token, ""),
187
187
  )
188
188
  response.raise_for_status()
189
- return response.json()
189
+ return cast(list[dict[Any, Any]], response.json())
190
190
 
191
191
  def transactions(
192
192
  self,
@@ -229,7 +229,7 @@ class TellerClient(BankingProvider):
229
229
  params=params,
230
230
  )
231
231
  response.raise_for_status()
232
- return response.json()
232
+ return cast(list[dict[Any, Any]], response.json())
233
233
 
234
234
  def balances(self, access_token: str, account_id: str | None = None) -> dict:
235
235
  """Fetch current balances.
@@ -261,7 +261,7 @@ class TellerClient(BankingProvider):
261
261
  )
262
262
 
263
263
  response.raise_for_status()
264
- return response.json()
264
+ return cast(dict[Any, Any], response.json())
265
265
 
266
266
  def identity(self, access_token: str) -> dict:
267
267
  """Fetch identity/account holder information.
@@ -285,7 +285,7 @@ class TellerClient(BankingProvider):
285
285
  auth=(access_token, ""),
286
286
  )
287
287
  response.raise_for_status()
288
- return response.json()
288
+ return cast(dict[Any, Any], response.json())
289
289
 
290
290
  def __del__(self) -> None:
291
291
  """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
@@ -15,7 +15,7 @@ try:
15
15
  ALPACA_AVAILABLE = True
16
16
  except ImportError:
17
17
  ALPACA_AVAILABLE = False
18
- REST = None # type: ignore
18
+ REST = None
19
19
 
20
20
  from ..base import BrokerageProvider
21
21
 
@@ -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))
@@ -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
 
@@ -22,7 +22,7 @@ from pydantic import BaseModel, ConfigDict, Field
22
22
  try:
23
23
  from ai_infra.llm import LLM
24
24
  except ImportError:
25
- LLM = None # type: ignore
25
+ LLM = None
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
@@ -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
 
@@ -23,7 +23,7 @@ from pydantic import BaseModel, ConfigDict, Field
23
23
  try:
24
24
  from ai_infra.llm import LLM
25
25
  except ImportError:
26
- LLM = None # type: ignore
26
+ LLM = None
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
@@ -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
 
@@ -24,7 +24,7 @@ from pydantic import BaseModel, ConfigDict, Field
24
24
  try:
25
25
  from ai_infra.llm import LLM
26
26
  except ImportError:
27
- LLM = None # type: ignore
27
+ LLM = None
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
@@ -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)
@@ -10,12 +10,16 @@ from typing import Optional
10
10
  from sqlalchemy import Column, DateTime, String, Text, select, update
11
11
  from sqlalchemy.dialects.postgresql import UUID
12
12
  from sqlalchemy.ext.asyncio import AsyncSession
13
- from sqlalchemy.orm import declarative_base
13
+ from sqlalchemy.orm import DeclarativeBase
14
14
 
15
15
  from .encryption import ProviderTokenEncryption
16
16
  from .models import ProviderTokenMetadata
17
17
 
18
- Base = declarative_base()
18
+
19
+ class Base(DeclarativeBase):
20
+ """Declarative base for provider token models."""
21
+
22
+ pass
19
23
 
20
24
 
21
25
  class ProviderToken(Base):
fin_infra/tax/add.py CHANGED
@@ -321,7 +321,7 @@ def add_tax_data(
321
321
  # broker = easy_brokerage(mode="paper")
322
322
  # positions = broker.positions() # Should accept user_id parameter
323
323
  # For now, return empty list (integration test will mock this)
324
- positions: list = [] # type: ignore
324
+ positions: list = []
325
325
 
326
326
  # TODO: Get recent trades for wash sale checking
327
327
  recent_trades = None
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())
fin_infra/utils/retry.py CHANGED
@@ -28,7 +28,7 @@ async def retry_async(
28
28
  for i in range(attempts):
29
29
  try:
30
30
  return await func()
31
- except tuple(retry_on) as exc: # type: ignore[misc]
31
+ except tuple(retry_on) as exc:
32
32
  last_exc = exc
33
33
  if i == attempts - 1:
34
34
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.1.61
3
+ Version: 0.1.63
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
@@ -11,11 +11,11 @@ fin_infra/analytics/rebalancing.py,sha256=K3S7KQiIU2LwyAwWN9VrSly4AOl24vN9tz_JX7
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
13
  fin_infra/analytics/spending.py,sha256=zXTcYAj_fWQtzOHgSN4P0dSIm80Q5eke6T3LbWltjyU,25882
14
- fin_infra/banking/__init__.py,sha256=wva1SEyrH2po79YycQ_00ZyC2tVeuO3uYcyvudOW484,22267
14
+ fin_infra/banking/__init__.py,sha256=IoVLc3FhfE_XWMj9Vt4_gpALStnxu7_8xLw1VBpTSxs,22296
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
17
  fin_infra/brokerage/__init__.py,sha256=RB0wbVlxM9PCbWUezzjrOf19JucVDpCvNlT62LoMzho,17023
18
- fin_infra/budgets/__init__.py,sha256=3VTYU_OdqblYiP5fjHHiw3m-FSj5trPz7XVTb3f3rBc,4106
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
21
21
  fin_infra/budgets/ease.py,sha256=vK5O8rvKzzJ1MUiwi7p9egayDFqyB23hPbbEhb1mhXE,8203
@@ -33,7 +33,7 @@ fin_infra/categorization/__init__.py,sha256=7551OjE668A_Bhm07QSTBkm4PD3uCOEwdz05
33
33
  fin_infra/categorization/add.py,sha256=jbxM51MyIFsAcleCMzP1I5jYV9EsKALzBCnuzKk76sc,6328
34
34
  fin_infra/categorization/ease.py,sha256=NudJBqFByS0YONPn_4O_Q7QYIiVCCgNbAhn-ugJpa0Y,5826
35
35
  fin_infra/categorization/engine.py,sha256=VxVuLym_RkKK0xpZrfLKuksFVoURmXICgdik7KpxXMs,12075
36
- fin_infra/categorization/llm_layer.py,sha256=JkzTTdlUWtiCBzKgNAGWvhR7Qt4-UVc12itD2BlxwlQ,12695
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,17 +46,17 @@ 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
- fin_infra/crypto/insights.py,sha256=J1aPT05soNJrhvClZGa-SNyJ-KYsQFm6PSGcoukwdfo,11651
59
+ fin_infra/crypto/insights.py,sha256=u5gzoLtVPuWbQcFYX-TW-bJtILqB_AvCIxXZ9hd8oQg,11625
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
@@ -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
@@ -82,11 +82,11 @@ fin_infra/insights/models.py,sha256=xov_YV8oBLJt3YdyVjbryRfcXqmGeGiPvZsZHSbvtl8,
82
82
  fin_infra/investments/__init__.py,sha256=UiWvTdKH7V9aaqZLunPT1_QGfXBAZbPk_w4QmeLWLqo,6324
83
83
  fin_infra/investments/add.py,sha256=3cbjXbWoTuDglwk9U48X6768Etv1XLTWysdDPgsn7Yg,17658
84
84
  fin_infra/investments/ease.py,sha256=ocs7xvnZ1u8riFjH9KHi1yFEUF0lfuEcd-QMpsuiOu8,9229
85
- fin_infra/investments/models.py,sha256=NHnkvtMa1QYp_TpuuqT4u3cWEJi3OhW-1e-orMuR47o,16107
85
+ fin_infra/investments/models.py,sha256=8GQuq-aGww2tzze-VrW71dBNYN918_TmtkwycCVa434,15975
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=z_f4NbJDhi_vcLDA_SR2yuEaRrjRxbRirlKYH6ofDAk,18086
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
@@ -109,7 +109,7 @@ fin_infra/net_worth/calculator.py,sha256=JERDtZyFurw5x2NYqfHvJzv6qigamI3AFfR-wes
109
109
  fin_infra/net_worth/ease.py,sha256=XvOaowgj64JLKOtEq-B8MIc3fgrysL4vG8m1e5j-ukY,15094
110
110
  fin_infra/net_worth/goals.py,sha256=BJGxdsMjvgQDELFEJo-ai3DvsAzUNXvzMXkwovHr8yQ,1238
111
111
  fin_infra/net_worth/insights.py,sha256=vVK4BtfHNJGb1wyk9XD0fLpoadATTdorF8OxHOgD9b0,25222
112
- fin_infra/net_worth/models.py,sha256=pTdPEA0TiIrV2PT0381ftaQIMnnKciUqJYntdFrAaCQ,22711
112
+ fin_infra/net_worth/models.py,sha256=A5idGtMEQy1J4jaLMq9ZZslmvOhxfkW7cjLKW23AMQo,22403
113
113
  fin_infra/net_worth/scaffold_templates/README.md,sha256=Wqd6ksqFjmtNdDFOWVV_duuAcePWwiu3_YgkVM9N_WY,14363
114
114
  fin_infra/net_worth/scaffold_templates/__init__.py,sha256=OKeMCC_JNw6m8rBWr_wesOIJ1OR9LCBeIkXKahbCGC4,132
115
115
  fin_infra/net_worth/scaffold_templates/models.py.tmpl,sha256=9BKsoD08RZbSdOm0wFTbx5OzKfAEtuA1NcWyS1Aywx4,5934
@@ -119,23 +119,23 @@ 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
- fin_infra/obs/classifier.py,sha256=6R2q-w71tk7WfXF5MBPqawxogcj6tILKZPlkpRZNDfg,5083
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=pdb5JK8hdYAMFVCXBFsxY12aDxhL8afLK4NeZ4KbsvA,9917
131
131
  fin_infra/providers/base.py,sha256=oLzdExPGE7yg-URtin3vGTQ8hEzG7UnTmDGDWJB5oL0,4273
132
- fin_infra/providers/brokerage/alpaca.py,sha256=eOzdRp45_VeD1r_2k0IudxFn0IRhGogn-htF5IJVKnk,9862
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
@@ -147,12 +147,12 @@ 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=Gz0rArX2MwAvjW97ceqKUFe8F0Fxt9RzLBwPPjd_NSg,11482
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=l0j0Qste9I3VGsUxneoVfEPLBI8-rLWzO8fn1jZHZd8,15868
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=8yFx2yshw7aaxMDInoF4YLfEyPkTERAM9vQwHBAFpW4,15912
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=lNnGUlQCJImc-OAcHbHij8xYHkV3jb6MgBpwEra-T_M,5862
166
+ fin_infra/security/pii_patterns.py,sha256=SM-o7cL6NdgkOmtBedsN2nJZ5QPbeYehZdYmAujk8Y8,3070
167
+ fin_infra/security/token_store.py,sha256=UucTXfgRbdbogahS_2q5CPSb7dFyctN9D3m-ecJkqX4,5929
168
168
  fin_infra/settings.py,sha256=xitpBQJmuvSy9prQhvXOW1scbwB1KAyGD8XqYgU_hQU,1388
169
169
  fin_infra/tax/__init__.py,sha256=NXUjV-k-rw4774pookY3UOwEXYRQauJze6Yift5RjW0,6107
170
- fin_infra/tax/add.py,sha256=xmy0hXsWzEj5p-_9A5hkljFjF_FpnbCQQZ5e8FPChBI,14568
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
174
- fin_infra/utils/retry.py,sha256=p4i4heGdHkLsqLHuHY4riwOkuLjbbfbUE8cA4t3UAgQ,1052
173
+ fin_infra/utils/http.py,sha256=pvcxbNQ9oisoGPkNe3xX9aAgWzEN6mmdtr1w-L02Xj8,629
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.61.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
- fin_infra-0.1.61.dist-info/METADATA,sha256=KMCtxNsFrBD0F_hOCvy9yYmnis_O0DMwR32pjxEUI2Q,10218
178
- fin_infra-0.1.61.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
- fin_infra-0.1.61.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
- fin_infra-0.1.61.dist-info/RECORD,,
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,,