fin-infra 0.1.62__py3-none-any.whl → 0.1.69__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 (83) hide show
  1. fin_infra/analytics/add.py +9 -11
  2. fin_infra/analytics/cash_flow.py +6 -5
  3. fin_infra/analytics/portfolio.py +13 -20
  4. fin_infra/analytics/rebalancing.py +2 -4
  5. fin_infra/analytics/savings.py +1 -1
  6. fin_infra/analytics/spending.py +15 -11
  7. fin_infra/banking/__init__.py +8 -5
  8. fin_infra/banking/history.py +3 -3
  9. fin_infra/banking/utils.py +93 -88
  10. fin_infra/brokerage/__init__.py +5 -3
  11. fin_infra/budgets/tracker.py +2 -3
  12. fin_infra/cashflows/__init__.py +6 -8
  13. fin_infra/categorization/__init__.py +1 -1
  14. fin_infra/categorization/add.py +15 -16
  15. fin_infra/categorization/ease.py +3 -4
  16. fin_infra/categorization/engine.py +4 -4
  17. fin_infra/categorization/llm_layer.py +5 -6
  18. fin_infra/categorization/models.py +1 -1
  19. fin_infra/chat/__init__.py +7 -16
  20. fin_infra/chat/planning.py +57 -0
  21. fin_infra/cli/cmds/scaffold_cmds.py +1 -1
  22. fin_infra/compliance/__init__.py +3 -3
  23. fin_infra/credit/add.py +3 -2
  24. fin_infra/credit/experian/auth.py +3 -2
  25. fin_infra/credit/experian/client.py +2 -2
  26. fin_infra/credit/experian/provider.py +16 -16
  27. fin_infra/crypto/__init__.py +1 -1
  28. fin_infra/crypto/insights.py +1 -3
  29. fin_infra/documents/add.py +5 -5
  30. fin_infra/documents/ease.py +4 -3
  31. fin_infra/documents/models.py +3 -3
  32. fin_infra/documents/ocr.py +1 -1
  33. fin_infra/documents/storage.py +2 -1
  34. fin_infra/exceptions.py +1 -1
  35. fin_infra/goals/add.py +2 -2
  36. fin_infra/goals/management.py +6 -6
  37. fin_infra/goals/milestones.py +2 -2
  38. fin_infra/insights/__init__.py +7 -8
  39. fin_infra/investments/__init__.py +13 -8
  40. fin_infra/investments/add.py +39 -59
  41. fin_infra/investments/ease.py +16 -13
  42. fin_infra/investments/models.py +130 -64
  43. fin_infra/investments/providers/base.py +3 -8
  44. fin_infra/investments/providers/plaid.py +23 -34
  45. fin_infra/investments/providers/snaptrade.py +22 -40
  46. fin_infra/markets/__init__.py +11 -8
  47. fin_infra/models/accounts.py +2 -1
  48. fin_infra/models/transactions.py +3 -2
  49. fin_infra/net_worth/add.py +8 -5
  50. fin_infra/net_worth/aggregator.py +5 -4
  51. fin_infra/net_worth/calculator.py +8 -6
  52. fin_infra/net_worth/ease.py +36 -15
  53. fin_infra/net_worth/insights.py +4 -4
  54. fin_infra/net_worth/models.py +237 -116
  55. fin_infra/normalization/__init__.py +15 -13
  56. fin_infra/normalization/providers/exchangerate.py +3 -3
  57. fin_infra/obs/classifier.py +2 -2
  58. fin_infra/providers/banking/plaid_client.py +20 -19
  59. fin_infra/providers/banking/teller_client.py +13 -7
  60. fin_infra/providers/base.py +105 -13
  61. fin_infra/providers/brokerage/alpaca.py +7 -7
  62. fin_infra/providers/credit/experian.py +5 -0
  63. fin_infra/providers/market/ccxt_crypto.py +8 -3
  64. fin_infra/providers/tax/mock.py +3 -3
  65. fin_infra/recurring/add.py +20 -9
  66. fin_infra/recurring/detector.py +1 -1
  67. fin_infra/recurring/detectors_llm.py +10 -9
  68. fin_infra/recurring/ease.py +1 -1
  69. fin_infra/recurring/insights.py +9 -8
  70. fin_infra/recurring/models.py +3 -3
  71. fin_infra/recurring/normalizer.py +3 -2
  72. fin_infra/recurring/normalizers.py +9 -8
  73. fin_infra/scaffold/__init__.py +1 -1
  74. fin_infra/security/encryption.py +2 -2
  75. fin_infra/security/pii_patterns.py +1 -1
  76. fin_infra/security/token_store.py +3 -1
  77. fin_infra/tax/__init__.py +1 -1
  78. fin_infra/utils/http.py +3 -2
  79. {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/METADATA +1 -2
  80. {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/RECORD +83 -83
  81. {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/LICENSE +0 -0
  82. {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/WHEEL +0 -0
  83. {fin_infra-0.1.62.dist-info → fin_infra-0.1.69.dist-info}/entry_points.txt +0 -0
@@ -19,6 +19,7 @@ Pydantic V2 models for net worth tracking.
19
19
 
20
20
  from datetime import datetime
21
21
  from enum import Enum
22
+ from typing import TYPE_CHECKING
22
23
 
23
24
  from pydantic import BaseModel, ConfigDict, Field, computed_field
24
25
 
@@ -207,54 +208,100 @@ class AssetAllocation(BaseModel):
207
208
  vehicles: float = Field(0.0, ge=0, description="Vehicle value")
208
209
  other_assets: float = Field(0.0, ge=0, description="Other asset value")
209
210
 
210
- @computed_field
211
- @property
212
- def total_assets(self) -> float:
213
- """Sum of all asset categories."""
214
- return (
215
- self.cash
216
- + self.investments
217
- + self.crypto
218
- + self.real_estate
219
- + self.vehicles
220
- + self.other_assets
221
- )
222
-
223
- @computed_field
224
- @property
225
- def cash_percentage(self) -> float:
226
- """Cash as percentage of total assets."""
227
- return (self.cash / self.total_assets * 100) if self.total_assets > 0 else 0.0
228
-
229
- @computed_field
230
- @property
231
- def investments_percentage(self) -> float:
232
- """Investments as percentage of total assets."""
233
- return (self.investments / self.total_assets * 100) if self.total_assets > 0 else 0.0
234
-
235
- @computed_field
236
- @property
237
- def crypto_percentage(self) -> float:
238
- """Crypto as percentage of total assets."""
239
- return (self.crypto / self.total_assets * 100) if self.total_assets > 0 else 0.0
240
-
241
- @computed_field
242
- @property
243
- def real_estate_percentage(self) -> float:
244
- """Real estate as percentage of total assets."""
245
- return (self.real_estate / self.total_assets * 100) if self.total_assets > 0 else 0.0
246
-
247
- @computed_field
248
- @property
249
- def vehicles_percentage(self) -> float:
250
- """Vehicles as percentage of total assets."""
251
- return (self.vehicles / self.total_assets * 100) if self.total_assets > 0 else 0.0
252
-
253
- @computed_field
254
- @property
255
- def other_percentage(self) -> float:
256
- """Other assets as percentage of total assets."""
257
- return (self.other_assets / self.total_assets * 100) if self.total_assets > 0 else 0.0
211
+ if TYPE_CHECKING:
212
+
213
+ @property
214
+ def total_assets(self) -> float:
215
+ """Sum of all asset categories."""
216
+ return (
217
+ self.cash
218
+ + self.investments
219
+ + self.crypto
220
+ + self.real_estate
221
+ + self.vehicles
222
+ + self.other_assets
223
+ )
224
+
225
+ @property
226
+ def cash_percentage(self) -> float:
227
+ """Cash as percentage of total assets."""
228
+ return (self.cash / self.total_assets * 100) if self.total_assets > 0 else 0.0
229
+
230
+ @property
231
+ def investments_percentage(self) -> float:
232
+ """Investments as percentage of total assets."""
233
+ return (self.investments / self.total_assets * 100) if self.total_assets > 0 else 0.0
234
+
235
+ @property
236
+ def crypto_percentage(self) -> float:
237
+ """Crypto as percentage of total assets."""
238
+ return (self.crypto / self.total_assets * 100) if self.total_assets > 0 else 0.0
239
+
240
+ @property
241
+ def real_estate_percentage(self) -> float:
242
+ """Real estate as percentage of total assets."""
243
+ return (self.real_estate / self.total_assets * 100) if self.total_assets > 0 else 0.0
244
+
245
+ @property
246
+ def vehicles_percentage(self) -> float:
247
+ """Vehicles as percentage of total assets."""
248
+ return (self.vehicles / self.total_assets * 100) if self.total_assets > 0 else 0.0
249
+
250
+ @property
251
+ def other_percentage(self) -> float:
252
+ """Other assets as percentage of total assets."""
253
+ return (self.other_assets / self.total_assets * 100) if self.total_assets > 0 else 0.0
254
+
255
+ else:
256
+
257
+ @computed_field
258
+ @property
259
+ def total_assets(self) -> float:
260
+ """Sum of all asset categories."""
261
+ return (
262
+ self.cash
263
+ + self.investments
264
+ + self.crypto
265
+ + self.real_estate
266
+ + self.vehicles
267
+ + self.other_assets
268
+ )
269
+
270
+ @computed_field
271
+ @property
272
+ def cash_percentage(self) -> float:
273
+ """Cash as percentage of total assets."""
274
+ return (self.cash / self.total_assets * 100) if self.total_assets > 0 else 0.0
275
+
276
+ @computed_field
277
+ @property
278
+ def investments_percentage(self) -> float:
279
+ """Investments as percentage of total assets."""
280
+ return (self.investments / self.total_assets * 100) if self.total_assets > 0 else 0.0
281
+
282
+ @computed_field
283
+ @property
284
+ def crypto_percentage(self) -> float:
285
+ """Crypto as percentage of total assets."""
286
+ return (self.crypto / self.total_assets * 100) if self.total_assets > 0 else 0.0
287
+
288
+ @computed_field
289
+ @property
290
+ def real_estate_percentage(self) -> float:
291
+ """Real estate as percentage of total assets."""
292
+ return (self.real_estate / self.total_assets * 100) if self.total_assets > 0 else 0.0
293
+
294
+ @computed_field
295
+ @property
296
+ def vehicles_percentage(self) -> float:
297
+ """Vehicles as percentage of total assets."""
298
+ return (self.vehicles / self.total_assets * 100) if self.total_assets > 0 else 0.0
299
+
300
+ @computed_field
301
+ @property
302
+ def other_percentage(self) -> float:
303
+ """Other assets as percentage of total assets."""
304
+ return (self.other_assets / self.total_assets * 100) if self.total_assets > 0 else 0.0
258
305
 
259
306
 
260
307
  class LiabilityBreakdown(BaseModel):
@@ -288,74 +335,148 @@ class LiabilityBreakdown(BaseModel):
288
335
  personal_loans: float = Field(0.0, ge=0, description="Personal loan balance")
289
336
  lines_of_credit: float = Field(0.0, ge=0, description="Line of credit balance")
290
337
 
291
- @computed_field
292
- @property
293
- def total_liabilities(self) -> float:
294
- """Sum of all liability categories."""
295
- return (
296
- self.credit_cards
297
- + self.mortgages
298
- + self.auto_loans
299
- + self.student_loans
300
- + self.personal_loans
301
- + self.lines_of_credit
302
- )
303
-
304
- @computed_field
305
- @property
306
- def credit_cards_percentage(self) -> float:
307
- """Credit cards as percentage of total liabilities."""
308
- return (
309
- (self.credit_cards / self.total_liabilities * 100)
310
- if self.total_liabilities > 0
311
- else 0.0
312
- )
313
-
314
- @computed_field
315
- @property
316
- def mortgages_percentage(self) -> float:
317
- """Mortgages as percentage of total liabilities."""
318
- return (
319
- (self.mortgages / self.total_liabilities * 100) if self.total_liabilities > 0 else 0.0
320
- )
321
-
322
- @computed_field
323
- @property
324
- def auto_loans_percentage(self) -> float:
325
- """Auto loans as percentage of total liabilities."""
326
- return (
327
- (self.auto_loans / self.total_liabilities * 100) if self.total_liabilities > 0 else 0.0
328
- )
329
-
330
- @computed_field
331
- @property
332
- def student_loans_percentage(self) -> float:
333
- """Student loans as percentage of total liabilities."""
334
- return (
335
- (self.student_loans / self.total_liabilities * 100)
336
- if self.total_liabilities > 0
337
- else 0.0
338
- )
339
-
340
- @computed_field
341
- @property
342
- def personal_loans_percentage(self) -> float:
343
- """Personal loans as percentage of total liabilities."""
344
- return (
345
- (self.personal_loans / self.total_liabilities * 100)
346
- if self.total_liabilities > 0
347
- else 0.0
348
- )
349
-
350
- @computed_field
351
- @property
352
- def lines_of_credit_percentage(self) -> float:
353
- """Lines of credit as percentage of total liabilities."""
354
- return (
355
- (self.lines_of_credit / self.total_liabilities * 100)
356
- if self.total_liabilities > 0
357
- else 0.0
358
- )
338
+ if TYPE_CHECKING:
339
+
340
+ @property
341
+ def total_liabilities(self) -> float:
342
+ """Sum of all liability categories."""
343
+ return (
344
+ self.credit_cards
345
+ + self.mortgages
346
+ + self.auto_loans
347
+ + self.student_loans
348
+ + self.personal_loans
349
+ + self.lines_of_credit
350
+ )
351
+
352
+ @property
353
+ def credit_cards_percentage(self) -> float:
354
+ """Credit cards as percentage of total liabilities."""
355
+ return (
356
+ (self.credit_cards / self.total_liabilities * 100)
357
+ if self.total_liabilities > 0
358
+ else 0.0
359
+ )
360
+
361
+ @property
362
+ def mortgages_percentage(self) -> float:
363
+ """Mortgages as percentage of total liabilities."""
364
+ return (
365
+ (self.mortgages / self.total_liabilities * 100)
366
+ if self.total_liabilities > 0
367
+ else 0.0
368
+ )
369
+
370
+ @property
371
+ def auto_loans_percentage(self) -> float:
372
+ """Auto loans as percentage of total liabilities."""
373
+ return (
374
+ (self.auto_loans / self.total_liabilities * 100)
375
+ if self.total_liabilities > 0
376
+ else 0.0
377
+ )
378
+
379
+ @property
380
+ def student_loans_percentage(self) -> float:
381
+ """Student loans as percentage of total liabilities."""
382
+ return (
383
+ (self.student_loans / self.total_liabilities * 100)
384
+ if self.total_liabilities > 0
385
+ else 0.0
386
+ )
387
+
388
+ @property
389
+ def personal_loans_percentage(self) -> float:
390
+ """Personal loans as percentage of total liabilities."""
391
+ return (
392
+ (self.personal_loans / self.total_liabilities * 100)
393
+ if self.total_liabilities > 0
394
+ else 0.0
395
+ )
396
+
397
+ @property
398
+ def lines_of_credit_percentage(self) -> float:
399
+ """Lines of credit as percentage of total liabilities."""
400
+ return (
401
+ (self.lines_of_credit / self.total_liabilities * 100)
402
+ if self.total_liabilities > 0
403
+ else 0.0
404
+ )
405
+
406
+ else:
407
+
408
+ @computed_field
409
+ @property
410
+ def total_liabilities(self) -> float:
411
+ """Sum of all liability categories."""
412
+ return (
413
+ self.credit_cards
414
+ + self.mortgages
415
+ + self.auto_loans
416
+ + self.student_loans
417
+ + self.personal_loans
418
+ + self.lines_of_credit
419
+ )
420
+
421
+ @computed_field
422
+ @property
423
+ def credit_cards_percentage(self) -> float:
424
+ """Credit cards as percentage of total liabilities."""
425
+ return (
426
+ (self.credit_cards / self.total_liabilities * 100)
427
+ if self.total_liabilities > 0
428
+ else 0.0
429
+ )
430
+
431
+ @computed_field
432
+ @property
433
+ def mortgages_percentage(self) -> float:
434
+ """Mortgages as percentage of total liabilities."""
435
+ return (
436
+ (self.mortgages / self.total_liabilities * 100)
437
+ if self.total_liabilities > 0
438
+ else 0.0
439
+ )
440
+
441
+ @computed_field
442
+ @property
443
+ def auto_loans_percentage(self) -> float:
444
+ """Auto loans as percentage of total liabilities."""
445
+ return (
446
+ (self.auto_loans / self.total_liabilities * 100)
447
+ if self.total_liabilities > 0
448
+ else 0.0
449
+ )
450
+
451
+ @computed_field
452
+ @property
453
+ def student_loans_percentage(self) -> float:
454
+ """Student loans as percentage of total liabilities."""
455
+ return (
456
+ (self.student_loans / self.total_liabilities * 100)
457
+ if self.total_liabilities > 0
458
+ else 0.0
459
+ )
460
+
461
+ @computed_field
462
+ @property
463
+ def personal_loans_percentage(self) -> float:
464
+ """Personal loans as percentage of total liabilities."""
465
+ return (
466
+ (self.personal_loans / self.total_liabilities * 100)
467
+ if self.total_liabilities > 0
468
+ else 0.0
469
+ )
470
+
471
+ @computed_field
472
+ @property
473
+ def lines_of_credit_percentage(self) -> float:
474
+ """Lines of credit as percentage of total liabilities."""
475
+ return (
476
+ (self.lines_of_credit / self.total_liabilities * 100)
477
+ if self.total_liabilities > 0
478
+ else 0.0
479
+ )
359
480
 
360
481
 
361
482
  class AssetDetail(BaseModel):
@@ -1,5 +1,10 @@
1
1
  """Data normalization module for financial symbols and currencies."""
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from fastapi import FastAPI
7
+
3
8
  from fin_infra.normalization.currency_converter import (
4
9
  CurrencyConverter,
5
10
  CurrencyNotSupportedError,
@@ -64,7 +69,7 @@ def easy_normalization(
64
69
 
65
70
 
66
71
  def add_normalization(
67
- app: "FastAPI", # type: ignore
72
+ app: "FastAPI",
68
73
  *,
69
74
  prefix: str = "/normalize",
70
75
  api_key: str | None = None,
@@ -110,11 +115,6 @@ def add_normalization(
110
115
  - Integrated with svc-infra observability (request metrics)
111
116
  - Scoped docs at {prefix}/docs for standalone documentation
112
117
  """
113
- from typing import TYPE_CHECKING
114
-
115
- if TYPE_CHECKING:
116
- from fastapi import FastAPI
117
-
118
118
  # Import FastAPI dependencies
119
119
  from fastapi import Query, HTTPException
120
120
 
@@ -145,19 +145,21 @@ def add_normalization(
145
145
  @router.get("/convert")
146
146
  async def convert_currency(
147
147
  amount: float = Query(..., description="Amount to convert"),
148
- from_currency: str = Query(..., alias="from", description="Source currency code (e.g., USD)"),
148
+ from_currency: str = Query(
149
+ ..., alias="from", description="Source currency code (e.g., USD)"
150
+ ),
149
151
  to_currency: str = Query(..., alias="to", description="Target currency code (e.g., EUR)"),
150
152
  ):
151
153
  """Convert amount between currencies."""
152
154
  try:
153
- result = await converter.convert(amount, from_currency, to_currency)
155
+ result = await converter.convert_with_details(amount, from_currency, to_currency)
154
156
  return {
155
- "amount": amount,
156
- "from_currency": from_currency,
157
- "to_currency": to_currency,
158
- "result": result.amount,
157
+ "amount": result.amount,
158
+ "from_currency": result.from_currency,
159
+ "to_currency": result.to_currency,
160
+ "result": result.converted,
159
161
  "rate": result.rate,
160
- "timestamp": result.timestamp.isoformat(),
162
+ "timestamp": result.date.isoformat() if result.date else None,
161
163
  }
162
164
  except CurrencyNotSupportedError as e:
163
165
  raise HTTPException(status_code=400, detail=str(e))
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from datetime import date as DateType
5
- from typing import Optional
5
+ from typing import Optional, cast
6
6
 
7
7
  import httpx
8
8
 
@@ -66,10 +66,10 @@ class ExchangeRateClient:
66
66
  raise ExchangeRateAPIError(
67
67
  f"API returned error: {data.get('error-type', 'unknown')}"
68
68
  )
69
- return data["conversion_rates"]
69
+ return cast(dict[str, float], data["conversion_rates"])
70
70
  else:
71
71
  # Free tier response format
72
- return data["rates"]
72
+ return cast(dict[str, float], data["rates"])
73
73
 
74
74
  except httpx.HTTPError as e:
75
75
  raise ExchangeRateAPIError(f"HTTP error fetching rates: {e}")
@@ -112,9 +112,9 @@ def financial_route_classifier(route_path: str, method: str) -> str:
112
112
 
113
113
 
114
114
  def compose_classifiers(
115
- *classifiers: Callable[[str], str],
115
+ *classifiers: Callable[[str, str], str],
116
116
  default: str = "public",
117
- ) -> Callable[[str], str]:
117
+ ) -> Callable[[str, str], str]:
118
118
  """
119
119
  Compose multiple route classifiers with fallback logic.
120
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:
@@ -33,7 +34,7 @@ class PlaidClient(BankingProvider):
33
34
  environment: str | None = None,
34
35
  ) -> None:
35
36
  """Initialize Plaid client with either Settings object or individual parameters.
36
-
37
+
37
38
  Args:
38
39
  settings: Settings object (legacy pattern)
39
40
  client_id: Plaid client ID (preferred - from env or passed directly)
@@ -44,14 +45,14 @@ class PlaidClient(BankingProvider):
44
45
  raise RuntimeError(
45
46
  "plaid-python SDK not available or import failed; check installed version (requires v25+)"
46
47
  )
47
-
48
+
48
49
  # Support both patterns: Settings object or individual params
49
50
  if settings is not None:
50
51
  # Legacy pattern with Settings object
51
52
  client_id = client_id or settings.plaid_client_id
52
53
  secret = secret or settings.plaid_secret
53
54
  environment = environment or settings.plaid_env
54
-
55
+
55
56
  # Map environment string to Plaid Environment enum
56
57
  # Note: Plaid only has Sandbox and Production (no Development in SDK)
57
58
  env_str = environment or "sandbox"
@@ -60,22 +61,22 @@ class PlaidClient(BankingProvider):
60
61
  "development": plaid.Environment.Sandbox, # Map development to sandbox (Plaid SDK limitation)
61
62
  "production": plaid.Environment.Production,
62
63
  }
63
-
64
+
64
65
  if env_str not in env_map:
65
66
  raise ValueError(
66
67
  f"Invalid Plaid environment: '{env_str}'. "
67
68
  f"Must be one of: sandbox, development, production"
68
69
  )
69
-
70
+
70
71
  host = env_map[env_str]
71
-
72
+
72
73
  # Configure Plaid client (v8.0.0+ API)
73
74
  configuration = plaid.Configuration(
74
75
  host=host,
75
76
  api_key={
76
77
  "clientId": client_id,
77
78
  "secret": secret,
78
- }
79
+ },
79
80
  )
80
81
  api_client = plaid.ApiClient(configuration)
81
82
  self.client = plaid_api.PlaidApi(api_client)
@@ -85,18 +86,18 @@ class PlaidClient(BankingProvider):
85
86
  user=LinkTokenCreateRequestUser(client_user_id=user_id),
86
87
  client_name="fin-infra",
87
88
  products=[
88
- Products("auth"), # Account/routing numbers for ACH
89
- Products("transactions"), # Transaction history
90
- Products("liabilities"), # Credit cards, loans, student loans
91
- Products("investments"), # Brokerage, retirement accounts
92
- Products("assets"), # Asset reports for lending/verification
93
- Products("identity"), # Account holder info (name, email, phone)
89
+ Products("auth"), # Account/routing numbers for ACH
90
+ Products("transactions"), # Transaction history
91
+ Products("liabilities"), # Credit cards, loans, student loans
92
+ Products("investments"), # Brokerage, retirement accounts
93
+ Products("assets"), # Asset reports for lending/verification
94
+ Products("identity"), # Account holder info (name, email, phone)
94
95
  ],
95
96
  country_codes=[CountryCode("US")],
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)
@@ -121,7 +122,7 @@ class PlaidClient(BankingProvider):
121
122
  start = end - timedelta(days=30)
122
123
  start_date = start_date or start.isoformat()
123
124
  end_date = end_date or end.isoformat()
124
-
125
+
125
126
  request = TransactionsGetRequest(
126
127
  access_token=access_token,
127
128
  start_date=date.fromisoformat(start_date),
@@ -135,19 +136,19 @@ class PlaidClient(BankingProvider):
135
136
  request = AccountsBalanceGetRequest(access_token=access_token)
136
137
  response = self.client.accounts_balance_get(request)
137
138
  accounts = [acc.to_dict() for acc in response["accounts"]]
138
-
139
+
139
140
  if account_id:
140
141
  # Filter to specific account
141
142
  for account in accounts:
142
143
  if account.get("account_id") == account_id:
143
144
  return {"balances": [account.get("balances", {})]}
144
145
  return {"balances": []}
145
-
146
+
146
147
  # Return all balances
147
148
  return {"balances": [acc.get("balances", {}) for acc in accounts]}
148
149
 
149
- def identity(self, access_token: str) -> dict:
150
+ def identity(self, access_token: str) -> dict[Any, Any]:
150
151
  """Fetch identity/account holder information."""
151
152
  request = IdentityGetRequest(access_token=access_token)
152
153
  response = self.client.identity_get(request)
153
- return response.to_dict()
154
+ return cast(dict[Any, Any], response.to_dict())
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import ssl
26
26
  import httpx
27
- from typing import Any
27
+ from typing import Any, cast
28
28
 
29
29
  from ..base import BankingProvider
30
30
 
@@ -93,7 +93,13 @@ class TellerClient(BankingProvider):
93
93
  ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
94
94
  client_kwargs["verify"] = ssl_context
95
95
 
96
- self.client = httpx.Client(**client_kwargs)
96
+ # Create client with explicit parameters to satisfy type checker
97
+ self.client = httpx.Client(
98
+ base_url=str(client_kwargs["base_url"]),
99
+ timeout=float(client_kwargs["timeout"]), # type: ignore[arg-type]
100
+ headers=client_kwargs["headers"], # type: ignore[arg-type]
101
+ verify=client_kwargs.get("verify", True), # type: ignore[arg-type]
102
+ )
97
103
 
98
104
  def _request(self, method: str, path: str, **kwargs: Any) -> Any:
99
105
  """Make HTTP request to Teller API with error handling.
@@ -139,7 +145,7 @@ class TellerClient(BankingProvider):
139
145
  "products": ["accounts", "transactions", "balances", "identity"],
140
146
  },
141
147
  )
142
- return response.get("enrollment_id", "")
148
+ return cast(str, response.get("enrollment_id", ""))
143
149
 
144
150
  def exchange_public_token(self, public_token: str) -> dict:
145
151
  """Exchange public token for access token.
@@ -186,7 +192,7 @@ class TellerClient(BankingProvider):
186
192
  auth=(access_token, ""),
187
193
  )
188
194
  response.raise_for_status()
189
- return response.json()
195
+ return cast(list[dict[Any, Any]], response.json())
190
196
 
191
197
  def transactions(
192
198
  self,
@@ -229,7 +235,7 @@ class TellerClient(BankingProvider):
229
235
  params=params,
230
236
  )
231
237
  response.raise_for_status()
232
- return response.json()
238
+ return cast(list[dict[Any, Any]], response.json())
233
239
 
234
240
  def balances(self, access_token: str, account_id: str | None = None) -> dict:
235
241
  """Fetch current balances.
@@ -261,7 +267,7 @@ class TellerClient(BankingProvider):
261
267
  )
262
268
 
263
269
  response.raise_for_status()
264
- return response.json()
270
+ return cast(dict[Any, Any], response.json())
265
271
 
266
272
  def identity(self, access_token: str) -> dict:
267
273
  """Fetch identity/account holder information.
@@ -285,7 +291,7 @@ class TellerClient(BankingProvider):
285
291
  auth=(access_token, ""),
286
292
  )
287
293
  response.raise_for_status()
288
- return response.json()
294
+ return cast(dict[Any, Any], response.json())
289
295
 
290
296
  def __del__(self) -> None:
291
297
  """Close HTTP client on cleanup."""