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
@@ -23,7 +23,7 @@ from .taxonomy import Category
23
23
  try:
24
24
  from .llm_layer import LLMCategorizer
25
25
  except ImportError:
26
- LLMCategorizer = None
26
+ LLMCategorizer = None # type: ignore[assignment,misc]
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
@@ -95,7 +95,7 @@ class CategorizationEngine:
95
95
  Returns:
96
96
  CategoryPrediction with category, confidence, and method
97
97
  """
98
- start_time = time.perf_counter()
98
+ time.perf_counter()
99
99
 
100
100
  # Normalize merchant name
101
101
  normalized = self._normalize(merchant_name)
@@ -333,7 +333,7 @@ def get_engine() -> CategorizationEngine:
333
333
  return _default_engine
334
334
 
335
335
 
336
- def categorize(
336
+ async def categorize(
337
337
  merchant_name: str,
338
338
  user_id: Optional[str] = None,
339
339
  include_alternatives: bool = False,
@@ -350,4 +350,4 @@ def categorize(
350
350
  CategoryPrediction with category, confidence, and method
351
351
  """
352
352
  engine = get_engine()
353
- return engine.categorize(merchant_name, user_id, include_alternatives)
353
+ return await engine.categorize(merchant_name, user_id, include_alternatives)
@@ -15,13 +15,12 @@ Expected performance:
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Optional, List, Tuple
18
+ from typing import Any, List, Optional, Tuple, cast
19
19
  from pydantic import BaseModel, Field
20
20
 
21
21
  # ai-infra imports
22
22
  try:
23
23
  from ai_infra.llm import LLM
24
- from ai_infra.llm.providers import Providers
25
24
  except ImportError:
26
25
  raise ImportError("ai-infra not installed. Install with: pip install ai-infra")
27
26
 
@@ -217,7 +216,7 @@ class LLMCategorizer:
217
216
  user_message = self._build_user_message(merchant_name, user_id)
218
217
 
219
218
  # Call LLM with retry logic
