fin-infra 0.5.1__py3-none-any.whl → 0.6.0__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.
@@ -89,6 +89,15 @@ class CreateLinkTokenResponse(BaseModel):
89
89
  link_token: str
90
90
 
91
91
 
92
+ class CreateUpdateLinkTokenRequest(BaseModel):
93
+ """Request model for creating a link token in update mode (re-authentication)."""
94
+
95
+ user_id: str
96
+ access_token: str = Field(
97
+ ..., description="Existing access token for the item requiring re-auth"
98
+ )
99
+
100
+
92
101
  class ExchangeTokenRequest(BaseModel):
93
102
  """Request model for exchanging public token."""
94
103
 
@@ -334,6 +343,23 @@ def add_banking(
334
343
  link_token = banking.create_link_token(user_id=request.user_id)
335
344
  return CreateLinkTokenResponse(link_token=link_token)
336
345
 
346
+ @router.post("/link/update", response_model=CreateLinkTokenResponse)
347
+ async def create_update_link_token(request: CreateUpdateLinkTokenRequest):
348
+ """Create link token in update mode for re-authentication.
349
+
350
+ Use this endpoint when a user's bank connection has expired
351
+ (ITEM_LOGIN_REQUIRED error). The returned link token will open
352
+ Plaid Link in update mode, allowing the user to re-authenticate
353
+ without creating a new connection.
354
+
355
+ After successful re-authentication, the existing access_token
356
+ remains valid and no token exchange is needed.
357
+ """
358
+ link_token = banking.create_link_token(
359
+ user_id=request.user_id, access_token=request.access_token
360
+ )
361
+ return CreateLinkTokenResponse(link_token=link_token)
362
+
337
363
  @router.post("/exchange", response_model=ExchangeTokenResponse)
338
364
  async def exchange_token(request: ExchangeTokenRequest):
339
365
  """Exchange public token for access token (Plaid flow)."""
@@ -343,8 +369,29 @@ def add_banking(
343
369
  @router.get("/accounts")
344
370
  async def get_accounts(access_token: str = Depends(get_access_token)):
345
371
  """List accounts for access token."""
346
- accounts = banking.accounts(access_token=access_token)
347
- return {"accounts": accounts}
372
+ try:
373
+ accounts = banking.accounts(access_token=access_token)
374
+ return {"accounts": accounts}
375
+ except Exception as e:
376
+ error_str = str(e)
377
+ # Check for Plaid-specific errors that require user action
378
+ if "ITEM_LOGIN_REQUIRED" in error_str:
379
+ raise HTTPException(
380
+ status_code=401,
381
+ detail="ITEM_LOGIN_REQUIRED: Your bank connection has expired. Please re-authenticate your bank account.",
382
+ )
383
+ elif "INVALID_ACCESS_TOKEN" in error_str:
384
+ raise HTTPException(
385
+ status_code=401,
386
+ detail="INVALID_ACCESS_TOKEN: The access token is invalid or expired. Please reconnect your bank account.",
387
+ )
388
+ elif "ITEM_NOT_FOUND" in error_str:
389
+ raise HTTPException(
390
+ status_code=404,
391
+ detail="ITEM_NOT_FOUND: This bank connection no longer exists. Please reconnect your bank account.",
392
+ )
393
+ # Re-raise other errors
394
+ raise
348
395
 
349
396
  @router.get("/transactions")
350
397
  async def get_transactions(
@@ -81,21 +81,41 @@ class PlaidClient(BankingProvider):
81
81
  api_client = plaid.ApiClient(configuration)
82
82
  self.client = plaid_api.PlaidApi(api_client)
83
83
 
84
- def create_link_token(self, user_id: str) -> str:
85
- request = LinkTokenCreateRequest(
86
- user=LinkTokenCreateRequestUser(client_user_id=user_id),
87
- client_name="fin-infra",
88
- products=[
84
+ def create_link_token(self, user_id: str, access_token: str | None = None) -> str:
85
+ """Create a Plaid Link token for new connections or re-authentication.
86
+
87
+ Args:
88
+ user_id: Client-defined user ID for the Link session
89
+ access_token: If provided, creates Link in update mode for re-authentication
90
+ (used when ITEM_LOGIN_REQUIRED error occurs)
91
+
92
+ Returns:
93
+ Link token string for Plaid Link initialization
94
+ """
95
+ # Build base request parameters
96
+ request_params = {
97
+ "user": LinkTokenCreateRequestUser(client_user_id=user_id),
98
+ "client_name": "fin-infra",
99
+ "country_codes": [CountryCode("US")],
100
+ "language": "en",
101
+ }
102
+
103
+ if access_token:
104
+ # Update mode: re-authenticate existing connection
105
+ # Don't include products - Plaid uses existing item's products
106
+ request_params["access_token"] = access_token
107
+ else:
108
+ # New connection: specify products to enable
109
+ request_params["products"] = [
89
110
  Products("auth"), # Account/routing numbers for ACH
90
111
  Products("transactions"), # Transaction history
91
112
  Products("liabilities"), # Credit cards, loans, student loans
92
113
  Products("investments"), # Brokerage, retirement accounts
93
114
  Products("assets"), # Asset reports for lending/verification
94
115
  Products("identity"), # Account holder info (name, email, phone)
95
- ],
96
- country_codes=[CountryCode("US")],
97
- language="en",
98
- )
116
+ ]
117
+
118
+ request = LinkTokenCreateRequest(**request_params)
99
119
  response = self.client.link_token_create(request)
100
120
  return cast("str", response["link_token"])
101
121
 
@@ -121,7 +121,7 @@ class TellerClient(BankingProvider):
121
121
  response.raise_for_status()
122
122
  return response.json()
123
123
 
124
- def create_link_token(self, user_id: str) -> str:
124
+ def create_link_token(self, user_id: str, access_token: str | None = None) -> str:
125
125
  """Create link token for user authentication.
126
126
 
127
127
  Note: Teller uses a simpler auth flow than Plaid. In production,
@@ -130,6 +130,9 @@ class TellerClient(BankingProvider):
130
130
 
131
131
  Args:
132
132
  user_id: Your application's user identifier
133
+ access_token: If provided, creates Link in update mode for re-authentication
134
+ (used when connection needs to be repaired). Teller handles
135
+ this differently than Plaid - see Teller Connect docs.
133
136
 
134
137
  Returns:
135
138
  Link token or enrollment ID for user to authenticate
@@ -138,13 +141,18 @@ class TellerClient(BankingProvider):
138
141
  httpx.HTTPStatusError: On HTTP errors
139
142
  """
140
143
  # Teller's enrollment endpoint for creating application links
144
+ payload: dict[str, Any] = {
145
+ "user_id": user_id,
146
+ "products": ["accounts", "transactions", "balances", "identity"],
147
+ }
148
+ # If access_token provided, add it for update mode (re-authentication)
149
+ if access_token:
150
+ payload["access_token"] = access_token
151
+
141
152
  response = self._request(
142
153
  "POST",
143
154
  "/enrollments",
144
- json={
145
- "user_id": user_id,
146
- "products": ["accounts", "transactions", "balances", "identity"],
147
- },
155
+ json=payload,
148
156
  )
149
157
  return cast("str", response.get("enrollment_id", ""))
150
158
 
@@ -70,8 +70,17 @@ class BankingProvider(ABC):
70
70
  """Abstract provider for bank account aggregation (Teller, Plaid, MX)."""
71
71
 
72
72
  @abstractmethod
73
- def create_link_token(self, user_id: str) -> str:
74
- """Create a link/connect token for user to authenticate with their bank."""
73
+ def create_link_token(self, user_id: str, access_token: str | None = None) -> str:
74
+ """Create a link/connect token for user to authenticate with their bank.
75
+
76
+ Args:
77
+ user_id: Client-defined user ID for the Link session
78
+ access_token: If provided, creates Link in update mode for re-authentication
79
+ (used when ITEM_LOGIN_REQUIRED error occurs)
80
+
81
+ Returns:
82
+ Link token string for initializing the bank connection UI
83
+ """
75
84
  pass
76
85
 
77
86
  @abstractmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.5.1
3
+ Version: 0.6.0
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,7 +11,7 @@ fin_infra/analytics/rebalancing.py,sha256=VM8MgoJofmrCXPK1rbmVqWGB4FauNmHCL5HOEb
11
11
  fin_infra/analytics/savings.py,sha256=n3rGNFP8TU5mW-uz9kOuqX_mDiVnDyAeDN06Q7Abotw,7570
12
12
  fin_infra/analytics/scenarios.py,sha256=LE_dZVkbxxAx5sxitGhiOhZfWTlYtVbIvS9pEXkijLc,12246
13
13
  fin_infra/analytics/spending.py,sha256=ogLcfF5ZOLMkBIj02RISnA3hiY_PsLWZ2_AAzA7FenY,26209
14
- fin_infra/banking/__init__.py,sha256=6wwGNITzyehC9MBQc5jy0ewSTuNetyQu2AdND51O55w,22450
14
+ fin_infra/banking/__init__.py,sha256=Lt82vzUZUmekNi_abWz5ySvZlO64FVefd05RAxEd4gU,24537
15
15
  fin_infra/banking/history.py,sha256=YB-v9A03IZ_qki6A6mA-RO5Y4imlqk1CyP0W482ufdQ,10563
16
16
  fin_infra/banking/utils.py,sha256=HhxZbeaA8zqVttgMiJGnShTo_r_0DaD7T3IMq8n8340,15252
17
17
  fin_infra/brokerage/__init__.py,sha256=IXm5ko5T607LodexYSbKNZc6I_2CQGaOwQUlb-w9-ks,17137
@@ -126,9 +126,9 @@ fin_infra/obs/__init__.py,sha256=kMMVl0fdwtJtZeKiusTuw0iO61Jo9-HNXsLmn3ffLRE,631
126
126
  fin_infra/obs/classifier.py,sha256=S7kSphgHN1O4GiMUdr3IjuXpoXU0XgGq132_U-njXX4,5153
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=8Nvd9Ow_v6Scnw79R86uSvRcBRHPgc3ytsQze50E7aM,6524
130
- fin_infra/providers/banking/teller_client.py,sha256=733Eq-o9Yt7Sm_aWOeunJU4EWYRAaZNS7vgDVGqR0W4,10279
131
- fin_infra/providers/base.py,sha256=qKRv8C3TBP7r9J9gFxqeSU3Podh6w_eKI5iCPa15Ta8,8668
129
+ fin_infra/providers/banking/plaid_client.py,sha256=6yaXbPxg4QvebZAXyUFUthBIAN1EuXN59BnmTi69n0U,7374
130
+ fin_infra/providers/banking/teller_client.py,sha256=4chf7fQIybp-sgOWOiW9uJfMq31A9KMPcyMfn0Yi_IY,10752
131
+ fin_infra/providers/base.py,sha256=T8XoHTc3SY30XibfrS4eAMXTujgY8s2mJjBeKzscgDo,9037
132
132
  fin_infra/providers/brokerage/alpaca.py,sha256=BObiI_dFQZ3fOpTfmZMkri8sVrsz5uW6i5ZVUb0etCU,9923
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
@@ -174,8 +174,8 @@ fin_infra/utils/deprecation.py,sha256=DTcqv7ECnrWOOwoA07JOnRci4Hqqo9YtKSSmoS-DVP
174
174
  fin_infra/utils/http.py,sha256=rDEgYsEBrEe75ml5RA-iSs3xeU5W-3j-czJlT7WbrM4,632
175
175
  fin_infra/utils/retry.py,sha256=YiyTgy26eJ1ah7fE2_-ZPa4hv4bIT4OzjYolkNWb5j0,1057
176
176
  fin_infra/version.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
177
- fin_infra-0.5.1.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
178
- fin_infra-0.5.1.dist-info/METADATA,sha256=u93UU3fU8TPtAPA7YcnL32rFqo7o8VIxQfVeZ6970aA,10842
179
- fin_infra-0.5.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
180
- fin_infra-0.5.1.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
181
- fin_infra-0.5.1.dist-info/RECORD,,
177
+ fin_infra-0.6.0.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
178
+ fin_infra-0.6.0.dist-info/METADATA,sha256=47DVsqLkuor-waj8iItVs9AECCOctwqOfzfqHM60Qsc,10842
179
+ fin_infra-0.6.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
180
+ fin_infra-0.6.0.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
181
+ fin_infra-0.6.0.dist-info/RECORD,,