fin-infra 0.1.62__py3-none-any.whl → 0.1.82__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 (126) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +30 -32
  4. fin_infra/analytics/cash_flow.py +6 -5
  5. fin_infra/analytics/ease.py +19 -20
  6. fin_infra/analytics/portfolio.py +19 -26
  7. fin_infra/analytics/projections.py +1 -3
  8. fin_infra/analytics/rebalancing.py +2 -4
  9. fin_infra/analytics/savings.py +1 -1
  10. fin_infra/analytics/spending.py +15 -11
  11. fin_infra/banking/__init__.py +33 -31
  12. fin_infra/banking/history.py +11 -12
  13. fin_infra/banking/utils.py +116 -110
  14. fin_infra/brokerage/__init__.py +27 -27
  15. fin_infra/budgets/__init__.py +3 -3
  16. fin_infra/budgets/add.py +16 -17
  17. fin_infra/budgets/alerts.py +3 -3
  18. fin_infra/budgets/tracker.py +4 -5
  19. fin_infra/cashflows/__init__.py +8 -10
  20. fin_infra/cashflows/core.py +1 -1
  21. fin_infra/categorization/__init__.py +1 -1
  22. fin_infra/categorization/add.py +17 -19
  23. fin_infra/categorization/ease.py +3 -4
  24. fin_infra/categorization/engine.py +21 -18
  25. fin_infra/categorization/llm_layer.py +10 -10
  26. fin_infra/categorization/models.py +1 -1
  27. fin_infra/categorization/rules.py +2 -4
  28. fin_infra/categorization/taxonomy.py +2 -2
  29. fin_infra/chat/__init__.py +13 -22
  30. fin_infra/chat/planning.py +57 -1
  31. fin_infra/cli/cmds/scaffold_cmds.py +11 -12
  32. fin_infra/clients/__init__.py +23 -1
  33. fin_infra/clients/base.py +1 -1
  34. fin_infra/clients/plaid.py +2 -2
  35. fin_infra/compliance/__init__.py +7 -6
  36. fin_infra/credit/add.py +7 -7
  37. fin_infra/credit/experian/auth.py +3 -2
  38. fin_infra/credit/experian/client.py +2 -2
  39. fin_infra/credit/experian/provider.py +19 -19
  40. fin_infra/crypto/__init__.py +8 -10
  41. fin_infra/crypto/insights.py +5 -6
  42. fin_infra/documents/add.py +11 -13
  43. fin_infra/documents/analysis.py +9 -9
  44. fin_infra/documents/ease.py +18 -17
  45. fin_infra/documents/models.py +7 -7
  46. fin_infra/documents/ocr.py +8 -8
  47. fin_infra/documents/storage.py +23 -14
  48. fin_infra/exceptions.py +1 -2
  49. fin_infra/goals/__init__.py +8 -8
  50. fin_infra/goals/add.py +36 -36
  51. fin_infra/goals/funding.py +4 -6
  52. fin_infra/goals/management.py +6 -7
  53. fin_infra/goals/milestones.py +2 -3
  54. fin_infra/goals/models.py +7 -11
  55. fin_infra/insights/__init__.py +12 -10
  56. fin_infra/insights/aggregator.py +1 -1
  57. fin_infra/investments/__init__.py +14 -9
  58. fin_infra/investments/add.py +53 -73
  59. fin_infra/investments/ease.py +16 -13
  60. fin_infra/investments/models.py +135 -69
  61. fin_infra/investments/providers/base.py +9 -15
  62. fin_infra/investments/providers/plaid.py +70 -55
  63. fin_infra/investments/providers/snaptrade.py +35 -53
  64. fin_infra/markets/__init__.py +16 -11
  65. fin_infra/models/__init__.py +10 -10
  66. fin_infra/models/accounts.py +2 -1
  67. fin_infra/models/brokerage.py +2 -1
  68. fin_infra/models/candle.py +1 -0
  69. fin_infra/models/money.py +1 -0
  70. fin_infra/models/quotes.py +4 -3
  71. fin_infra/models/tax.py +2 -1
  72. fin_infra/models/transactions.py +4 -4
  73. fin_infra/net_worth/__init__.py +7 -0
  74. fin_infra/net_worth/add.py +8 -5
  75. fin_infra/net_worth/aggregator.py +9 -6
  76. fin_infra/net_worth/calculator.py +8 -6
  77. fin_infra/net_worth/ease.py +36 -15
  78. fin_infra/net_worth/insights.py +4 -5
  79. fin_infra/net_worth/models.py +237 -116
  80. fin_infra/normalization/__init__.py +17 -15
  81. fin_infra/normalization/providers/exchangerate.py +5 -5
  82. fin_infra/obs/classifier.py +3 -3
  83. fin_infra/providers/banking/plaid_client.py +23 -22
  84. fin_infra/providers/banking/teller_client.py +14 -7
  85. fin_infra/providers/base.py +131 -14
  86. fin_infra/providers/brokerage/alpaca.py +7 -7
  87. fin_infra/providers/credit/experian.py +5 -0
  88. fin_infra/providers/market/alphavantage.py +6 -11
  89. fin_infra/providers/market/ccxt_crypto.py +25 -4
  90. fin_infra/providers/market/coingecko.py +5 -6
  91. fin_infra/providers/market/yahoo.py +23 -8
  92. fin_infra/providers/tax/__init__.py +1 -1
  93. fin_infra/providers/tax/irs.py +1 -1
  94. fin_infra/providers/tax/mock.py +8 -8
  95. fin_infra/providers/tax/taxbit.py +1 -1
  96. fin_infra/recurring/__init__.py +6 -6
  97. fin_infra/recurring/add.py +24 -12
  98. fin_infra/recurring/detector.py +8 -8
  99. fin_infra/recurring/detectors_llm.py +14 -13
  100. fin_infra/recurring/ease.py +3 -5
  101. fin_infra/recurring/insights.py +20 -19
  102. fin_infra/recurring/models.py +3 -3
  103. fin_infra/recurring/normalizer.py +3 -2
  104. fin_infra/recurring/normalizers.py +11 -10
  105. fin_infra/recurring/summary.py +13 -15
  106. fin_infra/scaffold/__init__.py +1 -1
  107. fin_infra/scaffold/budgets.py +9 -9
  108. fin_infra/scaffold/goals.py +5 -5
  109. fin_infra/security/__init__.py +8 -8
  110. fin_infra/security/encryption.py +6 -6
  111. fin_infra/security/models.py +7 -7
  112. fin_infra/security/pii_filter.py +6 -6
  113. fin_infra/security/pii_patterns.py +1 -1
  114. fin_infra/security/token_store.py +3 -1
  115. fin_infra/settings.py +2 -1
  116. fin_infra/tax/__init__.py +2 -2
  117. fin_infra/tax/add.py +3 -2
  118. fin_infra/tax/tlh.py +5 -5
  119. fin_infra/utils/http.py +5 -3
  120. fin_infra/utils/retry.py +2 -1
  121. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
  122. fin_infra-0.1.82.dist-info/RECORD +180 -0
  123. fin_infra-0.1.62.dist-info/RECORD +0 -180
  124. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
  125. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
  126. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