220
- extra = {
219
+ extra: dict[str, Any] = {
221
220
  "retry": {
222
221
  "max_tries": 3,
223
222
  "base": 0.5,
@@ -245,7 +244,7 @@ class LLMCategorizer:
245
244
  f"Must be one of {len(valid_categories)} valid categories."
246
245
  )
247
246
 
248
- return response
247
+ return cast(CategoryPrediction, response)
249
248
 
250
249
  def _build_system_prompt(self) -> str:
251
250
  """Build system prompt with few-shot examples (reused across all requests)."""
@@ -285,8 +284,8 @@ class LLMCategorizer:
285
284
  Merchant: "{merchant_name}"
286
285
 
287
286
  User context:
288
- - Frequently shops at: {context['top_merchants']}
289
- - Top spending categories: {context['top_categories']}
287
+ - Frequently shops at: {context["top_merchants"]}
288
+ - Top spending categories: {context["top_categories"]}
290
289
 
291
290
  Return JSON with category, confidence, and reasoning."""
292
291
  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
  )
@@ -28,6 +28,11 @@ 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
+
31
36
  from fin_infra.chat.planning import (
32
37
  FinancialPlanningConversation,
33
38
  ConversationResponse,
@@ -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,11 +115,6 @@ 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
119
 
120
120
  # Import svc-infra user router (requires auth)
@@ -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():
@@ -338,8 +338,65 @@ class FinancialPlanningConversation:
338
338
  # Save updated context (24h TTL)
339
339
  await self._save_context(context)
340
340
 
341
+ # Track latest session id for convenience endpoints (history/clear).
342
+ # Best-effort: failures here must not break the chat response.
343
+ try:
344
+ await self.cache.set(
345
+ self._latest_session_key(user_id),
346
+ context.session_id,
347
+ ttl=86400,
348
+ )
349
+ except Exception:
350
+ pass
351
+
341
352
  return response
342
353
 
354
+ # ---------------------------------------------------------------------
355
+ # Backward-compatible context helpers
356
+ # ---------------------------------------------------------------------
357
+
358
+ def _latest_session_key(self, user_id: str) -> str:
359
+ return f"fin_infra:conversation_latest_session:{user_id}"
360
+
361
+ async def _get_latest_session_id(self, user_id: str) -> str | None:
362
+ try:
363
+ value = await self.cache.get(self._latest_session_key(user_id))
364
+ except Exception:
365
+ return None
366
+
367
+ if value is None:
368
+ return None
369
+ if isinstance(value, bytes):
370
+ try:
371
+ return value.decode("utf-8")
372
+ except Exception:
373
+ return None
374
+ if isinstance(value, str):
375
+ return value
376
+ return str(value)
377
+
378
+ async def _get_context(
379
+ self, user_id: str, session_id: str | None = None
380
+ ) -> ConversationContext | None:
381
+ if session_id is None:
382
+ session_id = await self._get_latest_session_id(user_id)
383
+ if session_id is None:
384
+ return None
385
+
386
+ return await self._load_context(user_id=user_id, session_id=session_id)
387
+
388
+ async def _clear_context(self, user_id: str, session_id: str | None = None) -> None:
389
+ if session_id is None:
390
+ session_id = await self._get_latest_session_id(user_id)
391
+
392
+ if session_id is not None:
393
+ await self.clear_session(user_id=user_id, session_id=session_id)
394
+
395
+ try:
396
+ await self.cache.delete(self._latest_session_key(user_id))
397
+ except Exception:
398
+ pass
399
+
343
400
  async def _load_context(
344
401
  self,
345
402
  user_id: str,
@@ -108,7 +108,7 @@ def cmd_scaffold(
108
108
  err=True,
109
109
  )
110
110
  raise typer.Exit(1)
111
-
111
+
112
112
  # Import scaffold function based on domain
113
113
  if domain == "budgets":
114
114
  from fin_infra.scaffold.budgets import scaffold_budgets_core
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import logging
23
23
  from datetime import datetime
24
- from typing import Any, Callable, TYPE_CHECKING
24
+ from typing import Any, Callable, TYPE_CHECKING, cast
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from fastapi import FastAPI, Request, Response
@@ -118,7 +118,7 @@ def add_compliance_tracking(
118
118
 
119
119
  # Track only GET requests (data access)
120
120
  if method != "GET":
121
- return await call_next(request)
121
+ return cast("Response", await call_next(request))
122
122
 
123
123
  # Determine if path is a compliance-tracked endpoint
124
124
  event = None
@@ -148,7 +148,7 @@ def add_compliance_tracking(
148
148
  if on_event:
149
149
  on_event(event, context)
150
150
 
151
- return response
151
+ return cast("Response", response)
152
152
 
153
153
  logger.info(
154
154
  "Compliance tracking enabled",
fin_infra/credit/add.py CHANGED
@@ -23,6 +23,7 @@ Example:
23
23
  """
24
24
 
25
25
  import logging
26
+ from typing import cast
26
27
 
27
28
  from fastapi import FastAPI, Depends, HTTPException, status
28
29
 
@@ -175,7 +176,7 @@ def add_credit(
175
176
  # Don't fail request if webhook publishing fails
176
177
  logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
177
178
 
178
- return score
179
+ return cast(CreditScore, score)
179
180
 
180
181
  @router.post("/report", response_model=CreditReport)
181
182
  @credit_resource.cache_read(ttl=cache_ttl, suffix="report")
@@ -219,7 +220,7 @@ def add_credit(
219
220
  detail="Credit bureau service unavailable",
220
221
  )
221
222
 
222
- return report
223
+ return cast(CreditReport, report)
223
224
 
224
225
  # Mount router with dual routes (with/without trailing slash)
225
226
  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
@@ -31,7 +31,7 @@ Example:
31
31
 
32
32
  import logging
33
33
  from datetime import datetime, timezone
34
- from typing import Literal
34
+ from typing import Literal, cast
35
35
 
36
36
  from fin_infra.credit.experian.auth import ExperianAuthManager
37
37
  from fin_infra.credit.experian.client import ExperianClient
@@ -174,7 +174,7 @@ class ExperianProvider(CreditProvider):
174
174
  permissible_purpose = kwargs.get("permissible_purpose", "account_review")
175
175
  requester_ip = kwargs.get("requester_ip", "unknown")
176
176
  requester_user_id = kwargs.get("requester_user_id", "unknown")
177
-
177
+
178
178
  # FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
179
179
  # This log must be retained for at least 2 years per FCRA requirements
180
180
  timestamp = datetime.now(timezone.utc).isoformat()
@@ -190,7 +190,7 @@ class ExperianProvider(CreditProvider):
190
190
  "environment": self.environment,
191
191
  "timestamp": timestamp,
192
192
  "result": "pending",
193
- }
193
+ },
194
194
  )
195
195
 
196
196
  try:
