fin-infra 0.1.82__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. fin_infra/analytics/__init__.py +2 -2
  2. fin_infra/analytics/add.py +3 -3
  3. fin_infra/analytics/cash_flow.py +3 -3
  4. fin_infra/analytics/models.py +5 -5
  5. fin_infra/analytics/portfolio.py +12 -12
  6. fin_infra/analytics/spending.py +4 -5
  7. fin_infra/banking/history.py +4 -4
  8. fin_infra/brokerage/__init__.py +7 -7
  9. fin_infra/budgets/alerts.py +2 -2
  10. fin_infra/budgets/ease.py +1 -2
  11. fin_infra/budgets/models.py +1 -2
  12. fin_infra/budgets/templates.py +4 -4
  13. fin_infra/budgets/tracker.py +3 -3
  14. fin_infra/categorization/__init__.py +1 -1
  15. fin_infra/categorization/ease.py +3 -3
  16. fin_infra/categorization/engine.py +1 -1
  17. fin_infra/categorization/llm_layer.py +6 -4
  18. fin_infra/categorization/models.py +3 -4
  19. fin_infra/chat/planning.py +1 -1
  20. fin_infra/cli/cmds/scaffold_cmds.py +6 -6
  21. fin_infra/credit/experian/parser.py +5 -5
  22. fin_infra/crypto/__init__.py +2 -2
  23. fin_infra/documents/models.py +2 -3
  24. fin_infra/goals/management.py +3 -3
  25. fin_infra/goals/milestones.py +6 -6
  26. fin_infra/goals/models.py +2 -2
  27. fin_infra/investments/__init__.py +2 -2
  28. fin_infra/investments/ease.py +2 -2
  29. fin_infra/investments/models.py +24 -26
  30. fin_infra/investments/providers/base.py +4 -4
  31. fin_infra/investments/scaffold_templates/README.md +17 -17
  32. fin_infra/markets/__init__.py +2 -2
  33. fin_infra/models/accounts.py +4 -5
  34. fin_infra/models/transactions.py +2 -2
  35. fin_infra/net_worth/__init__.py +6 -6
  36. fin_infra/net_worth/aggregator.py +1 -1
  37. fin_infra/net_worth/calculator.py +1 -1
  38. fin_infra/net_worth/insights.py +7 -7
  39. fin_infra/normalization/__init__.py +2 -2
  40. fin_infra/normalization/currency_converter.py +7 -8
  41. fin_infra/normalization/models.py +9 -10
  42. fin_infra/normalization/providers/static_mappings.py +1 -1
  43. fin_infra/normalization/symbol_resolver.py +3 -4
  44. fin_infra/obs/classifier.py +2 -2
  45. fin_infra/providers/brokerage/alpaca.py +1 -1
  46. fin_infra/recurring/add.py +1 -1
  47. fin_infra/recurring/detectors_llm.py +5 -5
  48. fin_infra/recurring/ease.py +4 -4
  49. fin_infra/recurring/insights.py +15 -14
  50. fin_infra/recurring/normalizer.py +6 -6
  51. fin_infra/recurring/normalizers.py +27 -26
  52. fin_infra/scaffold/goals.py +4 -4
  53. fin_infra/security/add.py +1 -2
  54. fin_infra/security/audit.py +6 -7
  55. fin_infra/security/pii_filter.py +10 -10
  56. fin_infra/security/token_store.py +2 -3
  57. fin_infra/tax/add.py +2 -2
  58. fin_infra/tax/tlh.py +5 -5
  59. fin_infra/utils/__init__.py +15 -1
  60. fin_infra/utils/deprecation.py +161 -0
  61. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/METADATA +17 -9
  62. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/RECORD +65 -64
  63. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/LICENSE +0 -0
  64. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/WHEEL +0 -0
  65. {fin_infra-0.1.82.dist-info → fin_infra-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -8,8 +8,8 @@ This module provides comprehensive financial analytics capabilities including:
8
8
  - Growth projections (net worth forecasting with scenarios)
9
9
 
10
10
  Feature Status:
11
- STABLE: Core calculation functions (all analytics work with provided data)
12
- ⚠️ INTEGRATION: Auto-fetching from providers requires setup:
11
+ [OK] STABLE: Core calculation functions (all analytics work with provided data)
12
+ [!] INTEGRATION: Auto-fetching from providers requires setup:
13
13
  - Banking provider for transaction data
14
14
  - Brokerage provider for investment data
15
15
  - Categorization for expense categorization
@@ -239,9 +239,9 @@ def add_analytics(
239
239
 
240
240
  Note:
241
241
  Real holdings provide:
242
- - Accurate cost basis real profit/loss
243
- - Security types precise asset allocation
244
- - Current values live portfolio tracking
242
+ - Accurate cost basis -> real profit/loss
243
+ - Security types -> precise asset allocation
244
+ - Current values -> live portfolio tracking
245
245
  """
246
246
  # If with_holdings requested and investment provider available
247
247
  if with_holdings:
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from datetime import datetime, timedelta
9
9
  from decimal import Decimal
10
- from typing import Any, Optional
10
+ from typing import Any
11
11
 
12
12
  from ..models import Transaction
13
13
  from .models import CashFlowAnalysis
@@ -17,7 +17,7 @@ async def calculate_cash_flow(
17
17
  user_id: str,
18
18
  start_date: str | datetime,
19
19
  end_date: str | datetime,
20
- accounts: Optional[list[str]] = None,
20
+ accounts: list[str] | None = None,
21
21
  *,
22
22
  banking_provider=None,
23
23
  categorization_provider=None,
@@ -117,7 +117,7 @@ async def calculate_cash_flow(
117
117
  async def forecast_cash_flow(
118
118
  user_id: str,
119
119
  months: int = 6,
120
- assumptions: Optional[dict[str, Any]] = None,
120
+ assumptions: dict[str, Any] | None = None,
121
121
  *,
122
122
  recurring_provider=None,
123
123
  ) -> list[CashFlowAnalysis]:
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from datetime import datetime
9
9
  from enum import Enum
10
- from typing import Any, Optional
10
+ from typing import Any
11
11
 
12
12
  from pydantic import BaseModel, ConfigDict, Field
13
13
 
@@ -72,7 +72,7 @@ class SavingsRateData(BaseModel):
72
72
  expenses: float = Field(..., description="Total expenses for period")
73
73
  period: Period = Field(..., description="Period type")
74
74
  definition: SavingsDefinition = Field(..., description="Calculation method used")
75
- trend: Optional[TrendDirection] = Field(None, description="Trend over time")
75
+ trend: TrendDirection | None = Field(None, description="Trend over time")
76
76
 
77
77
 
78
78
  class SpendingAnomaly(BaseModel):
@@ -132,7 +132,7 @@ class PersonalizedSpendingAdvice(BaseModel):
132
132
  alerts: list[str] = Field(
133
133
  default_factory=list, description="Urgent spending issues requiring attention"
134
134
  )
135
- estimated_monthly_savings: Optional[float] = Field(
135
+ estimated_monthly_savings: float | None = Field(
136
136
  None, description="Potential monthly savings if recommendations followed"
137
137
  )
138
138
 
@@ -183,7 +183,7 @@ class BenchmarkComparison(BaseModel):
183
183
  benchmark_return_percent: float = Field(..., description="Benchmark return percentage")
184
184
  benchmark_symbol: str = Field(..., description="Benchmark ticker (e.g., SPY)")
185
185
  alpha: float = Field(..., description="Portfolio alpha (excess return)")
186
- beta: Optional[float] = Field(None, description="Portfolio beta (volatility vs benchmark)")
186
+ beta: float | None = Field(None, description="Portfolio beta (volatility vs benchmark)")
187
187
  period: str = Field(..., description="Comparison period (1y, 3y, 5y, etc.)")
188
188
 
189
189
 
@@ -213,6 +213,6 @@ class GrowthProjection(BaseModel):
213
213
  assumptions: dict[str, Any] = Field(
214
214
  default_factory=dict, description="Assumptions used (inflation, returns, etc.)"
215
215
  )
216
- confidence_intervals: Optional[dict[str, tuple[float, float]]] = Field(
216
+ confidence_intervals: dict[str, tuple[float, float]] | None = Field(
217
217
  None, description="95% confidence intervals by scenario"
218
218
  )
@@ -527,10 +527,10 @@ def portfolio_metrics_with_holdings(holdings: list) -> PortfolioMetrics:
527
527
  PortfolioMetrics with real portfolio analysis
528
528
 
529
529
  Real Data Advantages:
530
- - Actual cost basis accurate P/L calculations
531
- - Real security types precise asset allocation
532
- - Current market values live portfolio value
533
- - No mock data production-ready analytics
530
+ - Actual cost basis -> accurate P/L calculations
531
+ - Real security types -> precise asset allocation
532
+ - Current market values -> live portfolio value
533
+ - No mock data -> production-ready analytics
534
534
 
535
535
  Limitations:
536
536
  - Day/YTD/MTD returns require historical snapshots (not in holdings)
@@ -652,7 +652,7 @@ def calculate_day_change_with_snapshot(
652
652
  Function matches holdings by account_id + security_id for accurate tracking
653
653
  of individual position changes (accounts for buys/sells, not just price moves).
654
654
  """
655
- # Build lookup map for previous snapshot: (account_id, security_id) value
655
+ # Build lookup map for previous snapshot: (account_id, security_id) -> value
656
656
  previous_map = {}
657
657
  for holding in previous_snapshot:
658
658
  key = (holding.account_id, holding.security.security_id)
@@ -703,13 +703,13 @@ def _calculate_allocation_from_holdings(
703
703
  list[AssetAllocation] with asset_class, value, and percentage
704
704
 
705
705
  Asset Class Mapping:
706
- - equity Stocks
707
- - etf Stocks (equity ETFs grouped with stocks)
708
- - mutual_fund Bonds (conservative assumption)
709
- - bond Bonds
710
- - cash Cash
711
- - derivative Other
712
- - other Other
706
+ - equity -> Stocks
707
+ - etf -> Stocks (equity ETFs grouped with stocks)
708
+ - mutual_fund -> Bonds (conservative assumption)
709
+ - bond -> Bonds
710
+ - cash -> Cash
711
+ - derivative -> Other
712
+ - other -> Other
713
713
  """
714
714
  from collections import defaultdict
715
715
 
@@ -45,7 +45,6 @@ Examples:
45
45
  from collections import defaultdict
46
46
  from datetime import timedelta
47
47
  from decimal import Decimal
48
- from typing import Optional
49
48
 
50
49
  from fin_infra.analytics.models import (
51
50
  PersonalizedSpendingAdvice,
@@ -60,7 +59,7 @@ async def analyze_spending(
60
59
  user_id: str,
61
60
  *,
62
61
  period: str = "30d",
63
- categories: Optional[list[str]] = None,
62
+ categories: list[str] | None = None,
64
63
  banking_provider=None,
65
64
  categorization_provider=None,
66
65
  ) -> SpendingInsight:
@@ -426,7 +425,7 @@ def _generate_mock_transactions(days: int) -> list[Transaction]:
426
425
  async def generate_spending_insights(
427
426
  spending_insight: SpendingInsight,
428
427
  *,
429
- user_context: Optional[dict] = None,
428
+ user_context: dict | None = None,
430
429
  llm_provider=None,
431
430
  ) -> "PersonalizedSpendingAdvice":
432
431
  """Generate personalized spending insights using LLM.
@@ -521,7 +520,7 @@ async def generate_spending_insights(
521
520
 
522
521
  def _build_spending_insights_prompt(
523
522
  spending_insight: SpendingInsight,
524
- user_context: Optional[dict] = None,
523
+ user_context: dict | None = None,
525
524
  ) -> str:
526
525
  """Build LLM prompt with financial context.
527
526
 
@@ -621,7 +620,7 @@ Be specific, encouraging, and actionable. Focus on realistic savings, not extrem
621
620
 
622
621
  def _generate_rule_based_insights(
623
622
  spending_insight: SpendingInsight,
624
- user_context: Optional[dict] = None,
623
+ user_context: dict | None = None,
625
624
  ) -> "PersonalizedSpendingAdvice":
626
625
  """Generate rule-based insights when LLM is unavailable.
627
626
 
@@ -4,7 +4,7 @@ This module provides functionality to record and retrieve historical account bal
4
4
  snapshots over time. This enables balance trend analysis, sparklines, and time-series
5
5
  visualizations in fintech dashboards.
6
6
 
7
- ⚠️ WARNING: This module uses IN-MEMORY storage by default. All data is LOST on restart.
7
+ [!] WARNING: This module uses IN-MEMORY storage by default. All data is LOST on restart.
8
8
  For production use, integrate with svc-infra SQL database or set FIN_INFRA_STORAGE_BACKEND.
9
9
 
10
10
  Features:
@@ -57,7 +57,7 @@ __all__ = [
57
57
  _logger = logging.getLogger(__name__)
58
58
 
59
59
  # In-memory storage for testing (will be replaced with SQL database in production)
60
- # ⚠️ WARNING: All data is LOST on restart when using in-memory storage!
60
+ # [!] WARNING: All data is LOST on restart when using in-memory storage!
61
61
  _balance_snapshots: list[BalanceSnapshot] = []
62
62
  _production_warning_logged = False
63
63
 
@@ -73,7 +73,7 @@ def _check_in_memory_warning() -> None:
73
73
 
74
74
  if env in ("production", "staging") and storage_backend == "memory":
75
75
  _logger.warning(
76
- "⚠️ CRITICAL: Balance history using IN-MEMORY storage in %s environment! "
76
+ "[!] CRITICAL: Balance history using IN-MEMORY storage in %s environment! "
77
77
  "All balance snapshots will be LOST on restart. "
78
78
  "Set FIN_INFRA_STORAGE_BACKEND=sql for production persistence.",
79
79
  env,
@@ -114,7 +114,7 @@ def record_balance_snapshot(
114
114
  This function stores a point-in-time balance record for trend analysis.
115
115
  In production, this would write to a SQL database via svc-infra.
116
116
 
117
- ⚠️ WARNING: Uses in-memory storage by default. Data is LOST on restart!
117
+ [!] WARNING: Uses in-memory storage by default. Data is LOST on restart!
118
118
 
119
119
  Args:
120
120
  account_id: Account identifier
@@ -1,6 +1,6 @@
1
1
  """Brokerage module - easy setup for trading operations.
2
2
 
3
- ⚠️ **TRADING WARNING**: This module provides real trading capabilities.
3
+ [!] **TRADING WARNING**: This module provides real trading capabilities.
4
4
  Always use paper trading mode for development and testing.
5
5
  Live trading requires explicit opt-in and involves real financial risk.
6
6
 
@@ -51,11 +51,11 @@ def easy_brokerage(
51
51
  ) -> BrokerageProvider:
52
52
  """Create a brokerage provider with paper/live trading support.
53
53
 
54
- ⚠️ **SAFETY**: Defaults to paper trading mode. Live trading requires explicit mode="live".
54
+ [!] **SAFETY**: Defaults to paper trading mode. Live trading requires explicit mode="live".
55
55
 
56
56
  Auto-detects provider based on environment variables:
57
- 1. If ALPACA_API_KEY and ALPACA_API_SECRET are set Alpaca
58
- 2. Otherwise Raises error (credentials required)
57
+ 1. If ALPACA_API_KEY and ALPACA_API_SECRET are set -> Alpaca
58
+ 2. Otherwise -> Raises error (credentials required)
59
59
 
60
60
  Args:
61
61
  provider: Provider name ("alpaca"). If None, defaults to alpaca.
@@ -138,7 +138,7 @@ def add_brokerage(
138
138
  ) -> BrokerageProvider:
139
139
  """Wire brokerage provider to FastAPI app with routes and safety checks.
140
140
 
141
- ⚠️ **TRADING WARNING**: This mounts trading API endpoints.
141
+ [!] **TRADING WARNING**: This mounts trading API endpoints.
142
142
  Always use paper trading mode for development.
143
143
  Live trading requires explicit mode="live" and proper safeguards.
144
144
 
@@ -280,7 +280,7 @@ def add_brokerage(
280
280
  async def submit_order(order_request: OrderRequest):
281
281
  """Submit a new order.
282
282
 
283
- ⚠️ **TRADING WARNING**: This endpoint executes real trades in live mode.
283
+ [!] **TRADING WARNING**: This endpoint executes real trades in live mode.
284
284
  """
285
285
  try:
286
286
  order = brokerage_provider.submit_order(
@@ -447,7 +447,7 @@ def add_brokerage(
447
447
  add_prefixed_docs(
448
448
  app,
449
449
  prefix=prefix,
450
- title="Brokerage" + (" (Paper Trading)" if mode == "paper" else " ⚠️ LIVE"),
450
+ title="Brokerage" + (" (Paper Trading)" if mode == "paper" else " [!] LIVE"),
451
451
  auto_exclude_from_root=True,
452
452
  visible_envs=None, # Show in all environments
453
453
  )
@@ -35,7 +35,7 @@ Example:
35
35
  from __future__ import annotations
36
36
 
37
37
  from datetime import datetime
38
- from typing import TYPE_CHECKING, Optional
38
+ from typing import TYPE_CHECKING
39
39
 
40
40
  from fin_infra.budgets.models import (
41
41
  AlertSeverity,
@@ -51,7 +51,7 @@ if TYPE_CHECKING:
51
51
  async def check_budget_alerts(
52
52
  budget_id: str,
53
53
  tracker: BudgetTracker,
54
- thresholds: Optional[dict[str, float]] = None,
54
+ thresholds: dict[str, float] | None = None,
55
55
  ) -> list[BudgetAlert]:
56
56
  """
57
57
  Check budget for alerts (overspending, approaching limits, unusual patterns).
fin_infra/budgets/ease.py CHANGED
@@ -12,7 +12,6 @@ Generic Design:
12
12
  from __future__ import annotations
13
13
 
14
14
  import os
15
- from typing import Optional
16
15
 
17
16
  from sqlalchemy.ext.asyncio import create_async_engine
18
17
 
@@ -20,7 +19,7 @@ from fin_infra.budgets.tracker import BudgetTracker
20
19
 
21
20
 
22
21
  def easy_budgets(
23
- db_url: Optional[str] = None,
22
+ db_url: str | None = None,
24
23
  pool_size: int = 5,
25
24
  max_overflow: int = 10,
26
25
  pool_pre_ping: bool = True,
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  from datetime import datetime
10
10
  from enum import Enum
11
- from typing import Optional
12
11
 
13
12
  from pydantic import BaseModel, ConfigDict, Field
14
13
 
@@ -296,7 +295,7 @@ class BudgetAlert(BaseModel):
296
295
  """
297
296
 
298
297
  budget_id: str = Field(..., description="Budget identifier")
299
- category: Optional[str] = Field(None, description="Category triggering alert")
298
+ category: str | None = Field(None, description="Category triggering alert")
300
299
  alert_type: AlertType = Field(..., description="Type of alert")
301
300
  threshold: float = Field(..., description="Threshold that triggered alert")
302
301
  message: str = Field(..., description="Human-readable alert message")
@@ -16,7 +16,7 @@ Generic Design:
16
16
  from __future__ import annotations
17
17
 
18
18
  from datetime import datetime
19
- from typing import TYPE_CHECKING, Optional
19
+ from typing import TYPE_CHECKING
20
20
 
21
21
  from fin_infra.budgets.models import Budget, BudgetPeriod, BudgetType
22
22
 
@@ -177,9 +177,9 @@ async def apply_template(
177
177
  template_name: str,
178
178
  total_income: float,
179
179
  tracker: BudgetTracker,
180
- budget_name: Optional[str] = None,
181
- start_date: Optional[datetime] = None,
182
- custom_template: Optional[BudgetTemplate] = None,
180
+ budget_name: str | None = None,
181
+ start_date: datetime | None = None,
182
+ custom_template: BudgetTemplate | None = None,
183
183
  ) -> Budget:
184
184
  """Apply a budget template to create a new budget.
185
185
 
@@ -36,7 +36,7 @@ from __future__ import annotations
36
36
 
37
37
  import uuid
38
38
  from datetime import datetime, timedelta
39
- from typing import TYPE_CHECKING, Optional
39
+ from typing import TYPE_CHECKING
40
40
 
41
41
  from sqlalchemy.ext.asyncio import async_sessionmaker
42
42
 
@@ -116,7 +116,7 @@ class BudgetTracker:
116
116
  type: str, # BudgetType value
117
117
  period: str, # BudgetPeriod value
118
118
  categories: dict[str, float],
119
- start_date: Optional[datetime] = None,
119
+ start_date: datetime | None = None,
120
120
  rollover_enabled: bool = False,
121
121
  ) -> Budget:
122
122
  """
@@ -205,7 +205,7 @@ class BudgetTracker:
205
205
  async def get_budgets(
206
206
  self,
207
207
  user_id: str,
208
- type: Optional[str] = None,
208
+ type: str | None = None,
209
209
  ) -> list[Budget]:
210
210
  """
211
211
  Get all budgets for a user.
@@ -2,7 +2,7 @@
2
2
  Transaction categorization module.
3
3
 
4
4
  Provides ML-based categorization of merchant transactions into 56 categories
5
- using a hybrid approach (exact match regex sklearn Naive Bayes LLM).
5
+ using a hybrid approach (exact match -> regex -> sklearn Naive Bayes -> LLM).
6
6
 
7
7
  Basic usage:
8
8
  from fin_infra.categorization import categorize
@@ -5,7 +5,7 @@ Provides one-line setup with sensible defaults.
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
- from typing import Literal, Optional
8
+ from typing import Literal
9
9
 
10
10
  from .engine import CategorizationEngine
11
11
 
@@ -21,10 +21,10 @@ def easy_categorization(
21
21
  taxonomy: str = "mx",
22
22
  enable_ml: bool = False,
23
23
  confidence_threshold: float = 0.6,
24
- model_path: Optional[Path] = None,
24
+ model_path: Path | None = None,
25
25
  # LLM-specific parameters (V2)
26
26
  llm_provider: Literal["google", "openai", "anthropic", "none"] = "google",
27
- llm_model: Optional[str] = None,
27
+ llm_model: str | None = None,
28
28
  llm_confidence_threshold: float = 0.6,
29
29
  llm_cache_ttl: int = 86400, # 24 hours
30
30
  llm_max_cost_per_day: float = 0.10, # $0.10/day
@@ -1,5 +1,5 @@
1
1
  """
2
- Hybrid categorization engine (exact regex ML LLM).
2
+ Hybrid categorization engine (exact -> regex -> ML -> LLM).
3
3
 
4
4
  4-layer approach:
5
5
  1. Layer 1 (Exact Match): O(1) dictionary lookup, 85-90% coverage
@@ -197,7 +197,7 @@ class LLMCategorizer:
197
197
  self._track_cost()
198
198
 
199
199
  logger.info(
200
- f"LLM categorized '{merchant_name}' {prediction.category} "
200
+ f"LLM categorized '{merchant_name}' -> {prediction.category} "
201
201
  f"(confidence={prediction.confidence:.2f})"
202
202
  )
203
203
 
@@ -252,7 +252,7 @@ class LLMCategorizer:
252
252
  # Format few-shot examples
253
253
  examples_text = "\n\n".join(
254
254
  [
255
- f'Merchant: "{merchant}"\n Category: "{category}"\n Reasoning: "{reasoning}"'
255
+ f'Merchant: "{merchant}"\n-> Category: "{category}"\n-> Reasoning: "{reasoning}"'
256
256
  for merchant, category, reasoning in FEW_SHOT_EXAMPLES
257
257
  ]
258
258
  )
@@ -300,6 +300,8 @@ Return JSON with category, confidence, and reasoning."""
300
300
  def _get_cache_key(self, merchant_name: str) -> str:
301
301
  """Generate stable cache key from merchant name."""
302
302
  normalized = merchant_name.lower().strip()
303
+ # Security: B324 skip justified - MD5 used for cache key generation only,
304
+ # not for security. We need deterministic hashing for cache lookups.
303
305
  hash_value = hashlib.md5(normalized.encode()).hexdigest()
304
306
  return f"llm_category:{hash_value}"
305
307
 
@@ -338,10 +340,10 @@ Return JSON with category, confidence, and reasoning."""
338
340
 
339
341
  def reset_daily_cost(self):
340
342
  """Reset daily cost counter (called at midnight UTC)."""
341
- logger.info(f"Resetting daily cost: ${self.daily_cost:.5f} $0.00")
343
+ logger.info(f"Resetting daily cost: ${self.daily_cost:.5f} -> $0.00")
342
344
  self.daily_cost = 0.0
343
345
 
344
346
  def reset_monthly_cost(self):
345
347
  """Reset monthly cost counter (called on 1st of month)."""
346
- logger.info(f"Resetting monthly cost: ${self.monthly_cost:.5f} $0.00")
348
+ logger.info(f"Resetting monthly cost: ${self.monthly_cost:.5f} -> $0.00")
347
349
  self.monthly_cost = 0.0
@@ -4,7 +4,6 @@ Pydantic models for transaction categorization.
4
4
 
5
5
  from datetime import datetime
6
6
  from enum import Enum
7
- from typing import Optional
8
7
 
9
8
  from pydantic import BaseModel, ConfigDict, Field
10
9
 
@@ -34,7 +33,7 @@ class CategoryPrediction(BaseModel):
34
33
  default_factory=list,
35
34
  description="Alternative predictions (category, confidence)",
36
35
  )
37
- reasoning: Optional[str] = Field(None, description="Explanation of prediction (for LLM)")
36
+ reasoning: str | None = Field(None, description="Explanation of prediction (for LLM)")
38
37
 
39
38
  model_config = ConfigDict(
40
39
  json_schema_extra={
@@ -100,7 +99,7 @@ class CategorizationRequest(BaseModel):
100
99
  """Request to categorize a merchant."""
101
100
 
102
101
  merchant_name: str = Field(..., description="Merchant name to categorize")
103
- user_id: Optional[str] = Field(None, description="User ID for personalized overrides")
102
+ user_id: str | None = Field(None, description="User ID for personalized overrides")
104
103
  include_alternatives: bool = Field(default=False, description="Include alternative predictions")
105
104
  min_confidence: float = Field(
106
105
  default=0.0,
@@ -152,7 +151,7 @@ class CategoryStats(BaseModel):
152
151
  total_categories: int = Field(..., description="Total number of categories")
153
152
  categories_by_group: dict[str, int] = Field(..., description="Category counts by group")
154
153
  total_rules: int = Field(..., description="Total number of rules")
155
- cache_hit_rate: Optional[float] = Field(None, description="Cache hit rate (0-1)")
154
+ cache_hit_rate: float | None = Field(None, description="Cache hit rate (0-1)")
156
155
 
157
156
  model_config = ConfigDict(
158
157
  json_schema_extra={
@@ -173,7 +173,7 @@ Answer: "To assess your retirement progress, I need more information: (1) What's
173
173
  Follow-ups: ["I want to retire at 65 with $1.5M", "How much should I save monthly?", "What's a realistic retirement goal?"]
174
174
  Sources: []
175
175
 
176
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
176
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
177
177
  Verify calculations independently. For personalized advice, consult a professional."""
178
178
 
179
179
 
@@ -102,7 +102,7 @@ def cmd_scaffold(
102
102
  # Validate required parameters
103
103
  if dest_dir is None:
104
104
  typer.secho(
105
- " Error: --dest-dir is required",
105
+ "[X] Error: --dest-dir is required",
106
106
  fg=typer.colors.RED,
107
107
  err=True,
108
108
  )
@@ -137,7 +137,7 @@ def cmd_scaffold(
137
137
  )
138
138
  else:
139
139
  typer.secho(
140
- f" Unknown domain: {domain}. Must be one of: budgets, goals",
140
+ f"[X] Unknown domain: {domain}. Must be one of: budgets, goals",
141
141
  fg=typer.colors.RED,
142
142
  err=True,
143
143
  )
@@ -145,7 +145,7 @@ def cmd_scaffold(
145
145
 
146
146
  # Display results
147
147
  typer.echo("")
148
- typer.secho("📦 Scaffold Results:", bold=True)
148
+ typer.secho(" Scaffold Results:", bold=True)
149
149
  typer.echo("")
150
150
 
151
151
  files = result.get("files", [])
@@ -157,7 +157,7 @@ def cmd_scaffold(
157
157
  action = file_info.get("action", "unknown")
158
158
 
159
159
  if action == "wrote":
160
- typer.secho(f" Created: {path}", fg=typer.colors.GREEN)
160
+ typer.secho(f" [OK] Created: {path}", fg=typer.colors.GREEN)
161
161
  wrote_count += 1
162
162
  elif action == "skipped":
163
163
  reason = file_info.get("reason", "unknown")
@@ -168,7 +168,7 @@ def cmd_scaffold(
168
168
 
169
169
  # Summary
170
170
  typer.echo("")
171
- typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
171
+ typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
172
172
  typer.echo("")
173
173
 
174
174
  # Next steps
@@ -187,7 +187,7 @@ def cmd_scaffold(
187
187
  }
188
188
  route_prefix = prefix_map.get(domain, f"/{domain}")
189
189
 
190
- typer.secho("📝 Next Steps:", bold=True)
190
+ typer.secho(" Next Steps:", bold=True)
191
191
  typer.echo("")
192
192
  typer.echo(" 1. Review generated files and customize as needed")
193
193
  typer.echo(" 2. Run migrations:")
@@ -1,11 +1,11 @@
1
1
  """Response parsers for Experian API data to fin_infra models.
2
2
 
3
3
  Converts Experian API JSON responses to typed Pydantic models:
4
- - parse_credit_score(): dict CreditScore
5
- - parse_credit_report(): dict CreditReport
6
- - parse_account(): dict CreditAccount
7
- - parse_inquiry(): dict CreditInquiry
8
- - parse_public_record(): dict PublicRecord
4
+ - parse_credit_score(): dict -> CreditScore
5
+ - parse_credit_report(): dict -> CreditReport
6
+ - parse_account(): dict -> CreditAccount
7
+ - parse_inquiry(): dict -> CreditInquiry
8
+ - parse_public_record(): dict -> PublicRecord
9
9
 
10
10
  Example:
11
11
  >>> data = await client.get_credit_score("user123")
@@ -29,8 +29,8 @@ def easy_crypto(
29
29
  """Create a crypto data provider with zero or minimal configuration.
30
30
 
31
31
  Auto-detects provider based on environment variables:
32
- 1. If COINGECKO_API_KEY is set CoinGecko Pro
33
- 2. Otherwise CoinGecko Free (no key needed)
32
+ 1. If COINGECKO_API_KEY is set -> CoinGecko Pro
33
+ 2. Otherwise -> CoinGecko Free (no key needed)
34
34
 
35
35
  Args:
36
36
  provider: Provider name ("coingecko"). If None, defaults to coingecko.
@@ -31,7 +31,6 @@ from __future__ import annotations
31
31
 
32
32
  from datetime import datetime
33
33
  from enum import Enum
34
- from typing import Optional
35
34
 
36
35
  from pydantic import BaseModel, ConfigDict, Field
37
36
  from svc_infra.documents import Document as BaseDocument
@@ -111,8 +110,8 @@ class FinancialDocument(BaseDocument):
111
110
 
112
111
  # Financial-specific fields
113
112
  type: DocumentType = Field(..., description="Document type category (financial-specific)")
114
- tax_year: Optional[int] = Field(None, description="Tax year for tax documents (e.g., 2024)")
115
- form_type: Optional[str] = Field(None, description="Tax form type (W-2, 1099-INT, 1040, etc.)")
113
+ tax_year: int | None = Field(None, description="Tax year for tax documents (e.g., 2024)")
114
+ form_type: str | None = Field(None, description="Tax form type (W-2, 1099-INT, 1040, etc.)")
116
115
 
117
116
 
118
117
  # Backward compatibility: alias for existing code
@@ -182,7 +182,7 @@ Your response: {
182
182
  "confidence": 0.94
183
183
  }
184
184
 
185
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
185
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
186
186
  Verify calculations independently. For personalized advice, consult a professional."""
187
187
 
188
188
  GOAL_PROGRESS_SYSTEM_PROMPT = """You are a financial advisor reviewing goal progress.
@@ -237,7 +237,7 @@ Your response: {
237
237
  "projected_completion_date": "2029-06-01",
238
238
  "variance_from_target_days": -365,
239
239
  "course_corrections": [
240
- "⚠️ 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
240
+ "[!] 12 months behind! Current $1,000/month payment needs to increase to $1,500/month",
241
241
  "Emergency: reduce expenses by $500/month (cancel subscriptions, cut entertainment)",
242
242
  "Contact debt counselor for consolidation or negotiation options",
243
243
  "Consider side income: gig work, selling unused items ($500/month target)",
@@ -246,7 +246,7 @@ Your response: {
246
246
  "confidence": 0.95
247
247
  }
248
248
 
249
- ⚠️ This is AI-generated advice. Not a substitute for a certified financial advisor.
249
+ [!] This is AI-generated advice. Not a substitute for a certified financial advisor.
250
250
  Verify calculations independently. For personalized advice, consult a professional."""
251
251
 
252
252