fin-infra 0.1.80__py3-none-any.whl → 0.1.81__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.
@@ -42,7 +42,7 @@ from __future__ import annotations
42
42
  import logging
43
43
  import os
44
44
  from datetime import date, datetime, timedelta
45
- from typing import List, Optional
45
+ from typing import Optional
46
46
  from pydantic import BaseModel, Field, ConfigDict
47
47
 
48
48
 
@@ -59,7 +59,7 @@ _logger = logging.getLogger(__name__)
59
59
 
60
60
  # In-memory storage for testing (will be replaced with SQL database in production)
61
61
  # ⚠️ WARNING: All data is LOST on restart when using in-memory storage!
62
- _balance_snapshots: List[BalanceSnapshot] = []
62
+ _balance_snapshots: list[BalanceSnapshot] = []
63
63
  _production_warning_logged = False
64
64
 
65
65
 
@@ -157,7 +157,7 @@ def get_balance_history(
157
157
  days: int = 90,
158
158
  start_date: Optional[date] = None,
159
159
  end_date: Optional[date] = None,
160
- ) -> List[BalanceSnapshot]:
160
+ ) -> list[BalanceSnapshot]:
161
161
  """Get balance history for an account.
162
162
 
163
163
  Retrieves balance snapshots for the specified account within a date range.
@@ -216,8 +216,8 @@ def get_balance_history(
216
216
 
217
217
  def get_balance_snapshots(
218
218
  account_id: str,
219
- dates: List[date],
220
- ) -> List[BalanceSnapshot]:
219
+ dates: list[date],
220
+ ) -> list[BalanceSnapshot]:
221
221
  """Get balance snapshots for specific dates.
222
222
 
223
223
  Args:
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import re
11
11
  from datetime import datetime, timezone
12
- from typing import Any, Dict, Optional, Literal
12
+ from typing import Any, Optional, Literal
13
13
  from pydantic import BaseModel, ConfigDict, Field
14
14
 
15
15
  from ..providers.base import BankingProvider
@@ -179,7 +179,7 @@ def validate_provider_token(provider: str, access_token: str) -> bool:
179
179
  return validator(access_token)
180
180
 
181
181
 
182
- def parse_banking_providers(banking_providers: Dict[str, Any]) -> BankingConnectionStatus:
182
+ def parse_banking_providers(banking_providers: dict[str, Any]) -> BankingConnectionStatus:
183
183
  """
184
184
  Parse banking_providers JSON field into structured status.
185
185
 
@@ -257,7 +257,7 @@ def parse_banking_providers(banking_providers: Dict[str, Any]) -> BankingConnect
257
257
  return status
258
258
 
259
259
 
260
- def sanitize_connection_status(status: BankingConnectionStatus) -> Dict[str, Any]:
260
+ def sanitize_connection_status(status: BankingConnectionStatus) -> dict[str, Any]:
261
261
  """
262
262
  Sanitize connection status for API responses (removes access tokens).
263
263
 
@@ -298,10 +298,10 @@ def sanitize_connection_status(status: BankingConnectionStatus) -> Dict[str, Any
298
298
 
299
299
 
300
300
  def mark_connection_unhealthy(
301
- banking_providers: Dict[str, Any],
301
+ banking_providers: dict[str, Any],
302
302
  provider: str,
303
303
  error_message: str,
304
- ) -> Dict[str, Any]:
304
+ ) -> dict[str, Any]:
305
305
  """
306
306
  Mark a provider connection as unhealthy (for error handling).
307
307
 
@@ -335,9 +335,9 @@ def mark_connection_unhealthy(
335
335
 
336
336
 
337
337
  def mark_connection_healthy(
338
- banking_providers: Dict[str, Any],
338
+ banking_providers: dict[str, Any],
339
339
  provider: str,
340
- ) -> Dict[str, Any]:
340
+ ) -> dict[str, Any]:
341
341
  """
342
342
  Mark a provider connection as healthy (after successful sync).
343
343
 
