fin-infra 0.6.0__py3-none-any.whl → 0.8.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.
@@ -28,6 +28,8 @@ Environment Variables:
28
28
  Teller:
29
29
  TELLER_CERTIFICATE_PATH: Path to certificate.pem file
30
30
  TELLER_PRIVATE_KEY_PATH: Path to private_key.pem file
31
+ TELLER_CERTIFICATE: Inline certificate PEM content (alternative to path)
32
+ TELLER_PRIVATE_KEY: Inline private key PEM content (alternative to path)
31
33
  TELLER_ENVIRONMENT: "sandbox" or "production" (default: sandbox)
32
34
 
33
35
  Plaid:
@@ -191,6 +193,8 @@ def easy_banking(provider: str = "teller", **config) -> BankingProvider:
191
193
  config = {
192
194
  "cert_path": os.getenv("TELLER_CERTIFICATE_PATH"),
193
195
  "key_path": os.getenv("TELLER_PRIVATE_KEY_PATH"),
196
+ "cert_content": os.getenv("TELLER_CERTIFICATE"),
197
+ "key_content": os.getenv("TELLER_PRIVATE_KEY"),
194
198
  "environment": os.getenv("TELLER_ENVIRONMENT", "sandbox"),
195
199
  }
196
200
  elif provider == "plaid":
@@ -108,7 +108,7 @@ class LLMCategorizer:
108
108
 
109
109
  Args:
110
110
  provider: LLM provider ("google_genai", "openai", "anthropic")
111
- model_name: Model name (e.g., "gemini-2.5-flash", "gpt-4.1-mini")
111
+ model_name: Model name (e.g., "gemini-2.5-flash", "gpt-5-mini")
112
112
  max_cost_per_day: Daily budget cap in USD (default $0.10)
113
113
  max_cost_per_month: Monthly budget cap in USD (default $2.00)
114
114
  cache_ttl: Cache TTL in seconds (default 24 hours)
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING
16
16
  if TYPE_CHECKING:
17
17
  from fastapi import FastAPI
18
18
 
19
- from .aggregator import aggregate_insights, get_user_insights
19
+ from .aggregator import InsightTone, aggregate_insights, get_user_insights
20
20
  from .models import Insight, InsightCategory, InsightFeed, InsightPriority
21
21
 
22
22
  logger = logging.getLogger(__name__)