@@ -15,13 +15,13 @@ Expected performance:
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Optional, List, Tuple
18
+ from typing import Any, cast
19
+
19
20
  from pydantic import BaseModel, Field
20
21
 
21
22
  # ai-infra imports
22
23
  try:
23
24
  from ai_infra.llm import LLM
24
- from ai_infra.llm.providers import Providers
25
25
  except ImportError:
26
26
  raise ImportError("ai-infra not installed. Install with: pip install ai-infra")
27
27
 
@@ -41,7 +41,7 @@ class CategoryPrediction(BaseModel):
41
41
 
42
42
 
43
43
  # Few-shot examples (20 diverse merchants covering all major categories)
44
- FEW_SHOT_EXAMPLES: List[Tuple[str, str, str]] = [
44
+ FEW_SHOT_EXAMPLES: list[tuple[str, str, str]] = [
45
45
  # Food & Dining (5 examples)
46
46
  ("STARBUCKS #1234", "Coffee Shops", "Popular coffee shop chain"),
47
47
  ("MCDONALD'S", "Fast Food", "Fast food restaurant"),
@@ -158,7 +158,7 @@ class LLMCategorizer:
158
158
  async def categorize(
159
159
  self,
160
160
  merchant_name: str,
161
- user_id: Optional[str] = None,
161
+ user_id: str | None = None,
162
162
  ) -> CategoryPrediction:
163
163
  """
164
164
  Categorize merchant using LLM.
@@ -210,14 +210,14 @@ class LLMCategorizer:
210
210
  async def _call_llm(
211
211
  self,
212
212
  merchant_name: str,
213
- user_id: Optional[str] = None,
213
+ user_id: str | None = None,
214
214
  ) -> CategoryPrediction:
215
215
  """Call LLM API with structured output."""
216
216
  # Build user message
217
217
  user_message = self._build_user_message(merchant_name, user_id)
218
218
 
219
219
  # Call LLM with retry logic
220
- extra = {
220
+ extra: dict[str, Any] = {
221
221
  "retry": {
222
222
  "max_tries": 3,
223
223
  "base": 0.5,
@@ -245,7 +245,7 @@ class LLMCategorizer:
245
245
  f"Must be one of {len(valid_categories)} valid categories."
246
246
  )
247
247
 
248
- return response
248
+ return cast("CategoryPrediction", response)
249
249
 
250
250
  def _build_system_prompt(self) -> str:
251
251
  """Build system prompt with few-shot examples (reused across all requests)."""
@@ -270,7 +270,7 @@ class LLMCategorizer:
270
270
  def _build_user_message(
271
271
  self,
272
272
  merchant_name: str,
273
- user_id: Optional[str] = None,
273
+ user_id: str | None = None,
274
274
  ) -> str:
275
275
  """Build user message with optional personalization."""
276
276
  if self.enable_personalization and user_id:
@@ -285,8 +285,8 @@ class LLMCategorizer:
285
285
  Merchant: "{merchant_name}"
286
286
 
287
287
  User context:
288
- - Frequently shops at: {context['top_merchants']}
289
- - Top spending categories: {context['top_categories']}
288
+ - Frequently shops at: {context["top_merchants"]}
289
+ - Top spending categories: {context["top_categories"]}
290
290
 
291
291
  Return JSON with category, confidence, and reasoning."""
292
292
  else:
@@ -44,7 +44,7 @@ class CategoryPrediction(BaseModel):
44
44
  "category": "Coffee Shops",
45
45
  "confidence": 0.98,
46
46
  "method": "exact",
47
- "alternatives": [("Restaurants", 0.15), ("Fast Food", 0.10)],
47
+ "alternatives": [["Restaurants", 0.15], ["Fast Food", 0.10]],
48
48
  }
49
49
  }