@@ -368,7 +368,7 @@ def mark_connection_healthy(
368
368
 
369
369
 
370
370
  def get_primary_access_token(
371
- banking_providers: Dict[str, Any],
371
+ banking_providers: dict[str, Any],
372
372
  ) -> tuple[Optional[str], Optional[str]]:
373
373
  """
374
374
  Get the primary access token and provider name.
@@ -437,7 +437,7 @@ async def test_connection_health(
437
437
  return False, error_msg
438
438
 
439
439
 
440
- def should_refresh_token(banking_providers: Dict[str, Any], provider: str) -> bool:
440
+ def should_refresh_token(banking_providers: dict[str, Any], provider: str) -> bool:
441
441
  """
442
442
  Check if a provider token should be refreshed.
443
443
 
@@ -35,7 +35,7 @@ Example:
35
35
  from __future__ import annotations
36
36
 
37
37
  from datetime import datetime
38
- from typing import TYPE_CHECKING, List, Optional
38
+ from typing import TYPE_CHECKING, Optional
39
39
 
40
40
  from fin_infra.budgets.models import (
41
41
  AlertSeverity,
@@ -52,7 +52,7 @@ async def check_budget_alerts(
52
52
  budget_id: str,
53
53
  tracker: BudgetTracker,
54
54
  thresholds: Optional[dict[str, float]] = None,
55
- ) -> List[BudgetAlert]:
55
+ ) -> list[BudgetAlert]:
56
56
  """
57
57
  Check budget for alerts (overspending, approaching limits, unusual patterns).
58
58
 
@@ -111,7 +111,7 @@ async def check_budget_alerts(
111
111
  # Get budget progress
112
112
  progress = await tracker.get_budget_progress(budget_id)
113
113
 
114
- alerts: List[BudgetAlert] = []
114
+ alerts: list[BudgetAlert] = []
115
115
 
116
116
  # Check each category for alerts
117
117
  for category in progress.categories:
@@ -36,7 +36,7 @@ from __future__ import annotations
36
36
 
37
37
  import uuid
38
38
  from datetime import datetime, timedelta
39
- from typing import TYPE_CHECKING, List, Optional
39
+ from typing import TYPE_CHECKING, Optional
40
40
 
41
41
  from sqlalchemy.ext.asyncio import async_sessionmaker
42
42
 
@@ -206,7 +206,7 @@ class BudgetTracker:
206
206
  self,
207
207
  user_id: str,
208
208
  type: Optional[str] = None,
209
- ) -> List[Budget]:
209
+ ) -> list[Budget]:
210
210
  """
211
211
  Get all budgets for a user.
212
212
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Iterable
3
+ from collections.abc import Iterable
4
4
 
5
5
  import numpy as np
6
6
  import numpy_financial as npf
@@ -15,7 +15,7 @@ Expected performance:
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Any, List, Optional, Tuple, cast
18
+ from typing import Any, Optional, cast
19
19
  from pydantic import BaseModel, Field
20
20
 
21
21
  # ai-infra imports
@@ -40,7 +40,7 @@ class CategoryPrediction(BaseModel):
40
40
 
41
41
 
42
42
  # Few-shot examples (20 diverse merchants covering all major categories)
43
- FEW_SHOT_EXAMPLES: List[Tuple[str, str, str]] = [
43
+ FEW_SHOT_EXAMPLES: list[tuple[str, str, str]] = [
44
44
  # Food & Dining (5 examples)
45
45
  ("STARBUCKS #1234", "Coffee Shops", "Popular coffee shop chain"),
46
46
  ("MCDONALD'S", "Fast Food", "Fast food restaurant"),
fin_infra/clients/base.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import abc
4
- from typing import Iterable, Sequence
4
+ from collections.abc import Iterable, Sequence
5
5
 
6
6
  from ..models import Account, Quote, Transaction
7
7
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Sequence
3
+ from collections.abc import Sequence
4
4
 
5
5
  from .base import BankingClient
6
6
  from ..models import Account
@@ -21,7 +21,8 @@ from __future__ import annotations
21
21
 
22
22
  import logging
23
23
  from datetime import datetime
24
- from typing import Any, Callable, TYPE_CHECKING, cast
24
+ from typing import Any, TYPE_CHECKING, cast
25
+ from collections.abc import Callable
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from fastapi import FastAPI, Request, Response
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import re
26
26
  from datetime import datetime
27
- from typing import TYPE_CHECKING, Dict
27
+ from typing import TYPE_CHECKING
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from svc_infra.storage.base import StorageBackend
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
32
32
  from .models import DocumentAnalysis
33
33
 
34
34
  # In-memory analysis cache (production: use svc-infra cache)
35
- _analysis_cache: Dict[str, "DocumentAnalysis"] = {}
35
+ _analysis_cache: dict[str, "DocumentAnalysis"] = {}
36
36
 
37
37
 
38
38
  async def analyze_document(
@@ -31,7 +31,7 @@ from __future__ import annotations
31
31
 
32
32
  from datetime import datetime
33
33
  from enum import Enum
34
- from typing import Dict, List, Optional
34
+ from typing import Optional
35
35
 
36
36
  from pydantic import BaseModel, ConfigDict, Field
37
37
  from svc_infra.documents import Document as BaseDocument
@@ -145,7 +145,7 @@ class OCRResult(BaseModel):
145
145
  confidence: float = Field(
146
146
  ..., description="Overall OCR confidence score (0.0-1.0)", ge=0.0, le=1.0
147
147
  )
148
- fields_extracted: Dict[str, str] = Field(
148
+ fields_extracted: dict[str, str] = Field(
149
149
  default_factory=dict,
150
150
  description="Structured fields extracted from document (names, amounts, dates)",
151
151
  )
@@ -181,10 +181,10 @@ class DocumentAnalysis(BaseModel):
181
181
 
182
182
  document_id: str = Field(..., description="Document that was analyzed")
183
183
  summary: str = Field(..., description="High-level document summary")
184
- key_findings: List[str] = Field(
184
+ key_findings: list[str] = Field(
185
185
  default_factory=list, description="Important facts extracted from document"
186
186
  )
187
- recommendations: List[str] = Field(
187
+ recommendations: list[str] = Field(
188
188
  default_factory=list, description="Action items or suggestions based on document content"
189
189
  )
190
190
  analysis_date: datetime = Field(
@@ -25,7 +25,7 @@ from __future__ import annotations
25
25
 
26
26
  import re
27
27
  from datetime import datetime
28
- from typing import TYPE_CHECKING, Dict, Optional
28
+ from typing import TYPE_CHECKING, Optional
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from svc_infra.storage.base import StorageBackend
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
33
33
  from .models import OCRResult
34
34
 
35
35
  # In-memory OCR cache (production: use svc-infra cache)
36
- _ocr_cache: Dict[str, "OCRResult"] = {}
36
+ _ocr_cache: dict[str, "OCRResult"] = {}
37
37
 
38
38
 
39
39
  async def extract_text(
@@ -36,7 +36,7 @@ Quick Start:
36
36
 
37
37
  from __future__ import annotations
38
38
 
39
- from typing import TYPE_CHECKING, List, Optional
39
+ from typing import TYPE_CHECKING, Optional
40
40
 
41
41
  try:
42
42
  from svc_infra.documents import (
@@ -242,7 +242,7 @@ def list_documents(
242
242
  tax_year: Optional[int] = None,
243
243
  limit: int = 100,
244
244
  offset: int = 0,
245
- ) -> List["FinancialDocument"]:
245
+ ) -> list["FinancialDocument"]:
246
246
  """
247
247
  List user's financial documents with optional filters (delegates to svc-infra).
248
248
 
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 Any, List, Optional, cast
32
+ from typing import Any, Optional, cast
33
33
 
34
34
  from fastapi import FastAPI, HTTPException, status, Query, Body
35
35
  from pydantic import BaseModel, Field
@@ -89,7 +89,7 @@ class CreateGoalRequest(BaseModel):
89
89
  description: Optional[str] = Field(None, description="Goal description")
90
90
  current_amount: Optional[float] = Field(0.0, ge=0, description="Current amount")
91
91
  auto_contribute: Optional[bool] = Field(False, description="Auto-contribute enabled")
92
- tags: Optional[List[str]] = Field(None, description="Goal tags")
92
+ tags: Optional[list[str]] = Field(None, description="Goal tags")
93
93
 
94
94
 
95
95
  class UpdateGoalRequest(BaseModel):
@@ -102,7 +102,7 @@ class UpdateGoalRequest(BaseModel):
102
102
  current_amount: Optional[float] = Field(None, ge=0)
103
103
  status: Optional[GoalStatus] = None
104
104
  auto_contribute: Optional[bool] = None
105
- tags: Optional[List[str]] = None
105
+ tags: Optional[list[str]] = None
106
106
 
107
107
 
108
108
  class AddMilestoneRequest(BaseModel):
@@ -238,14 +238,14 @@ def add_goals(
238
238
  except ValueError as e:
239
239
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
240
240
 
241
- @router.get("", response_model=List[dict])
241
+ @router.get("", response_model=list[dict])
242
242
  async def list_goals_endpoint(
243
243
  user_id: Optional[str] = Query(
244
244
  None, description="User identifier (optional, returns all if not provided)"
245
245
  ),
246
246
  goal_type: Optional[str] = Query(None, description="Filter by goal type"),
247
247
  status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
248
- ) -> List[dict]:
248
+ ) -> list[dict]:
249
249
  """
250
250
  List all goals for a user with optional filters.
251
251
 
@@ -442,8 +442,8 @@ def add_goals(
442
442
  except ValueError as e:
443
443
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
444
444
 
445
- @router.get("/{goal_id}/milestones", response_model=List[dict])
446
- async def list_milestones_endpoint(goal_id: str) -> List[dict]:
445
+ @router.get("/{goal_id}/milestones", response_model=list[dict])
446
+ async def list_milestones_endpoint(goal_id: str) -> list[dict]:
447
447
  """
448
448
  List all milestones for a goal.
449
449
 
@@ -540,8 +540,8 @@ def add_goals(
540
540
  except ValueError as e:
541
541
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
542
542
 
543
- @router.get("/{goal_id}/funding", response_model=List[dict])
544
- async def list_funding_sources_endpoint(goal_id: str) -> List[dict]:
543
+ @router.get("/{goal_id}/funding", response_model=list[dict])
544
+ async def list_funding_sources_endpoint(goal_id: str) -> list[dict]:
545
545
  """
546
546
  List all funding sources for a goal.
547
547
 
@@ -22,14 +22,12 @@ Example:
22
22
  >>> # Raises ValueError if total allocation > 100%
23
23
  """
24
24
 
25
- from typing import Dict, List
26
-
27
25
  from fin_infra.goals.models import FundingSource
28
26
  from fin_infra.goals.management import get_goal
29
27
 
30
28
  # In-memory storage for funding allocations
31
29
  # Structure: {account_id: {goal_id: allocation_percent}}
32
- _FUNDING_STORE: Dict[str, Dict[str, float]] = {}
30
+ _FUNDING_STORE: dict[str, dict[str, float]] = {}
33
31
 
34
32
 
35
33
  def link_account_to_goal(
@@ -108,7 +106,7 @@ def link_account_to_goal(
108
106
  )
109
107
 
110
108
 
111
- def get_goal_funding_sources(goal_id: str) -> List[FundingSource]:
109
+ def get_goal_funding_sources(goal_id: str) -> list[FundingSource]:
112
110
  """
113
111
  Get all accounts funding a specific goal.
114
112
 
@@ -154,7 +152,7 @@ def get_goal_funding_sources(goal_id: str) -> List[FundingSource]:
154
152
  return funding_sources
155
153
 
156
154
 
157
- def get_account_allocations(account_id: str) -> Dict[str, float]:
155
+ def get_account_allocations(account_id: str) -> dict[str, float]:
158
156
  """
159
157
  Get all goal allocations for a specific account.
160
158
 
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  from datetime import date
21
21
  from decimal import Decimal
22
22
  from enum import Enum
23
- from typing import TYPE_CHECKING, Dict, List, Optional
23
+ from typing import TYPE_CHECKING, Optional
24
24
 
25
25
  from pydantic import BaseModel, ConfigDict, Field, computed_field
26
26
 
@@ -374,12 +374,12 @@ class InvestmentAccount(BaseModel):
374
374
  subtype: Optional[str] = Field(None, description="Account subtype (401k, ira, brokerage)")
375
375
 
376
376
  # Balances
377
- balances: Dict[str, Optional[Decimal]] = Field(
377
+ balances: dict[str, Optional[Decimal]] = Field(
378
378
  ..., description="Current, available, and limit balances"
379
379
  )
380
380
 
381
381
  # Holdings
382
- holdings: List[Holding] = Field(default_factory=list, description="List of holdings in account")
382
+ holdings: list[Holding] = Field(default_factory=list, description="List of holdings in account")
383
383
 
384
384
  if TYPE_CHECKING:
385
385
 
@@ -487,11 +487,11 @@ class AssetAllocation(BaseModel):
487
487
  },
488
488
  )
489
489
 
490
- by_security_type: Dict[SecurityType, float] = Field(
490
+ by_security_type: dict[SecurityType, float] = Field(
491
491
  default_factory=dict,
492
492
  description="Percentage breakdown by security type (equity, bond, etc.)",
493
493
  )
494
- by_sector: Dict[str, float] = Field(
494
+ by_sector: dict[str, float] = Field(
495
495
  default_factory=dict,
496
496
  description="Percentage breakdown by sector (Technology, Healthcare, etc.)",
497
497
  )
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
  from datetime import date
7
- from typing import List, Optional
7
+ from typing import Optional
8
8
 
9
9
  # Import will work once models.py is fully implemented in Task 3
10
10
  # For now, using TYPE_CHECKING to avoid circular imports
@@ -30,8 +30,8 @@ class InvestmentProvider(ABC):
30
30
 
31
31
  @abstractmethod
32
32
  async def get_holdings(
33
- self, access_token: str, account_ids: Optional[List[str]] = None
34
- ) -> List[Holding]:
33
+ self, access_token: str, account_ids: Optional[list[str]] = None
34
+ ) -> list[Holding]:
35
35
  """Fetch holdings for investment accounts.
36
36
 
37
37
  Args:
@@ -54,8 +54,8 @@ class InvestmentProvider(ABC):
54
54
  access_token: str,
55
55
  start_date: date,
56
56
  end_date: date,
57
- account_ids: Optional[List[str]] = None,
58
- ) -> List[InvestmentTransaction]:
57
+ account_ids: Optional[list[str]] = None,
58
+ ) -> list[InvestmentTransaction]:
59
59
  """Fetch investment transactions within date range.
60
60
 
61
61
  Args:
@@ -77,7 +77,7 @@ class InvestmentProvider(ABC):
77
77
  pass
78
78
 
79
79
  @abstractmethod
80
- async def get_securities(self, access_token: str, security_ids: List[str]) -> List[Security]:
80
+ async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
81
81
  """Fetch security details (ticker, name, type, current price).
82
82
 
83
83
  Args:
@@ -95,7 +95,7 @@ class InvestmentProvider(ABC):
95
95
  pass
96
96
 
97
97
  @abstractmethod
98
- async def get_investment_accounts(self, access_token: str) -> List[InvestmentAccount]:
98
+ async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
99
99
  """Fetch investment accounts with aggregated holdings.
100
100
 
101
101
  Args:
@@ -113,7 +113,7 @@ class InvestmentProvider(ABC):
113
113
 
114
114
  # Helper methods (concrete - shared across all providers)
115
115
 
116
- def calculate_allocation(self, holdings: List[Holding]) -> AssetAllocation:
116
+ def calculate_allocation(self, holdings: list[Holding]) -> AssetAllocation:
117
117
  """Calculate asset allocation by security type and sector.
118
118
 
119
119
  Groups holdings by security type (equity, bond, ETF, etc.) and calculates
@@ -183,7 +183,7 @@ class InvestmentProvider(ABC):
183
183
  cash_percent=cash_percent,
184
184
  )
185
185
 
186
- def calculate_portfolio_metrics(self, holdings: List[Holding]) -> dict:
186
+ def calculate_portfolio_metrics(self, holdings: list[Holding]) -> dict:
187
187
  """Calculate total value, cost basis, unrealized gain/loss.
188
188
 
189
189
  Aggregates holdings to calculate portfolio-level metrics.
@@ -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, cast
13
+ from typing import Any, Optional, cast
14
14
 
15
15
  try:
16
16
  from plaid.api import plaid_api
@@ -131,8 +131,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
131
131
  return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
132
132
 
133
133
  async def get_holdings(
134
- self, access_token: str, account_ids: Optional[List[str]] = None
135
- ) -> List[Holding]:
134
+ self, access_token: str, account_ids: Optional[list[str]] = None
135
+ ) -> list[Holding]:
136
136
  """Fetch investment holdings from Plaid.
137
137
 
138
138
  Retrieves holdings with security details, quantity, cost basis, and current value.
@@ -189,8 +189,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
189
189
  access_token: str,
190
190
  start_date: date,
191
191
  end_date: date,
192
- account_ids: Optional[List[str]] = None,
193
- ) -> List[InvestmentTransaction]:
192
+ account_ids: Optional[list[str]] = None,
193
+ ) -> list[InvestmentTransaction]:
194
194
  """Fetch investment transactions from Plaid.
195
195
 
196
196
  Retrieves buy/sell/dividend transactions within the specified date range.
@@ -252,7 +252,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
252
252
  except ApiException as e:
253
253
  raise self._transform_error(e)
254
254
 
255
- async def get_securities(self, access_token: str, security_ids: List[str]) -> List[Security]:
255
+ async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
256
256
  """Fetch security details from Plaid holdings.
257
257
 
258
258
  Note: Plaid doesn't have a dedicated securities endpoint.
@@ -290,7 +290,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
290
290
  except ApiException as e:
291
291
  raise self._transform_error(e)
292
292
 
293
- async def get_investment_accounts(self, access_token: str) -> List[InvestmentAccount]:
293
+ async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
294
294
  """Fetch investment accounts with aggregated holdings.
295
295
 
296
296
  Returns accounts with total value, cost basis, and unrealized P&L.
@@ -321,7 +321,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
321
321
  }
322
322
 
323
323
  # Group holdings by account
324
- accounts_map: Dict[str, Dict[str, Any]] = {}
324
+ accounts_map: dict[str, dict[str, Any]] = {}
325
325
  for plaid_holding in response.holdings:
326
326
  holding_dict = plaid_holding.to_dict()
327
327
  account_id = holding_dict["account_id"]
@@ -373,7 +373,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
373
373
 
374
374
  # Helper methods for data transformation
375
375
 
376
- def _transform_security(self, plaid_security: Dict[str, Any]) -> Security:
376
+ def _transform_security(self, plaid_security: dict[str, Any]) -> Security:
377
377
  """Transform Plaid security data to Security model."""
378
378
  # Handle close_price - Plaid may return None for securities without recent pricing
379
379
  close_price_raw = plaid_security.get("close_price")
@@ -394,7 +394,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
394
394
  currency=plaid_security.get("iso_currency_code", "USD"),
395
395
  )
396
396
 
397
- def _transform_holding(self, plaid_holding: Dict[str, Any], security: Security) -> Holding:
397
+ def _transform_holding(self, plaid_holding: dict[str, Any], security: Security) -> Holding:
398
398
  """Transform Plaid holding data to Holding model."""
399
399
  return Holding(
400
400
  account_id=plaid_holding["account_id"],
@@ -410,7 +410,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
410
410
  )
411
411
 
412
412
  def _transform_transaction(
413
- self, plaid_transaction: Dict[str, Any], security: Security
413
+ self, plaid_transaction: dict[str, Any], security: Security
414
414
  ) -> InvestmentTransaction:
415
415
  """Transform Plaid investment transaction to InvestmentTransaction model."""
416
416
  # Map Plaid transaction type to our enum
@@ -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, cast
14
+ from typing import Any, Optional, cast
15
15
 
16
16
  import httpx
17
17
 
@@ -93,7 +93,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
93
93
  timeout=30.0,
94
94
  )
95
95
 
96
- def _auth_headers(self, user_id: str, user_secret: str) -> Dict[str, str]:
96
+ def _auth_headers(self, user_id: str, user_secret: str) -> dict[str, str]:
97
97
  """Build authentication headers for SnapTrade API requests.
98
98
 
99
99
  SECURITY: User secrets are passed in headers, NOT URL params.
@@ -115,8 +115,8 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
115
115
  async def get_holdings(
116
116
  self,
117
117
  access_token: str,
118
- account_ids: Optional[List[str]] = None,
119
- ) -> List[Holding]:
118
+ account_ids: Optional[list[str]] = None,
119
+ ) -> list[Holding]:
120
120
  """Fetch investment holdings from SnapTrade.
121
121
 
122
122
  Note: SnapTrade uses user_id + user_secret, passed as access_token in format "user_id:user_secret"
@@ -178,8 +178,8 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
178
178
  access_token: str,
179
179
  start_date: date,
180
180
  end_date: date,
181
- account_ids: Optional[List[str]] = None,
182
- ) -> List[InvestmentTransaction]:
181
+ account_ids: Optional[list[str]] = None,
182
+ ) -> list[InvestmentTransaction]:
183
183
  """Fetch investment transactions from SnapTrade.
184
184
 
185
185
  Args:
@@ -246,7 +246,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
246
246
  except Exception as e:
247
247
  raise ValueError(f"SnapTrade API error: {str(e)}")
248
248
 
249
- async def get_securities(self, access_token: str, security_ids: List[str]) -> List[Security]:
249
+ async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
250
250
  """Fetch security details from SnapTrade positions.
251
251
 
252
252
  Note: SnapTrade doesn't have a dedicated securities endpoint.
@@ -284,7 +284,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
284
284
  except Exception as e:
285
285
  raise ValueError(f"SnapTrade API error: {str(e)}")
286
286
 
287
- async def get_investment_accounts(self, access_token: str) -> List[InvestmentAccount]:
287
+ async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
288
288
  """Fetch investment accounts with aggregated holdings.
289
289
 
290
290
  Returns accounts with total value, cost basis, and unrealized P&L.
@@ -358,7 +358,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
358
358
  except Exception as e:
359
359
  raise ValueError(f"SnapTrade API error: {str(e)}")
360
360
 
361
- async def list_connections(self, access_token: str) -> List[Dict[str, Any]]:
361
+ async def list_connections(self, access_token: str) -> list[dict[str, Any]]:
362
362
  """List brokerage connections for a user.
363
363
 
364
364
  Returns which brokerages the user has connected (E*TRADE, Robinhood, etc.).
@@ -388,7 +388,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
388
388
  except Exception as e:
389
389
  raise ValueError(f"SnapTrade API error: {str(e)}")
390
390
 
391
- def get_brokerage_capabilities(self, brokerage_name: str) -> Dict[str, Any]:
391
+ def get_brokerage_capabilities(self, brokerage_name: str) -> dict[str, Any]:
392
392
  """Get capabilities for a specific brokerage.
393
393
 
394
394
  Important: Robinhood is READ-ONLY (no trading support).
@@ -480,7 +480,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
480
480
  except ValueError:
481
481
  raise ValueError("Invalid access_token format. Expected 'user_id:user_secret'")
482
482
 
483
- def _transform_holding(self, snaptrade_position: Dict[str, Any], account_id: str) -> Holding:
483
+ def _transform_holding(self, snaptrade_position: dict[str, Any], account_id: str) -> Holding:
484
484
  """Transform SnapTrade position data to Holding model."""
485
485
  symbol_data = snaptrade_position.get("symbol", {})
486
486
 
@@ -510,7 +510,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
510
510
  )
511
511
 
512
512
  def _transform_transaction(
513
- self, snaptrade_tx: Dict[str, Any], account_id: str
513
+ self, snaptrade_tx: dict[str, Any], account_id: str
514
514
  ) -> InvestmentTransaction:
515
515
  """Transform SnapTrade transaction to InvestmentTransaction model."""
516
516
  symbol_data = snaptrade_tx.get("symbol", {})
@@ -37,7 +37,7 @@ Usage:
37
37
 
38
38
  from __future__ import annotations
39
39
 
40
- from typing import Callable
40
+ from collections.abc import Callable
41
41
 
42
42
  # Financial capability prefix patterns (extensible)
43
43
  FINANCIAL_ROUTE_PREFIXES = (
@@ -25,7 +25,8 @@ Provider Categories:
25
25
  from __future__ import annotations
26
26
 
27
27
  from abc import ABC, abstractmethod
28
- from typing import Any, Iterable, Sequence
28
+ from typing import Any
29
+ from collections.abc import Iterable, Sequence
29
30
 
30
31
  from ..models import Candle, Quote
31
32
 
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
  import time
11
- from typing import Sequence
11
+ from collections.abc import Sequence
12
12
  from decimal import Decimal
13
13
  from datetime import datetime, timezone
14
14
 
@@ -10,7 +10,7 @@ For production, consider Alpha Vantage or other official providers.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- from typing import Sequence
13
+ from collections.abc import Sequence
14
14
  from decimal import Decimal
15
15
  from datetime import datetime, timezone
16
16
 
@@ -33,7 +33,7 @@ Integration with svc-infra:
33
33
 
34
34
  from __future__ import annotations
35
35
 
36
- from typing import List, Dict, Optional
36
+ from typing import Optional
37
37
  from datetime import datetime
38
38
  from collections import defaultdict
39
39
 
@@ -121,16 +121,16 @@ class RecurringSummary(BaseModel):
121
121
  user_id: str = Field(..., description="User identifier")
122
122
  total_monthly_cost: float = Field(..., description="Total monthly recurring expenses")
123
123
  total_monthly_income: float = Field(0.0, description="Total monthly recurring income")
124
- subscriptions: List[RecurringItem] = Field(
124
+ subscriptions: list[RecurringItem] = Field(
125
125
  default_factory=list, description="List of recurring expense items"
126
126
  )
127
- recurring_income: List[RecurringItem] = Field(
127
+ recurring_income: list[RecurringItem] = Field(
128
128
  default_factory=list, description="List of recurring income items"
129
129
  )
130
- by_category: Dict[str, float] = Field(
130
+ by_category: dict[str, float] = Field(
131
131
  default_factory=dict, description="Monthly cost grouped by category"
132
132
  )
133
- cancellation_opportunities: List[CancellationOpportunity] = Field(
133
+ cancellation_opportunities: list[CancellationOpportunity] = Field(
134
134
  default_factory=list, description="Potential subscriptions to cancel"
135
135
  )
136
136
  generated_at: str = Field(
@@ -165,8 +165,8 @@ def _calculate_monthly_cost(amount: float, cadence: str) -> float:
165
165
 
166
166
 
167
167
  def _identify_cancellation_opportunities(
168
- subscriptions: List[RecurringItem],
169
- ) -> List[CancellationOpportunity]:
168
+ subscriptions: list[RecurringItem],
169
+ ) -> list[CancellationOpportunity]:
170
170
  """Identify potential cancellation opportunities from subscriptions.
171
171
 
172
172
  Looks for:
@@ -183,7 +183,7 @@ def _identify_cancellation_opportunities(
183
183
  opportunities = []
184
184
 
185
185
  # Group by category
186
- by_category: Dict[str, List[RecurringItem]] = defaultdict(list)
186
+ by_category: dict[str, list[RecurringItem]] = defaultdict(list)
187
187
  for sub in subscriptions:
188
188
  by_category[sub.category].append(sub)
189
189
 
@@ -253,8 +253,8 @@ def _identify_cancellation_opportunities(
253
253
 
254
254
  def get_recurring_summary(
255
255
  user_id: str,
256
- patterns: List[RecurringPattern],
257
- category_map: Optional[Dict[str, str]] = None,
256
+ patterns: list[RecurringPattern],
257
+ category_map: Optional[dict[str, str]] = None,
258
258
  ) -> RecurringSummary:
259
259
  """Generate a comprehensive recurring transaction summary for a user.
260
260
 
@@ -283,7 +283,7 @@ def get_recurring_summary(
283
283
  """
284
284
  subscriptions = []
285
285
  recurring_income = []
286
- by_category: Dict[str, float] = defaultdict(float)
286
+ by_category: dict[str, float] = defaultdict(float)
287
287
 
288
288
  for pattern in patterns:
289
289
  # Determine amount (use fixed amount or average of range)
@@ -19,7 +19,7 @@ Typical usage:
19
19
  from __future__ import annotations
20
20
 
21
21
  from pathlib import Path
22
- from typing import Any, Dict, List, Optional
22
+ from typing import Any, Optional
23
23
 
24
24
  # Use svc-infra's scaffold utilities to avoid duplication
25
25
  from svc_infra.utils import render_template, write, ensure_init_py
@@ -34,7 +34,7 @@ def scaffold_budgets_core(
34
34
  models_filename: Optional[str] = None,
35
35
  schemas_filename: Optional[str] = None,
36
36
  repository_filename: Optional[str] = None,
37
- ) -> Dict[str, Any]:
37
+ ) -> dict[str, Any]:
38
38
  """Generate budget persistence code from templates.
39
39
 
40
40
  Args:
@@ -72,7 +72,7 @@ def scaffold_budgets_core(
72
72
  subs = _generate_substitutions(include_tenant, include_soft_delete)
73
73
 
74
74
  # Track all file operations
75
- files: List[Dict[str, Any]] = []
75
+ files: list[dict[str, Any]] = []
76
76
 
77
77
  # Render and write models
78
78
  models_content = render_template("fin_infra.budgets.scaffold_templates", "models.py.tmpl", subs)
@@ -114,7 +114,7 @@ def scaffold_budgets_core(
114
114
  def _generate_substitutions(
115
115
  include_tenant: bool,
116
116
  include_soft_delete: bool,
117
- ) -> Dict[str, str]:
117
+ ) -> dict[str, str]:
118
118
  """Generate template variable substitutions for budgets.
119
119
 
120
120
  Args:
@@ -17,7 +17,7 @@ Typical usage:
17
17
  """
18
18
 
19
19
  from pathlib import Path
20
- from typing import Any, Dict
20
+ from typing import Any
21
21
 
22
22
  from svc_infra.utils import (
23
23
  render_template,
@@ -29,7 +29,7 @@ from svc_infra.utils import (
29
29
  def _generate_substitutions(
30
30
  include_tenant: bool = False,
31
31
  include_soft_delete: bool = False,
32
- ) -> Dict[str, str]:
32
+ ) -> dict[str, str]:
33
33
  """
34
34
  Generate template substitutions for goals domain.
35
35
 
@@ -49,7 +49,7 @@ def _generate_substitutions(
49
49
  Returns:
50
50
  Dict mapping variable names to their substitution values
51
51
  """
52
- subs: Dict[str, str] = {
52
+ subs: dict[str, str] = {
53
53
  "Entity": "Goal",
54
54
  "entity": "goal",
55
55
  "table_name": "goals",
@@ -173,7 +173,7 @@ def scaffold_goals_core(
173
173
  models_filename: str = "goal.py",
174
174
  schemas_filename: str = "goal_schemas.py",
175
175
  repository_filename: str = "goal_repository.py",
176
- ) -> Dict[str, Any]:
176
+ ) -> dict[str, Any]:
177
177
  """
178
178
  Scaffold goals domain files: models, schemas, repository (optional), and __init__.py.
179
179
 
@@ -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, cast
10
+ from typing import Any, Optional, cast
11
11
 
12
12
  from cryptography.fernet import Fernet, InvalidToken
13
13
 
@@ -64,7 +64,7 @@ class ProviderTokenEncryption:
64
64
  raise ValueError(f"Invalid encryption key: {e}") from e
65
65
 
66
66
  def encrypt(
67
- self, token: str, context: Optional[Dict[str, Any]] = None, key_id: Optional[str] = None
67
+ self, token: str, context: Optional[dict[str, Any]] = None, key_id: Optional[str] = None
68
68
  ) -> str:
69
69
  """
70
70
  Encrypt provider token with optional context.
@@ -104,7 +104,7 @@ class ProviderTokenEncryption:
104
104
  def decrypt(
105
105
  self,
106
106
  encrypted_token: str,
107
- context: Optional[Dict[str, Any]] = None,
107
+ context: Optional[dict[str, Any]] = None,
108
108
  verify_context: bool = True,
109
109
  ) -> str:
110
110
  """
@@ -154,7 +154,7 @@ class ProviderTokenEncryption:
154
154
  raise ValueError(f"Decryption failed: {e}") from e
155
155
 
156
156
  def rotate_key(
157
- self, encrypted_token: str, new_key: bytes, context: Optional[Dict[str, Any]] = None
157
+ self, encrypted_token: str, new_key: bytes, context: Optional[dict[str, Any]] = None
158
158
  ) -> str:
159
159
  """
160
160
  Re-encrypt token with new key (for key rotation).
fin_infra/utils/retry.py CHANGED
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import random
5
- from typing import Awaitable, Callable, Iterable, TypeVar
5
+ from typing import TypeVar
6
+ from collections.abc import Awaitable, Callable, Iterable
6
7
 
7
8
  from fin_infra.exceptions import RetryError
8
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.1.80
3
+ Version: 0.1.81
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
@@ -12,12 +12,12 @@ fin_infra/analytics/savings.py,sha256=n3rGNFP8TU5mW-uz9kOuqX_mDiVnDyAeDN06Q7Abot
12
12
  fin_infra/analytics/scenarios.py,sha256=LE_dZVkbxxAx5sxitGhiOhZfWTlYtVbIvS9pEXkijLc,12246
13
13
  fin_infra/analytics/spending.py,sha256=wCa8mhtdMTnI3eG9W28ljCgB_AQRVGxkrRA2ZwLi_RQ,26249
14
14
  fin_infra/banking/__init__.py,sha256=5yB_Pi8cnlKzbJu1JiwtaismVFJXrBU7tmThZJ76Pqc,22499
15
- fin_infra/banking/history.py,sha256=2C3R9rZMYSyPtR0oe-kCZ14SMhPAy_M9v_Xc5XzBVLs,10618
16
- fin_infra/banking/utils.py,sha256=mCDwU34SpwTqeXoDYhNGAUqxooVxW6TAtqYorzFmPfQ,15341
15
+ fin_infra/banking/history.py,sha256=71Q5jHzPCmRdMKQBKPY9KQuis599AAh9DUu0lYo0S5M,10612
16
+ fin_infra/banking/utils.py,sha256=TcNTI2ujqqYGNCwoCBsICnaYwGRs2YRjGzlqVU_iy0M,15335
17
17
  fin_infra/brokerage/__init__.py,sha256=8-q1NfKZW9fUQ_2_49Vc19sskZkmHo67TJ5GQDcXqTQ,17259
18
18
  fin_infra/budgets/__init__.py,sha256=V6euagDkFHvWyjHeI64fxfddhOHDlIWwOc-PnTyyQb4,3986
19
19
  fin_infra/budgets/add.py,sha256=tdfNXD9deEEzy0xCRbTwbx70SX9wiJkWj0Huk_uCjFg,13584
20
- fin_infra/budgets/alerts.py,sha256=i0lQa3mLWuLQorWL-77VKhXQG2F_0U1cpdZnK3g1y3M,9720
20
+ fin_infra/budgets/alerts.py,sha256=KlK3r2qPzMw4naFMHsFjqkwKDxdj_2b_y-ofD1nldkg,9714
21
21
  fin_infra/budgets/ease.py,sha256=vK5O8rvKzzJ1MUiwi7p9egayDFqyB23hPbbEhb1mhXE,8203
22
22
  fin_infra/budgets/models.py,sha256=qd6bcjl2cOtFqRtNe1Xso_05cQlGG-4qhBkiTQKchy0,14335
23
23
  fin_infra/budgets/scaffold_templates/README.md,sha256=FBtRSrQSkg7Xp8SPiuSmzGR94I2zzjOckb0_vKtcksY,14084
@@ -26,14 +26,14 @@ fin_infra/budgets/scaffold_templates/models.py.tmpl,sha256=rpKhXwnx1gQjV_GGVqs8C
26
26
  fin_infra/budgets/scaffold_templates/repository.py.tmpl,sha256=khFgnQnVNnOo8DWYDmYz58MvdeSJpoS9QvTXcGhsa8g,10023
27
27
  fin_infra/budgets/scaffold_templates/schemas.py.tmpl,sha256=x5gSQ7Kiuq08tum5joKmeY0ib2r3ekLKk09dFOl0PS0,5658
28
28
  fin_infra/budgets/templates.py,sha256=Sbc7RcHXscq34g4t7J8OXM2Kfkt5DHuvqVnFU0Jiddc,12112
29
- fin_infra/budgets/tracker.py,sha256=mSa4K8_OxV0lZ9hIeBGrNh_5mytMaFDiPxQaehSODSE,16466
29
+ fin_infra/budgets/tracker.py,sha256=gF68a6sivdSfVDZanSwdGdhEcg6SEic0K_rO9ed7lvI,16460
30
30
  fin_infra/cashflows/__init__.py,sha256=Tdaa83y3sc38ndNJwg9gsKfUQNpIdQQPUS3GPK2ZurY,8517
31
- fin_infra/cashflows/core.py,sha256=Or0hPqCvY_ypV0YiMXh-mle6xWK0tE8WuPPAqHGUp8E,532
31
+ fin_infra/cashflows/core.py,sha256=YWvF0DVOfBkvO_MuDODjE-V_g52H2ixFRH_TjMXExDE,541
32
32
  fin_infra/categorization/__init__.py,sha256=efLje12AW-ec9Vs5ynb41r4XCIWx5a-Z9WoGb3kQdIE,2030
33
33
  fin_infra/categorization/add.py,sha256=JDOvxngh-7oWHTddOyP4GAse9vLuxSTfoIhrDKUHOKg,6278
34
34
  fin_infra/categorization/ease.py,sha256=bomEtJAgwk9uiemNt1rk-IsTjJIhyJn0GJ_c58YEmJs,5836
35
35
  fin_infra/categorization/engine.py,sha256=vpwxtQGEbjCMyvzB5EQV2etjHHNOu1R05o99mHP_WZY,12132
36
- fin_infra/categorization/llm_layer.py,sha256=KbX7o2c-BqWDbPdQXD6qxk98gHRNtEDvf3-Q3kniT9k,12699
36
+ fin_infra/categorization/llm_layer.py,sha256=zJ8yHc78Lrc3TjmpA5krFpeZyoSQRh7SttGb_dSRAqA,12686
37
37
  fin_infra/categorization/models.py,sha256=-rGXR0RW2EU_FQ7ZfDWBIXxx8QGJDxeBF9zKGYyVgqY,5931
38
38
  fin_infra/categorization/rules.py,sha256=m3OogJY0hJe5BrmZqOvOKS2-HRdW4Y5jvvtlPDn9Pn8,12884
39
39
  fin_infra/categorization/taxonomy.py,sha256=qsgo7VJkM6GFBBOaTRHWP82vl5SinRKnMsj4ICarEyQ,13281
@@ -44,9 +44,9 @@ 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=SwNE8AnszmsjyUXDpmn-bauQx8HAQB_fw-6er2QaTCU,7655
46
46
  fin_infra/clients/__init__.py,sha256=CL_NUNxLAKFvG7wL_F-rAhUucp6pm0sNHVtvCzVvLcw,708
47
- fin_infra/clients/base.py,sha256=K5nI4GJzT36oUUYGynV3b4eywJdTTa5EX26QK7XdTcc,970
48
- fin_infra/clients/plaid.py,sha256=jwcLdQe0G7afLO5JH5jsBosE4iz_cFsb04I8_hCbIb0,807
49
- fin_infra/compliance/__init__.py,sha256=IpeFi0ztw619Etvq2ln6kYFVwr6OrcVX6W7n5Mh3Nf4,5222
47
+ fin_infra/clients/base.py,sha256=fZebDGIfQQybqeXpFpORlDZ6wSKf33aIAkUUpmyvZrA,979
48
+ fin_infra/clients/plaid.py,sha256=Cj2QNj3xgmPb86wcYg-ZVTPVJ5pXM7qwUgeO8AAWpV4,816
49
+ fin_infra/compliance/__init__.py,sha256=0y3Lx3ai1hhFS2ypvEtBJXEZnEMC0dq30sqHwnqFCeU,5249
50
50
  fin_infra/credit/__init__.py,sha256=cwCP_WlrG-0yb_L4zYsuzEsSalcfiCY9ItqXfD7Jx9E,6719
51
51
  fin_infra/credit/add.py,sha256=D3btx9pmZ3tF6AYC6P4Y3dYaUuWp7M3FpDrFksxi5uM,8553
52
52
  fin_infra/credit/experian/__init__.py,sha256=g3IJGvDOMsnB0er0Uwdvl6hGKKTOazqJxSDnB2oIBm0,761
@@ -59,15 +59,15 @@ fin_infra/crypto/__init__.py,sha256=p-gEoF59XzyV1RouubW4onl5mE62XvXSAw9isMc48qc,
59
59
  fin_infra/crypto/insights.py,sha256=cgMsjdrR7vOTKvEDbFCXpS7O7I8dMs1oz6cIbtrJWZI,11393
60
60
  fin_infra/documents/__init__.py,sha256=Ub1hbX3PTrBSsBdcbL8PFf6oq8jSH4pYxW45-qOYPqs,1909
61
61
  fin_infra/documents/add.py,sha256=dxzhdCsDcVVyTYKrgM30j-Wr0BAG797p4xa4j9UXST8,8118
62
- fin_infra/documents/analysis.py,sha256=zY5OQEIlq3JLNND_cg2KheFdryUmIecPOR2lR6oKhPw,13992
62
+ fin_infra/documents/analysis.py,sha256=QrRgpqVsV96jOjyJEI05o-hxi-c4AuXi4wLJncDSX68,13986
63
63
  fin_infra/documents/ease.py,sha256=YutA7EDYzysH6ppf6iVFhe4v1XOzhmUM3tDwtQPku28,9621
64
- fin_infra/documents/models.py,sha256=h__Ln3lfQ50F5q3MurpThTrBEnoxJPbc_eL-NwzdZ7E,6877
65
- fin_infra/documents/ocr.py,sha256=NxVWT4SB8bWK6EGXCWvATejYyIzZLZCh0fDCWT0KZ1Y,9594
66
- fin_infra/documents/storage.py,sha256=AM2MjVYuZ-77MxXGuvV6NFcWAH6Y_kMRe7xH1lZ29qQ,10174
64
+ fin_infra/documents/models.py,sha256=RpazzhE9qBcTRk_NO7Dl_uvcHw9UpGtnpXFcJURMtYc,6865
65
+ fin_infra/documents/ocr.py,sha256=R5v3lwMr-xyAlA1zjTw2MWbT2phawNw7kzbAxrSwYpg,9588
66
+ fin_infra/documents/storage.py,sha256=yu2ZvjJ-iDrt6zhPQ_VZhKZ8Iv99D9X3cUyIb7e7hZ0,10168
67
67
  fin_infra/exceptions.py,sha256=va2rZnGhAkAi5LUqk93iGhpfkChUBaBLUNnCx6bykSM,16952
68
68
  fin_infra/goals/__init__.py,sha256=Vg8LKLlDoRiWHsJX7wu5Zcc-86NNLpHoLTjYVkGi2c4,2130
69
- fin_infra/goals/add.py,sha256=vhExYtXrIId4ZMBr3WH9iLsCT3gAvuAxeHHE8xNTT0U,20605
70
- fin_infra/goals/funding.py,sha256=6wn25N0VTYfKLzZWhEn0xdC0ft49qdElkQFc9IwmdPk,9334
69
+ fin_infra/goals/add.py,sha256=46SbPuDuymd3LcDHmQAoQAMthyrKBP-Hv35fXcOccPw,20599
70
+ fin_infra/goals/funding.py,sha256=pJx9W1hpzXwWFUxIXEdsFZt-ytXIo7DK6uRpyO6aO44,9303
71
71
  fin_infra/goals/management.py,sha256=Rj-yCcXk4HK5Tg8VauRU3osbK3kRh5O-YR-tupULx8A,33865
72
72
  fin_infra/goals/milestones.py,sha256=LEJ9M7yOKJ-8thPuH0byHACabCUA9qW7mMATsPomaJA,9995
73
73
  fin_infra/goals/models.py,sha256=DxUrYJqlfKdrmFBucNikLbto3NgxoiJAmsL3v0LR4DQ,10237
@@ -82,11 +82,11 @@ fin_infra/insights/models.py,sha256=xov_YV8oBLJt3YdyVjbryRfcXqmGeGiPvZsZHSbvtl8,
82
82
  fin_infra/investments/__init__.py,sha256=o4p_8slq-CzIK0ditVhNfcyoWsDdyFaxRl-IMBHtLNE,6732
83
83
  fin_infra/investments/add.py,sha256=Yyh3VIjJ5OBJbvEZQboFDqlYkzrHoEPnWzSY2iBAJiM,17263
84
84
  fin_infra/investments/ease.py,sha256=d5ISfxpCius6JM2LZNReztW6-IizaqoxNU4aEbXWA74,9487
85
- fin_infra/investments/models.py,sha256=y3OgetML7OGRQqdESxt8eNv3ifPBHtBMtsFAf0vz5NQ,18424
85
+ fin_infra/investments/models.py,sha256=UyOvtU1-uxrgE0zLP5WY3wpn3YOdOv6_b05oGismxNU,18412
86
86
  fin_infra/investments/providers/__init__.py,sha256=V1eIzz6EnGJ-pq-9L3S2-evmcExF-YdZfd5P6JMyDtc,383
87
- fin_infra/investments/providers/base.py,sha256=MwwwVaijq-8SpM6oR_HYwFWkONCiTV6zyteVP3RMM_s,9850
88
- fin_infra/investments/providers/plaid.py,sha256=O98rTo7QVfBrn8ObPSrUiLMMAgtmRF3K0T6jx15qTnA,18777
89
- fin_infra/investments/providers/snaptrade.py,sha256=RMkYzmQba0NKw4xlaHT7w0j1e5zSqmlTfWL36M28zJE,23290
87
+ fin_infra/investments/providers/base.py,sha256=sQMK5qlIbLqBS6FyTzUWxJy5rPPzAnp5lwbRUM1jHCI,9844
88
+ fin_infra/investments/providers/plaid.py,sha256=Twt2XHxmrXrS-7N3NGoA3yXOXmdTfuuQ2j0Nqn7Kkok,18765
89
+ fin_infra/investments/providers/snaptrade.py,sha256=FNa8c9QI8xBF3NLCg-_3vf0oZZ5o0LiFnj9d3UwNQuk,23278
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
@@ -123,21 +123,21 @@ fin_infra/normalization/providers/exchangerate.py,sha256=vA1W2yVpCf89kOx6lctbHOQ
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=FEwL1lEV_f9bFBr36PaZsV4DxU3spwIXPoegwBgzSMc,5146
126
+ fin_infra/obs/classifier.py,sha256=JasWCqSkYjllJNZ5Gwbrd53ZhLwhYNZ0i2nbTcklEog,5155
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=LiBIwQLJp-5bA98oEQtTZpkQMC7kwhShq0mPATa9wD8,6520
130
130
  fin_infra/providers/banking/teller_client.py,sha256=QmrsBlk3_rHT-pTQPrIAA74kjIjcgdi-gOb8NA3oBO8,10268
131
- fin_infra/providers/base.py,sha256=B2YTKU_NWk1opAV1fxT5y_3wYmm1ezF2QbzWpUP74CM,7695
131
+ fin_infra/providers/base.py,sha256=tn153HUMhicv39lsPJ6-0W2Ylg4yYUPiHBltWF7cggE,7722
132
132
  fin_infra/providers/brokerage/alpaca.py,sha256=M8Z2i6dY9mgrU4-SDei2nYRKgbxHuPDMHubMb7ZMbEY,9920
133
133
  fin_infra/providers/brokerage/base.py,sha256=JJFH0Cqca4Rg4rmxfiwcQt-peRoBf4JpG3g6jx8DVks,106
134
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
- fin_infra/providers/market/alphavantage.py,sha256=srZdkf-frBuKyPTdWasMmVrpnh76BEBDXa-nsYtLzNc,8963
136
+ fin_infra/providers/market/alphavantage.py,sha256=or0YmEBbpa2p4-DUA97JrdaobxUhI3X_AOkfspaPzSY,8972
137
137
  fin_infra/providers/market/base.py,sha256=ljBzZTfjYQS9tXahmxFic7JQSZeyoiDMUZ1NY0R7yto,108
138
138
  fin_infra/providers/market/ccxt_crypto.py,sha256=AknYS3ZRBOtVxsr1MrI_ECL2vLlTR2hT60SeuAoECXE,1610
139
139
  fin_infra/providers/market/coingecko.py,sha256=F1Bwdk28xSsIaFEuT7lhT3F6Vkd0Lp-CMp1rnYiLfaE,2702
140
- fin_infra/providers/market/yahoo.py,sha256=5MZv4Kb-eCPbZZxi7ZCJHR_3L8eMDXHNROVoyhgXgto,5416
140
+ fin_infra/providers/market/yahoo.py,sha256=Ay95DQ2BtYOseWLJ57ozGxcLgf0m73VzIw7CC7l56lw,5425
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
@@ -153,14 +153,14 @@ fin_infra/recurring/insights.py,sha256=a4fhASyiNQTdUl2ijaxi2HqhWDhf-aJND2c62d9lg
153
153
  fin_infra/recurring/models.py,sha256=o0N8G-QhVb4zILEyry6M1VZ7liFJIOHwlejvn6p4K8M,8894
154
154
  fin_infra/recurring/normalizer.py,sha256=HZ4N7lXaeOZqphjSbjRmaneONJ5ELOSrqBlM5WM1tww,9758
155
155
  fin_infra/recurring/normalizers.py,sha256=37-ER4deeJhywrNVEe9KteecC_eH7xesTl2CWhRbdi0,15928
156
- fin_infra/recurring/summary.py,sha256=1Wte58ZZkEFulkb-nnpwfC5h7C_JrqByy47itdVdWwc,14665
156
+ fin_infra/recurring/summary.py,sha256=Myb1llV8imKtQ3xYJu_guO2GmrPHHB4PSXjLbktFTQ8,14653
157
157
  fin_infra/scaffold/__init__.py,sha256=IfL_CHHMpQB1efqY37BlIu07356tLaeVI2Mv3C0qYDs,827
158
- fin_infra/scaffold/budgets.py,sha256=XXOLlEcyBXVwdbJB__qObRXJ0oe1okwDT_-5tG8c9Yk,9515
159
- fin_infra/scaffold/goals.py,sha256=uVYzbbfbXGrf8qeGvq8mtY6o_YIk17aZ0DfSGQx6Y58,9690
158
+ fin_infra/scaffold/budgets.py,sha256=l6cdNfc7v8YI6PiWV-pJ69L6PDtcX5qSIgXFtykaRIM,9503
159
+ fin_infra/scaffold/goals.py,sha256=_ymL1HgAF8bvpGSCd_GeoMgVXtog8xCJ8mFIjOn-hQc,9684
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=z1k5LFkuuMCjAUnBzBCOviyi0F1R_vabdHhdJJdbPx4,6168
163
+ fin_infra/security/encryption.py,sha256=KWgLqt8-mRH2I53WyVAFUx-T-M5F4qX67qneFfxY-_A,6162
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
@@ -171,10 +171,10 @@ 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
173
  fin_infra/utils/http.py,sha256=pvcxbNQ9oisoGPkNe3xX9aAgWzEN6mmdtr1w-L02Xj8,629
174
- fin_infra/utils/retry.py,sha256=ISBrup5XCuXqHZh9kjTGvGQYcuyYyqZE4u26wW7r3CM,1030
174
+ fin_infra/utils/retry.py,sha256=gC49Kp9Y2219UKIa1rJuxkU0ikRHMN5eotZQityLPIU,1057
175
175
  fin_infra/version.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
176
- fin_infra-0.1.80.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
- fin_infra-0.1.80.dist-info/METADATA,sha256=9eljCnpLxJO-6qbAsTCM2IM93N7nt0iLNHM3Cbmq-60,10479
178
- fin_infra-0.1.80.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
- fin_infra-0.1.80.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
- fin_infra-0.1.80.dist-info/RECORD,,
176
+ fin_infra-0.1.81.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
177
+ fin_infra-0.1.81.dist-info/METADATA,sha256=dJVWcVEVOd45d-Vq_94YO_GjgvyECxmQoWk74V3-aZw,10479
178
+ fin_infra-0.1.81.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
179
+ fin_infra-0.1.81.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
180
+ fin_infra-0.1.81.dist-info/RECORD,,