@@ -202,7 +202,7 @@ class ExperianProvider(CreditProvider):
202
202
 
203
203
  # Parse response to CreditScore model
204
204
  result = parse_credit_score(data, user_id=user_id)
205
-
205
+
206
206
  # Log successful pull
207
207
  fcra_audit_logger.info(
208
208
  "FCRA_CREDIT_PULL_SUCCESS",
@@ -213,11 +213,11 @@ class ExperianProvider(CreditProvider):
213
213
  "timestamp": timestamp,
214
214
  "result": "success",
215
215
  "score_returned": result.score is not None,
216
- }
216
+ },
217
217
  )
218
-
218
+
219
219
  return result
220
-
220
+
221
221
  except Exception as e:
222
222
  # Log failed pull - still required for FCRA audit trail
223
223
  fcra_audit_logger.warning(
@@ -229,7 +229,7 @@ class ExperianProvider(CreditProvider):
229
229
  "timestamp": timestamp,
230
230
  "result": "error",
231
231
  "error_type": type(e).__name__,
232
- }
232
+ },
233
233
  )
234
234
  raise
235
235
 
@@ -262,7 +262,7 @@ class ExperianProvider(CreditProvider):
262
262
  permissible_purpose = kwargs.get("permissible_purpose", "account_review")
263
263
  requester_ip = kwargs.get("requester_ip", "unknown")
264
264
  requester_user_id = kwargs.get("requester_user_id", "unknown")
265
-
265
+
266
266
  # FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
267
267
  # Full credit report pulls have stricter requirements than score-only pulls
268
268
  # This log must be retained for at least 2 years per FCRA requirements
@@ -280,7 +280,7 @@ class ExperianProvider(CreditProvider):
280
280
  "timestamp": timestamp,
281
281
  "result": "pending",
282
282
  "report_type": "full",
283
- }
283
+ },
284
284
  )
285
285
 
286
286
  try:
@@ -292,7 +292,7 @@ class ExperianProvider(CreditProvider):
292
292
 
293
293
  # Parse response to CreditReport model
294
294
  result = parse_credit_report(data, user_id=user_id)
295
-
295
+
296
296
  # Log successful pull
297
297
  fcra_audit_logger.info(
298
298
  "FCRA_CREDIT_PULL_SUCCESS",
@@ -304,11 +304,11 @@ class ExperianProvider(CreditProvider):
304
304
  "result": "success",
305
305
  "accounts_returned": len(result.accounts) if result.accounts else 0,
306
306
  "inquiries_returned": len(result.inquiries) if result.inquiries else 0,
307
- }
307
+ },
308
308
  )
309
-
309
+
310
310
  return result
311
-
311
+
312
312
  except Exception as e:
313
313
  # Log failed pull - still required for FCRA audit trail
314
314
  fcra_audit_logger.warning(
@@ -320,7 +320,7 @@ class ExperianProvider(CreditProvider):
320
320
  "timestamp": timestamp,
321
321
  "result": "error",
322
322
  "error_type": type(e).__name__,
323
- }
323
+ },
324
324
  )
325
325
  raise
326
326
 
@@ -360,4 +360,4 @@ class ExperianProvider(CreditProvider):
360
360
  signature_key=signature_key,
361
361
  )
362
362
 