50
50
  )
@@ -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
 
@@ -28,15 +28,20 @@ Example:
28
28
  )
29
29
  """
30
30
 
31
+ from typing import TYPE_CHECKING, Any
32
+
33
+ if TYPE_CHECKING:
34
+ from fastapi import FastAPI
35
+
36
+ from fin_infra.chat.ease import easy_financial_conversation
31
37
  from fin_infra.chat.planning import (
32
- FinancialPlanningConversation,
33
- ConversationResponse,
38
+ SENSITIVE_PATTERNS,
34
39
  ConversationContext,
40
+ ConversationResponse,
35
41
  Exchange,
42
+ FinancialPlanningConversation,
36
43
  is_sensitive_question,
37
- SENSITIVE_PATTERNS,
38
44
  )
39
- from fin_infra.chat.ease import easy_financial_conversation
40
45
 
41
46
  __all__ = [
42
47
  "FinancialPlanningConversation",
@@ -51,7 +56,7 @@ __all__ = [
51
56
 
52
57
 
53
58
  def add_financial_conversation(
54
- app: "FastAPI", # type: ignore
59
+ app: "FastAPI",
55
60
  *,
56
61
  prefix: str = "/chat",
57
62
  conversation: FinancialPlanningConversation | None = None,
@@ -110,16 +115,11 @@ def add_financial_conversation(
110
115
  - Includes financial advice disclaimer in all responses
111
116
  - Logs all LLM calls for compliance (via svc-infra logging)
112
117
  """
