fin-infra 0.1.65__py3-none-any.whl → 0.1.67__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/cash_flow.py +6 -5
- fin_infra/analytics/portfolio.py +1 -2
- fin_infra/analytics/spending.py +6 -6
- fin_infra/banking/__init__.py +1 -1
- fin_infra/banking/utils.py +5 -6
- fin_infra/cashflows/__init__.py +6 -8
- fin_infra/categorization/engine.py +3 -3
- fin_infra/categorization/llm_layer.py +3 -4
- fin_infra/categorization/models.py +1 -1
- fin_infra/chat/__init__.py +6 -6
- fin_infra/chat/ease.py +1 -1
- fin_infra/compliance/__init__.py +0 -1
- fin_infra/crypto/insights.py +2 -4
- fin_infra/documents/add.py +1 -1
- fin_infra/insights/__init__.py +7 -7
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +2 -3
- fin_infra/investments/models.py +100 -46
- fin_infra/investments/providers/plaid.py +2 -3
- fin_infra/investments/providers/snaptrade.py +2 -2
- fin_infra/markets/__init__.py +0 -4
- fin_infra/models/transactions.py +1 -1
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +5 -4
- fin_infra/net_worth/ease.py +1 -1
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +6 -6
- fin_infra/obs/classifier.py +2 -2
- fin_infra/providers/base.py +12 -17
- fin_infra/recurring/add.py +8 -8
- fin_infra/recurring/detectors_llm.py +9 -8
- fin_infra/recurring/ease.py +1 -1
- fin_infra/recurring/insights.py +8 -7
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizers.py +8 -7
- {fin_infra-0.1.65.dist-info → fin_infra-0.1.67.dist-info}/METADATA +1 -2
- {fin_infra-0.1.65.dist-info → fin_infra-0.1.67.dist-info}/RECORD +40 -40
- {fin_infra-0.1.65.dist-info → fin_infra-0.1.67.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.65.dist-info → fin_infra-0.1.67.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.65.dist-info → fin_infra-0.1.67.dist-info}/entry_points.txt +0 -0
fin_infra/net_worth/add.py
CHANGED
|
@@ -36,13 +36,16 @@ tracker = add_net_worth_tracking(app)
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
from datetime import datetime, timedelta
|
|
39
|
+
from typing import Any
|
|
39
40
|
|
|
40
41
|
from fastapi import FastAPI, HTTPException, Query
|
|
41
42
|
|
|
42
43
|
from fin_infra.net_worth.ease import NetWorthTracker, easy_net_worth
|
|
43
44
|
from fin_infra.net_worth.models import (
|
|
45
|
+
AssetDetail,
|
|
44
46
|
ConversationResponse,
|
|
45
47
|
GoalProgressResponse,
|
|
48
|
+
LiabilityDetail,
|
|
46
49
|
NetWorthResponse,
|
|
47
50
|
SnapshotHistoryResponse,
|
|
48
51
|
)
|
|
@@ -188,8 +191,8 @@ def add_net_worth_tracking(
|
|
|
188
191
|
# Persistence: Asset/liability details stored in snapshot JSON fields or separate tables.
|
|
189
192
|
# Generate with: fin-infra scaffold net_worth --dest-dir app/models/
|
|
190
193
|
# For now, create empty lists for testing/examples.
|
|
191
|
-
asset_details = []
|
|
192
|
-
liability_details = []
|
|
194
|
+
asset_details: list[AssetDetail] = []
|
|
195
|
+
liability_details: list[LiabilityDetail] = []
|
|
193
196
|
|
|
194
197
|
# Calculate breakdowns
|
|
195
198
|
asset_allocation = calculate_asset_allocation(asset_details)
|
|
@@ -508,7 +511,7 @@ def add_net_worth_tracking(
|
|
|
508
511
|
snapshot = await tracker.calculate_net_worth(user_id=user_id, access_token=access_token)
|
|
509
512
|
|
|
510
513
|
# Get goals for context (if goal_tracker available)
|
|
511
|
-
goals = []
|
|
514
|
+
goals: list[Any] = []
|
|
512
515
|
if tracker.goal_tracker:
|
|
513
516
|
# TODO: Implement get_goals() method
|
|
514
517
|
pass
|
|
@@ -657,10 +660,10 @@ def add_net_worth_tracking(
|
|
|
657
660
|
|
|
658
661
|
try:
|
|
659
662
|
# Get current snapshot
|
|
660
|
-
|
|
663
|
+
await tracker.calculate_net_worth(user_id=user_id, access_token=access_token)
|
|
661
664
|
|
|
662
665
|
# Get historical snapshots for progress tracking
|
|
663
|
-
|
|
666
|
+
await tracker.get_snapshots(user_id=user_id, days=90)
|
|
664
667
|
|
|
665
668
|
# Persistence: Goal retrieval via scaffolded goals repository.
|
|
666
669
|
# Generate with: fin-infra scaffold goals --dest-dir app/models/
|
|
@@ -213,16 +213,17 @@ class NetWorthAggregator:
|
|
|
213
213
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
214
214
|
|
|
215
215
|
# Aggregate results (skip failed providers)
|
|
216
|
-
all_assets = []
|
|
217
|
-
all_liabilities = []
|
|
218
|
-
actual_providers = []
|
|
216
|
+
all_assets: list[AssetDetail] = []
|
|
217
|
+
all_liabilities: list[LiabilityDetail] = []
|
|
218
|
+
actual_providers: list[str] = []
|
|
219
219
|
|
|
220
220
|
for i, result in enumerate(results):
|
|
221
|
-
if isinstance(result,
|
|
221
|
+
if isinstance(result, BaseException):
|
|
222
222
|
# Log error but continue (graceful degradation)
|
|
223
223
|
print(f"Provider {providers_used[i]} failed: {result}")
|
|
224
224
|
continue
|
|
225
225
|
|
|
226
|
+
# result is now tuple[list[AssetDetail], list[LiabilityDetail]]
|
|
226
227
|
assets, liabilities = result
|
|
227
228
|
all_assets.extend(assets)
|
|
228
229
|
all_liabilities.extend(liabilities)
|
fin_infra/net_worth/ease.py
CHANGED
|
@@ -377,7 +377,7 @@ def easy_net_worth(
|
|
|
377
377
|
|
|
378
378
|
if enable_llm:
|
|
379
379
|
try:
|
|
380
|
-
from ai_infra.llm.llm import LLM
|
|
380
|
+
from ai_infra.llm.llm import LLM
|
|
381
381
|
except ImportError:
|
|
382
382
|
raise ImportError(
|
|
383
383
|
"LLM features require ai-infra package. " "Install with: pip install ai-infra"
|
fin_infra/net_worth/models.py
CHANGED
|
@@ -19,6 +19,7 @@ Pydantic V2 models for net worth tracking.
|
|
|
19
19
|
|
|
20
20
|
from datetime import datetime
|
|
21
21
|
from enum import Enum
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
22
23
|
|
|
23
24
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
24
25
|
|
|
@@ -207,54 +208,100 @@ class AssetAllocation(BaseModel):
|
|
|
207
208
|
vehicles: float = Field(0.0, ge=0, description="Vehicle value")
|
|
208
209
|
other_assets: float = Field(0.0, ge=0, description="Other asset value")
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
211
|
+
if TYPE_CHECKING:
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def total_assets(self) -> float:
|
|
215
|
+
"""Sum of all asset categories."""
|
|
216
|
+
return (
|
|
217
|
+
self.cash
|
|
218
|
+
+ self.investments
|
|
219
|
+
+ self.crypto
|
|
220
|
+
+ self.real_estate
|
|
221
|
+
+ self.vehicles
|
|
222
|
+
+ self.other_assets
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def cash_percentage(self) -> float:
|
|
227
|
+
"""Cash as percentage of total assets."""
|
|
228
|
+
return (self.cash / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def investments_percentage(self) -> float:
|
|
232
|
+
"""Investments as percentage of total assets."""
|
|
233
|
+
return (self.investments / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def crypto_percentage(self) -> float:
|
|
237
|
+
"""Crypto as percentage of total assets."""
|
|
238
|
+
return (self.crypto / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def real_estate_percentage(self) -> float:
|
|
242
|
+
"""Real estate as percentage of total assets."""
|
|
243
|
+
return (self.real_estate / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def vehicles_percentage(self) -> float:
|
|
247
|
+
"""Vehicles as percentage of total assets."""
|
|
248
|
+
return (self.vehicles / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def other_percentage(self) -> float:
|
|
252
|
+
"""Other assets as percentage of total assets."""
|
|
253
|
+
return (self.other_assets / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
|
|
257
|
+
@computed_field
|
|
258
|
+
@property
|
|
259
|
+
def total_assets(self) -> float:
|
|
260
|
+
"""Sum of all asset categories."""
|
|
261
|
+
return (
|
|
262
|
+
self.cash
|
|
263
|
+
+ self.investments
|
|
264
|
+
+ self.crypto
|
|
265
|
+
+ self.real_estate
|
|
266
|
+
+ self.vehicles
|
|
267
|
+
+ self.other_assets
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@computed_field
|
|
271
|
+
@property
|
|
272
|
+
def cash_percentage(self) -> float:
|
|
273
|
+
"""Cash as percentage of total assets."""
|
|
274
|
+
return (self.cash / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
275
|
+
|
|
276
|
+
@computed_field
|
|
277
|
+
@property
|
|
278
|
+
def investments_percentage(self) -> float:
|
|
279
|
+
"""Investments as percentage of total assets."""
|
|
280
|
+
return (self.investments / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
281
|
+
|
|
282
|
+
@computed_field
|
|
283
|
+
@property
|
|
284
|
+
def crypto_percentage(self) -> float:
|
|
285
|
+
"""Crypto as percentage of total assets."""
|
|
286
|
+
return (self.crypto / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
287
|
+
|
|
288
|
+
@computed_field
|
|
289
|
+
@property
|
|
290
|
+
def real_estate_percentage(self) -> float:
|
|
291
|
+
"""Real estate as percentage of total assets."""
|
|
292
|
+
return (self.real_estate / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
293
|
+
|
|
294
|
+
@computed_field
|
|
295
|
+
@property
|
|
296
|
+
def vehicles_percentage(self) -> float:
|
|
297
|
+
"""Vehicles as percentage of total assets."""
|
|
298
|
+
return (self.vehicles / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
299
|
+
|
|
300
|
+
@computed_field
|
|
301
|
+
@property
|
|
302
|
+
def other_percentage(self) -> float:
|
|
303
|
+
"""Other assets as percentage of total assets."""
|
|
304
|
+
return (self.other_assets / self.total_assets * 100) if self.total_assets > 0 else 0.0
|
|
258
305
|
|
|
259
306
|
|
|
260
307
|
class LiabilityBreakdown(BaseModel):
|
|
@@ -288,74 +335,148 @@ class LiabilityBreakdown(BaseModel):
|
|
|
288
335
|
personal_loans: float = Field(0.0, ge=0, description="Personal loan balance")
|
|
289
336
|
lines_of_credit: float = Field(0.0, ge=0, description="Line of credit balance")
|
|
290
337
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
338
|
+
if TYPE_CHECKING:
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def total_liabilities(self) -> float:
|
|
342
|
+
"""Sum of all liability categories."""
|
|
343
|
+
return (
|
|
344
|
+
self.credit_cards
|
|
345
|
+
+ self.mortgages
|
|
346
|
+
+ self.auto_loans
|
|
347
|
+
+ self.student_loans
|
|
348
|
+
+ self.personal_loans
|
|
349
|
+
+ self.lines_of_credit
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def credit_cards_percentage(self) -> float:
|
|
354
|
+
"""Credit cards as percentage of total liabilities."""
|
|
355
|
+
return (
|
|
356
|
+
(self.credit_cards / self.total_liabilities * 100)
|
|
357
|
+
if self.total_liabilities > 0
|
|
358
|
+
else 0.0
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def mortgages_percentage(self) -> float:
|
|
363
|
+
"""Mortgages as percentage of total liabilities."""
|
|
364
|
+
return (
|
|
365
|
+
(self.mortgages / self.total_liabilities * 100)
|
|
366
|
+
if self.total_liabilities > 0
|
|
367
|
+
else 0.0
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def auto_loans_percentage(self) -> float:
|
|
372
|
+
"""Auto loans as percentage of total liabilities."""
|
|
373
|
+
return (
|
|
374
|
+
(self.auto_loans / self.total_liabilities * 100)
|
|
375
|
+
if self.total_liabilities > 0
|
|
376
|
+
else 0.0
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def student_loans_percentage(self) -> float:
|
|
381
|
+
"""Student loans as percentage of total liabilities."""
|
|
382
|
+
return (
|
|
383
|
+
(self.student_loans / self.total_liabilities * 100)
|
|
384
|
+
if self.total_liabilities > 0
|
|
385
|
+
else 0.0
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def personal_loans_percentage(self) -> float:
|
|
390
|
+
"""Personal loans as percentage of total liabilities."""
|
|
391
|
+
return (
|
|
392
|
+
(self.personal_loans / self.total_liabilities * 100)
|
|
393
|
+
if self.total_liabilities > 0
|
|
394
|
+
else 0.0
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def lines_of_credit_percentage(self) -> float:
|
|
399
|
+
"""Lines of credit as percentage of total liabilities."""
|
|
400
|
+
return (
|
|
401
|
+
(self.lines_of_credit / self.total_liabilities * 100)
|
|
402
|
+
if self.total_liabilities > 0
|
|
403
|
+
else 0.0
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
else:
|
|
407
|
+
|
|
408
|
+
@computed_field
|
|
409
|
+
@property
|
|
410
|
+
def total_liabilities(self) -> float:
|
|
411
|
+
"""Sum of all liability categories."""
|
|
412
|
+
return (
|
|
413
|
+
self.credit_cards
|
|
414
|
+
+ self.mortgages
|
|
415
|
+
+ self.auto_loans
|
|
416
|
+
+ self.student_loans
|
|
417
|
+
+ self.personal_loans
|
|
418
|
+
+ self.lines_of_credit
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
@computed_field
|
|
422
|
+
@property
|
|
423
|
+
def credit_cards_percentage(self) -> float:
|
|
424
|
+
"""Credit cards as percentage of total liabilities."""
|
|
425
|
+
return (
|
|
426
|
+
(self.credit_cards / self.total_liabilities * 100)
|
|
427
|
+
if self.total_liabilities > 0
|
|
428
|
+
else 0.0
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
@computed_field
|
|
432
|
+
@property
|
|
433
|
+
def mortgages_percentage(self) -> float:
|
|
434
|
+
"""Mortgages as percentage of total liabilities."""
|
|
435
|
+
return (
|
|
436
|
+
(self.mortgages / self.total_liabilities * 100)
|
|
437
|
+
if self.total_liabilities > 0
|
|
438
|
+
else 0.0
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
@computed_field
|
|
442
|
+
@property
|
|
443
|
+
def auto_loans_percentage(self) -> float:
|
|
444
|
+
"""Auto loans as percentage of total liabilities."""
|
|
445
|
+
return (
|
|
446
|
+
(self.auto_loans / self.total_liabilities * 100)
|
|
447
|
+
if self.total_liabilities > 0
|
|
448
|
+
else 0.0
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
@computed_field
|
|
452
|
+
@property
|
|
453
|
+
def student_loans_percentage(self) -> float:
|
|
454
|
+
"""Student loans as percentage of total liabilities."""
|
|
455
|
+
return (
|
|
456
|
+
(self.student_loans / self.total_liabilities * 100)
|
|
457
|
+
if self.total_liabilities > 0
|
|
458
|
+
else 0.0
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
@computed_field
|
|
462
|
+
@property
|
|
463
|
+
def personal_loans_percentage(self) -> float:
|
|
464
|
+
"""Personal loans as percentage of total liabilities."""
|
|
465
|
+
return (
|
|
466
|
+
(self.personal_loans / self.total_liabilities * 100)
|
|
467
|
+
if self.total_liabilities > 0
|
|
468
|
+
else 0.0
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
@computed_field
|
|
472
|
+
@property
|
|
473
|
+
def lines_of_credit_percentage(self) -> float:
|
|
474
|
+
"""Lines of credit as percentage of total liabilities."""
|
|
475
|
+
return (
|
|
476
|
+
(self.lines_of_credit / self.total_liabilities * 100)
|
|
477
|
+
if self.total_liabilities > 0
|
|
478
|
+
else 0.0
|
|
479
|
+
)
|
|
359
480
|
|
|
360
481
|
|
|
361
482
|
class AssetDetail(BaseModel):
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""Data normalization module for financial symbols and currencies."""
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
3
8
|
from fin_infra.normalization.currency_converter import (
|
|
4
9
|
CurrencyConverter,
|
|
5
10
|
CurrencyNotSupportedError,
|
|
@@ -64,7 +69,7 @@ def easy_normalization(
|
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
def add_normalization(
|
|
67
|
-
app: "FastAPI",
|
|
72
|
+
app: "FastAPI",
|
|
68
73
|
*,
|
|
69
74
|
prefix: str = "/normalize",
|
|
70
75
|
api_key: str | None = None,
|
|
@@ -110,11 +115,6 @@ def add_normalization(
|
|
|
110
115
|
- Integrated with svc-infra observability (request metrics)
|
|
111
116
|
- Scoped docs at {prefix}/docs for standalone documentation
|
|
112
117
|
"""
|
|
113
|
-
from typing import TYPE_CHECKING
|
|
114
|
-
|
|
115
|
-
if TYPE_CHECKING:
|
|
116
|
-
from fastapi import FastAPI
|
|
117
|
-
|
|
118
118
|
# Import FastAPI dependencies
|
|
119
119
|
from fastapi import Query, HTTPException
|
|
120
120
|
|
fin_infra/obs/classifier.py
CHANGED
|
@@ -112,9 +112,9 @@ def financial_route_classifier(route_path: str, method: str) -> str:
|
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def compose_classifiers(
|
|
115
|
-
*classifiers: Callable[[str], str],
|
|
115
|
+
*classifiers: Callable[[str, str], str],
|
|
116
116
|
default: str = "public",
|
|
117
|
-
) -> Callable[[str], str]:
|
|
117
|
+
) -> Callable[[str, str], str]:
|
|
118
118
|
"""
|
|
119
119
|
Compose multiple route classifiers with fallback logic.
|
|
120
120
|
|
fin_infra/providers/base.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Iterable, Sequence
|
|
4
|
+
from typing import Any, Iterable, Sequence
|
|
5
5
|
|
|
6
6
|
from ..models import Quote, Candle
|
|
7
7
|
|
|
@@ -20,11 +20,11 @@ class MarketDataProvider(ABC):
|
|
|
20
20
|
|
|
21
21
|
class CryptoDataProvider(ABC):
|
|
22
22
|
@abstractmethod
|
|
23
|
-
def ticker(self, symbol_pair: str) ->
|
|
23
|
+
def ticker(self, symbol_pair: str) -> Any:
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
26
|
@abstractmethod
|
|
27
|
-
def ohlcv(self, symbol_pair: str, timeframe: str = "1d", limit: int = 100) ->
|
|
27
|
+
def ohlcv(self, symbol_pair: str, timeframe: str = "1d", limit: int = 100) -> Any:
|
|
28
28
|
pass
|
|
29
29
|
|
|
30
30
|
|
|
@@ -161,11 +161,11 @@ class IdentityProvider(ABC):
|
|
|
161
161
|
|
|
162
162
|
class CreditProvider(ABC):
|
|
163
163
|
@abstractmethod
|
|
164
|
-
def get_credit_score(self, user_id: str, **kwargs) ->
|
|
164
|
+
def get_credit_score(self, user_id: str, **kwargs: Any) -> Any:
|
|
165
165
|
pass
|
|
166
166
|
|
|
167
167
|
@abstractmethod
|
|
168
|
-
def get_credit_report(self, user_id: str, **kwargs) ->
|
|
168
|
+
def get_credit_report(self, user_id: str, **kwargs: Any) -> Any:
|
|
169
169
|
"""Retrieve full credit report for a user."""
|
|
170
170
|
pass
|
|
171
171
|
|
|
@@ -174,36 +174,31 @@ class TaxProvider(ABC):
|
|
|
174
174
|
"""Provider for tax data and document retrieval."""
|
|
175
175
|
|
|
176
176
|
@abstractmethod
|
|
177
|
-
def get_tax_forms(self, user_id: str, tax_year: int, **kwargs) ->
|
|
177
|
+
def get_tax_forms(self, user_id: str, tax_year: int, **kwargs: Any) -> Any:
|
|
178
178
|
"""Retrieve tax forms for a user and tax year."""
|
|
179
179
|
pass
|
|
180
180
|
|
|
181
181
|
@abstractmethod
|
|
182
|
-
def get_tax_documents(self, user_id: str, tax_year: int, **kwargs) ->
|
|
182
|
+
def get_tax_documents(self, user_id: str, tax_year: int, **kwargs: Any) -> Any:
|
|
183
183
|
"""Retrieve tax documents for a user and tax year."""
|
|
184
184
|
pass
|
|
185
185
|
|
|
186
186
|
@abstractmethod
|
|
187
|
-
def get_tax_document(self, document_id: str, **kwargs) ->
|
|
187
|
+
def get_tax_document(self, document_id: str, **kwargs: Any) -> Any:
|
|
188
188
|
"""Retrieve a specific tax document by ID."""
|
|
189
189
|
pass
|
|
190
190
|
|
|
191
191
|
@abstractmethod
|
|
192
|
-
def calculate_crypto_gains(self,
|
|
192
|
+
def calculate_crypto_gains(self, *args: Any, **kwargs: Any) -> Any:
|
|
193
193
|
"""Calculate capital gains from crypto transactions."""
|
|
194
194
|
pass
|
|
195
195
|
|
|
196
196
|
@abstractmethod
|
|
197
197
|
def calculate_tax_liability(
|
|
198
198
|
self,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
filing_status: str,
|
|
203
|
-
tax_year: int,
|
|
204
|
-
state: str | None = None,
|
|
205
|
-
**kwargs,
|
|
206
|
-
) -> dict:
|
|
199
|
+
*args: Any,
|
|
200
|
+
**kwargs: Any,
|
|
201
|
+
) -> Any:
|
|
207
202
|
"""Calculate estimated tax liability."""
|
|
208
203
|
pass
|
|
209
204
|
|
fin_infra/recurring/add.py
CHANGED
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import time
|
|
13
13
|
from datetime import datetime, timedelta
|
|
14
|
-
from typing import TYPE_CHECKING, Optional
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
15
15
|
|
|
16
16
|
from .ease import easy_recurring_detection
|
|
17
17
|
from .models import (
|
|
@@ -93,7 +93,7 @@ def add_recurring_detection(
|
|
|
93
93
|
llm_model=llm_model,
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
# Store on app.state
|
|
97
97
|
app.state.recurring_detector = detector
|
|
98
98
|
|
|
99
99
|
# Use svc-infra user_router for authentication (recurring detection is user-specific)
|
|
@@ -133,7 +133,7 @@ def add_recurring_detection(
|
|
|
133
133
|
# For now, return empty result with structure.
|
|
134
134
|
# In production: transactions = get_user_transactions(user.id, days=request.days)
|
|
135
135
|
|
|
136
|
-
transactions = [] # Placeholder
|
|
136
|
+
transactions: list[dict[str, Any]] = [] # Placeholder
|
|
137
137
|
|
|
138
138
|
# Detect patterns
|
|
139
139
|
patterns = detector.detect_patterns(transactions)
|
|
@@ -180,7 +180,7 @@ def add_recurring_detection(
|
|
|
180
180
|
# return cached
|
|
181
181
|
|
|
182
182
|
# Detect patterns (same as /detect endpoint)
|
|
183
|
-
transactions = [] # Placeholder
|
|
183
|
+
transactions: list[dict[str, Any]] = [] # Placeholder
|
|
184
184
|
patterns = detector.detect_patterns(transactions)
|
|
185
185
|
patterns = [p for p in patterns if p.confidence >= min_confidence]
|
|
186
186
|
|
|
@@ -208,7 +208,7 @@ def add_recurring_detection(
|
|
|
208
208
|
List of predicted charges with expected dates and amounts
|
|
209
209
|
"""
|
|
210
210
|
# Get detected patterns
|
|
211
|
-
transactions = [] # Placeholder
|
|
211
|
+
transactions: list[dict[str, Any]] = [] # Placeholder
|
|
212
212
|
patterns = detector.detect_patterns(transactions)
|
|
213
213
|
patterns = [p for p in patterns if p.confidence >= min_confidence]
|
|
214
214
|
|
|
@@ -230,7 +230,7 @@ def add_recurring_detection(
|
|
|
230
230
|
- Top merchants by amount
|
|
231
231
|
"""
|
|
232
232
|
# Get all detected patterns
|
|
233
|
-
transactions = [] # Placeholder
|
|
233
|
+
transactions: list[dict[str, Any]] = [] # Placeholder
|
|
234
234
|
patterns = detector.detect_patterns(transactions)
|
|
235
235
|
|
|
236
236
|
# Calculate stats
|
|
@@ -321,7 +321,7 @@ def add_recurring_detection(
|
|
|
321
321
|
from .summary import get_recurring_summary
|
|
322
322
|
|
|
323
323
|
# Get detected patterns for user
|
|
324
|
-
transactions = [] # Placeholder - in production: get_user_transactions(user_id)
|
|
324
|
+
transactions: list[dict[str, Any]] = [] # Placeholder - in production: get_user_transactions(user_id)
|
|
325
325
|
patterns = detector.detect_patterns(transactions)
|
|
326
326
|
|
|
327
327
|
# Generate summary
|
|
@@ -375,7 +375,7 @@ def add_recurring_detection(
|
|
|
375
375
|
**Cost:** ~$0.0002/generation with Google Gemini, <$0.00004 effective with caching
|
|
376
376
|
"""
|
|
377
377
|
# Get detected patterns
|
|
378
|
-
transactions = [] # Placeholder
|
|
378
|
+
transactions: list[dict[str, Any]] = [] # Placeholder
|
|
379
379
|
patterns = detector.detect_patterns(transactions)
|
|
380
380
|
|
|
381
381
|
# Convert patterns to subscription dicts for LLM
|