fin-infra 0.1.63__py3-none-any.whl → 0.1.65__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.
- fin_infra/analytics/spending.py +9 -7
- fin_infra/banking/__init__.py +5 -2
- fin_infra/brokerage/__init__.py +4 -2
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +15 -16
- fin_infra/categorization/ease.py +1 -1
- fin_infra/categorization/engine.py +1 -1
- fin_infra/categorization/llm_layer.py +2 -2
- fin_infra/chat/__init__.py +1 -10
- fin_infra/chat/ease.py +1 -1
- fin_infra/chat/planning.py +57 -0
- fin_infra/crypto/insights.py +1 -1
- fin_infra/investments/__init__.py +11 -4
- fin_infra/investments/ease.py +11 -7
- fin_infra/investments/models.py +1 -1
- fin_infra/investments/providers/plaid.py +1 -1
- fin_infra/markets/__init__.py +8 -3
- fin_infra/net_worth/ease.py +34 -13
- fin_infra/normalization/__init__.py +6 -6
- fin_infra/providers/banking/teller_client.py +7 -1
- fin_infra/providers/base.py +98 -1
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/tax/mock.py +3 -3
- fin_infra/recurring/add.py +10 -1
- fin_infra/recurring/detectors_llm.py +1 -1
- fin_infra/recurring/insights.py +1 -1
- fin_infra/recurring/normalizers.py +1 -1
- fin_infra/security/token_store.py +3 -1
- {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/METADATA +1 -1
- {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/RECORD +33 -33
- {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.63.dist-info → fin_infra-0.1.65.dist-info}/entry_points.txt +0 -0
fin_infra/analytics/spending.py
CHANGED
|
@@ -162,12 +162,13 @@ async def analyze_spending(
|
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
return SpendingInsight(
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
# Convert Decimal to float for model compatibility (intentional for Pydantic field types)
|
|
166
|
+
top_merchants=[(m, float(v)) for m, v in top_merchants],
|
|
167
|
+
category_breakdown={k: float(v) for k, v in category_totals.items()},
|
|
167
168
|
spending_trends=spending_trends,
|
|
168
169
|
anomalies=anomalies,
|
|
169
170
|
period_days=days,
|
|
170
|
-
total_spending=total_spending,
|
|
171
|
+
total_spending=float(total_spending) if total_spending else 0.0,
|
|
171
172
|
)
|
|
172
173
|
|
|
173
174
|
|
|
@@ -359,9 +360,10 @@ async def _detect_spending_anomalies(
|
|
|
359
360
|
anomalies.append(
|
|
360
361
|
SpendingAnomaly(
|
|
361
362
|
category=category,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
# Convert Decimal to float for model compatibility (intentional for Pydantic field types)
|
|
364
|
+
current_amount=float(current_amount),
|
|
365
|
+
average_amount=float(average_amount),
|
|
366
|
+
deviation_percent=float(deviation_percent),
|
|
365
367
|
severity=severity,
|
|
366
368
|
)
|
|
367
369
|
)
|
|
@@ -468,7 +470,7 @@ async def generate_spending_insights(
|
|
|
468
470
|
|
|
469
471
|
# Try to import ai-infra LLM (optional dependency)
|
|
470
472
|
try:
|
|
471
|
-
from ai_infra.llm import LLM
|
|
473
|
+
from ai_infra.llm import LLM # type: ignore[attr-defined]
|
|
472
474
|
except ImportError:
|
|
473
475
|
# Graceful degradation: return rule-based insights
|
|
474
476
|
return _generate_rule_based_insights(spending_insight, user_context)
|
fin_infra/banking/__init__.py
CHANGED
|
@@ -397,10 +397,13 @@ def add_banking(
|
|
|
397
397
|
}
|
|
398
398
|
"""
|
|
399
399
|
# Get all transactions from provider
|
|
400
|
+
# Convert date to ISO string format as expected by BankingProvider.transactions()
|
|
401
|
+
start_date_str: str | None = start_date.isoformat() if start_date else None
|
|
402
|
+
end_date_str: str | None = end_date.isoformat() if end_date else None
|
|
400
403
|
transactions = banking.transactions(
|
|
401
404
|
access_token=access_token,
|
|
402
|
-
start_date=
|
|
403
|
-
end_date=
|
|
405
|
+
start_date=start_date_str,
|
|
406
|
+
end_date=end_date_str,
|
|
404
407
|
)
|
|
405
408
|
|
|
406
409
|
# Apply filters
|
fin_infra/brokerage/__init__.py
CHANGED
|
@@ -212,7 +212,9 @@ def add_brokerage(
|
|
|
212
212
|
|
|
213
213
|
# Initialize provider if string or None
|
|
214
214
|
if isinstance(provider, str):
|
|
215
|
-
|
|
215
|
+
# Cast provider string to Literal type for type checker
|
|
216
|
+
provider_literal: Literal["alpaca"] | None = provider if provider == "alpaca" else None # type: ignore[assignment]
|
|
217
|
+
brokerage_provider = easy_brokerage(provider=provider_literal, mode=mode, **config)
|
|
216
218
|
elif provider is None:
|
|
217
219
|
brokerage_provider = easy_brokerage(mode=mode, **config)
|
|
218
220
|
else:
|
|
@@ -241,7 +243,7 @@ def add_brokerage(
|
|
|
241
243
|
Returns list of positions with symbol, quantity, P/L, etc.
|
|
242
244
|
"""
|
|
243
245
|
try:
|
|
244
|
-
positions = brokerage_provider.positions()
|
|
246
|
+
positions = list(brokerage_provider.positions()) # Convert Iterable to list for len()
|
|
245
247
|
return {"positions": positions, "count": len(positions)}
|
|
246
248
|
except Exception as e:
|
|
247
249
|
raise HTTPException(status_code=500, detail=f"Error fetching positions: {str(e)}")
|
|
@@ -44,7 +44,7 @@ from .taxonomy import Category, CategoryGroup, get_all_categories, get_category_
|
|
|
44
44
|
try:
|
|
45
45
|
from .llm_layer import LLMCategorizer
|
|
46
46
|
except ImportError:
|
|
47
|
-
LLMCategorizer = None
|
|
47
|
+
LLMCategorizer = None # type: ignore[assignment,misc]
|
|
48
48
|
|
|
49
49
|
__all__ = [
|
|
50
50
|
# Easy setup
|
fin_infra/categorization/add.py
CHANGED
|
@@ -96,7 +96,8 @@ def add_categorization(
|
|
|
96
96
|
start_time = time.perf_counter()
|
|
97
97
|
|
|
98
98
|
try:
|
|
99
|
-
|
|
99
|
+
# Await the async categorize method
|
|
100
|
+
prediction = await engine.categorize(
|
|
100
101
|
merchant_name=request.merchant_name,
|
|
101
102
|
user_id=request.user_id,
|
|
102
103
|
include_alternatives=request.include_alternatives,
|
|
@@ -135,21 +136,19 @@ def add_categorization(
|
|
|
135
136
|
categories = get_all_categories()
|
|
136
137
|
|
|
137
138
|
# Return category metadata
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for cat in categories
|
|
152
|
-
]
|
|
139
|
+
result = []
|
|
140
|
+
for cat in categories:
|
|
141
|
+
meta = get_category_metadata(cat)
|
|
142
|
+
result.append(
|
|
143
|
+
{
|
|
144
|
+
"name": cat.value,
|
|
145
|
+
"group": meta.group.value if meta else None,
|
|
146
|
+
"display_name": meta.display_name if meta else cat.value,
|
|
147
|
+
"description": meta.description if meta else None,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return result
|
|
153
152
|
|
|
154
153
|
@router.get("/stats", response_model=CategoryStats)
|
|
155
154
|
async def get_stats():
|
fin_infra/categorization/ease.py
CHANGED
|
@@ -20,8 +20,8 @@ from pydantic import BaseModel, Field
|
|
|
20
20
|
|
|
21
21
|
# ai-infra imports
|
|
22
22
|
try:
|
|
23
|
-
from ai_infra.llm import LLM
|
|
24
|
-
from ai_infra.llm.providers import Providers
|
|
23
|
+
from ai_infra.llm import LLM # type: ignore[attr-defined]
|
|
24
|
+
from ai_infra.llm.providers import Providers # type: ignore[attr-defined]
|
|
25
25
|
except ImportError:
|
|
26
26
|
raise ImportError("ai-infra not installed. Install with: pip install ai-infra")
|
|
27
27
|
|
fin_infra/chat/__init__.py
CHANGED
|
@@ -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.
|
|
167
|
+
return context.previous_exchanges if context else []
|
|
177
168
|
|
|
178
169
|
@router.delete("/history")
|
|
179
170
|
async def clear_history():
|
fin_infra/chat/ease.py
CHANGED
fin_infra/chat/planning.py
CHANGED
|
@@ -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,
|
fin_infra/crypto/insights.py
CHANGED
|
@@ -51,7 +51,8 @@ from typing import TYPE_CHECKING, Literal
|
|
|
51
51
|
if TYPE_CHECKING:
|
|
52
52
|
from fastapi import FastAPI
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Use the local InvestmentProvider base class (same as providers use)
|
|
55
|
+
from .providers.base import InvestmentProvider
|
|
55
56
|
|
|
56
57
|
# Lazy imports to avoid loading provider SDKs unless needed
|
|
57
58
|
_provider_cache: dict[str, InvestmentProvider] = {}
|
|
@@ -114,6 +115,7 @@ def easy_investments(
|
|
|
114
115
|
return _provider_cache[cache_key]
|
|
115
116
|
|
|
116
117
|
# Lazy import and initialize provider
|
|
118
|
+
instance: InvestmentProvider
|
|
117
119
|
if provider == "plaid":
|
|
118
120
|
from .providers.plaid import PlaidInvestmentProvider
|
|
119
121
|
|
|
@@ -172,14 +174,19 @@ def add_investments(
|
|
|
172
174
|
>>> # GET /investments/transactions
|
|
173
175
|
>>> # etc.
|
|
174
176
|
"""
|
|
175
|
-
from .add import add_investments_impl
|
|
177
|
+
from .add import add_investments as add_investments_impl
|
|
178
|
+
from .providers.base import InvestmentProvider as InvestmentProviderBase
|
|
179
|
+
|
|
180
|
+
# Resolve provider from string Literal to actual InvestmentProvider instance
|
|
181
|
+
resolved_provider: InvestmentProviderBase | None = None
|
|
182
|
+
if provider is not None:
|
|
183
|
+
resolved_provider = easy_investments(provider=provider, **provider_config) # type: ignore[assignment]
|
|
176
184
|
|
|
177
185
|
return add_investments_impl(
|
|
178
186
|
app,
|
|
179
|
-
provider=
|
|
187
|
+
provider=resolved_provider,
|
|
180
188
|
prefix=prefix,
|
|
181
189
|
tags=tags or ["Investments"],
|
|
182
|
-
**provider_config,
|
|
183
190
|
)
|
|
184
191
|
|
|
185
192
|
|
fin_infra/investments/ease.py
CHANGED
|
@@ -112,19 +112,20 @@ def easy_investments(
|
|
|
112
112
|
- Most other SnapTrade brokerages support trading operations
|
|
113
113
|
"""
|
|
114
114
|
# Auto-detect provider from environment if not specified
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
detected_provider: str | None = provider
|
|
116
|
+
if detected_provider is None:
|
|
117
|
+
detected_provider = _detect_provider()
|
|
117
118
|
|
|
118
119
|
# Validate provider
|
|
119
|
-
if
|
|
120
|
+
if detected_provider not in ("plaid", "snaptrade"):
|
|
120
121
|
raise ValueError(
|
|
121
|
-
f"Invalid provider: {
|
|
122
|
+
f"Invalid provider: {detected_provider}. Must be 'plaid' or 'snaptrade'."
|
|
122
123
|
)
|
|
123
124
|
|
|
124
125
|
# Instantiate provider
|
|
125
|
-
if
|
|
126
|
+
if detected_provider == "plaid":
|
|
126
127
|
return _create_plaid_provider(**config)
|
|
127
|
-
elif
|
|
128
|
+
elif detected_provider == "snaptrade":
|
|
128
129
|
return _create_snaptrade_provider(**config)
|
|
129
130
|
|
|
130
131
|
# Should never reach here
|
|
@@ -233,10 +234,13 @@ def _create_snaptrade_provider(**config: Any) -> InvestmentProvider:
|
|
|
233
234
|
"Example: easy_investments(provider='snaptrade', client_id='...', consumer_key='...')"
|
|
234
235
|
)
|
|
235
236
|
|
|
237
|
+
# Ensure base_url is a string (default is set in SnapTradeInvestmentProvider)
|
|
238
|
+
resolved_base_url: str = base_url if isinstance(base_url, str) else "https://api.snaptrade.com/api/v1"
|
|
239
|
+
|
|
236
240
|
return SnapTradeInvestmentProvider(
|
|
237
241
|
client_id=client_id,
|
|
238
242
|
consumer_key=consumer_key,
|
|
239
|
-
base_url=
|
|
243
|
+
base_url=resolved_base_url,
|
|
240
244
|
)
|
|
241
245
|
|
|
242
246
|
|
fin_infra/investments/models.py
CHANGED
|
@@ -370,7 +370,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
370
370
|
isin=plaid_security.get("isin"),
|
|
371
371
|
sedol=plaid_security.get("sedol"),
|
|
372
372
|
ticker_symbol=plaid_security.get("ticker_symbol"),
|
|
373
|
-
name=plaid_security.get("name"),
|
|
373
|
+
name=plaid_security.get("name") or "Unknown Security",
|
|
374
374
|
type=self._normalize_security_type(plaid_security.get("type", "other")),
|
|
375
375
|
sector=plaid_security.get("sector"),
|
|
376
376
|
close_price=close_price,
|
fin_infra/markets/__init__.py
CHANGED
|
@@ -193,7 +193,11 @@ def add_market_data(
|
|
|
193
193
|
if isinstance(provider, MarketDataProvider):
|
|
194
194
|
market = provider
|
|
195
195
|
else:
|
|
196
|
-
|
|
196
|
+
# Cast provider to Literal type for type checker
|
|
197
|
+
provider_literal: Literal["alphavantage", "yahoo"] | None = (
|
|
198
|
+
provider if provider in ("alphavantage", "yahoo", None) else None # type: ignore[assignment]
|
|
199
|
+
)
|
|
200
|
+
market = easy_market(provider=provider_literal, **config)
|
|
197
201
|
|
|
198
202
|
# Create router (public - no auth required)
|
|
199
203
|
router = public_router(prefix=prefix, tags=["Market Data"])
|
|
@@ -223,14 +227,15 @@ def add_market_data(
|
|
|
223
227
|
try:
|
|
224
228
|
candles = market.history(symbol, period=period, interval=interval)
|
|
225
229
|
# Convert to dicts if they're Pydantic models
|
|
226
|
-
candles_list = []
|
|
230
|
+
candles_list: list[dict] = []
|
|
227
231
|
for candle in candles:
|
|
228
232
|
if hasattr(candle, "model_dump"):
|
|
229
233
|
candles_list.append(candle.model_dump())
|
|
230
234
|
elif hasattr(candle, "dict"):
|
|
231
235
|
candles_list.append(candle.dict())
|
|
232
236
|
else:
|
|
233
|
-
|
|
237
|
+
# Cast to dict for type compatibility
|
|
238
|
+
candles_list.append(dict(candle) if hasattr(candle, "__iter__") else {"data": candle})
|
|
234
239
|
return {"candles": candles_list}
|
|
235
240
|
except Exception as e:
|
|
236
241
|
raise HTTPException(status_code=400, detail=str(e))
|
fin_infra/net_worth/ease.py
CHANGED
|
@@ -121,6 +121,15 @@ class NetWorthTracker:
|
|
|
121
121
|
self.goal_tracker = goal_tracker
|
|
122
122
|
self.conversation = conversation
|
|
123
123
|
|
|
124
|
+
# Configuration set by easy_net_worth(); declared here for type checkers.
|
|
125
|
+
self.snapshot_schedule: str = "daily"
|
|
126
|
+
self.change_threshold_percent: float = 5.0
|
|
127
|
+
self.change_threshold_amount: float = 10000.0
|
|
128
|
+
self.enable_llm: bool = False
|
|
129
|
+
self.llm_provider: str | None = None
|
|
130
|
+
self.llm_model: str | None = None
|
|
131
|
+
self.config: dict[str, Any] = {}
|
|
132
|
+
|
|
124
133
|
async def calculate_net_worth(
|
|
125
134
|
self,
|
|
126
135
|
user_id: str,
|
|
@@ -368,12 +377,20 @@ def easy_net_worth(
|
|
|
368
377
|
|
|
369
378
|
if enable_llm:
|
|
370
379
|
try:
|
|
371
|
-
from ai_infra.llm import LLM
|
|
380
|
+
from ai_infra.llm.llm import LLM # type: ignore[attr-defined]
|
|
372
381
|
except ImportError:
|
|
373
382
|
raise ImportError(
|
|
374
383
|
"LLM features require ai-infra package. " "Install with: pip install ai-infra"
|
|
375
384
|
)
|
|
376
385
|
|
|
386
|
+
cache = None
|
|
387
|
+
try:
|
|
388
|
+
from svc_infra.cache import get_cache
|
|
389
|
+
|
|
390
|
+
cache = get_cache()
|
|
391
|
+
except Exception:
|
|
392
|
+
cache = None
|
|
393
|
+
|
|
377
394
|
# Determine default model
|
|
378
395
|
default_models = {
|
|
379
396
|
"google": "gemini-2.0-flash-exp",
|
|
@@ -416,18 +433,22 @@ def easy_net_worth(
|
|
|
416
433
|
# goals.management not yet implemented, skip
|
|
417
434
|
pass
|
|
418
435
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
436
|
+
if cache is not None:
|
|
437
|
+
try:
|
|
438
|
+
from fin_infra.conversation import FinancialPlanningConversation
|
|
439
|
+
|
|
440
|
+
conversation = FinancialPlanningConversation(
|
|
441
|
+
llm=llm,
|
|
442
|
+
cache=cache, # Required for context storage
|
|
443
|
+
provider=llm_provider,
|
|
444
|
+
model_name=model_name,
|
|
445
|
+
)
|
|
446
|
+
except ImportError:
|
|
447
|
+
# conversation module not yet implemented, skip
|
|
448
|
+
pass
|
|
449
|
+
except Exception:
|
|
450
|
+
# Cache not configured or other runtime issue; skip optional conversation wiring.
|
|
451
|
+
pass
|
|
431
452
|
|
|
432
453
|
# Create tracker
|
|
433
454
|
tracker = NetWorthTracker(
|
|
@@ -150,14 +150,14 @@ def add_normalization(
|
|
|
150
150
|
):
|
|
151
151
|
"""Convert amount between currencies."""
|
|
152
152
|
try:
|
|
153
|
-
result = await converter.
|
|
153
|
+
result = await converter.convert_with_details(amount, from_currency, to_currency)
|
|
154
154
|
return {
|
|
155
|
-
"amount": amount,
|
|
156
|
-
"from_currency": from_currency,
|
|
157
|
-
"to_currency": to_currency,
|
|
158
|
-
"result": result.
|
|
155
|
+
"amount": result.amount,
|
|
156
|
+
"from_currency": result.from_currency,
|
|
157
|
+
"to_currency": result.to_currency,
|
|
158
|
+
"result": result.converted,
|
|
159
159
|
"rate": result.rate,
|
|
160
|
-
"timestamp": result.
|
|
160
|
+
"timestamp": result.date.isoformat() if result.date else None,
|
|
161
161
|
}
|
|
162
162
|
except CurrencyNotSupportedError as e:
|
|
163
163
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -93,7 +93,13 @@ class TellerClient(BankingProvider):
|
|
|
93
93
|
ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
|
|
94
94
|
client_kwargs["verify"] = ssl_context
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
# Create client with explicit parameters to satisfy type checker
|
|
97
|
+
self.client = httpx.Client(
|
|
98
|
+
base_url=str(client_kwargs["base_url"]),
|
|
99
|
+
timeout=float(client_kwargs["timeout"]), # type: ignore[arg-type]
|
|
100
|
+
headers=client_kwargs["headers"], # type: ignore[arg-type]
|
|
101
|
+
verify=client_kwargs.get("verify", True), # type: ignore[arg-type]
|
|
102
|
+
)
|
|
97
103
|
|
|
98
104
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
99
105
|
"""Make HTTP request to Teller API with error handling.
|
fin_infra/providers/base.py
CHANGED
|
@@ -67,7 +67,15 @@ class BankingProvider(ABC):
|
|
|
67
67
|
class BrokerageProvider(ABC):
|
|
68
68
|
@abstractmethod
|
|
69
69
|
def submit_order(
|
|
70
|
-
self,
|
|
70
|
+
self,
|
|
71
|
+
symbol: str,
|
|
72
|
+
qty: float,
|
|
73
|
+
side: str,
|
|
74
|
+
type_: str,
|
|
75
|
+
time_in_force: str,
|
|
76
|
+
limit_price: float | None = None,
|
|
77
|
+
stop_price: float | None = None,
|
|
78
|
+
client_order_id: str | None = None,
|
|
71
79
|
) -> dict:
|
|
72
80
|
pass
|
|
73
81
|
|
|
@@ -75,6 +83,71 @@ class BrokerageProvider(ABC):
|
|
|
75
83
|
def positions(self) -> Iterable[dict]:
|
|
76
84
|
pass
|
|
77
85
|
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def get_account(self) -> dict:
|
|
88
|
+
"""Get trading account information."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def get_position(self, symbol: str) -> dict:
|
|
93
|
+
"""Get position for a specific symbol."""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def close_position(self, symbol: str) -> dict:
|
|
98
|
+
"""Close a position (market sell/cover)."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def list_orders(self, status: str = "open", limit: int = 50) -> list[dict]:
|
|
103
|
+
"""List orders."""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def get_order(self, order_id: str) -> dict:
|
|
108
|
+
"""Get order by ID."""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def cancel_order(self, order_id: str) -> None:
|
|
113
|
+
"""Cancel an order."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def get_portfolio_history(self, period: str = "1M", timeframe: str = "1D") -> dict:
|
|
118
|
+
"""Get portfolio value history."""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def create_watchlist(self, name: str, symbols: list[str] | None = None) -> dict:
|
|
123
|
+
"""Create a new watchlist."""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def list_watchlists(self) -> list[dict]:
|
|
128
|
+
"""List all watchlists."""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def get_watchlist(self, watchlist_id: str) -> dict:
|
|
133
|
+
"""Get a watchlist by ID."""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def delete_watchlist(self, watchlist_id: str) -> None:
|
|
138
|
+
"""Delete a watchlist."""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def add_to_watchlist(self, watchlist_id: str, symbol: str) -> dict:
|
|
143
|
+
"""Add a symbol to a watchlist."""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def remove_from_watchlist(self, watchlist_id: str, symbol: str) -> dict:
|
|
148
|
+
"""Remove a symbol from a watchlist."""
|
|
149
|
+
pass
|
|
150
|
+
|
|
78
151
|
|
|
79
152
|
class IdentityProvider(ABC):
|
|
80
153
|
@abstractmethod
|
|
@@ -91,6 +164,11 @@ class CreditProvider(ABC):
|
|
|
91
164
|
def get_credit_score(self, user_id: str, **kwargs) -> dict | None:
|
|
92
165
|
pass
|
|
93
166
|
|
|
167
|
+
@abstractmethod
|
|
168
|
+
def get_credit_report(self, user_id: str, **kwargs) -> dict | None:
|
|
169
|
+
"""Retrieve full credit report for a user."""
|
|
170
|
+
pass
|
|
171
|
+
|
|
94
172
|
|
|
95
173
|
class TaxProvider(ABC):
|
|
96
174
|
"""Provider for tax data and document retrieval."""
|
|
@@ -100,6 +178,11 @@ class TaxProvider(ABC):
|
|
|
100
178
|
"""Retrieve tax forms for a user and tax year."""
|
|
101
179
|
pass
|
|
102
180
|
|
|
181
|
+
@abstractmethod
|
|
182
|
+
def get_tax_documents(self, user_id: str, tax_year: int, **kwargs) -> list[dict]:
|
|
183
|
+
"""Retrieve tax documents for a user and tax year."""
|
|
184
|
+
pass
|
|
185
|
+
|
|
103
186
|
@abstractmethod
|
|
104
187
|
def get_tax_document(self, document_id: str, **kwargs) -> dict:
|
|
105
188
|
"""Retrieve a specific tax document by ID."""
|
|
@@ -110,6 +193,20 @@ class TaxProvider(ABC):
|
|
|
110
193
|
"""Calculate capital gains from crypto transactions."""
|
|
111
194
|
pass
|
|
112
195
|
|
|
196
|
+
@abstractmethod
|
|
197
|
+
def calculate_tax_liability(
|
|
198
|
+
self,
|
|
199
|
+
user_id: str,
|
|
200
|
+
income: float,
|
|
201
|
+
deductions: float,
|
|
202
|
+
filing_status: str,
|
|
203
|
+
tax_year: int,
|
|
204
|
+
state: str | None = None,
|
|
205
|
+
**kwargs,
|
|
206
|
+
) -> dict:
|
|
207
|
+
"""Calculate estimated tax liability."""
|
|
208
|
+
pass
|
|
209
|
+
|
|
113
210
|
|
|
114
211
|
class InvestmentProvider(ABC):
|
|
115
212
|
"""Provider for investment holdings and portfolio data (Plaid, SnapTrade).
|
|
@@ -11,3 +11,8 @@ class ExperianCredit(CreditProvider):
|
|
|
11
11
|
self, user_id: str, **kwargs
|
|
12
12
|
) -> dict | None: # pragma: no cover - placeholder
|
|
13
13
|
return None
|
|
14
|
+
|
|
15
|
+
def get_credit_report(
|
|
16
|
+
self, user_id: str, **kwargs
|
|
17
|
+
) -> dict | None: # pragma: no cover - placeholder
|
|
18
|
+
return None
|
fin_infra/providers/tax/mock.py
CHANGED
|
@@ -313,9 +313,9 @@ class MockTaxProvider(TaxProvider):
|
|
|
313
313
|
return CryptoTaxReport(
|
|
314
314
|
user_id=user_id,
|
|
315
315
|
tax_year=tax_year,
|
|
316
|
-
total_gain_loss=short_term + long_term,
|
|
317
|
-
short_term_gain_loss=short_term,
|
|
318
|
-
long_term_gain_loss=long_term,
|
|
316
|
+
total_gain_loss=Decimal(short_term + long_term),
|
|
317
|
+
short_term_gain_loss=Decimal(short_term),
|
|
318
|
+
long_term_gain_loss=Decimal(long_term),
|
|
319
319
|
transaction_count=len(crypto_transactions),
|
|
320
320
|
cost_basis_method=cost_basis_method,
|
|
321
321
|
transactions=crypto_transactions,
|
fin_infra/recurring/add.py
CHANGED
|
@@ -403,7 +403,16 @@ def add_recurring_detection(
|
|
|
403
403
|
|
|
404
404
|
# Generate insights with LLM
|
|
405
405
|
# TODO: Pass user_id for better caching (currently uses subscriptions hash)
|
|
406
|
-
|
|
406
|
+
insights_generator = detector.insights_generator
|
|
407
|
+
if insights_generator is None:
|
|
408
|
+
from fastapi import HTTPException
|
|
409
|
+
|
|
410
|
+
raise HTTPException(
|
|
411
|
+
status_code=500,
|
|
412
|
+
detail="Subscription insights generator not configured (enable_llm=True required).",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
insights = await insights_generator.generate(subscriptions)
|
|
407
416
|
|
|
408
417
|
return insights
|
|
409
418
|
else:
|
fin_infra/recurring/insights.py
CHANGED
|
@@ -162,7 +162,9 @@ async def get_provider_token(
|
|
|
162
162
|
|
|
163
163
|
# Decrypt token
|
|
164
164
|
context = {"user_id": user_id, "provider": provider}
|
|
165
|
-
|
|
165
|
+
# Cast to str since SQLAlchemy Column[str] needs explicit conversion for type checker
|
|
166
|
+
encrypted_token_str: str = str(token_obj.encrypted_token)
|
|
167
|
+
token = encryption.decrypt(encrypted_token_str, context=context)
|
|
166
168
|
|
|
167
169
|
# Update last_used_at
|
|
168
170
|
update_stmt = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fin-infra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.65
|
|
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
|
|
@@ -10,11 +10,11 @@ fin_infra/analytics/projections.py,sha256=7cuG6w1KXq8sd3UNufu5aOcxG5n-foswrHqrgW
|
|
|
10
10
|
fin_infra/analytics/rebalancing.py,sha256=K3S7KQiIU2LwyAwWN9VrSly4AOl24vN9tz_JX7I9FJ8,14642
|
|
11
11
|
fin_infra/analytics/savings.py,sha256=tavIRZtu9FjCm-DeWg5f060GcsdgD-cl-vgKOnieOUw,7574
|
|
12
12
|
fin_infra/analytics/scenarios.py,sha256=LE_dZVkbxxAx5sxitGhiOhZfWTlYtVbIvS9pEXkijLc,12246
|
|
13
|
-
fin_infra/analytics/spending.py,sha256
|
|
14
|
-
fin_infra/banking/__init__.py,sha256
|
|
13
|
+
fin_infra/analytics/spending.py,sha256=-11Pm2E-UjuLfYWLD8wQFf5Xyk_0es1xmiedTsBj-Bc,26227
|
|
14
|
+
fin_infra/banking/__init__.py,sha256=-Pn8YxpE_aOefFGL4bBmOogywNWShEV271CKxlrvTIM,22556
|
|
15
15
|
fin_infra/banking/history.py,sha256=1ufAwkTnXr-QJetFzJl4xA2e3dqd1-TkT8pf46MNfho,10630
|
|
16
16
|
fin_infra/banking/utils.py,sha256=B2ebnTeUz-56l8XMBWnf2txFOr0bXIo3cKPio7_bhc4,15711
|
|
17
|
-
fin_infra/brokerage/__init__.py,sha256=
|
|
17
|
+
fin_infra/brokerage/__init__.py,sha256=2Qmiubu9kxouidrCcIHOMvYGHEgVDD5y2S2XRBAWng8,17263
|
|
18
18
|
fin_infra/budgets/__init__.py,sha256=V6euagDkFHvWyjHeI64fxfddhOHDlIWwOc-PnTyyQb4,3986
|
|
19
19
|
fin_infra/budgets/add.py,sha256=tdfNXD9deEEzy0xCRbTwbx70SX9wiJkWj0Huk_uCjFg,13584
|
|
20
20
|
fin_infra/budgets/alerts.py,sha256=i0lQa3mLWuLQorWL-77VKhXQG2F_0U1cpdZnK3g1y3M,9720
|
|
@@ -29,17 +29,17 @@ fin_infra/budgets/templates.py,sha256=Sbc7RcHXscq34g4t7J8OXM2Kfkt5DHuvqVnFU0Jidd
|
|
|
29
29
|
fin_infra/budgets/tracker.py,sha256=U8C7k2VV8bOjtjPIWR8qXktsBNSyBAUnE9o2mjEs1MU,16490
|
|
30
30
|
fin_infra/cashflows/__init__.py,sha256=OEleZSwEHffxTvz0J52qqlkBwnu4BHbQUW0vKwzsWAs,8579
|
|
31
31
|
fin_infra/cashflows/core.py,sha256=Or0hPqCvY_ypV0YiMXh-mle6xWK0tE8WuPPAqHGUp8E,532
|
|
32
|
-
fin_infra/categorization/__init__.py,sha256=
|
|
33
|
-
fin_infra/categorization/add.py,sha256=
|
|
34
|
-
fin_infra/categorization/ease.py,sha256=
|
|
35
|
-
fin_infra/categorization/engine.py,sha256=
|
|
36
|
-
fin_infra/categorization/llm_layer.py,sha256=
|
|
32
|
+
fin_infra/categorization/__init__.py,sha256=efLje12AW-ec9Vs5ynb41r4XCIWx5a-Z9WoGb3kQdIE,2030
|
|
33
|
+
fin_infra/categorization/add.py,sha256=JDOvxngh-7oWHTddOyP4GAse9vLuxSTfoIhrDKUHOKg,6278
|
|
34
|
+
fin_infra/categorization/ease.py,sha256=oIcPVuxXOPaAhSe_OcfO4eumCg9WpfIZUdg-k-Xx800,5859
|
|
35
|
+
fin_infra/categorization/engine.py,sha256=ZJm-V6I1okDSFQA34GFZCOTsLCztNtmHlbm2-r51mwQ,12108
|
|
36
|
+
fin_infra/categorization/llm_layer.py,sha256=sOR3gtyPR4y58-5a_XW7U4GMwPsvBwaL_mtmaExC_zk,12787
|
|
37
37
|
fin_infra/categorization/models.py,sha256=O8ceQOM0ljRh0jkmnjV7CK5Jyq1DI3lG07UTeeMheNg,5931
|
|
38
38
|
fin_infra/categorization/rules.py,sha256=m3OogJY0hJe5BrmZqOvOKS2-HRdW4Y5jvvtlPDn9Pn8,12884
|
|
39
39
|
fin_infra/categorization/taxonomy.py,sha256=qsgo7VJkM6GFBBOaTRHWP82vl5SinRKnMsj4ICarEyQ,13281
|
|
40
|
-
fin_infra/chat/__init__.py,sha256=
|
|
41
|
-
fin_infra/chat/ease.py,sha256=
|
|
42
|
-
fin_infra/chat/planning.py,sha256=
|
|
40
|
+
fin_infra/chat/__init__.py,sha256=NsYyRxZGwUFvYEmoLfuTaAfBWn34KOKqeRi6_hSVgGE,6356
|
|
41
|
+
fin_infra/chat/ease.py,sha256=b99CtKSe9UJqXPLqlqoUI0mI6bmxHVlbpdFq4K1oCGg,3143
|
|
42
|
+
fin_infra/chat/planning.py,sha256=eKUW6VDHJS-xQTks7bgjNQaO32Fr5gA_oP5NLt2y5Zs,19916
|
|
43
43
|
fin_infra/cli/__init__.py,sha256=7M8gKULnui4__9kXRKRHgETuFwZlacK9xrq5rSZ31CM,376
|
|
44
44
|
fin_infra/cli/cmds/__init__.py,sha256=BvL3wRoUl3cO5wesv1Cqoatup7VeYMhq82tS19iNZHE,136
|
|
45
45
|
fin_infra/cli/cmds/scaffold_cmds.py,sha256=HZrnJ6NgTBYbt6LuJeoi7JKJgWE_umX9v7zjtwYfP-g,7659
|
|
@@ -56,7 +56,7 @@ fin_infra/credit/experian/parser.py,sha256=7ptdLyTWWqHWqCo1CXn6L7XaIn9ZRRuOaATbF
|
|
|
56
56
|
fin_infra/credit/experian/provider.py,sha256=l3NW6dppgxeUkrThftH-IB43bwuZGhNcW0jVBGF8XGY,13783
|
|
57
57
|
fin_infra/credit/mock.py,sha256=xKWZk3fhuIYRfiZkNc9fbHUNViNKjmOLSj0MTI1f4ik,5356
|
|
58
58
|
fin_infra/crypto/__init__.py,sha256=HpplYEY8GiBz55ehYRDQxs8SWJIW1smBs9eFOKt_nzI,8318
|
|
59
|
-
fin_infra/crypto/insights.py,sha256=
|
|
59
|
+
fin_infra/crypto/insights.py,sha256=2Q0QnpOC-nXJa8pYZmjNre6csz2peGbb5chN5-cipWI,11655
|
|
60
60
|
fin_infra/documents/__init__.py,sha256=Ub1hbX3PTrBSsBdcbL8PFf6oq8jSH4pYxW45-qOYPqs,1909
|
|
61
61
|
fin_infra/documents/add.py,sha256=ztSFCY42hfLLgUbXefxvf_AAbzaxJ6xpEZJpcHE8g7c,8133
|
|
62
62
|
fin_infra/documents/analysis.py,sha256=zY5OQEIlq3JLNND_cg2KheFdryUmIecPOR2lR6oKhPw,13992
|
|
@@ -79,20 +79,20 @@ fin_infra/goals/scaffold_templates/schemas.py.tmpl,sha256=M1hS1pK9UDXcNqPW-NGu98
|
|
|
79
79
|
fin_infra/insights/__init__.py,sha256=crIXNlztTCcYHNcEVMo8FwCTCUBwIK2wovb4HahzRYw,3988
|
|
80
80
|
fin_infra/insights/aggregator.py,sha256=XG32mN5w5Nc4AZllmfl1esL4q44mFAf0Fvj9mWev_zk,10249
|
|
81
81
|
fin_infra/insights/models.py,sha256=xov_YV8oBLJt3YdyVjbryRfcXqmGeGiPvZsZHSbvtl8,3202
|
|
82
|
-
fin_infra/investments/__init__.py,sha256=
|
|
82
|
+
fin_infra/investments/__init__.py,sha256=mDHYMLSo14KByrpW9HuxIDFBdfEESZ6tFKC09Vr_aIE,6786
|
|
83
83
|
fin_infra/investments/add.py,sha256=3cbjXbWoTuDglwk9U48X6768Etv1XLTWysdDPgsn7Yg,17658
|
|
84
|
-
fin_infra/investments/ease.py,sha256=
|
|
85
|
-
fin_infra/investments/models.py,sha256=
|
|
84
|
+
fin_infra/investments/ease.py,sha256=7oyMcTVmnc8pn3lqriLRruBLEznI_grpDkH7hfXmGhE,9527
|
|
85
|
+
fin_infra/investments/models.py,sha256=KwGLw8jdgX5tw4zZjBWBEKNqKsMo3hJnmsx5hIV-rQU,16032
|
|
86
86
|
fin_infra/investments/providers/__init__.py,sha256=V1eIzz6EnGJ-pq-9L3S2-evmcExF-YdZfd5P6JMyDtc,383
|
|
87
87
|
fin_infra/investments/providers/base.py,sha256=KaJdIdeWi2WaWAogcFZw7jcqQ_IzMZw6misBNk-n6bE,9890
|
|
88
|
-
fin_infra/investments/providers/plaid.py,sha256=
|
|
88
|
+
fin_infra/investments/providers/plaid.py,sha256=2WqXwPVckIc56dx9BleC10z-WHDt7y1dO1xhsI_8kT4,18108
|
|
89
89
|
fin_infra/investments/providers/snaptrade.py,sha256=ILpup62u5zCOgnQ_4RF1_m2BY7qowyINB-FUgq-jUrI,23542
|
|
90
90
|
fin_infra/investments/scaffold_templates/README.md,sha256=PhgxfMLrro2Jz83b7XEnBi7lexiWKqlMrd2UU2Rbs8A,12149
|
|
91
91
|
fin_infra/investments/scaffold_templates/__init__.py,sha256=iR0oiAzXFYXHBnVJjaEnAzk6omncYOLg0TKMJ7xomBc,82
|
|
92
92
|
fin_infra/investments/scaffold_templates/models.py.tmpl,sha256=5inP5-jw-qEfPYxSN71tn4AojZ9PesOIeuHTw181N-c,5849
|
|
93
93
|
fin_infra/investments/scaffold_templates/repository.py.tmpl,sha256=XwOEpQZfuXut1WLiq-GSSvv0oX0iYCW54eJNL0Cav94,14656
|
|
94
94
|
fin_infra/investments/scaffold_templates/schemas.py.tmpl,sha256=knWmn-Kyr7AdgPD4ZPMb6T49ZuPXeuOMqmjYNyA0CA0,5451
|
|
95
|
-
fin_infra/markets/__init__.py,sha256=
|
|
95
|
+
fin_infra/markets/__init__.py,sha256=S6vaHI5T3NnP_Kwf5bym7P0lFUhewqmXKh6sEPbaBRs,9892
|
|
96
96
|
fin_infra/models/__init__.py,sha256=q3SkGzDGFkoAMxwqJw8i4cHWt5NGU5ypjOgntxDGVKo,860
|
|
97
97
|
fin_infra/models/accounts.py,sha256=PeobjGg6WM70OvOTe0JIo0zo7tBM0PDAcyClQT-Jo4o,1141
|
|
98
98
|
fin_infra/models/brokerage.py,sha256=z6Zyf0N5zmmXtrN2y_4fNmtIP5wNq40H8lrHLBwY7rc,8311
|
|
@@ -106,7 +106,7 @@ fin_infra/net_worth/__init__.py,sha256=EjEuHNg8gEfFwbfko1-o5j-gSUZ2FcO9h7l05C-zA
|
|
|
106
106
|
fin_infra/net_worth/add.py,sha256=5xYy2L5hEEPiQNF79i-ArWVztLXk2XM97DoZYNWGAz8,23100
|
|
107
107
|
fin_infra/net_worth/aggregator.py,sha256=grif-N8qk77L_JQ4IlcOJaKKP1qpxel0lIV_ll3HgjI,12646
|
|
108
108
|
fin_infra/net_worth/calculator.py,sha256=JERDtZyFurw5x2NYqfHvJzv6qigamI3AFfR-wesTj_E,13133
|
|
109
|
-
fin_infra/net_worth/ease.py,sha256=
|
|
109
|
+
fin_infra/net_worth/ease.py,sha256=n1YI5rEmh48homMJvJMETCbcnICVE5myOnd1OZDJgiY,15920
|
|
110
110
|
fin_infra/net_worth/goals.py,sha256=BJGxdsMjvgQDELFEJo-ai3DvsAzUNXvzMXkwovHr8yQ,1238
|
|
111
111
|
fin_infra/net_worth/insights.py,sha256=vVK4BtfHNJGb1wyk9XD0fLpoadATTdorF8OxHOgD9b0,25222
|
|
112
112
|
fin_infra/net_worth/models.py,sha256=A5idGtMEQy1J4jaLMq9ZZslmvOhxfkW7cjLKW23AMQo,22403
|
|
@@ -115,7 +115,7 @@ fin_infra/net_worth/scaffold_templates/__init__.py,sha256=OKeMCC_JNw6m8rBWr_wesO
|
|
|
115
115
|
fin_infra/net_worth/scaffold_templates/models.py.tmpl,sha256=9BKsoD08RZbSdOm0wFTbx5OzKfAEtuA1NcWyS1Aywx4,5934
|
|
116
116
|
fin_infra/net_worth/scaffold_templates/repository.py.tmpl,sha256=DSErnNxeAe4pWeefARRK3bU0hHltqdIFffENfVwdd7c,12798
|
|
117
117
|
fin_infra/net_worth/scaffold_templates/schemas.py.tmpl,sha256=VkFsxyZx4DFDhXDhn-7KT0IgrXCvgaS5ZdWbjyezWj0,4709
|
|
118
|
-
fin_infra/normalization/__init__.py,sha256
|
|
118
|
+
fin_infra/normalization/__init__.py,sha256=foCru-Nf9M1zP1jdrT0oNazzmp6AWiaDbUDoiyJefvA,6252
|
|
119
119
|
fin_infra/normalization/currency_converter.py,sha256=uuu8ASa5ppEniWLEVEpiDxXjZzln9nopWrhrATcD6Z4,7058
|
|
120
120
|
fin_infra/normalization/models.py,sha256=gNC9chpbQPRN58V2j__VEPVNReO1N8jH_AHObwGPWu0,1928
|
|
121
121
|
fin_infra/normalization/providers/__init__.py,sha256=LFU1tB2hVO42Yrkw-IDpPexD4mIlxob9lRrJEeGYqpE,559
|
|
@@ -127,11 +127,11 @@ fin_infra/obs/classifier.py,sha256=qZHgUV6J2sXdOhHCPOxmonyvE4V1vY-A5MDwFpzk2lk,5
|
|
|
127
127
|
fin_infra/providers/__init__.py,sha256=jxhQm79T6DVXf7Wpy7luL-p50cE_IMUbjt4o3apzJQU,768
|
|
128
128
|
fin_infra/providers/banking/base.py,sha256=KeNU4ur3zLKHVsBF1LQifcs2AKX06IEE-Rx_SetFeAs,102
|
|
129
129
|
fin_infra/providers/banking/plaid_client.py,sha256=21m6ZkovwXuUuj0-dgQVDLxSfxZVjhuXj8di_-q3jGc,6617
|
|
130
|
-
fin_infra/providers/banking/teller_client.py,sha256=
|
|
131
|
-
fin_infra/providers/base.py,sha256=
|
|
130
|
+
fin_infra/providers/banking/teller_client.py,sha256=QmrsBlk3_rHT-pTQPrIAA74kjIjcgdi-gOb8NA3oBO8,10268
|
|
131
|
+
fin_infra/providers/base.py,sha256=zQBiIPYrWELl65bvekAsl7WnuUYHAkM96cYmKQYTb3U,6857
|
|
132
132
|
fin_infra/providers/brokerage/alpaca.py,sha256=wRVfVmExiYXCk1pLRmHSrfo91714JIm3rrD0djrNfT8,9938
|
|
133
133
|
fin_infra/providers/brokerage/base.py,sha256=JJFH0Cqca4Rg4rmxfiwcQt-peRoBf4JpG3g6jx8DVks,106
|
|
134
|
-
fin_infra/providers/credit/experian.py,sha256=
|
|
134
|
+
fin_infra/providers/credit/experian.py,sha256=r7lpFecgOdNEhb_Lxz2Z-BG8R3p2n0XlqDKL7y8NZ-0,482
|
|
135
135
|
fin_infra/providers/identity/stripe_identity.py,sha256=JQGJRuQdWP5dWDcROgtz1RrmpkytRv95H6Fn-x1kifU,501
|
|
136
136
|
fin_infra/providers/market/alphavantage.py,sha256=srZdkf-frBuKyPTdWasMmVrpnh76BEBDXa-nsYtLzNc,8963
|
|
137
137
|
fin_infra/providers/market/base.py,sha256=ljBzZTfjYQS9tXahmxFic7JQSZeyoiDMUZ1NY0R7yto,108
|
|
@@ -141,18 +141,18 @@ fin_infra/providers/market/yahoo.py,sha256=FNhqkCFC0In-Z3zpzmuknEORHLRK5Evk2KSk0
|
|
|
141
141
|
fin_infra/providers/registry.py,sha256=yPFmHHaSQERXZTcGkdXAtMU7rL7VwAzW4FOr14o6KS8,8409
|
|
142
142
|
fin_infra/providers/tax/__init__.py,sha256=Tq2gLyTXL_U_ht6r7HXgaDMCAPylgcRD2ZN-COjSSQU,207
|
|
143
143
|
fin_infra/providers/tax/irs.py,sha256=f7l6w0byprBszTlCB4ef60K8GrYV-03Dicl1a1Q2oVk,4701
|
|
144
|
-
fin_infra/providers/tax/mock.py,sha256=
|
|
144
|
+
fin_infra/providers/tax/mock.py,sha256=AxI3RmEn3exdQeeUNkQYqZ-war5PS--WnLGXfRRee8o,14442
|
|
145
145
|
fin_infra/providers/tax/taxbit.py,sha256=DEA7vgQPYMjz4ZdC0DpY7112FLZJ2kvwgAbDZnpHFy0,4271
|
|
146
146
|
fin_infra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
147
147
|
fin_infra/recurring/__init__.py,sha256=ihMPywft8pGqzMu6EXxbQCU7ByoMl_dvad00gWV1mnk,2308
|
|
148
|
-
fin_infra/recurring/add.py,sha256=
|
|
148
|
+
fin_infra/recurring/add.py,sha256=bPAVHknGoTLqk3T4vGZw1ROiFGHvASSakafGGmMlL9k,18917
|
|
149
149
|
fin_infra/recurring/detector.py,sha256=1e6PRoBAT2NxoGAgcVHAWwpPtznkJMaYSrJtvSq0YqM,20154
|
|
150
|
-
fin_infra/recurring/detectors_llm.py,sha256=
|
|
150
|
+
fin_infra/recurring/detectors_llm.py,sha256=jCW7xXaFRISnsCYDgvyiovXThNceVK_ypWYEawUfE78,11534
|
|
151
151
|
fin_infra/recurring/ease.py,sha256=OrpxGHi8kt6LkMmww5l0Xy2pU-5hP_dR4IgdOiaIRaU,11179
|
|
152
|
-
fin_infra/recurring/insights.py,sha256=
|
|
152
|
+
fin_infra/recurring/insights.py,sha256=bpZGPZiCgrPK0Nr24QS3jc1F9sCldm6Jd5t-i6wbu7M,15916
|
|
153
153
|
fin_infra/recurring/models.py,sha256=N4G_LM0xZr3ptHtlqOmcsw3AL2v9g7IX92SmBljkNek,8894
|
|
154
154
|
fin_infra/recurring/normalizer.py,sha256=Rc1ntIDGir6X-I5lgv49kdLry_zHGJ8cys_Jf3F6Lhk,9761
|
|
155
|
-
fin_infra/recurring/normalizers.py,sha256=
|
|
155
|
+
fin_infra/recurring/normalizers.py,sha256=9by3lF7EVkCWy_-rLiEKpgw-qJWzEiDOKUMscFx8hmI,15958
|
|
156
156
|
fin_infra/recurring/summary.py,sha256=1Wte58ZZkEFulkb-nnpwfC5h7C_JrqByy47itdVdWwc,14665
|
|
157
157
|
fin_infra/scaffold/__init__.py,sha256=OyD8ZtIC4eNTHqD16rbpT8KU0TpZUI6VV4xne4vpaHg,831
|
|
158
158
|
fin_infra/scaffold/budgets.py,sha256=XXOLlEcyBXVwdbJB__qObRXJ0oe1okwDT_-5tG8c9Yk,9515
|
|
@@ -164,7 +164,7 @@ fin_infra/security/encryption.py,sha256=z1k5LFkuuMCjAUnBzBCOviyi0F1R_vabdHhdJJdb
|
|
|
164
164
|
fin_infra/security/models.py,sha256=riQO-083p5rDMRrFxRnc2PTkxkAf-HsSpGvrnzboCNE,1734
|
|
165
165
|
fin_infra/security/pii_filter.py,sha256=lfARBmPRekkyXKJV0tWI_0KVaDsdV61VH-8RHxvbqUs,8307
|
|
166
166
|
fin_infra/security/pii_patterns.py,sha256=SM-o7cL6NdgkOmtBedsN2nJZ5QPbeYehZdYmAujk8Y8,3070
|
|
167
|
-
fin_infra/security/token_store.py,sha256=
|
|
167
|
+
fin_infra/security/token_store.py,sha256=qgxhBKwhtVpchyHv30mM-cttuGZlzZvZLC4Oa-gTTeg,6075
|
|
168
168
|
fin_infra/settings.py,sha256=xitpBQJmuvSy9prQhvXOW1scbwB1KAyGD8XqYgU_hQU,1388
|
|
169
169
|
fin_infra/tax/__init__.py,sha256=NXUjV-k-rw4774pookY3UOwEXYRQauJze6Yift5RjW0,6107
|
|
170
170
|
fin_infra/tax/add.py,sha256=8INSAv721ir9ICQxQ_oA0hL-Bjg6wLyrtj9tafrcCsA,14552
|
|
@@ -173,8 +173,8 @@ fin_infra/utils/__init__.py,sha256=gKacLSWMAis--pasd8AuVN7ap0e9Z1TjRGur0J23EDo,6
|
|
|
173
173
|
fin_infra/utils/http.py,sha256=pvcxbNQ9oisoGPkNe3xX9aAgWzEN6mmdtr1w-L02Xj8,629
|
|
174
174
|
fin_infra/utils/retry.py,sha256=ISBrup5XCuXqHZh9kjTGvGQYcuyYyqZE4u26wW7r3CM,1030
|
|
175
175
|
fin_infra/version.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
|
|
176
|
-
fin_infra-0.1.
|
|
177
|
-
fin_infra-0.1.
|
|
178
|
-
fin_infra-0.1.
|
|
179
|
-
fin_infra-0.1.
|
|
180
|
-
fin_infra-0.1.
|
|
176
|
+
fin_infra-0.1.65.dist-info/LICENSE,sha256=wK-Ya7Ylxa38dSIZRhvNj1ZVLIrHC-BAI8v38PNADiA,1061
|
|
177
|
+
fin_infra-0.1.65.dist-info/METADATA,sha256=qOIl3r1vojckM7SamBAKGx-kTPqhqfp5Pji27lxINsk,10218
|
|
178
|
+
fin_infra-0.1.65.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
179
|
+
fin_infra-0.1.65.dist-info/entry_points.txt,sha256=Sr1uikvALZMeKm-DIkeKG4L9c4SNqysXGO_IRF8_9eU,53
|
|
180
|
+
fin_infra-0.1.65.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|