113
- from typing import TYPE_CHECKING, Any
114
-
115
- if TYPE_CHECKING:
116
- from fastapi import FastAPI
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:
@@ -149,15 +149,6 @@ def add_financial_conversation(
149
149
  # TODO: Get user_id from svc-infra auth context
150
150
  user_id = "demo_user" # Placeholder
151
151
 
152
- # Check for sensitive content
153
- if is_sensitive_question(request.question):
154
- return ConversationResponse(
155
- answer="I cannot process requests containing sensitive information like SSNs, passwords, or account numbers. Please rephrase your question without this information.",
156
- follow_up_questions=[],
157
- conversation_id=f"{user_id}_denied",
158
- disclaimer="This is an automated safety response.",
159
- )
160
-
161
152
  # Ask conversation
162
153
  response = await conversation.ask(
163
154
  user_id=user_id,
@@ -173,7 +164,7 @@ def add_financial_conversation(
173
164
  # TODO: Get user_id from svc-infra auth context
174
165
  user_id = "demo_user"
175
166
  context = await conversation._get_context(user_id)
176
- return context.exchanges if context else []
167
+ return context.previous_exchanges if context else []
177
168
 
178
169
  @router.delete("/history")
179
170
  async def clear_history():
@@ -198,6 +189,6 @@ def add_financial_conversation(
198
189
  # Store on app.state for programmatic access
199
190
  app.state.financial_conversation = conversation
200
191
 
201
- print(f"Financial chat enabled (AI-powered Q&A with {provider})")
192
+ print(f"Financial chat enabled (AI-powered Q&A with {provider})")
202
193
 
203
194
  return conversation
@@ -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
  # ============================================================================
@@ -338,8 +337,65 @@ class FinancialPlanningConversation:
338
337
  # Save updated context (24h TTL)
339
338
  await self._save_context(context)
340
339
 
340
+ # Track latest session id for convenience endpoints (history/clear).
341
+ # Best-effort: failures here must not break the chat response.
342
+ try:
343
+ await self.cache.set(
344
+ self._latest_session_key(user_id),
345
+ context.session_id,
346
+ ttl=86400,
347
+ )
348
+ except Exception:
349
+ pass
350
+
341
351
  return response
342
352
 
353
+ # ---------------------------------------------------------------------
354
+ # Backward-compatible context helpers
355
+ # ---------------------------------------------------------------------
356
+
357
+ def _latest_session_key(self, user_id: str) -> str:
358
+ return f"fin_infra:conversation_latest_session:{user_id}"
359
+
360
+ async def _get_latest_session_id(self, user_id: str) -> str | None:
361
+ try:
362
+ value = await self.cache.get(self._latest_session_key(user_id))
363
+ except Exception:
364
+ return None
365
+
366
+ if value is None:
367
+ return None
368
+ if isinstance(value, bytes):
369
+ try:
370
+ return value.decode("utf-8")
371
+ except Exception:
372
+ return None
373
+ if isinstance(value, str):
374
+ return value
375
+ return str(value)
376
+
377
+ async def _get_context(
378
+ self, user_id: str, session_id: str | None = None
379
+ ) -> ConversationContext | None:
380
+ if session_id is None:
381
+ session_id = await self._get_latest_session_id(user_id)
382
+ if session_id is None:
383
+ return None
384
+
385
+ return await self._load_context(user_id=user_id, session_id=session_id)
386
+
387
+ async def _clear_context(self, user_id: str, session_id: str | None = None) -> None:
388
+ if session_id is None:
389
+ session_id = await self._get_latest_session_id(user_id)
390
+
391
+ if session_id is not None:
392
+ await self.clear_session(user_id=user_id, session_id=session_id)
393
+
394
+ try:
395
+ await self.cache.delete(self._latest_session_key(user_id))
396
+ except Exception:
397
+ pass
398
+
343
399
  async def _load_context(
344
400
  self,
345
401
  user_id: str,
@@ -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
@@ -108,7 +107,7 @@ def cmd_scaffold(
108
107
  err=True,
109
108
  )
110
109
  raise typer.Exit(1)
111
-
110
+
112
111
  # Import scaffold function based on domain
113
112
  if domain == "budgets":
114
113
  from fin_infra.scaffold.budgets import scaffold_budgets_core
@@ -1,3 +1,25 @@
1
- from .base import BankingClient, MarketDataClient, CreditClient
1
+ """DEPRECATED: Use fin_infra.providers instead.
2
+
3
+ This module is deprecated and will be removed in a future version.
4
+ All ABCs have been consolidated into fin_infra.providers.base.
5
+
6
+ Migration:
7
+ # Old (deprecated)
8
+ from fin_infra.clients import BankingClient, MarketDataClient
9
+
10
+ # New
11
+ from fin_infra.providers.base import BankingProvider, MarketDataProvider
12
+ """
13
+
14
+ import warnings
15
+
16
+ from .base import BankingClient, CreditClient, MarketDataClient
17
+
18
+ warnings.warn(
19
+ "fin_infra.clients is deprecated. Use fin_infra.providers instead. "
20
+ "This module will be removed in a future version.",
21
+ DeprecationWarning,
22
+ stacklevel=2,
23
+ )
2
24
 
3
25
  __all__ = ["BankingClient", "MarketDataClient", "CreditClient"]
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,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Sequence
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,8 +20,9 @@ Example:
20
20
  from __future__ import annotations
21
21
 
22
22
  import logging
23
+ from collections.abc import Callable
23
24
  from datetime import datetime
24
- from typing import Any, Callable, TYPE_CHECKING
25
+ from typing import TYPE_CHECKING, Any, cast
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from fastapi import FastAPI, Request, Response
@@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
32
33
 
33
34
 
34
35
  def log_compliance_event(
35
- app: "FastAPI",
36
+ app: FastAPI,
36
37
  event: str,
37
38
  context: dict[str, Any] | None = None,
38
39
  ) -> None:
@@ -62,7 +63,7 @@ def log_compliance_event(
62
63
 
63
64
 
64
65
  def add_compliance_tracking(
65
- app: "FastAPI",
66
+ app: FastAPI,
66
67
  *,
67
68
  track_banking: bool = True,
68
69
  track_credit: bool = True,
@@ -111,14 +112,14 @@ def add_compliance_tracking(
111
112
  """
112
113
 
113
114
  @app.middleware("http")
114
- async def compliance_tracking_middleware(request: "Request", call_next: Callable) -> "Response":
115
+ async def compliance_tracking_middleware(request: Request, call_next: Callable) -> Response:
115
116
  """Middleware to track compliance events for financial endpoints."""
116
117
  path = request.url.path
117
118
  method = request.method
118
119
 
119
120
  # Track only GET requests (data access)
120
121
  if method != "GET":
121
- return await call_next(request)
122
+ return cast("Response", await call_next(request))
122
123
 
123
124
  # Determine if path is a compliance-tracked endpoint
124
125
  event = None
@@ -148,7 +149,7 @@ def add_compliance_tracking(
148
149
  if on_event:
149
150
  on_event(event, context)
150
151
 
151
- return response
152
+ return cast("Response", response)
152
153
 
153
154
  logger.info(
154
155
  "Compliance tracking enabled",
fin_infra/credit/add.py CHANGED
@@ -23,16 +23,16 @@ Example:
23
23
  """
24
24
 
25
25
  import logging
26
+ from typing import cast
26
27
 
27
- from fastapi import FastAPI, Depends, HTTPException, status
28
-
29
- from svc_infra.api.fastapi.dual.protected import user_router, RequireUser
28
+ from fastapi import Depends, FastAPI, HTTPException, status
30
29
  from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
30
+ from svc_infra.api.fastapi.dual.protected import RequireUser, user_router
31
31
  from svc_infra.cache import resource
32
32
  from svc_infra.webhooks import add_webhooks
33
33
 
34
+ from fin_infra.models.credit import CreditReport, CreditScore
34
35
  from fin_infra.providers.base import CreditProvider
35
- from fin_infra.models.credit import CreditScore, CreditReport
36
36
 
37
37
  logger = logging.getLogger(__name__)
38
38
 
@@ -155,8 +155,8 @@ def add_credit(
155
155
  if enable_webhooks and hasattr(app.state, "webhooks_outbox"):
156
156
  try:
157
157
  # Get webhook service from app state
158
- from svc_infra.webhooks.service import WebhookService
159
158
  from svc_infra.db.outbox import OutboxStore
159
+ from svc_infra.webhooks.service import WebhookService
160
160
 
161
161
  outbox: OutboxStore = app.state.webhooks_outbox
162
162
  subs = app.state.webhooks_subscriptions
@@ -175,7 +175,7 @@ def add_credit(
175
175
  # Don't fail request if webhook publishing fails
176
176
  logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
177
177
 
178
- return score
178
+ return cast("CreditScore", score)
179
179
 
180
180
  @router.post("/report", response_model=CreditReport)
181
181
  @credit_resource.cache_read(ttl=cache_ttl, suffix="report")
@@ -219,7 +219,7 @@ def add_credit(
219
219
  detail="Credit bureau service unavailable",
220
220
  )
221
221
 
222
- return report
222
+ return cast("CreditReport", report)
223
223
 
224
224
  # Mount router with dual routes (with/without trailing slash)
225
225
  app.include_router(router, include_in_schema=True)
@@ -24,6 +24,7 @@ Example:
24
24
  """
25
25
 
26
26
  import base64
27
+ from typing import cast
27
28
 
28
29
  import httpx
29
30
  from svc_infra.cache import cache_read
@@ -85,7 +86,7 @@ class ExperianAuthManager:
85
86
  >>> headers = {"Authorization": f"Bearer {token}"}
86
87
  """
87
88
  # Call the cached implementation with client_id for cache key
88
- return await self._get_token_cached(client_id=self.client_id)
89
+ return cast("str", await self._get_token_cached(client_id=self.client_id))
89
90
 
90
91
  @cache_read(
91
92
  key="oauth_token:experian:{client_id}", # Use client_id for uniqueness
@@ -140,7 +141,7 @@ class ExperianAuthManager:
140
141
 
141
142
  # Parse and return token
142
143
  data = response.json()
143
- return data["access_token"]
144
+ return cast("str", data["access_token"])
144
145
 
145
146
  async def invalidate(self) -> None:
146
147
  """Invalidate cached token for THIS client (force refresh on next get_token call).
@@ -14,7 +14,7 @@ Example:
14
14
  >>> data = await client.get_credit_score("user123")
15
15
  """
16
16
 
17
- from typing import Any
17
+ from typing import Any, cast
18
18
 
19
19
  import httpx
20
20
  from tenacity import (
@@ -155,7 +155,7 @@ class ExperianClient:
155
155
  **kwargs,
156
156
  )
157
157
  response.raise_for_status()
158
- return response.json()
158
+ return cast("dict[str, Any]", response.json())
159
159
 
160
160
  except httpx.HTTPStatusError as e:
161
161
  # Parse error response