fin-infra 0.1.69__py3-none-any.whl → 0.1.82__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/__init__.py +53 -3
- fin_infra/analytics/__init__.py +13 -2
- fin_infra/analytics/add.py +21 -21
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +6 -6
- fin_infra/analytics/projections.py +1 -3
- fin_infra/banking/__init__.py +27 -28
- fin_infra/banking/history.py +8 -9
- fin_infra/banking/utils.py +27 -26
- fin_infra/brokerage/__init__.py +22 -24
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/budgets/alerts.py +3 -3
- fin_infra/budgets/tracker.py +2 -2
- fin_infra/cashflows/__init__.py +3 -3
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/engine.py +17 -14
- fin_infra/categorization/llm_layer.py +7 -6
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +6 -6
- fin_infra/chat/planning.py +0 -1
- fin_infra/cli/cmds/scaffold_cmds.py +10 -11
- fin_infra/clients/__init__.py +23 -1
- fin_infra/clients/base.py +1 -1
- fin_infra/clients/plaid.py +2 -2
- fin_infra/compliance/__init__.py +5 -4
- fin_infra/credit/add.py +6 -7
- fin_infra/credit/experian/auth.py +2 -2
- fin_infra/credit/experian/client.py +1 -1
- fin_infra/credit/experian/provider.py +4 -4
- fin_infra/crypto/__init__.py +7 -9
- fin_infra/crypto/insights.py +4 -3
- fin_infra/documents/add.py +6 -8
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +14 -14
- fin_infra/documents/models.py +4 -4
- fin_infra/documents/ocr.py +7 -7
- fin_infra/documents/storage.py +21 -13
- fin_infra/exceptions.py +0 -1
- fin_infra/goals/__init__.py +8 -8
- fin_infra/goals/add.py +36 -36
- fin_infra/goals/funding.py +4 -6
- fin_infra/goals/management.py +2 -3
- fin_infra/goals/milestones.py +1 -2
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +6 -3
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/models.py +5 -5
- fin_infra/investments/providers/base.py +8 -9
- fin_infra/investments/providers/plaid.py +52 -26
- fin_infra/investments/providers/snaptrade.py +19 -19
- fin_infra/markets/__init__.py +5 -3
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/brokerage.py +2 -1
- fin_infra/models/candle.py +1 -0
- fin_infra/models/money.py +1 -0
- fin_infra/models/quotes.py +4 -3
- fin_infra/models/tax.py +2 -1
- fin_infra/models/transactions.py +3 -4
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/net_worth/aggregator.py +4 -2
- fin_infra/net_worth/insights.py +0 -1
- fin_infra/normalization/__init__.py +2 -2
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/obs/classifier.py +1 -1
- fin_infra/providers/banking/plaid_client.py +5 -5
- fin_infra/providers/banking/teller_client.py +7 -6
- fin_infra/providers/base.py +27 -2
- fin_infra/providers/brokerage/alpaca.py +3 -3
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +19 -3
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +23 -8
- fin_infra/providers/tax/__init__.py +1 -1
- fin_infra/providers/tax/irs.py +1 -1
- fin_infra/providers/tax/mock.py +5 -5
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +5 -4
- fin_infra/recurring/detector.py +7 -7
- fin_infra/recurring/detectors_llm.py +6 -6
- fin_infra/recurring/ease.py +2 -4
- fin_infra/recurring/insights.py +13 -13
- fin_infra/recurring/normalizer.py +1 -1
- fin_infra/recurring/normalizers.py +4 -4
- fin_infra/recurring/summary.py +13 -15
- fin_infra/scaffold/budgets.py +9 -9
- fin_infra/scaffold/goals.py +5 -5
- fin_infra/security/__init__.py +8 -8
- fin_infra/security/encryption.py +6 -6
- fin_infra/security/models.py +7 -7
- fin_infra/security/pii_filter.py +6 -6
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +1 -1
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +4 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -8
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.69.dist-info/RECORD +0 -180
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.69.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
|
@@ -15,7 +15,8 @@ Expected performance:
|
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import logging
|
|
18
|
-
from typing import Any,
|
|
18
|
+
from typing import Any, cast
|
|
19
|
+
|
|
19
20
|
from pydantic import BaseModel, Field
|
|
20
21
|
|
|
21
22
|
# ai-infra imports
|
|
@@ -40,7 +41,7 @@ class CategoryPrediction(BaseModel):
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
# Few-shot examples (20 diverse merchants covering all major categories)
|
|
43
|
-
FEW_SHOT_EXAMPLES:
|
|
44
|
+
FEW_SHOT_EXAMPLES: list[tuple[str, str, str]] = [
|
|
44
45
|
# Food & Dining (5 examples)
|
|
45
46
|
("STARBUCKS #1234", "Coffee Shops", "Popular coffee shop chain"),
|
|
46
47
|
("MCDONALD'S", "Fast Food", "Fast food restaurant"),
|
|
@@ -157,7 +158,7 @@ class LLMCategorizer:
|
|
|
157
158
|
async def categorize(
|
|
158
159
|
self,
|
|
159
160
|
merchant_name: str,
|
|
160
|
-
user_id:
|
|
161
|
+
user_id: str | None = None,
|
|
161
162
|
) -> CategoryPrediction:
|
|
162
163
|
"""
|
|
163
164
|
Categorize merchant using LLM.
|
|
@@ -209,7 +210,7 @@ class LLMCategorizer:
|
|
|
209
210
|
async def _call_llm(
|
|
210
211
|
self,
|
|
211
212
|
merchant_name: str,
|
|
212
|
-
user_id:
|
|
213
|
+
user_id: str | None = None,
|
|
213
214
|
) -> CategoryPrediction:
|
|
214
215
|
"""Call LLM API with structured output."""
|
|
215
216
|
# Build user message
|
|
@@ -244,7 +245,7 @@ class LLMCategorizer:
|
|
|
244
245
|
f"Must be one of {len(valid_categories)} valid categories."
|
|
245
246
|
)
|
|
246
247
|
|
|
247
|
-
return cast(CategoryPrediction, response)
|
|
248
|
+
return cast("CategoryPrediction", response)
|
|
248
249
|
|
|
249
250
|
def _build_system_prompt(self) -> str:
|
|
250
251
|
"""Build system prompt with few-shot examples (reused across all requests)."""
|
|
@@ -269,7 +270,7 @@ class LLMCategorizer:
|
|
|
269
270
|
def _build_user_message(
|
|
270
271
|
self,
|
|
271
272
|
merchant_name: str,
|
|
272
|
-
user_id:
|
|
273
|
+
user_id: str | None = None,
|
|
273
274
|
) -> str:
|
|
274
275
|
"""Build user message with optional personalization."""
|
|
275
276
|
if self.enable_personalization and user_id:
|
|
@@ -6,12 +6,10 @@ Organized by category for maintainability.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
from .models import CategoryRule
|
|
12
11
|
from .taxonomy import Category
|
|
13
12
|
|
|
14
|
-
|
|
15
13
|
# ===== HELPER FUNCTIONS (defined first) =====
|
|
16
14
|
|
|
17
15
|
|
|
@@ -306,7 +304,7 @@ COMPILED_REGEX_RULES = [
|
|
|
306
304
|
# ===== PUBLIC FUNCTIONS =====
|
|
307
305
|
|
|
308
306
|
|
|
309
|
-
def get_exact_match(merchant: str) ->
|
|
307
|
+
def get_exact_match(merchant: str) -> Category | None:
|
|
310
308
|
"""
|
|
311
309
|
Get category by exact match.
|
|
312
310
|
|
|
@@ -320,7 +318,7 @@ def get_exact_match(merchant: str) -> Optional[Category]:
|
|
|
320
318
|
return EXACT_RULES_NORMALIZED.get(normalized)
|
|
321
319
|
|
|
322
320
|
|
|
323
|
-
def get_regex_match(merchant: str) ->
|
|
321
|
+
def get_regex_match(merchant: str) -> tuple[Category, int] | None:
|
|
324
322
|
"""
|
|
325
323
|
Get category by regex match.
|
|
326
324
|
|
|
@@ -12,7 +12,7 @@ Total: 56 leaf categories
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from enum import Enum
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
from pydantic import BaseModel
|
|
17
17
|
|
|
18
18
|
|
|
@@ -315,7 +315,7 @@ def get_category_group(category: Category) -> CategoryGroup:
|
|
|
315
315
|
return CATEGORY_GROUPS.get(category, CategoryGroup.UNCATEGORIZED)
|
|
316
316
|
|
|
317
317
|
|
|
318
|
-
def get_category_metadata(category: Category) ->
|
|
318
|
+
def get_category_metadata(category: Category) -> CategoryMetadata | None:
|
|
319
319
|
"""Get metadata for a category."""
|
|
320
320
|
return CATEGORY_METADATA.get(category)
|
|
321
321
|
|
fin_infra/chat/__init__.py
CHANGED
|
@@ -33,15 +33,15 @@ from typing import TYPE_CHECKING, Any
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from fastapi import FastAPI
|
|
35
35
|
|
|
36
|
+
from fin_infra.chat.ease import easy_financial_conversation
|
|
36
37
|
from fin_infra.chat.planning import (
|
|
37
|
-
|
|
38
|
-
ConversationResponse,
|
|
38
|
+
SENSITIVE_PATTERNS,
|
|
39
39
|
ConversationContext,
|
|
40
|
+
ConversationResponse,
|
|
40
41
|
Exchange,
|
|
42
|
+
FinancialPlanningConversation,
|
|
41
43
|
is_sensitive_question,
|
|
42
|
-
SENSITIVE_PATTERNS,
|
|
43
44
|
)
|
|
44
|
-
from fin_infra.chat.ease import easy_financial_conversation
|
|
45
45
|
|
|
46
46
|
__all__ = [
|
|
47
47
|
"FinancialPlanningConversation",
|
|
@@ -116,10 +116,10 @@ def add_financial_conversation(
|
|
|
116
116
|
- Logs all LLM calls for compliance (via svc-infra logging)
|
|
117
117
|
"""
|
|
118
118
|
from pydantic import BaseModel, Field
|
|
119
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
119
120
|
|
|
120
121
|
# Import svc-infra user router (requires auth)
|
|
121
122
|
from svc_infra.api.fastapi.dual.protected import user_router
|
|
122
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
123
123
|
|
|
124
124
|
# Auto-create conversation if not provided
|
|
125
125
|
if conversation is None:
|
|
@@ -189,6 +189,6 @@ def add_financial_conversation(
|
|
|
189
189
|
# Store on app.state for programmatic access
|
|
190
190
|
app.state.financial_conversation = conversation
|
|
191
191
|
|
|
192
|
-
print(f"
|
|
192
|
+
print(f"Financial chat enabled (AI-powered Q&A with {provider})")
|
|
193
193
|
|
|
194
194
|
return conversation
|
fin_infra/chat/planning.py
CHANGED
|
@@ -54,7 +54,6 @@ from typing import Any
|
|
|
54
54
|
|
|
55
55
|
from pydantic import BaseModel, Field
|
|
56
56
|
|
|
57
|
-
|
|
58
57
|
# ============================================================================
|
|
59
58
|
# Pydantic Schemas (Structured Output)
|
|
60
59
|
# ============================================================================
|
|
@@ -12,7 +12,6 @@ Usage:
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Optional
|
|
16
15
|
|
|
17
16
|
import click
|
|
18
17
|
import typer
|
|
@@ -54,47 +53,47 @@ def cmd_scaffold(
|
|
|
54
53
|
"--overwrite/--no-overwrite",
|
|
55
54
|
help="Overwrite existing files",
|
|
56
55
|
),
|
|
57
|
-
models_filename:
|
|
56
|
+
models_filename: str | None = typer.Option(
|
|
58
57
|
None,
|
|
59
58
|
"--models-filename",
|
|
60
59
|
help="Custom filename for models (default: {domain}.py)",
|
|
61
60
|
),
|
|
62
|
-
schemas_filename:
|
|
61
|
+
schemas_filename: str | None = typer.Option(
|
|
63
62
|
None,
|
|
64
63
|
"--schemas-filename",
|
|
65
64
|
help="Custom filename for schemas (default: {domain}_schemas.py)",
|
|
66
65
|
),
|
|
67
|
-
repository_filename:
|
|
66
|
+
repository_filename: str | None = typer.Option(
|
|
68
67
|
None,
|
|
69
68
|
"--repository-filename",
|
|
70
69
|
help="Custom filename for repository (default: {domain}_repository.py)",
|
|
71
70
|
),
|
|
72
71
|
) -> None:
|
|
73
72
|
"""Generate SQLAlchemy models, Pydantic schemas, and repository code from templates.
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
The scaffold command generates production-ready persistence layer code that works
|
|
76
75
|
seamlessly with svc-infra's add_sql_resources() for automatic CRUD APIs.
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
Examples:
|
|
79
78
|
# Basic scaffold (models + schemas + repository)
|
|
80
79
|
fin-infra scaffold budgets --dest-dir app/models/
|
|
81
|
-
|
|
80
|
+
|
|
82
81
|
# Financial goals tracking
|
|
83
82
|
fin-infra scaffold goals --dest-dir app/models/goals/
|
|
84
|
-
|
|
83
|
+
|
|
85
84
|
# With multi-tenancy and soft deletes
|
|
86
85
|
fin-infra scaffold budgets --dest-dir app/models/ \
|
|
87
86
|
--include-tenant --include-soft-delete
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# Without repository (use svc-infra SqlRepository directly)
|
|
90
89
|
fin-infra scaffold goals --dest-dir app/models/ \\
|
|
91
90
|
--no-with-repository
|
|
92
|
-
|
|
91
|
+
|
|
93
92
|
# Custom filenames
|
|
94
93
|
fin-infra scaffold budgets --dest-dir app/models/ \\
|
|
95
94
|
--models-filename custom_budget.py \\
|
|
96
95
|
--schemas-filename custom_schemas.py
|
|
97
|
-
|
|
96
|
+
|
|
98
97
|
After scaffolding, integrate with svc-infra:
|
|
99
98
|
1. Run migrations: svc-infra revision -m "add budgets" --autogenerate
|
|
100
99
|
2. Apply: svc-infra upgrade head
|
fin_infra/clients/__init__.py
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
"""DEPRECATED: Use fin_infra.providers instead.
|
|
2
|
+
|
|
3
|
+
This module is deprecated and will be removed in a future version.
|
|
4
|
+
All ABCs have been consolidated into fin_infra.providers.base.
|
|
5
|
+
|
|
6
|
+
Migration:
|
|
7
|
+
# Old (deprecated)
|
|
8
|
+
from fin_infra.clients import BankingClient, MarketDataClient
|
|
9
|
+
|
|
10
|
+
# New
|
|
11
|
+
from fin_infra.providers.base import BankingProvider, MarketDataProvider
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
from .base import BankingClient, CreditClient, MarketDataClient
|
|
17
|
+
|
|
18
|
+
warnings.warn(
|
|
19
|
+
"fin_infra.clients is deprecated. Use fin_infra.providers instead. "
|
|
20
|
+
"This module will be removed in a future version.",
|
|
21
|
+
DeprecationWarning,
|
|
22
|
+
stacklevel=2,
|
|
23
|
+
)
|
|
2
24
|
|
|
3
25
|
__all__ = ["BankingClient", "MarketDataClient", "CreditClient"]
|
fin_infra/clients/base.py
CHANGED
fin_infra/clients/plaid.py
CHANGED
fin_infra/compliance/__init__.py
CHANGED
|
@@ -20,8 +20,9 @@ Example:
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
|
+
from collections.abc import Callable
|
|
23
24
|
from datetime import datetime
|
|
24
|
-
from typing import
|
|
25
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
28
|
from fastapi import FastAPI, Request, Response
|
|
@@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def log_compliance_event(
|
|
35
|
-
app:
|
|
36
|
+
app: FastAPI,
|
|
36
37
|
event: str,
|
|
37
38
|
context: dict[str, Any] | None = None,
|
|
38
39
|
) -> None:
|
|
@@ -62,7 +63,7 @@ def log_compliance_event(
|
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def add_compliance_tracking(
|
|
65
|
-
app:
|
|
66
|
+
app: FastAPI,
|
|
66
67
|
*,
|
|
67
68
|
track_banking: bool = True,
|
|
68
69
|
track_credit: bool = True,
|
|
@@ -111,7 +112,7 @@ def add_compliance_tracking(
|
|
|
111
112
|
"""
|
|
112
113
|
|
|
113
114
|
@app.middleware("http")
|
|
114
|
-
async def compliance_tracking_middleware(request:
|
|
115
|
+
async def compliance_tracking_middleware(request: Request, call_next: Callable) -> Response:
|
|
115
116
|
"""Middleware to track compliance events for financial endpoints."""
|
|
116
117
|
path = request.url.path
|
|
117
118
|
method = request.method
|
fin_infra/credit/add.py
CHANGED
|
@@ -25,15 +25,14 @@ Example:
|
|
|
25
25
|
import logging
|
|
26
26
|
from typing import cast
|
|
27
27
|
|
|
28
|
-
from fastapi import
|
|
29
|
-
|
|
30
|
-
from svc_infra.api.fastapi.dual.protected import user_router, RequireUser
|
|
28
|
+
from fastapi import Depends, FastAPI, HTTPException, status
|
|
31
29
|
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
30
|
+
from svc_infra.api.fastapi.dual.protected import RequireUser, user_router
|
|
32
31
|
from svc_infra.cache import resource
|
|
33
32
|
from svc_infra.webhooks import add_webhooks
|
|
34
33
|
|
|
34
|
+
from fin_infra.models.credit import CreditReport, CreditScore
|
|
35
35
|
from fin_infra.providers.base import CreditProvider
|
|
36
|
-
from fin_infra.models.credit import CreditScore, CreditReport
|
|
37
36
|
|
|
38
37
|
logger = logging.getLogger(__name__)
|
|
39
38
|
|
|
@@ -156,8 +155,8 @@ def add_credit(
|
|
|
156
155
|
if enable_webhooks and hasattr(app.state, "webhooks_outbox"):
|
|
157
156
|
try:
|
|
158
157
|
# Get webhook service from app state
|
|
159
|
-
from svc_infra.webhooks.service import WebhookService
|
|
160
158
|
from svc_infra.db.outbox import OutboxStore
|
|
159
|
+
from svc_infra.webhooks.service import WebhookService
|
|
161
160
|
|
|
162
161
|
outbox: OutboxStore = app.state.webhooks_outbox
|
|
163
162
|
subs = app.state.webhooks_subscriptions
|
|
@@ -176,7 +175,7 @@ def add_credit(
|
|
|
176
175
|
# Don't fail request if webhook publishing fails
|
|
177
176
|
logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
|
|
178
177
|
|
|
179
|
-
return cast(CreditScore, score)
|
|
178
|
+
return cast("CreditScore", score)
|
|
180
179
|
|
|
181
180
|
@router.post("/report", response_model=CreditReport)
|
|
182
181
|
@credit_resource.cache_read(ttl=cache_ttl, suffix="report")
|
|
@@ -220,7 +219,7 @@ def add_credit(
|
|
|
220
219
|
detail="Credit bureau service unavailable",
|
|
221
220
|
)
|
|
222
221
|
|
|
223
|
-
return cast(CreditReport, report)
|
|
222
|
+
return cast("CreditReport", report)
|
|
224
223
|
|
|
225
224
|
# Mount router with dual routes (with/without trailing slash)
|
|
226
225
|
app.include_router(router, include_in_schema=True)
|
|
@@ -86,7 +86,7 @@ class ExperianAuthManager:
|
|
|
86
86
|
>>> headers = {"Authorization": f"Bearer {token}"}
|
|
87
87
|
"""
|
|
88
88
|
# Call the cached implementation with client_id for cache key
|
|
89
|
-
return cast(str, await self._get_token_cached(client_id=self.client_id))
|
|
89
|
+
return cast("str", await self._get_token_cached(client_id=self.client_id))
|
|
90
90
|
|
|
91
91
|
@cache_read(
|
|
92
92
|
key="oauth_token:experian:{client_id}", # Use client_id for uniqueness
|
|
@@ -141,7 +141,7 @@ class ExperianAuthManager:
|
|
|
141
141
|
|
|
142
142
|
# Parse and return token
|
|
143
143
|
data = response.json()
|
|
144
|
-
return cast(str, data["access_token"])
|
|
144
|
+
return cast("str", data["access_token"])
|
|
145
145
|
|
|
146
146
|
async def invalidate(self) -> None:
|
|
147
147
|
"""Invalidate cached token for THIS client (force refresh on next get_token call).
|
|
@@ -30,7 +30,7 @@ Example:
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
import logging
|
|
33
|
-
from datetime import
|
|
33
|
+
from datetime import UTC, datetime
|
|
34
34
|
from typing import Literal, cast
|
|
35
35
|
|
|
36
36
|
from fin_infra.credit.experian.auth import ExperianAuthManager
|
|
@@ -177,7 +177,7 @@ class ExperianProvider(CreditProvider):
|
|
|
177
177
|
|
|
178
178
|
# FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
|
|
179
179
|
# This log must be retained for at least 2 years per FCRA requirements
|
|
180
|
-
timestamp = datetime.now(
|
|
180
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
181
181
|
fcra_audit_logger.info(
|
|
182
182
|
"FCRA_CREDIT_PULL",
|
|
183
183
|
extra={
|
|
@@ -266,7 +266,7 @@ class ExperianProvider(CreditProvider):
|
|
|
266
266
|
# FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
|
|
267
267
|
# Full credit report pulls have stricter requirements than score-only pulls
|
|
268
268
|
# This log must be retained for at least 2 years per FCRA requirements
|
|
269
|
-
timestamp = datetime.now(
|
|
269
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
270
270
|
fcra_audit_logger.info(
|
|
271
271
|
"FCRA_CREDIT_PULL",
|
|
272
272
|
extra={
|
|
@@ -360,4 +360,4 @@ class ExperianProvider(CreditProvider):
|
|
|
360
360
|
signature_key=signature_key,
|
|
361
361
|
)
|
|
362
362
|
|
|
363
|
-
return cast(str, data.get("subscriptionId", "unknown"))
|
|
363
|
+
return cast("str", data.get("subscriptionId", "unknown"))
|
fin_infra/crypto/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ Quick start:
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from datetime import
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
17
|
from typing import TYPE_CHECKING, Literal
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
@@ -74,7 +74,7 @@ def easy_crypto(
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def add_crypto_data(
|
|
77
|
-
app:
|
|
77
|
+
app: FastAPI,
|
|
78
78
|
*,
|
|
79
79
|
provider: str | CryptoDataProvider | None = None,
|
|
80
80
|
prefix: str = "/crypto",
|
|
@@ -131,9 +131,9 @@ def add_crypto_data(
|
|
|
131
131
|
>>> add_observability(app)
|
|
132
132
|
>>> crypto = add_crypto_data(app)
|
|
133
133
|
"""
|
|
134
|
-
from svc_infra.api.fastapi.dual.public import public_router
|
|
135
|
-
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
136
134
|
from fastapi import HTTPException, Query
|
|
135
|
+
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
136
|
+
from svc_infra.api.fastapi.dual.public import public_router
|
|
137
137
|
|
|
138
138
|
# Initialize provider if string or None
|
|
139
139
|
if isinstance(provider, str):
|
|
@@ -168,11 +168,11 @@ def add_crypto_data(
|
|
|
168
168
|
"price": float(ticker.price),
|
|
169
169
|
"as_of": ticker.as_of.isoformat()
|
|
170
170
|
if ticker.as_of
|
|
171
|
-
else datetime.now(
|
|
171
|
+
else datetime.now(UTC).isoformat(),
|
|
172
172
|
}
|
|
173
173
|
except Exception as e:
|
|
174
174
|
raise HTTPException(
|
|
175
|
-
status_code=400, detail=f"Error fetching ticker for {symbol}: {
|
|
175
|
+
status_code=400, detail=f"Error fetching ticker for {symbol}: {e!s}"
|
|
176
176
|
)
|
|
177
177
|
|
|
178
178
|
@router.get("/ohlcv/{symbol}")
|
|
@@ -216,9 +216,7 @@ def add_crypto_data(
|
|
|
216
216
|
],
|
|
217
217
|
}
|
|
218
218
|
except Exception as e:
|
|
219
|
-
raise HTTPException(
|
|
220
|
-
status_code=400, detail=f"Error fetching OHLCV for {symbol}: {str(e)}"
|
|
221
|
-
)
|
|
219
|
+
raise HTTPException(status_code=400, detail=f"Error fetching OHLCV for {symbol}: {e!s}")
|
|
222
220
|
|
|
223
221
|
# Mount router
|
|
224
222
|
app.include_router(router, include_in_schema=True)
|
fin_infra/crypto/insights.py
CHANGED
|
@@ -8,6 +8,7 @@ CRITICAL: Uses ai-infra.llm.LLM (NEVER custom LLM clients).
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import logging
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
from decimal import Decimal
|
|
13
14
|
from typing import TYPE_CHECKING
|
|
@@ -17,6 +18,8 @@ from pydantic import BaseModel, Field
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from ai_infra.llm import LLM
|
|
19
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class CryptoInsight(BaseModel):
|
|
22
25
|
"""Personalized cryptocurrency insight.
|
|
@@ -284,8 +287,6 @@ Provide your insight:"""
|
|
|
284
287
|
)
|
|
285
288
|
)
|
|
286
289
|
except Exception as e:
|
|
287
|
-
|
|
288
|
-
# In production, use svc-infra logging
|
|
289
|
-
print(f"Warning: LLM insight generation failed: {e}")
|
|
290
|
+
logger.warning("LLM insight generation failed: %s", e)
|
|
290
291
|
|
|
291
292
|
return insights
|
fin_infra/documents/add.py
CHANGED
|
@@ -23,23 +23,22 @@ Quick Start:
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
-
from typing import TYPE_CHECKING
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
from fastapi import FastAPI
|
|
30
|
-
|
|
31
30
|
from svc_infra.storage.base import StorageBackend
|
|
32
31
|
|
|
33
32
|
from .ease import FinancialDocumentManager
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def add_documents(
|
|
37
|
-
app:
|
|
38
|
-
storage:
|
|
36
|
+
app: FastAPI,
|
|
37
|
+
storage: StorageBackend | None = None,
|
|
39
38
|
default_ocr_provider: str = "tesseract",
|
|
40
39
|
prefix: str = "/documents",
|
|
41
|
-
tags:
|
|
42
|
-
) ->
|
|
40
|
+
tags: list[str] | None = None,
|
|
41
|
+
) -> FinancialDocumentManager:
|
|
43
42
|
"""
|
|
44
43
|
Add financial document management endpoints to FastAPI app.
|
|
45
44
|
|
|
@@ -87,7 +86,6 @@ def add_documents(
|
|
|
87
86
|
- Stores manager on app.state.financial_documents
|
|
88
87
|
"""
|
|
89
88
|
from fastapi import HTTPException
|
|
90
|
-
|
|
91
89
|
from svc_infra.api.fastapi.dual.protected import user_router
|
|
92
90
|
|
|
93
91
|
# Import svc-infra base function to mount base endpoints (with fallback)
|
|
@@ -128,7 +126,7 @@ def add_documents(
|
|
|
128
126
|
@router.post("/{document_id}/ocr", response_model=OCRResult)
|
|
129
127
|
async def extract_text_ocr(
|
|
130
128
|
document_id: str,
|
|
131
|
-
provider:
|
|
129
|
+
provider: str | None = None,
|
|
132
130
|
force_refresh: bool = False,
|
|
133
131
|
) -> OCRResult:
|
|
134
132
|
"""
|
fin_infra/documents/analysis.py
CHANGED
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import re
|
|
26
26
|
from datetime import datetime
|
|
27
|
-
from typing import TYPE_CHECKING
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -32,14 +32,14 @@ if TYPE_CHECKING:
|
|
|
32
32
|
from .models import DocumentAnalysis
|
|
33
33
|
|
|
34
34
|
# In-memory analysis cache (production: use svc-infra cache)
|
|
35
|
-
_analysis_cache:
|
|
35
|
+
_analysis_cache: dict[str, DocumentAnalysis] = {}
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
async def analyze_document(
|
|
39
|
-
storage:
|
|
39
|
+
storage: StorageBackend,
|
|
40
40
|
document_id: str,
|
|
41
41
|
force_refresh: bool = False,
|
|
42
|
-
) ->
|
|
42
|
+
) -> DocumentAnalysis:
|
|
43
43
|
"""
|
|
44
44
|
Analyze a document using AI to extract insights and recommendations.
|
|
45
45
|
|
|
@@ -165,7 +165,7 @@ Important: This analysis is not a substitute for professional financial advice.
|
|
|
165
165
|
return prompt
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def _validate_analysis(analysis:
|
|
168
|
+
def _validate_analysis(analysis: DocumentAnalysis) -> bool:
|
|
169
169
|
"""
|
|
170
170
|
Validate LLM analysis output.
|
|
171
171
|
|
|
@@ -201,7 +201,7 @@ def _validate_analysis(analysis: "DocumentAnalysis") -> bool:
|
|
|
201
201
|
return True
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) ->
|
|
204
|
+
def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
|
|
205
205
|
"""
|
|
206
206
|
Specialized analysis for tax documents.
|
|
207
207
|
|
|
@@ -301,7 +301,7 @@ def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> "D
|
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
|
|
304
|
-
def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) ->
|
|
304
|
+
def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
|
|
305
305
|
"""
|
|
306
306
|
Specialized analysis for bank statements.
|
|
307
307
|
|
|
@@ -352,7 +352,7 @@ def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) ->
|
|
|
352
352
|
)
|
|
353
353
|
|
|
354
354
|
|
|
355
|
-
def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) ->
|
|
355
|
+
def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
|
|
356
356
|
"""
|
|
357
357
|
Specialized analysis for receipts.
|
|
358
358
|
|
|
@@ -394,7 +394,7 @@ def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> "Docume
|
|
|
394
394
|
|
|
395
395
|
def _analyze_generic_document(
|
|
396
396
|
ocr_text: str, document_type: str, metadata: dict, document_id: str
|
|
397
|
-
) ->
|
|
397
|
+
) -> DocumentAnalysis:
|
|
398
398
|
"""
|
|
399
399
|
Generic analysis for other document types.
|
|
400
400
|
|