@@ -26,6 +26,7 @@ __all__ = [
26
26
  "InsightFeed",
27
27
  "InsightPriority",
28
28
  "InsightCategory",
29
+ "InsightTone",
29
30
  "aggregate_insights",
30
31
  "get_user_insights",
31
32
  "add_insights",
@@ -4,10 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  from datetime import datetime
6
6
  from decimal import Decimal
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Literal
8
8
 
9
9
  from .models import Insight, InsightCategory, InsightFeed, InsightPriority
10
10
 
11
+ # Tone type for insight text generation
12
+ InsightTone = Literal["professional", "fun"]
13
+
11
14
  if TYPE_CHECKING:
12
15
  from fin_infra.budgets.models import Budget
13
16
  from fin_infra.goals.models import Goal
@@ -23,6 +26,7 @@ def aggregate_insights(
23
26
  recurring_patterns: list[RecurringPattern] | None = None,
24
27
  portfolio_value: Decimal | None = None,
25
28
  tax_opportunities: list[dict] | None = None,
29
+ tone: InsightTone = "professional",
26
30
  ) -> InsightFeed:
27
31
  """
28
32
  Aggregate insights from multiple financial data sources.
@@ -35,6 +39,7 @@ def aggregate_insights(
35
39
  recurring_patterns: Detected recurring transactions
36
40
  portfolio_value: Current portfolio value
37
41
  tax_opportunities: Tax-loss harvesting or other tax insights
42
+ tone: Insight text tone - "professional" (formal) or "fun" (casual with emojis)
38
43
 
39
44
  Returns:
40
45
  InsightFeed with prioritized insights
@@ -44,6 +49,7 @@ def aggregate_insights(
44
49
  ... user_id="user_123",
45
50
  ... budgets=[budget1, budget2],
46
51
  ... goals=[goal1],
52
+ ... tone="fun",
47
53
  ... )
48
54
  >>> print(insights.critical_count)
49
55
  2
@@ -52,27 +58,27 @@ def aggregate_insights(
52
58
 
53
59
  # Net worth insights
54
60
  if net_worth_snapshots and len(net_worth_snapshots) >= 2:
55
- insights.extend(_generate_net_worth_insights(user_id, net_worth_snapshots))
61
+ insights.extend(_generate_net_worth_insights(user_id, net_worth_snapshots, tone))
56
62
 
57
63
  # Budget insights (critical if overspending)
58
64
  if budgets:
59
- insights.extend(_generate_budget_insights(user_id, budgets))
65
+ insights.extend(_generate_budget_insights(user_id, budgets, tone))
60
66
 
61
67
  # Goal insights
62
68
  if goals:
63
- insights.extend(_generate_goal_insights(user_id, goals))
69
+ insights.extend(_generate_goal_insights(user_id, goals, tone))
64
70
 
65
71
  # Recurring pattern insights
66
72
  if recurring_patterns:
67
- insights.extend(_generate_recurring_insights(user_id, recurring_patterns))
73
+ insights.extend(_generate_recurring_insights(user_id, recurring_patterns, tone))
68
74
 
69
75
  # Portfolio insights
70
76
  if portfolio_value:
71
- insights.extend(_generate_portfolio_insights(user_id, portfolio_value))
77
+ insights.extend(_generate_portfolio_insights(user_id, portfolio_value, tone))
72
78
 
73
79
  # Tax insights (high priority)
74
80
  if tax_opportunities:
75
- insights.extend(_generate_tax_insights(user_id, tax_opportunities))
81
+ insights.extend(_generate_tax_insights(user_id, tax_opportunities, tone))
76
82
 
77
83
  # Sort by priority: critical > high > medium > low
78
84
  priority_order = {
@@ -113,7 +119,9 @@ def get_user_insights(user_id: str, include_read: bool = False) -> InsightFeed:
113
119
  return InsightFeed(user_id=user_id, insights=[])
114
120
 
115
121
 
116
- def _generate_net_worth_insights(user_id: str, snapshots: list[NetWorthSnapshot]) -> list[Insight]:
122
+ def _generate_net_worth_insights(
123
+ user_id: str, snapshots: list[NetWorthSnapshot], tone: InsightTone
124
+ ) -> list[Insight]:
117
125
  """Generate insights from net worth trends."""
118
126
  insights = []
119
127
 
@@ -131,28 +139,42 @@ def _generate_net_worth_insights(user_id: str, snapshots: list[NetWorthSnapshot]
131
139
  )
132
140
 
133
141
  if change > 0:
142
+ if tone == "fun":
143
+ title = "Net Worth Glow Up! 📈"
144
+ desc = f"You're up ${change:,.2f} ({change_pct:.1f}%)! That's what we call winning 💪"
145
+ else:
146
+ title = "Net Worth Increased"
147
+ desc = f"Your net worth grew by ${change:,.2f} ({change_pct:.1f}%) this period"
134
148
  insights.append(
135
149
  Insight(
136
150
  id=f"nw_{user_id}_{datetime.now().timestamp()}",
137
151
  user_id=user_id,
138
152
  category=InsightCategory.NET_WORTH,
139
153
  priority=InsightPriority.MEDIUM,
140
- title="Net Worth Increased",
141
- description=f"Your net worth grew by ${change:,.2f} ({change_pct:.1f}%) this period",
154
+ title=title,
155
+ description=desc,
142
156
  value=change,
143
157
  )
144
158
  )
145
159
  elif change < 0:
146
160
  priority = InsightPriority.HIGH if abs(change_pct) > 10 else InsightPriority.MEDIUM
161
+ if tone == "fun":
162
+ title = "Net Worth Took a Hit 📉"
163
+ desc = f"Down ${abs(change):,.2f} ({abs(change_pct):.1f}%) - no stress, let's figure this out"
164
+ action = "Time to check what's up with your transactions 🔍"
165
+ else:
166
+ title = "Net Worth Decreased"
167
+ desc = f"Your net worth declined by ${abs(change):,.2f} ({abs(change_pct):.1f}%) this period"
168
+ action = "Review recent transactions and market changes"
147
169
  insights.append(
148
170
  Insight(
149
171
  id=f"nw_{user_id}_{datetime.now().timestamp()}",
150
172
  user_id=user_id,
151
173
  category=InsightCategory.NET_WORTH,
152
174
  priority=priority,
153
- title="Net Worth Decreased",
154
- description=f"Your net worth declined by ${abs(change):,.2f} ({abs(change_pct):.1f}%) this period",
155
- action="Review recent transactions and market changes",
175
+ title=title,
176
+ description=desc,
177
+ action=action,
156
178
  value=change,
157
179
  )
158
180
  )
@@ -160,7 +182,9 @@ def _generate_net_worth_insights(user_id: str, snapshots: list[NetWorthSnapshot]
160
182
  return insights
161
183
 
162
184
 
163
- def _generate_budget_insights(user_id: str, budgets: list[Budget]) -> list[Insight]:
185
+ def _generate_budget_insights(
186
+ user_id: str, budgets: list[Budget], tone: InsightTone
187
+ ) -> list[Insight]:
164
188
  """Generate insights from budget tracking."""
165
189
  insights: list[Insight] = []
166
190
 
@@ -174,7 +198,7 @@ def _generate_budget_insights(user_id: str, budgets: list[Budget]) -> list[Insig
174
198
  return insights
175
199
 
176
200
 
177
- def _generate_goal_insights(user_id: str, goals: list[Goal]) -> list[Insight]:
201
+ def _generate_goal_insights(user_id: str, goals: list[Goal], tone: InsightTone) -> list[Insight]:
178
202
  """Generate insights from goal progress."""
179
203
  insights = []
180
204
 
@@ -185,29 +209,45 @@ def _generate_goal_insights(user_id: str, goals: list[Goal]) -> list[Insight]:
185
209
 
186
210
  # Goal milestones
187
211
  if pct >= 100:
212
+ if tone == "fun":
213
+ title = f"🎉 '{goal.name}' Goal Crushed!"
214
+ desc = f"You hit ${target:,.2f}! Absolute legend 👑"
215
+ action = "Time to dream bigger - what's the next goal? 🚀"
216
+ else:
217
+ title = f"Goal '{goal.name}' Achieved!"
218
+ desc = f"You've reached your ${target:,.2f} goal"
219
+ action = "Consider setting a new goal or increasing this one"
188
220
  insights.append(
189
221
  Insight(
190
222
  id=f"goal_{goal.id}_{datetime.now().timestamp()}",
191
223
  user_id=user_id,
192
224
  category=InsightCategory.GOAL,
193
225
  priority=InsightPriority.HIGH,
194
- title=f"Goal '{goal.name}' Achieved!",
195
- description=f"You've reached your ${target:,.2f} goal",
196
- action="Consider setting a new goal or increasing this one",
226
+ title=title,
227
+ description=desc,
228
+ action=action,
197
229
  value=current,
198
230
  metadata={"goal_id": goal.id},
199
231
  )
200
232
  )
201
233
  elif pct >= 75:
234
+ if tone == "fun":
235
+ title = f"🔥 '{goal.name}' Almost There!"
236
+ desc = f"${current:,.2f} of ${target:,.2f} - you're {pct:.0f}% there, keep going!"
237
+ action = f"Just ${target - current:,.2f} more and you're golden ✨"
238
+ else:
239
+ title = f"Goal '{goal.name}' Almost There"
240
+ desc = f"${current:,.2f} of ${target:,.2f} saved ({pct:.0f}%)"
241
+ action = f"${target - current:,.2f} more to reach your goal"
202
242
  insights.append(
203
243
  Insight(
204
244
  id=f"goal_{goal.id}_{datetime.now().timestamp()}",
205
245
  user_id=user_id,
206
246
  category=InsightCategory.GOAL,
207
247
  priority=InsightPriority.MEDIUM,
208
- title=f"Goal '{goal.name}' Almost There",
209
- description=f"${current:,.2f} of ${target:,.2f} saved ({pct:.0f}%)",
210
- action=f"${target - current:,.2f} more to reach your goal",
248
+ title=title,
249
+ description=desc,
250
+ action=action,
211
251
  value=current,
212
252
  metadata={"goal_id": goal.id},
213
253
  )
@@ -216,7 +256,9 @@ def _generate_goal_insights(user_id: str, goals: list[Goal]) -> list[Insight]:
216
256
  return insights
217
257
 
218
258
 
219
- def _generate_recurring_insights(user_id: str, patterns: list[RecurringPattern]) -> list[Insight]:
259
+ def _generate_recurring_insights(
260
+ user_id: str, patterns: list[RecurringPattern], tone: InsightTone
261
+ ) -> list[Insight]:
220
262
  """Generate insights from recurring transactions."""
221
263
  insights = []
222
264
 
@@ -235,15 +277,23 @@ def _generate_recurring_insights(user_id: str, patterns: list[RecurringPattern])
235
277
  # Use average of range
236
278
  total += Decimal(str((p.amount_range[0] + p.amount_range[1]) / 2))
237
279
 
280
+ if tone == "fun":
281
+ title = "💸 Subscription Check!"
282
+ desc = f"{len(high_cost)} subscriptions over $50/mo = ${total:,.2f}. That's some serious recurring vibes"
283
+ action = "Time for a subscription audit? 🧐"
284
+ else:
285
+ title = "High-Cost Subscriptions Detected"
286
+ desc = f"You have {len(high_cost)} subscriptions over $50/month totaling ${total:,.2f}"
287
+ action = "Review if all subscriptions are still needed"
238
288
  insights.append(
239
289
  Insight(
240
290
  id=f"recurring_{user_id}_{datetime.now().timestamp()}",
241
291
  user_id=user_id,
242
292
  category=InsightCategory.RECURRING,
243
293
  priority=InsightPriority.MEDIUM,
244
- title="High-Cost Subscriptions Detected",
245
- description=f"You have {len(high_cost)} subscriptions over $50/month totaling ${total:,.2f}",
246
- action="Review if all subscriptions are still needed",
294
+ title=title,
295
+ description=desc,
296
+ action=action,
247
297
  value=total,
248
298
  )
249
299
  )
@@ -251,41 +301,52 @@ def _generate_recurring_insights(user_id: str, patterns: list[RecurringPattern])
251
301
  return insights
252
302
 
253
303
 
254
- def _generate_portfolio_insights(user_id: str, portfolio_value: Decimal) -> list[Insight]:
255
- """Generate insights from portfolio analysis."""
256
- insights = []
304
+ def _generate_portfolio_insights(
305
+ user_id: str, portfolio_value: Decimal, tone: InsightTone
306
+ ) -> list[Insight]:
307
+ """Generate insights from portfolio analysis.
257
308
 
258
- # Simple insight: portfolio exists
259
- if portfolio_value > 0:
260
- insights.append(
261
- Insight(
262
- id=f"portfolio_{user_id}_{datetime.now().timestamp()}",
263
- user_id=user_id,
264
- category=InsightCategory.PORTFOLIO,
265
- priority=InsightPriority.LOW,
266
- title="Portfolio Tracked",
267
- description=f"Your portfolio is valued at ${portfolio_value:,.2f}",
268
- value=portfolio_value,
269
- )
270
- )
309
+ Focus on actionable insights, not redundant value statements.
310
+ """
311
+ insights: list[Insight] = []
312
+
313
+ # Skip the basic "portfolio is valued at X" - that's shown in KPI cards
314
+ # Only generate insights when there's something actionable
315
+
316
+ # Example: Diversification check (would need account breakdown data)
317
+ # This is a placeholder - in production, pass account-level data
318
+
319
+ # Example: Rebalancing reminder based on market conditions
320
+ # This would integrate with market data APIs
271
321
 
272
322
  return insights
273
323
 
274
324
 
275
- def _generate_tax_insights(user_id: str, opportunities: list[dict]) -> list[Insight]:
325
+ def _generate_tax_insights(
326
+ user_id: str, opportunities: list[dict], tone: InsightTone
327
+ ) -> list[Insight]:
276
328
  """Generate insights from tax opportunities."""
277
329
  insights = []
278
330
 
279
331
  for opp in opportunities:
332
+ # Tax insights use the provided text, but we can add tone to default values
333
+ if tone == "fun":
334
+ default_title = "💰 Tax Savings Alert!"
335
+ default_desc = "Found a way to keep more of your money 🎉"
336
+ default_action = "Chat with a tax pro to lock this in 🔐"
337
+ else:
338
+ default_title = "Tax Opportunity"
339
+ default_desc = "Review this tax opportunity"
340
+ default_action = "Consult with tax professional"
280
341
  insights.append(
281
342
  Insight(
282
343
  id=f"tax_{user_id}_{datetime.now().timestamp()}",
283
344
  user_id=user_id,
284
345
  category=InsightCategory.TAX,
285
346
  priority=InsightPriority.HIGH,
286
- title=opp.get("title", "Tax Opportunity"),
287
- description=opp.get("description", "Review this tax opportunity"),
288
- action=opp.get("action", "Consult with tax professional"),
347
+ title=opp.get("title", default_title),
348
+ description=opp.get("description", default_desc),
349
+ action=opp.get("action", default_action),
289
350
  value=opp.get("value"),
290
351
  metadata=opp.get("metadata"),
291
352
  )
@@ -74,6 +74,7 @@ class Position(BaseModel):
74
74
  """Position model for current holdings."""
75
75
 
76
76
  symbol: str = Field(description="Trading symbol")
77
+ name: str | None = Field(None, description="Security name")
77
78
  qty: Decimal = Field(description="Total quantity held")
78
79
  side: Literal["long", "short"] = Field(description="Position side: long or short")
79
80
  avg_entry_price: Decimal = Field(description="Average entry price")
@@ -52,25 +52,42 @@ class TellerClient(BankingProvider):
52
52
  self,
53
53
  cert_path: str | None = None,
54
54
  key_path: str | None = None,
55
+ cert_content: str | None = None,
56
+ key_content: str | None = None,
55
57
  environment: str = "sandbox",
56
58
  timeout: float = 30.0,
57
59
  ) -> None:
58
60
  """Initialize TellerClient banking provider.
59
61
 
60
62
  Args:
61
- cert_path: Path to certificate.pem file (required for real usage)
62
- key_path: Path to private_key.pem file (required for real usage)
63
+ cert_path: Path to certificate.pem file
64
+ key_path: Path to private_key.pem file
65
+ cert_content: Inline certificate PEM content (alternative to cert_path)
66
+ key_content: Inline private key PEM content (alternative to key_path)
63
67
  environment: "sandbox" or "production" (default: sandbox)
64
68
  timeout: HTTP request timeout in seconds (default: 30.0)
65
69
 
70
+ Note:
71
+ Either (cert_path, key_path) or (cert_content, key_content) must be provided
72
+ for production. The inline content options are useful for Railway/Vercel
73
+ where env vars are preferred over mounted files.
74
+
66
75
  Raises:
67
- ValueError: If cert/key paths are missing in production environment
76
+ ValueError: If cert/key are missing in production environment
68
77
  """
69
- if environment == "production" and (not cert_path or not key_path):
70
- raise ValueError("cert_path and key_path are required for production environment")
78
+ has_file_creds = cert_path and key_path
79
+ has_inline_creds = cert_content and key_content
80
+
81
+ if environment == "production" and not (has_file_creds or has_inline_creds):
82
+ raise ValueError(
83
+ "Either (cert_path, key_path) or (cert_content, key_content) "
84
+ "are required for production environment"
85
+ )
71
86
 
72
87
  self.cert_path = cert_path
73
88
  self.key_path = key_path
89
+ self.cert_content = cert_content
90
+ self.key_content = key_content
74
91
  self.environment = environment
75
92
  self.timeout = timeout
76
93
 
@@ -81,18 +98,36 @@ class TellerClient(BankingProvider):
81
98
  self.base_url = "https://api.teller.io"
82
99
 
83
100
  # Create HTTP client with mTLS certificate authentication
84
- client_kwargs = {
101
+ client_kwargs: dict = {
85
102
  "base_url": self.base_url,
86
103
  "timeout": timeout,
87
104
  "headers": {"User-Agent": "fin-infra/1.0"},
88
105
  }
89
106
 
90
107
  # Add certificate using SSL context (recommended approach, not deprecated)
91
- if cert_path and key_path:
92
- # Create SSL context with client certificate
108
+ if has_file_creds:
109
+ # Use file paths directly (assertions for type narrowing)
110
+ assert cert_path is not None
111
+ assert key_path is not None
93
112
  ssl_context = ssl.create_default_context()
94
113
  ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
95
114
  client_kwargs["verify"] = ssl_context
115
+ elif has_inline_creds:
116
+ # Write inline content to temp files for SSL context (assertions for type narrowing)
117
+ assert cert_content is not None
118
+ assert key_content is not None
119
+ import tempfile
120
+
121
+ self._cert_file = tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False)
122
+ self._key_file = tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False)
123
+ self._cert_file.write(cert_content)
124
+ self._key_file.write(key_content)
125
+ self._cert_file.close()
126
+ self._key_file.close()
127
+
128
+ ssl_context = ssl.create_default_context()
129
+ ssl_context.load_cert_chain(certfile=self._cert_file.name, keyfile=self._key_file.name)
130
+ client_kwargs["verify"] = ssl_context
96
131
 
97
132
  # Create client with explicit parameters to satisfy type checker
98
133
  self.client = httpx.Client(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: Financial infrastructure toolkit: banking connections, market data, credit, cashflows, and brokerage integrations
5
5
  License: MIT
6
6
  Keywords: finance,banking,plaid,brokerage,markets,credit,tax,cashflow,fintech,infra
@@ -26,20 +26,26 @@ Provides-Extra: markets
26
26
  Provides-Extra: plaid
27
27
  Provides-Extra: yahoo
28
28
  Requires-Dist: ai-infra (>=0.1.142)
29
+ Requires-Dist: authlib (>=1.6.6)
29
30
  Requires-Dist: cashews[redis] (>=7.0)
30
31
  Requires-Dist: ccxt (>=4.0.0) ; extra == "markets" or extra == "crypto" or extra == "all"
31
32
  Requires-Dist: fastapi-users (>=15.0.2,<16.0.0)
33
+ Requires-Dist: filelock (>=3.20.3)
32
34
  Requires-Dist: httpx (>=0.25.0)
33
35
  Requires-Dist: langchain-core (>=1.2.5,<2.0.0)
34
36
  Requires-Dist: loguru (>=0.7.0)
35
37
  Requires-Dist: numpy (>=1.24.0)
36
38
  Requires-Dist: numpy-financial (>=1.0.0)
37
39
  Requires-Dist: plaid-python (>=25.0.0) ; extra == "plaid" or extra == "banking" or extra == "all"
40
+ Requires-Dist: protobuf (>=6.33.3)
38
41
  Requires-Dist: pydantic (>=2.0)
39
42
  Requires-Dist: pydantic-settings (>=2.0)
40
43
  Requires-Dist: python-dotenv (>=1.0.0)
41
44
  Requires-Dist: tenacity (>=8.0.0)
42
45
  Requires-Dist: typing-extensions (>=4.0)
46
+ Requires-Dist: urllib3 (>=2.6.3)
47
+ Requires-Dist: virtualenv (>=20.36.1)
48
+ Requires-Dist: werkzeug (>=3.1.5)
43
49
  Requires-Dist: yahooquery (>=2.3.0) ; extra == "markets" or extra == "yahoo" or extra == "all"
44
50
  Project-URL: Documentation, https://nfrax.com/fin-infra
45
51
  Project-URL: Homepage, https://github.com/nfraxlab/fin-infra
@@ -1,17 +1,19 @@
1
1
  fin_infra/__init__.py,sha256=7oL-CCsALNifBODAn9LriicaIrzgJkmVPvE-9duP0mw,1574
2
2
  fin_infra/__main__.py,sha256=1qNP7j0ffw0wFs1dBwDcJ9TNXlC6FcYuulzoV87pMi8,262
3
- fin_infra/analytics/__init__.py,sha256=-OsQUqLgOdafSNzbcIHy3G37nX2OwD0OzGj8C2Aiufs,2325
3
+ fin_infra/analytics/__init__.py,sha256=8RSyh2QieAZPWgnEBYzuG4pv97GgywwjiuAFcMu6Mtw,3006
4
4
  fin_infra/analytics/add.py,sha256=OdITXA5CjijhbBzokV3xlTcJCTiMLBDuUJ8OH9Pwkhw,12651
5
+ fin_infra/analytics/benchmark.py,sha256=YRtIamjh8HRvmvA5yPWWuJIzxyrdS0K-imV__DqVaTk,20356
5
6
  fin_infra/analytics/cash_flow.py,sha256=VSsQHwTi6r6VFoScH9nu9Fj8XHC4O4B9TkAI8Alobm4,10284
6
- fin_infra/analytics/ease.py,sha256=aTAPc-Lmh6XP7REPVlAom38MtKCqeS-auNW-rpXRZJs,14282
7
- fin_infra/analytics/models.py,sha256=XFHeVZoPE0PcSFj59dNsdxISFVwdrTLU4sDCtExumrg,8193
8
- fin_infra/analytics/portfolio.py,sha256=3xP34nQXvLdj_6Bpz5sufRoND5hpGDnmespWAixgh80,26405
7
+ fin_infra/analytics/ease.py,sha256=1xlr0TxJzGzCodktbCzb59tRG9jTLrcLQAxSWKFEs2I,15410
8
+ fin_infra/analytics/models.py,sha256=oebYQUI9-qIYKNyNSlr0P6BTO8lJJPUX0TeR6hFN_0s,8426
9
+ fin_infra/analytics/portfolio.py,sha256=pT6m19H8GVYD3gZvGPjTDtT9l3ieMxmx70Jx_Y6tmR8,29967
9
10
  fin_infra/analytics/projections.py,sha256=T7LLG0Pe5f-WwgfDITdTMk-l8cCLZTM9cEC_Vxf6mkc,9154
10
- fin_infra/analytics/rebalancing.py,sha256=VM8MgoJofmrCXPK1rbmVqWGB4FauNmHCL5HOEbrZR2g,14610
11
+ fin_infra/analytics/rebalancing.py,sha256=K4qbQa6p4CZ0T3j3mvZLadI45ilIA1rB1vtdlxlFWSc,16189
12
+ fin_infra/analytics/rebalancing_llm.py,sha256=gniB_tIahrYNZEodIZ02uXo6VLJQgq_sGzlA4glJRbg,26935
11
13
  fin_infra/analytics/savings.py,sha256=n3rGNFP8TU5mW-uz9kOuqX_mDiVnDyAeDN06Q7Abotw,7570
12
14
  fin_infra/analytics/scenarios.py,sha256=LE_dZVkbxxAx5sxitGhiOhZfWTlYtVbIvS9pEXkijLc,12246
13
15
  fin_infra/analytics/spending.py,sha256=ogLcfF5ZOLMkBIj02RISnA3hiY_PsLWZ2_AAzA7FenY,26209
14
- fin_infra/banking/__init__.py,sha256=Lt82vzUZUmekNi_abWz5ySvZlO64FVefd05RAxEd4gU,24537
16
+ fin_infra/banking/__init__.py,sha256=ZkWhm9QsKyYYKZ4VJ6sw0HjFdUkO9-mTYtPpUXsJjuQ,24828
15
17
  fin_infra/banking/history.py,sha256=YB-v9A03IZ_qki6A6mA-RO5Y4imlqk1CyP0W482ufdQ,10563
16
18
  fin_infra/banking/utils.py,sha256=HhxZbeaA8zqVttgMiJGnShTo_r_0DaD7T3IMq8n8340,15252
17
19
  fin_infra/brokerage/__init__.py,sha256=IXm5ko5T607LodexYSbKNZc6I_2CQGaOwQUlb-w9-ks,17137
@@ -33,7 +35,7 @@ fin_infra/categorization/__init__.py,sha256=p5uEFX0piPI5QmYtQ4lTrr0Cph5r01XpHDOi
33
35
  fin_infra/categorization/add.py,sha256=-I-V7R8vWj2-B8yjtFn_pNryQV5uascFR18a1Ltcqm0,6247
34
36
  fin_infra/categorization/ease.py,sha256=gvlVb_TL7kwm5oM_gWdrhZt4cJOSvYEgGipNJKK8CpM,5820
35
37
  fin_infra/categorization/engine.py,sha256=tZ-WTkPUUcKe6rDFoCSu5BF_gezcjJfx8Yn9eUX4lDQ,12114
36
- fin_infra/categorization/llm_layer.py,sha256=K3vn3Muf6VfsSDT-iRcKBJQyO-BSx8nfLQlK-UmBmG0,12824
38
+ fin_infra/categorization/llm_layer.py,sha256=7nFZf-S-JppGz6nXfvEmjr1-ouE1BR7fyiOYkUE6gnw,12822
37
39
  fin_infra/categorization/models.py,sha256=A8m3hAIbsUUV3eNjXpTiPWD1sYGjSdXEByTS3ZZASEA,5894
38
40
  fin_infra/categorization/rules.py,sha256=IpDnHBeuykRdu5vs3Lph4Y9-3RseIjjleQ5hZphQvNk,12849
39
41
  fin_infra/categorization/taxonomy.py,sha256=p-tSOwJ0O-rFZ1LIlHSYdaYdSc65084j0fdMI_6LW84,13251
@@ -76,8 +78,8 @@ fin_infra/goals/scaffold_templates/__init__.py,sha256=rLFam-mRsj8LvJu5kRBEIJtw9r
76
78
  fin_infra/goals/scaffold_templates/models.py.tmpl,sha256=b23Nlwm05MFMQE4qkrylTPXqulsN6cuFzNev2liY7DI,5714
77
79
  fin_infra/goals/scaffold_templates/repository.py.tmpl,sha256=4BFy-fPBR412p8wb8VzsekxM3uGno-odqZP_BuMAXBU,11046
78
80
  fin_infra/goals/scaffold_templates/schemas.py.tmpl,sha256=M1hS1pK9UDXcNqPW-NGu9804hTFe4FPdUDVgDSMcQl4,5331
79
- fin_infra/insights/__init__.py,sha256=fFYymoAY2zd7eooE-RqyFifXB_J-vVkHjyMak7o4wnQ,3984
80
- fin_infra/insights/aggregator.py,sha256=HtaJipSA-O_HVComBcyQdkGs6guoW81sYwRYHXGdBJI,10251
81
+ fin_infra/insights/__init__.py,sha256=zvyUNrs8Eyss0uip-yHMaRZo2afR9hO16r3mt5UWZY0,4016
82
+ fin_infra/insights/aggregator.py,sha256=a7fPc_nDb7OZ1rH0kPhdFL19vquoGqLhot7Kv3gRp2E,12762
81
83
  fin_infra/insights/models.py,sha256=xov_YV8oBLJt3YdyVjbryRfcXqmGeGiPvZsZHSbvtl8,3202
82
84
  fin_infra/investments/__init__.py,sha256=hJDHlKNXG0Hr3zsJeXdTEyvxL9PkVR8NkS2ULKnELWY,6727
83
85
  fin_infra/investments/add.py,sha256=UV2_99z1p8cUiifVFJXOF0lGd0ucMgZ5s9N7IFyE_NY,17193
@@ -95,7 +97,7 @@ fin_infra/investments/scaffold_templates/schemas.py.tmpl,sha256=knWmn-Kyr7AdgPD4
95
97
  fin_infra/markets/__init__.py,sha256=pVPtfOZxIeHsPuCDOCydmJjdsOc44R9B2d5EbCZNhRU,9863
96
98
  fin_infra/models/__init__.py,sha256=y94RJ_1-bzgNUCxqE76X56WIOk3-El_Jueqy7uB0rb8,860
97
99
  fin_infra/models/accounts.py,sha256=m_HdYHOe_m0GLnc_f5njo9n-zscWu-C0rJB6SAd5-aY,1098
98
- fin_infra/models/brokerage.py,sha256=TV5KMe78e-ttjcUbZIfdGo3x0NisAQ3puwv_ehtgSHc,8312
100
+ fin_infra/models/brokerage.py,sha256=0U--q2uEJrNankuC3zNMnQEp4Zej0l7ZE7Zq7Hum8X4,8376
99
101
  fin_infra/models/candle.py,sha256=5JeqUvqlFfYE61uViBeLKZCh2wKdJZH7Pvi-TLzN4FI,536
100
102
  fin_infra/models/credit.py,sha256=rSdSURsMe9_i2gxmwPTDwNQWOuM2zutL-OhvHsnbtmw,12144
101
103
  fin_infra/models/money.py,sha256=63pdGD1WBMHicJ1w7pbU1g5fqt4gIzPuqQQ2-NSlBuc,401
@@ -127,7 +129,7 @@ fin_infra/obs/classifier.py,sha256=S7kSphgHN1O4GiMUdr3IjuXpoXU0XgGq132_U-njXX4,5
127
129
  fin_infra/providers/__init__.py,sha256=jxhQm79T6DVXf7Wpy7luL-p50cE_IMUbjt4o3apzJQU,768
128
130
  fin_infra/providers/banking/base.py,sha256=KeNU4ur3zLKHVsBF1LQifcs2AKX06IEE-Rx_SetFeAs,102
129
131
  fin_infra/providers/banking/plaid_client.py,sha256=6yaXbPxg4QvebZAXyUFUthBIAN1EuXN59BnmTi69n0U,7374
130
- fin_infra/providers/banking/teller_client.py,sha256=4chf7fQIybp-sgOWOiW9uJfMq31A9KMPcyMfn0Yi_IY,10752
132
+ fin_infra/providers/banking/teller_client.py,sha256=c8wT0M0jLHdgxCxeNnJ7X9S8-qITUHEdd3oTWe_6z1U,12371
131
133
  fin_infra/providers/base.py,sha256=T8XoHTc3SY30XibfrS4eAMXTujgY8s2mJjBeKzscgDo,9037
132
134
  fin_infra/providers/brokerage/alpaca.py,sha256=BObiI_dFQZ3fOpTfmZMkri8sVrsz5uW6i5ZVUb0etCU,9923
133
135
  fin_infra/providers/brokerage/base.py,sha256=JJFH0Cqca4Rg4rmxfiwcQt-peRoBf4JpG3g6jx8DVks,106
@@ -174,8 +176,8 @@ fin_infra/utils/deprecation.py,sha256=DTcqv7ECnrWOOwoA07JOnRci4Hqqo9YtKSSmoS-DVP
174
176
  fin_infra/utils/http.py,sha256=rDEgYsEBrEe75ml5RA-iSs3xeU5W-3j-czJlT7WbrM4,632
175
177
  fin_infra/utils/retry.py,sha256=YiyTgy26eJ1ah7fE2_-ZPa4hv4bIT4OzjYolkNWb5j0,1057
176
178
  fin_infra/version.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
177
- fin_infra-0.6.0.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
178
- fin_infra-0.6.0.dist-info/METADATA,sha256=47DVsqLkuor-waj8iItVs9AECCOctwqOfzfqHM60Qsc,10842
179
- fin_infra-0.6.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
180
- fin_infra-0.6.0.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
181
- fin_infra-0.6.0.dist-info/RECORD,,
179
+ fin_infra-0.8.0.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
180
+ fin_infra-0.8.0.dist-info/METADATA,sha256=9jjpWcj8Jb0gD9sX9xs0P3uJvm2Y4y6M53H_sfxzy7A,11050
181
+ fin_infra-0.8.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
182
+ fin_infra-0.8.0.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
183
+ fin_infra-0.8.0.dist-info/RECORD,,