fin-infra 0.1.81__py3-none-any.whl → 0.1.83__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 (96) hide show
  1. fin_infra/analytics/__init__.py +3 -2
  2. fin_infra/analytics/add.py +21 -21
  3. fin_infra/analytics/ease.py +19 -20
  4. fin_infra/analytics/portfolio.py +6 -6
  5. fin_infra/analytics/projections.py +1 -3
  6. fin_infra/banking/__init__.py +27 -27
  7. fin_infra/banking/history.py +4 -5
  8. fin_infra/banking/utils.py +19 -18
  9. fin_infra/brokerage/__init__.py +22 -24
  10. fin_infra/budgets/__init__.py +3 -3
  11. fin_infra/budgets/add.py +16 -17
  12. fin_infra/cashflows/__init__.py +2 -2
  13. fin_infra/categorization/add.py +2 -3
  14. fin_infra/categorization/engine.py +6 -6
  15. fin_infra/categorization/llm_layer.py +6 -5
  16. fin_infra/categorization/rules.py +2 -4
  17. fin_infra/categorization/taxonomy.py +2 -2
  18. fin_infra/chat/__init__.py +5 -5
  19. fin_infra/chat/planning.py +0 -1
  20. fin_infra/cli/cmds/scaffold_cmds.py +10 -11
  21. fin_infra/clients/plaid.py +1 -1
  22. fin_infra/compliance/__init__.py +5 -5
  23. fin_infra/credit/add.py +6 -7
  24. fin_infra/credit/experian/auth.py +2 -2
  25. fin_infra/credit/experian/client.py +1 -1
  26. fin_infra/credit/experian/provider.py +4 -4
  27. fin_infra/crypto/__init__.py +7 -9
  28. fin_infra/documents/add.py +6 -8
  29. fin_infra/documents/analysis.py +8 -8
  30. fin_infra/documents/ease.py +14 -14
  31. fin_infra/documents/ocr.py +7 -7
  32. fin_infra/documents/storage.py +21 -13
  33. fin_infra/exceptions.py +0 -1
  34. fin_infra/goals/__init__.py +8 -8
  35. fin_infra/goals/add.py +30 -30
  36. fin_infra/goals/funding.py +1 -1
  37. fin_infra/goals/management.py +2 -3
  38. fin_infra/goals/milestones.py +1 -2
  39. fin_infra/goals/models.py +7 -11
  40. fin_infra/insights/__init__.py +2 -2
  41. fin_infra/insights/aggregator.py +1 -1
  42. fin_infra/investments/__init__.py +1 -1
  43. fin_infra/investments/add.py +23 -23
  44. fin_infra/investments/providers/base.py +2 -3
  45. fin_infra/investments/providers/plaid.py +9 -9
  46. fin_infra/investments/providers/snaptrade.py +10 -10
  47. fin_infra/markets/__init__.py +1 -1
  48. fin_infra/models/__init__.py +10 -10
  49. fin_infra/models/brokerage.py +2 -1
  50. fin_infra/models/candle.py +1 -0
  51. fin_infra/models/money.py +1 -0
  52. fin_infra/models/quotes.py +4 -3
  53. fin_infra/models/tax.py +2 -1
  54. fin_infra/models/transactions.py +3 -4
  55. fin_infra/net_worth/insights.py +0 -1
  56. fin_infra/normalization/__init__.py +2 -2
  57. fin_infra/normalization/providers/exchangerate.py +5 -5
  58. fin_infra/providers/banking/plaid_client.py +5 -5
  59. fin_infra/providers/banking/teller_client.py +7 -6
  60. fin_infra/providers/base.py +1 -1
  61. fin_infra/providers/brokerage/alpaca.py +3 -3
  62. fin_infra/providers/market/alphavantage.py +5 -10
  63. fin_infra/providers/market/ccxt_crypto.py +2 -2
  64. fin_infra/providers/market/coingecko.py +5 -6
  65. fin_infra/providers/market/yahoo.py +5 -5
  66. fin_infra/providers/tax/__init__.py +1 -1
  67. fin_infra/providers/tax/irs.py +1 -1
  68. fin_infra/providers/tax/mock.py +5 -5
  69. fin_infra/providers/tax/taxbit.py +1 -1
  70. fin_infra/recurring/__init__.py +6 -6
  71. fin_infra/recurring/add.py +5 -4
  72. fin_infra/recurring/detector.py +7 -7
  73. fin_infra/recurring/detectors_llm.py +6 -6
  74. fin_infra/recurring/ease.py +2 -4
  75. fin_infra/recurring/insights.py +13 -13
  76. fin_infra/recurring/normalizer.py +1 -1
  77. fin_infra/recurring/normalizers.py +4 -4
  78. fin_infra/recurring/summary.py +4 -6
  79. fin_infra/scaffold/budgets.py +6 -6
  80. fin_infra/scaffold/goals.py +1 -1
  81. fin_infra/security/__init__.py +8 -8
  82. fin_infra/security/encryption.py +6 -6
  83. fin_infra/security/models.py +7 -7
  84. fin_infra/security/pii_filter.py +6 -6
  85. fin_infra/settings.py +2 -1
  86. fin_infra/tax/__init__.py +1 -1
  87. fin_infra/tax/add.py +3 -2
  88. fin_infra/tax/tlh.py +5 -5
  89. fin_infra/utils/http.py +4 -3
  90. fin_infra/utils/retry.py +1 -1
  91. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/METADATA +1 -1
  92. fin_infra-0.1.83.dist-info/RECORD +180 -0
  93. fin_infra-0.1.81.dist-info/RECORD +0 -180
  94. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/LICENSE +0 -0
  95. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/WHEEL +0 -0
  96. {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/entry_points.txt +0 -0
@@ -22,10 +22,12 @@ from typing import TYPE_CHECKING, Literal
22
22
  if TYPE_CHECKING:
23
23
  from fastapi import FastAPI
24
24
 
25
- from ..providers.base import BrokerageProvider
26
- from pydantic import BaseModel, Field
27
25
  from decimal import Decimal
28
26
 
27
+ from pydantic import BaseModel, Field
28
+
29
+ from ..providers.base import BrokerageProvider
30
+
29
31
 
30
32
  # Request model for order submission (used by add_brokerage FastAPI routes)
31
33
  class OrderRequest(BaseModel):
@@ -127,7 +129,7 @@ def easy_brokerage(
127
129
 
128
130
 
129
131
  def add_brokerage(
130
- app: "FastAPI",
132
+ app: FastAPI,
131
133
  *,
132
134
  provider: str | BrokerageProvider | None = None,
133
135
  mode: Literal["paper", "live"] = "paper",
@@ -206,9 +208,9 @@ def add_brokerage(
206
208
  >>> broker = add_brokerage(app, mode="live")
207
209
  >>> # Only use in production with proper safeguards and risk management
208
210
  """
209
- from svc_infra.api.fastapi.dual.public import public_router
210
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
211
211
  from fastapi import HTTPException, Query
212
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
213
+ from svc_infra.api.fastapi.dual.public import public_router
212
214
 
213
215
  # Initialize provider if string or None
214
216
  if isinstance(provider, str):
@@ -234,7 +236,7 @@ def add_brokerage(
234
236
  account = brokerage_provider.get_account()
235
237
  return account
236
238
  except Exception as e:
237
- raise HTTPException(status_code=500, detail=f"Error fetching account: {str(e)}")
239
+ raise HTTPException(status_code=500, detail=f"Error fetching account: {e!s}")
238
240
 
239
241
  @router.get("/positions")
240
242
  async def list_positions():
@@ -246,7 +248,7 @@ def add_brokerage(
246
248
  positions = list(brokerage_provider.positions()) # Convert Iterable to list for len()
247
249
  return {"positions": positions, "count": len(positions)}
248
250
  except Exception as e:
249
- raise HTTPException(status_code=500, detail=f"Error fetching positions: {str(e)}")
251
+ raise HTTPException(status_code=500, detail=f"Error fetching positions: {e!s}")
250
252
 
251
253
  @router.get("/positions/{symbol}")
252
254
  async def get_position(symbol: str):
@@ -259,9 +261,7 @@ def add_brokerage(
259
261
  position = brokerage_provider.get_position(symbol)
260
262
  return position
261
263
  except Exception as e:
262
- raise HTTPException(
263
- status_code=404, detail=f"Position not found for {symbol}: {str(e)}"
264
- )
264
+ raise HTTPException(status_code=404, detail=f"Position not found for {symbol}: {e!s}")
265
265
 
266
266
  @router.delete("/positions/{symbol}")
267
267
  async def close_position(symbol: str):
@@ -274,7 +274,7 @@ def add_brokerage(
274
274
  order = brokerage_provider.close_position(symbol)
275
275
  return {"message": f"Closing position for {symbol}", "order": order}
276
276
  except Exception as e:
277
- raise HTTPException(status_code=400, detail=f"Error closing position: {str(e)}")
277
+ raise HTTPException(status_code=400, detail=f"Error closing position: {e!s}")
278
278
 
279
279
  @router.post("/orders")
280
280
  async def submit_order(order_request: OrderRequest):
@@ -295,7 +295,7 @@ def add_brokerage(
295
295
  )
296
296
  return order
297
297
  except Exception as e:
298
- raise HTTPException(status_code=400, detail=f"Error submitting order: {str(e)}")
298
+ raise HTTPException(status_code=400, detail=f"Error submitting order: {e!s}")
299
299
 
300
300
  @router.get("/orders")
301
301
  async def list_orders(
@@ -312,7 +312,7 @@ def add_brokerage(
312
312
  orders = brokerage_provider.list_orders(status=status, limit=limit)
313
313
  return {"orders": orders, "count": len(orders)}
314
314
  except Exception as e:
315
- raise HTTPException(status_code=500, detail=f"Error fetching orders: {str(e)}")
315
+ raise HTTPException(status_code=500, detail=f"Error fetching orders: {e!s}")
316
316
 
317
317
  @router.get("/orders/{order_id}")
318
318
  async def get_order(order_id: str):
@@ -325,7 +325,7 @@ def add_brokerage(
325
325
  order = brokerage_provider.get_order(order_id)
326
326
  return order
327
327
  except Exception as e:
328
- raise HTTPException(status_code=404, detail=f"Order not found: {str(e)}")
328
+ raise HTTPException(status_code=404, detail=f"Order not found: {e!s}")
329
329
 
330
330
  @router.delete("/orders/{order_id}")
331
331
  async def cancel_order(order_id: str):
@@ -338,7 +338,7 @@ def add_brokerage(
338
338
  brokerage_provider.cancel_order(order_id)
339
339
  return {"message": f"Order {order_id} canceled successfully"}
340
340
  except Exception as e:
341
- raise HTTPException(status_code=400, detail=f"Error canceling order: {str(e)}")
341
+ raise HTTPException(status_code=400, detail=f"Error canceling order: {e!s}")
342
342
 
343
343
  @router.get("/portfolio/history")
344
344
  async def get_portfolio_history(
@@ -355,9 +355,7 @@ def add_brokerage(
355
355
  history = brokerage_provider.get_portfolio_history(period=period, timeframe=timeframe)
356
356
  return history
357
357
  except Exception as e:
358
- raise HTTPException(
359
- status_code=500, detail=f"Error fetching portfolio history: {str(e)}"
360
- )
358
+ raise HTTPException(status_code=500, detail=f"Error fetching portfolio history: {e!s}")
361
359
 
362
360
  # Watchlist routes
363
361
  @router.post("/watchlists")
@@ -375,7 +373,7 @@ def add_brokerage(
375
373
  watchlist = brokerage_provider.create_watchlist(name=name, symbols=symbols)
376
374
  return watchlist
377
375
  except Exception as e:
378
- raise HTTPException(status_code=400, detail=f"Error creating watchlist: {str(e)}")
376
+ raise HTTPException(status_code=400, detail=f"Error creating watchlist: {e!s}")
379
377
 
380
378
  @router.get("/watchlists")
381
379
  async def list_watchlists():
@@ -384,7 +382,7 @@ def add_brokerage(
384
382
  watchlists = brokerage_provider.list_watchlists()
385
383
  return {"watchlists": watchlists, "count": len(watchlists)}
386
384
  except Exception as e:
387
- raise HTTPException(status_code=500, detail=f"Error fetching watchlists: {str(e)}")
385
+ raise HTTPException(status_code=500, detail=f"Error fetching watchlists: {e!s}")
388
386
 
389
387
  @router.get("/watchlists/{watchlist_id}")
390
388
  async def get_watchlist(watchlist_id: str):
@@ -397,7 +395,7 @@ def add_brokerage(
397
395
  watchlist = brokerage_provider.get_watchlist(watchlist_id)
398
396
  return watchlist
399
397
  except Exception as e:
400
- raise HTTPException(status_code=404, detail=f"Watchlist not found: {str(e)}")
398
+ raise HTTPException(status_code=404, detail=f"Watchlist not found: {e!s}")
401
399
 
402
400
  @router.delete("/watchlists/{watchlist_id}")
403
401
  async def delete_watchlist(watchlist_id: str):
@@ -410,7 +408,7 @@ def add_brokerage(
410
408
  brokerage_provider.delete_watchlist(watchlist_id)
411
409
  return {"message": f"Watchlist {watchlist_id} deleted successfully"}
412
410
  except Exception as e:
413
- raise HTTPException(status_code=400, detail=f"Error deleting watchlist: {str(e)}")
411
+ raise HTTPException(status_code=400, detail=f"Error deleting watchlist: {e!s}")
414
412
 
415
413
  @router.post("/watchlists/{watchlist_id}/symbols")
416
414
  async def add_to_watchlist(
@@ -426,7 +424,7 @@ def add_brokerage(
426
424
  watchlist = brokerage_provider.add_to_watchlist(watchlist_id, symbol)
427
425
  return watchlist
428
426
  except Exception as e:
429
- raise HTTPException(status_code=400, detail=f"Error adding symbol: {str(e)}")
427
+ raise HTTPException(status_code=400, detail=f"Error adding symbol: {e!s}")
430
428
 
431
429
  @router.delete("/watchlists/{watchlist_id}/symbols/{symbol}")
432
430
  async def remove_from_watchlist(watchlist_id: str, symbol: str):
@@ -440,7 +438,7 @@ def add_brokerage(
440
438
  watchlist = brokerage_provider.remove_from_watchlist(watchlist_id, symbol)
441
439
  return watchlist
442
440
  except Exception as e:
443
- raise HTTPException(status_code=400, detail=f"Error removing symbol: {str(e)}")
441
+ raise HTTPException(status_code=400, detail=f"Error removing symbol: {e!s}")
444
442
 
445
443
  # Mount router
446
444
  app.include_router(router, include_in_schema=True)
@@ -105,12 +105,12 @@ def __getattr__(name: str):
105
105
  ):
106
106
  from fin_infra.budgets.models import ( # noqa: F401
107
107
  Budget,
108
- BudgetType,
109
- BudgetPeriod,
108
+ BudgetAlert,
110
109
  BudgetCategory,
110
+ BudgetPeriod,
111
111
  BudgetProgress,
112
- BudgetAlert,
113
112
  BudgetTemplate,
113
+ BudgetType,
114
114
  )
115
115
 
116
116
  return locals()[name]
fin_infra/budgets/add.py CHANGED
@@ -22,7 +22,6 @@ Generic Design:
22
22
  from __future__ import annotations
23
23
 
24
24
  from datetime import datetime
25
- from typing import Optional
26
25
 
27
26
  from fastapi import FastAPI, HTTPException, Query
28
27
  from pydantic import BaseModel, Field
@@ -42,16 +41,16 @@ class CreateBudgetRequest(BaseModel):
42
41
  type: BudgetType = Field(..., description="Budget type")
43
42
  period: BudgetPeriod = Field(..., description="Budget period")
44
43
  categories: dict[str, float] = Field(..., description="Category allocations")
45
- start_date: Optional[datetime] = Field(None, description="Start date (defaults to now)")
44
+ start_date: datetime | None = Field(None, description="Start date (defaults to now)")
46
45
  rollover_enabled: bool = Field(False, description="Enable rollover")
47
46
 
48
47
 
49
48
  class UpdateBudgetRequest(BaseModel):
50
49
  """Request model for updating a budget."""
51
50
 
52
- name: Optional[str] = Field(None, description="Updated budget name")
53
- categories: Optional[dict[str, float]] = Field(None, description="Updated categories")
54
- rollover_enabled: Optional[bool] = Field(None, description="Updated rollover setting")
51
+ name: str | None = Field(None, description="Updated budget name")
52
+ categories: dict[str, float] | None = Field(None, description="Updated categories")
53
+ rollover_enabled: bool | None = Field(None, description="Updated rollover setting")
55
54
 
56
55
 
57
56
  class ApplyTemplateRequest(BaseModel):
@@ -60,14 +59,14 @@ class ApplyTemplateRequest(BaseModel):
60
59
  user_id: str = Field(..., description="User identifier")
61
60
  template_name: str = Field(..., description="Template name (e.g., '50_30_20')")
62
61
  total_income: float = Field(..., description="Total income/budget amount", gt=0)
63
- budget_name: Optional[str] = Field(None, description="Optional budget name")
64
- start_date: Optional[datetime] = Field(None, description="Optional start date")
62
+ budget_name: str | None = Field(None, description="Optional budget name")
63
+ start_date: datetime | None = Field(None, description="Optional start date")
65
64
 
66
65
 
67
66
  def add_budgets(
68
67
  app: FastAPI,
69
- tracker: Optional[BudgetTracker] = None,
70
- db_url: Optional[str] = None,
68
+ tracker: BudgetTracker | None = None,
69
+ db_url: str | None = None,
71
70
  prefix: str = "/budgets",
72
71
  ) -> BudgetTracker:
73
72
  """Add budget management endpoints to FastAPI app.
@@ -162,13 +161,13 @@ def add_budgets(
162
161
  except ValueError as e:
163
162
  raise HTTPException(status_code=400, detail=str(e))
164
163
  except Exception as e:
165
- raise HTTPException(status_code=500, detail=f"Failed to create budget: {str(e)}")
164
+ raise HTTPException(status_code=500, detail=f"Failed to create budget: {e!s}")
166
165
 
167
166
  # Endpoint 2: List budgets
168
167
  @router.get("", response_model=list[Budget], summary="List Budgets")
169
168
  async def list_budgets(
170
169
  user_id: str = Query(..., description="User identifier"),
171
- type: Optional[BudgetType] = Query(None, description="Filter by budget type"),
170
+ type: BudgetType | None = Query(None, description="Filter by budget type"),
172
171
  ) -> list[Budget]:
173
172
  """
174
173
  List budgets for a user.
@@ -189,7 +188,7 @@ def add_budgets(
189
188
  budgets = await tracker.get_budgets(user_id=user_id, type=type)
190
189
  return budgets
191
190
  except Exception as e:
192
- raise HTTPException(status_code=500, detail=f"Failed to list budgets: {str(e)}")
191
+ raise HTTPException(status_code=500, detail=f"Failed to list budgets: {e!s}")
193
192
 
194
193
  # Endpoint 3: Get single budget
195
194
  @router.get("/{budget_id}", response_model=Budget, summary="Get Budget")
@@ -217,7 +216,7 @@ def add_budgets(
217
216
  except ValueError as e:
218
217
  raise HTTPException(status_code=404, detail=str(e))
219
218
  except Exception as e:
220
- raise HTTPException(status_code=500, detail=f"Failed to get budget: {str(e)}")
219
+ raise HTTPException(status_code=500, detail=f"Failed to get budget: {e!s}")
221
220
 
222
221
  # Endpoint 4: Update budget
223
222
  @router.patch("/{budget_id}", response_model=Budget, summary="Update Budget")
@@ -269,7 +268,7 @@ def add_budgets(
269
268
  else:
270
269
  raise HTTPException(status_code=400, detail=error_msg)
271
270
  except Exception as e:
272
- raise HTTPException(status_code=500, detail=f"Failed to update budget: {str(e)}")
271
+ raise HTTPException(status_code=500, detail=f"Failed to update budget: {e!s}")
273
272
 
274
273
  # Endpoint 5: Delete budget
275
274
  @router.delete("/{budget_id}", status_code=204, summary="Delete Budget", response_model=None)
@@ -296,7 +295,7 @@ def add_budgets(
296
295
  except ValueError as e:
297
296
  raise HTTPException(status_code=404, detail=str(e))
298
297
  except Exception as e:
299
- raise HTTPException(status_code=500, detail=f"Failed to delete budget: {str(e)}")
298
+ raise HTTPException(status_code=500, detail=f"Failed to delete budget: {e!s}")
300
299
 
301
300
  # Endpoint 6: Get budget progress
302
301
  @router.get(
@@ -328,7 +327,7 @@ def add_budgets(
328
327
  except ValueError as e:
329
328
  raise HTTPException(status_code=404, detail=str(e))
330
329
  except Exception as e:
331
- raise HTTPException(status_code=500, detail=f"Failed to get budget progress: {str(e)}")
330
+ raise HTTPException(status_code=500, detail=f"Failed to get budget progress: {e!s}")
332
331
 
333
332
  # Endpoint 7: List templates
334
333
  @router.get("/templates/list", response_model=dict, summary="List Budget Templates")
@@ -402,7 +401,7 @@ def add_budgets(
402
401
  raise HTTPException(status_code=400, detail=str(e))
403
402
  except Exception as e:
404
403
  raise HTTPException(
405
- status_code=500, detail=f"Failed to create budget from template: {str(e)}"
404
+ status_code=500, detail=f"Failed to create budget from template: {e!s}"
406
405
  )
407
406
 
408
407
  # Mount router
@@ -29,7 +29,7 @@ import numpy_financial as npf
29
29
  if TYPE_CHECKING:
30
30
  from fastapi import FastAPI
31
31
 
32
- from .core import npv, irr
32
+ from .core import irr, npv
33
33
 
34
34
  __all__ = ["npv", "irr", "pmt", "fv", "pv", "add_cashflows"]
35
35
 
@@ -173,10 +173,10 @@ def add_cashflows(
173
173
  - Scoped docs at {prefix}/docs
174
174
  """
175
175
  from pydantic import BaseModel, Field
176
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
176
177
 
177
178
  # Import svc-infra public router (no auth - utility calculations)
178
179
  from svc_infra.api.fastapi.dual.public import public_router
179
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
180
180
 
181
181
  # Request/Response models
182
182
  class NPVRequest(BaseModel):
@@ -6,10 +6,10 @@ Uses svc-infra dual routers for consistent behavior.
6
6
  """
7
7
 
8
8
  import time
9
- from typing import Optional
10
9
 
11
10
  from fastapi import FastAPI, HTTPException
12
11
 
12
+ from . import rules
13
13
  from .ease import easy_categorization
14
14
  from .engine import CategorizationEngine
15
15
  from .models import (
@@ -18,7 +18,6 @@ from .models import (
18
18
  CategoryStats,
19
19
  )
20
20
  from .taxonomy import CategoryGroup, count_categories, get_all_categories
21
- from . import rules
22
21
 
23
22
 
24
23
  def add_categorization(
@@ -122,7 +121,7 @@ def add_categorization(
122
121
  raise HTTPException(status_code=400, detail=str(e))
123
122
 
124
123
  @router.get("/categories")
125
- async def list_categories(group: Optional[CategoryGroup] = None):
124
+ async def list_categories(group: CategoryGroup | None = None):
126
125
  """
127
126
  List all available categories.
128
127
 
@@ -10,8 +10,8 @@ Hybrid categorization engine (exact → regex → ML → LLM).
10
10
  Expected overall accuracy: 95-97% (V2 with LLM)
11
11
  """
12
12
 
13
- import time
14
13
  import logging
14
+ import time
15
15
  from pathlib import Path
16
16
  from typing import Optional
17
17
 
@@ -51,7 +51,7 @@ class CategorizationEngine:
51
51
  enable_ml: bool = False,
52
52
  enable_llm: bool = False,
53
53
  confidence_threshold: float = 0.6,
54
- model_path: Optional[Path] = None,
54
+ model_path: Path | None = None,
55
55
  llm_categorizer: Optional["LLMCategorizer"] = None,
56
56
  ):
57
57
  self.enable_ml = enable_ml
@@ -81,7 +81,7 @@ class CategorizationEngine:
81
81
  async def categorize(
82
82
  self,
83
83
  merchant_name: str,
84
- user_id: Optional[str] = None,
84
+ user_id: str | None = None,
85
85
  include_alternatives: bool = False,
86
86
  ) -> CategoryPrediction:
87
87
  """
@@ -195,7 +195,7 @@ class CategorizationEngine:
195
195
 
196
196
  def _predict_ml(
197
197
  self, merchant_name: str, include_alternatives: bool = False
198
- ) -> Optional[CategoryPrediction]:
198
+ ) -> CategoryPrediction | None:
199
199
  """
200
200
  Predict category using ML model.
201
201
 
@@ -325,7 +325,7 @@ class CategorizationEngine:
325
325
 
326
326
 
327
327
  # Singleton instance (for easy access)
328
- _default_engine: Optional[CategorizationEngine] = None
328
+ _default_engine: CategorizationEngine | None = None
329
329
 
330
330
 
331
331
  def get_engine() -> CategorizationEngine:
@@ -338,7 +338,7 @@ def get_engine() -> CategorizationEngine:
338
338
 
339
339
  async def categorize(
340
340
  merchant_name: str,
341
- user_id: Optional[str] = None,
341
+ user_id: str | None = None,
342
342
  include_alternatives: bool = False,
343
343
  ) -> CategoryPrediction:
344
344
  """
@@ -15,7 +15,8 @@ Expected performance:
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Any, Optional, cast
18
+ from typing import Any, cast
19
+
19
20
  from pydantic import BaseModel, Field
20
21
 
21
22
  # ai-infra imports
@@ -157,7 +158,7 @@ class LLMCategorizer:
157
158
  async def categorize(
158
159
  self,
159
160
  merchant_name: str,
160
- user_id: Optional[str] = None,
161
+ user_id: str | None = None,
161
162
  ) -> CategoryPrediction:
162
163
  """
163
164
  Categorize merchant using LLM.
@@ -209,7 +210,7 @@ class LLMCategorizer:
209
210
  async def _call_llm(
210
211
  self,
211
212
  merchant_name: str,
212
- user_id: Optional[str] = None,
213
+ user_id: str | None = None,
213
214
  ) -> CategoryPrediction:
214
215
  """Call LLM API with structured output."""
215
216
  # Build user message
@@ -244,7 +245,7 @@ class LLMCategorizer:
244
245
  f"Must be one of {len(valid_categories)} valid categories."
245
246
  )
246
247
 
247
- return cast(CategoryPrediction, response)
248
+ return cast("CategoryPrediction", response)
248
249
 
249
250
  def _build_system_prompt(self) -> str:
250
251
  """Build system prompt with few-shot examples (reused across all requests)."""
@@ -269,7 +270,7 @@ class LLMCategorizer:
269
270
  def _build_user_message(
270
271
  self,
271
272
  merchant_name: str,
272
- user_id: Optional[str] = None,
273
+ user_id: str | None = None,
273
274
  ) -> str:
274
275
  """Build user message with optional personalization."""
275
276
  if self.enable_personalization and user_id:
@@ -6,12 +6,10 @@ Organized by category for maintainability.
6
6
  """
7
7
 
8
8
  import re
9
- from typing import Optional
10
9
 
11
10
  from .models import CategoryRule
12
11
  from .taxonomy import Category
13
12
 
14
-
15
13
  # ===== HELPER FUNCTIONS (defined first) =====
16
14
 
17
15
 
@@ -306,7 +304,7 @@ COMPILED_REGEX_RULES = [
306
304
  # ===== PUBLIC FUNCTIONS =====
307
305
 
308
306
 
309
- def get_exact_match(merchant: str) -> Optional[Category]:
307
+ def get_exact_match(merchant: str) -> Category | None:
310
308
  """
311
309
  Get category by exact match.
312
310
 
@@ -320,7 +318,7 @@ def get_exact_match(merchant: str) -> Optional[Category]:
320
318
  return EXACT_RULES_NORMALIZED.get(normalized)
321
319
 
322
320
 
323
- def get_regex_match(merchant: str) -> Optional[tuple[Category, int]]:
321
+ def get_regex_match(merchant: str) -> tuple[Category, int] | None:
324
322
  """
325
323
  Get category by regex match.
326
324
 
@@ -12,7 +12,7 @@ Total: 56 leaf categories
12
12
  """
13
13
 
14
14
  from enum import Enum
15
- from typing import Optional
15
+
16
16
  from pydantic import BaseModel
17
17
 
18
18
 
@@ -315,7 +315,7 @@ def get_category_group(category: Category) -> CategoryGroup:
315
315
  return CATEGORY_GROUPS.get(category, CategoryGroup.UNCATEGORIZED)
316
316
 
317
317
 
318
- def get_category_metadata(category: Category) -> Optional[CategoryMetadata]:
318
+ def get_category_metadata(category: Category) -> CategoryMetadata | None:
319
319
  """Get metadata for a category."""
320
320
  return CATEGORY_METADATA.get(category)
321
321
 
@@ -33,15 +33,15 @@ from typing import TYPE_CHECKING, Any
33
33
  if TYPE_CHECKING:
34
34
  from fastapi import FastAPI
35
35
 
36
+ from fin_infra.chat.ease import easy_financial_conversation
36
37
  from fin_infra.chat.planning import (
37
- FinancialPlanningConversation,
38
- ConversationResponse,
38
+ SENSITIVE_PATTERNS,
39
39
  ConversationContext,
40
+ ConversationResponse,
40
41
  Exchange,
42
+ FinancialPlanningConversation,
41
43
  is_sensitive_question,
42
- SENSITIVE_PATTERNS,
43
44
  )
44
- from fin_infra.chat.ease import easy_financial_conversation
45
45
 
46
46
  __all__ = [
47
47
  "FinancialPlanningConversation",
@@ -116,10 +116,10 @@ def add_financial_conversation(
116
116
  - Logs all LLM calls for compliance (via svc-infra logging)
117
117
  """
118
118
  from pydantic import BaseModel, Field
119
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
119
120
 
120
121
  # Import svc-infra user router (requires auth)
121
122
  from svc_infra.api.fastapi.dual.protected import user_router
122
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
123
123
 
124
124
  # Auto-create conversation if not provided
125
125
  if conversation is None:
@@ -54,7 +54,6 @@ from typing import Any
54
54
 
55
55
  from pydantic import BaseModel, Field
56
56
 
57
-
58
57
  # ============================================================================
59
58
  # Pydantic Schemas (Structured Output)
60
59
  # ============================================================================
@@ -12,7 +12,6 @@ Usage:
12
12
  from __future__ import annotations
13
13
 
14
14
  from pathlib import Path
15
- from typing import Optional
16
15
 
17
16
  import click
18
17
  import typer
@@ -54,47 +53,47 @@ def cmd_scaffold(
54
53
  "--overwrite/--no-overwrite",
55
54
  help="Overwrite existing files",
56
55
  ),
57
- models_filename: Optional[str] = typer.Option(
56
+ models_filename: str | None = typer.Option(
58
57
  None,
59
58
  "--models-filename",
60
59
  help="Custom filename for models (default: {domain}.py)",
61
60
  ),
62
- schemas_filename: Optional[str] = typer.Option(
61
+ schemas_filename: str | None = typer.Option(
63
62
  None,
64
63
  "--schemas-filename",
65
64
  help="Custom filename for schemas (default: {domain}_schemas.py)",
66
65
  ),
67
- repository_filename: Optional[str] = typer.Option(
66
+ repository_filename: str | None = typer.Option(
68
67
  None,
69
68
  "--repository-filename",
70
69
  help="Custom filename for repository (default: {domain}_repository.py)",
71
70
  ),
72
71
  ) -> None:
73
72
  """Generate SQLAlchemy models, Pydantic schemas, and repository code from templates.
74
-
73
+
75
74
  The scaffold command generates production-ready persistence layer code that works
76
75
  seamlessly with svc-infra's add_sql_resources() for automatic CRUD APIs.
77
-
76
+
78
77
  Examples:
79
78
  # Basic scaffold (models + schemas + repository)
80
79
  fin-infra scaffold budgets --dest-dir app/models/
81
-
80
+
82
81
  # Financial goals tracking
83
82
  fin-infra scaffold goals --dest-dir app/models/goals/
84
-
83
+
85
84
  # With multi-tenancy and soft deletes
86
85
  fin-infra scaffold budgets --dest-dir app/models/ \
87
86
  --include-tenant --include-soft-delete
88
-
87
+
89
88
  # Without repository (use svc-infra SqlRepository directly)
90
89
  fin-infra scaffold goals --dest-dir app/models/ \\
91
90
  --no-with-repository
92
-
91
+
93
92
  # Custom filenames
94
93
  fin-infra scaffold budgets --dest-dir app/models/ \\
95
94
  --models-filename custom_budget.py \\
96
95
  --schemas-filename custom_schemas.py
97
-
96
+
98
97
  After scaffolding, integrate with svc-infra:
99
98
  1. Run migrations: svc-infra revision -m "add budgets" --autogenerate
100
99
  2. Apply: svc-infra upgrade head
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
4
 
5
- from .base import BankingClient
6
5
  from ..models import Account
6
+ from .base import BankingClient
7
7
 
8
8
 
9
9
  class PlaidClient(BankingClient):
@@ -20,9 +20,9 @@ Example:
20
20
  from __future__ import annotations
21
21
 
22
22
  import logging
23
- from datetime import datetime
24
- from typing import Any, TYPE_CHECKING, cast
25
23
  from collections.abc import Callable
24
+ from datetime import datetime
25
+ from typing import TYPE_CHECKING, Any, cast
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from fastapi import FastAPI, Request, Response
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
33
33
 
34
34
 
35
35
  def log_compliance_event(
36
- app: "FastAPI",
36
+ app: FastAPI,
37
37
  event: str,
38
38
  context: dict[str, Any] | None = None,
39
39
  ) -> None:
@@ -63,7 +63,7 @@ def log_compliance_event(
63
63
 
64
64
 
65
65
  def add_compliance_tracking(
66
- app: "FastAPI",
66
+ app: FastAPI,
67
67
  *,
68
68
  track_banking: bool = True,
69
69
  track_credit: bool = True,
@@ -112,7 +112,7 @@ def add_compliance_tracking(
112
112
  """
113
113
 
114
114
  @app.middleware("http")
115
- async def compliance_tracking_middleware(request: "Request", call_next: Callable) -> "Response":
115
+ async def compliance_tracking_middleware(request: Request, call_next: Callable) -> Response:
116
116
  """Middleware to track compliance events for financial endpoints."""
117
117
  path = request.url.path
118
118
  method = request.method