363
- return data.get("subscriptionId", "unknown")
363
+ return cast(str, data.get("subscriptionId", "unknown"))
@@ -70,7 +70,7 @@ def easy_crypto(
70
70
  return CoinGeckoCryptoData()
71
71
 
72
72
  else:
73
- raise ValueError(f"Unknown crypto data provider: {provider_name}. " f"Supported: coingecko")
73
+ raise ValueError(f"Unknown crypto data provider: {provider_name}. Supported: coingecko")
74
74
 
75
75
 
76
76
  def add_crypto_data(
@@ -258,10 +258,8 @@ Provide your insight:"""
258
258
 
259
259
  try:
260
260
  # Use natural language conversation (no output_schema)
261
- # Note: In tests, achat is mocked with messages= parameter
262
- # In production, this should use user_msg, provider, model_name parameters
263
261
  response = await llm.achat(
264
- messages=[{"role": "user", "content": prompt}],
262
+ user_msg=prompt,
265
263
  )
266
264
 
267
265
  # Parse response text
@@ -93,6 +93,7 @@ def add_documents(
93
93
  # Import svc-infra base function to mount base endpoints (with fallback)
94
94
  try:
95
95
  from svc_infra.documents import add_documents as add_base_documents
96
+
96
97
  HAS_SVC_INFRA_DOCUMENTS = True
97
98
  except ImportError:
98
99
  # Fallback for older svc-infra versions - skip base endpoints
@@ -104,16 +105,17 @@ def add_documents(
104
105
 
105
106
  # Step 1: Mount base endpoints (upload, list, get, delete) via svc-infra
106
107
  # This returns the base DocumentManager, but we'll create our own FinancialDocumentManager
107
- if HAS_SVC_INFRA_DOCUMENTS and add_base_documents:
108
+ if HAS_SVC_INFRA_DOCUMENTS and add_base_documents is not None:
108
109
  add_base_documents(app, storage_backend=storage, prefix=prefix, tags=tags)
109
110
  else:
110
111
  # Legacy mode: mount basic endpoints inline (for svc-infra < 0.1.668)
111
112
  import warnings
113
+
112
114
  warnings.warn(
113
115
  "svc_infra.documents not found. Using legacy document endpoints. "
114
116
  "Please upgrade svc-infra to >=0.1.668 for full functionality.",
115
117
  DeprecationWarning,
116
- stacklevel=2
118
+ stacklevel=2,
117
119
  )
118
120
 
119
121
  # Step 2: Create financial document manager with OCR/AI capabilities
@@ -210,9 +212,7 @@ def add_documents(
210
212
  ```
211
213
  """
212
214
  try:
213
- return await manager.analyze(
214
- document_id=document_id, force_refresh=force_refresh
215
- )
215
+ return await manager.analyze(document_id=document_id, force_refresh=force_refresh)
216
216
  except ValueError as e:
217
217
  raise HTTPException(status_code=404, detail=str(e))
218
218
 
@@ -44,11 +44,12 @@ except ImportError:
44
44
  # Fallback for older svc-infra versions without documents module
45
45
  # This provides backward compatibility until svc-infra 0.1.668+ is published
46
46
  import warnings
47
+
47
48
  warnings.warn(
48
49
  "svc_infra.documents not found. Using legacy implementation. "
49
50
  "Please upgrade svc-infra to >=0.1.668 for layered architecture support.",
50
51
  DeprecationWarning,
51
- stacklevel=2
52
+ stacklevel=2,
52
53
  )
53
54
  BaseDocumentManager = object # type: ignore
54
55
 
@@ -65,10 +66,10 @@ class FinancialDocumentManager(BaseDocumentManager):
65
66
  Inherits from svc-infra DocumentManager:
66
67
  - upload(), download(), delete(), get(), list() for base document CRUD
67
68
  - storage backend integration
68
-
69
+
69
70
  Adds financial-specific methods:
70
71
  - upload_financial(): Upload with DocumentType, tax_year, form_type
71
- - extract_text(): OCR for tax forms
72
+ - extract_text(): OCR for tax forms
72
73
  - analyze(): AI-powered financial insights
73
74
 
74
75
  Attributes:
@@ -52,17 +52,17 @@ class DocumentType(str, Enum):
52
52
  class FinancialDocument(BaseDocument):
53
53
  """
54
54
  Financial document extending base Document with financial-specific fields.
55
-
55
+
56
56
  Inherits from svc-infra Document:
57
57
  - id, user_id, filename, file_size, upload_date
58
58
  - storage_path, content_type, checksum
59
59
  - metadata (Dict[str, Any])
60
-
60
+
61
61
  Adds financial-specific fields:
62
62
  - type: DocumentType enum
63
63
  - tax_year: Optional year for tax documents
64
64
  - form_type: Optional form identifier (W-2, 1099, etc.)
65
-
65
+
66
66
  Examples:
67
67
  >>> # Tax document with W-2 form
68
68
  >>> doc = FinancialDocument(
@@ -46,7 +46,7 @@ async def extract_text(
46
46
  Extract text from a document using OCR (uses svc-infra storage).
47
47
 
48
48
  Args:
49
- storage: Storage backend instance
49
+ storage: Storage backend instance
50
50
  document_id: Document identifier
51
51
  provider: OCR provider ("tesseract" or "textract")
52
52
  force_refresh: Force re-extraction even if cached result exists
@@ -46,6 +46,7 @@ try:
46
46
  list_documents as base_list_documents,
47
47
  upload_document as base_upload_document,
48
48
  )
49
+
49
50
  HAS_SVC_INFRA_DOCUMENTS = True
50
51
  except ImportError:
51
52
  # Fallback for older svc-infra versions - use legacy implementation
@@ -153,7 +154,7 @@ def get_document(document_id: str) -> Optional["FinancialDocument"]:
153
154
  >>> doc = get_document("doc_abc123")
154
155
  >>> if doc:
155
156
  ... print(doc.filename, doc.type, doc.tax_year)
156
-
157
+
157
158
  Notes:
158
159
  - Delegates to svc-infra.documents.get_document
159
160
  - Converts base Document to FinancialDocument
fin_infra/exceptions.py CHANGED
@@ -6,7 +6,7 @@ This module provides a consistent exception hierarchy across all fin-infra compo
6
6
  - Validation errors (data validation, compliance)
7
7
  - Calculation errors (financial calculations)
8
8
 
9
- All exceptions inherit from FinInfraError, allowing users to catch all library
9
+ All exceptions inherit from FinInfraError, allowing users to catch all library
10
10
  errors with a single except clause.
11
11
 
12
12
  Example:
fin_infra/goals/add.py CHANGED
@@ -29,7 +29,7 @@ add_goals(app)
29
29
 
30
30
  import logging
31
31
  from datetime import datetime
32
- from typing import List, Optional
32
+ from typing import Any, List, Optional, cast
33
33
 
34
34
  from fastapi import FastAPI, HTTPException, status, Query, Body
35
35
  from pydantic import BaseModel, Field
@@ -469,7 +469,7 @@ def add_goals(
469
469
  # Get all milestones from the goal (check_milestones only returns newly reached ones)
470
470
  goal = get_goal(goal_id)
471
471
  milestones = goal.get("milestones", [])
472
- return milestones
472
+ return cast(list[dict[Any, Any]], milestones)
473
473
  except KeyError:
474
474
  raise HTTPException(
475
475
  status_code=status.HTTP_404_NOT_FOUND, detail=f"Goal {goal_id} not found"
@@ -41,7 +41,7 @@ Example:
41
41
  """
42
42
 
43
43
  from datetime import datetime
44
- from typing import Any
44
+ from typing import Any, cast
45
45
 
46
46
  from pydantic import BaseModel, Field
47
47
 
@@ -597,10 +597,10 @@ Goal type: {goal_type}
597
597
  Goal data: {goal}
598
598
 
599
599
  CALCULATED VALUES (use these exactly, don't recalculate):
600
- - Feasibility: {calc['feasibility']}
601
- - Required monthly: ${calc['required_monthly']:,.0f}
600
+ - Feasibility: {calc["feasibility"]}
601
+ - Required monthly: ${calc["required_monthly"]:,.0f}
602
602
  - Projected completion: {projected_date}
603
- - Current progress: {calc['current_progress']:.1%}
603
+ - Current progress: {calc["current_progress"]:.1%}
604
604
 
605
605
  Provide context and advice around these calculations. Suggest 2-3 alternative paths and 3-5 specific recommendations."""
606
606
 
@@ -839,7 +839,7 @@ def get_goal(goal_id: str) -> dict[str, Any]:
839
839
  if goal_id not in _GOALS_STORE:
840
840
  raise KeyError(f"Goal not found: {goal_id}")
841
841
 
842
- return _GOALS_STORE[goal_id]
842
+ return cast(dict[str, Any], _GOALS_STORE[goal_id])
843
843
 
844
844
 
845
845
  def update_goal(
@@ -885,7 +885,7 @@ def update_goal(
885
885
 
886
886
  Goal(**goal) # Will raise ValidationError if invalid
887
887
 
888
- return goal
888
+ return cast(dict[str, Any], goal)
889
889
 
890
890
 
891
891
  def delete_goal(goal_id: str) -> None:
@@ -26,7 +26,7 @@ Example:
26
26
  """
27
27
 
28
28
  from datetime import datetime
29
- from typing import Any
29
+ from typing import Any, cast
30
30
 
31
31
  from fin_infra.goals.management import get_goal, update_goal
32
32
  from fin_infra.goals.models import Milestone
@@ -229,7 +229,7 @@ def get_next_milestone(goal_id: str) -> dict[str, Any] | None:
229
229
  # Find first unreached milestone (sorted by amount)
230
230
  for milestone in milestones:
231
231
  if not milestone.get("reached", False):
232
- return milestone
232
+ return cast(dict[str, Any], milestone)
233
233
 
234
234
  return None
235
235