fin-infra 0.1.81__py3-none-any.whl → 0.1.83__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/__init__.py +3 -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 -27
- fin_infra/banking/history.py +4 -5
- fin_infra/banking/utils.py +19 -18
- fin_infra/brokerage/__init__.py +22 -24
- fin_infra/budgets/__init__.py +3 -3
- fin_infra/budgets/add.py +16 -17
- fin_infra/cashflows/__init__.py +2 -2
- fin_infra/categorization/add.py +2 -3
- fin_infra/categorization/engine.py +6 -6
- fin_infra/categorization/llm_layer.py +6 -5
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +5 -5
- fin_infra/chat/planning.py +0 -1
- fin_infra/cli/cmds/scaffold_cmds.py +10 -11
- fin_infra/clients/plaid.py +1 -1
- fin_infra/compliance/__init__.py +5 -5
- 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/documents/add.py +6 -8
- fin_infra/documents/analysis.py +8 -8
- fin_infra/documents/ease.py +14 -14
- 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 +30 -30
- fin_infra/goals/funding.py +1 -1
- 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 +2 -2
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +1 -1
- fin_infra/investments/add.py +23 -23
- fin_infra/investments/providers/base.py +2 -3
- fin_infra/investments/providers/plaid.py +9 -9
- fin_infra/investments/providers/snaptrade.py +10 -10
- fin_infra/markets/__init__.py +1 -1
- 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/insights.py +0 -1
- fin_infra/normalization/__init__.py +2 -2
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/providers/banking/plaid_client.py +5 -5
- fin_infra/providers/banking/teller_client.py +7 -6
- fin_infra/providers/base.py +1 -1
- fin_infra/providers/brokerage/alpaca.py +3 -3
- fin_infra/providers/market/alphavantage.py +5 -10
- fin_infra/providers/market/ccxt_crypto.py +2 -2
- fin_infra/providers/market/coingecko.py +5 -6
- fin_infra/providers/market/yahoo.py +5 -5
- 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 +4 -6
- fin_infra/scaffold/budgets.py +6 -6
- fin_infra/scaffold/goals.py +1 -1
- 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 +1 -1
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/METADATA +1 -1
- fin_infra-0.1.83.dist-info/RECORD +180 -0
- fin_infra-0.1.81.dist-info/RECORD +0 -180
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.81.dist-info → fin_infra-0.1.83.dist-info}/entry_points.txt +0 -0
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/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
|
@@ -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: dict[str,
|
|
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
|
|
fin_infra/documents/ease.py
CHANGED
|
@@ -36,7 +36,7 @@ Quick Start:
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
-
from typing import TYPE_CHECKING
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
42
|
from svc_infra.documents import DocumentManager as BaseDocumentManager
|
|
@@ -94,7 +94,7 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
94
94
|
|
|
95
95
|
def __init__(
|
|
96
96
|
self,
|
|
97
|
-
storage:
|
|
97
|
+
storage: StorageBackend,
|
|
98
98
|
default_ocr_provider: str = "tesseract",
|
|
99
99
|
):
|
|
100
100
|
"""
|
|
@@ -111,12 +111,12 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
111
111
|
self,
|
|
112
112
|
user_id: str,
|
|
113
113
|
file: bytes,
|
|
114
|
-
document_type:
|
|
114
|
+
document_type: DocumentType,
|
|
115
115
|
filename: str,
|
|
116
|
-
metadata:
|
|
117
|
-
tax_year:
|
|
118
|
-
form_type:
|
|
119
|
-
) ->
|
|
116
|
+
metadata: dict | None = None,
|
|
117
|
+
tax_year: int | None = None,
|
|
118
|
+
form_type: str | None = None,
|
|
119
|
+
) -> FinancialDocument:
|
|
120
120
|
"""
|
|
121
121
|
Upload a financial document with financial-specific fields.
|
|
122
122
|
|
|
@@ -159,11 +159,11 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
159
159
|
def list_financial(
|
|
160
160
|
self,
|
|
161
161
|
user_id: str,
|
|
162
|
-
document_type:
|
|
163
|
-
tax_year:
|
|
162
|
+
document_type: DocumentType | None = None,
|
|
163
|
+
tax_year: int | None = None,
|
|
164
164
|
limit: int = 100,
|
|
165
165
|
offset: int = 0,
|
|
166
|
-
) -> list[
|
|
166
|
+
) -> list[FinancialDocument]:
|
|
167
167
|
"""
|
|
168
168
|
List user's financial documents with filters.
|
|
169
169
|
|
|
@@ -207,9 +207,9 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
207
207
|
async def extract_text(
|
|
208
208
|
self,
|
|
209
209
|
document_id: str,
|
|
210
|
-
provider:
|
|
210
|
+
provider: str | None = None,
|
|
211
211
|
force_refresh: bool = False,
|
|
212
|
-
) ->
|
|
212
|
+
) -> OCRResult:
|
|
213
213
|
"""
|
|
214
214
|
Extract text from document using OCR (financial extension).
|
|
215
215
|
|
|
@@ -239,7 +239,7 @@ class FinancialDocumentManager(BaseDocumentManager):
|
|
|
239
239
|
self,
|
|
240
240
|
document_id: str,
|
|
241
241
|
force_refresh: bool = False,
|
|
242
|
-
) ->
|
|
242
|
+
) -> DocumentAnalysis:
|
|
243
243
|
"""
|
|
244
244
|
Analyze document using AI (financial extension).
|
|
245
245
|
|
|
@@ -268,7 +268,7 @@ DocumentManager = FinancialDocumentManager
|
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
def easy_documents(
|
|
271
|
-
storage:
|
|
271
|
+
storage: StorageBackend | None = None,
|
|
272
272
|
default_ocr_provider: str = "tesseract",
|
|
273
273
|
) -> FinancialDocumentManager:
|
|
274
274
|
"""
|
fin_infra/documents/ocr.py
CHANGED
|
@@ -25,7 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import re
|
|
27
27
|
from datetime import datetime
|
|
28
|
-
from typing import TYPE_CHECKING
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -33,15 +33,15 @@ if TYPE_CHECKING:
|
|
|
33
33
|
from .models import OCRResult
|
|
34
34
|
|
|
35
35
|
# In-memory OCR cache (production: use svc-infra cache)
|
|
36
|
-
_ocr_cache: dict[str,
|
|
36
|
+
_ocr_cache: dict[str, OCRResult] = {}
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
async def extract_text(
|
|
40
|
-
storage:
|
|
40
|
+
storage: StorageBackend,
|
|
41
41
|
document_id: str,
|
|
42
42
|
provider: str = "tesseract",
|
|
43
43
|
force_refresh: bool = False,
|
|
44
|
-
) ->
|
|
44
|
+
) -> OCRResult:
|
|
45
45
|
"""
|
|
46
46
|
Extract text from a document using OCR (uses svc-infra storage).
|
|
47
47
|
|
|
@@ -106,7 +106,7 @@ async def extract_text(
|
|
|
106
106
|
|
|
107
107
|
def _extract_with_tesseract(
|
|
108
108
|
file_content: bytes, filename: str, metadata: dict, document_id: str
|
|
109
|
-
) ->
|
|
109
|
+
) -> OCRResult:
|
|
110
110
|
"""
|
|
111
111
|
Extract text using Tesseract OCR (simulated).
|
|
112
112
|
|
|
@@ -158,7 +158,7 @@ def _extract_with_tesseract(
|
|
|
158
158
|
|
|
159
159
|
def _extract_with_textract(
|
|
160
160
|
file_content: bytes, filename: str, metadata: dict, document_id: str
|
|
161
|
-
) ->
|
|
161
|
+
) -> OCRResult:
|
|
162
162
|
"""
|
|
163
163
|
Extract text using AWS Textract (simulated).
|
|
164
164
|
|
|
@@ -207,7 +207,7 @@ def _extract_with_textract(
|
|
|
207
207
|
)
|
|
208
208
|
|
|
209
209
|
|
|
210
|
-
def _parse_tax_form(text: str, form_type:
|
|
210
|
+
def _parse_tax_form(text: str, form_type: str | None = None) -> dict[str, str]:
|
|
211
211
|
"""
|
|
212
212
|
Parse tax form text into structured fields.
|
|
213
213
|
|
fin_infra/documents/storage.py
CHANGED
|
@@ -36,14 +36,22 @@ Quick Start:
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
-
from typing import TYPE_CHECKING
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
42
|
from svc_infra.documents import (
|
|
43
43
|
delete_document as base_delete_document,
|
|
44
|
+
)
|
|
45
|
+
from svc_infra.documents import (
|
|
44
46
|
download_document as base_download_document,
|
|
47
|
+
)
|
|
48
|
+
from svc_infra.documents import (
|
|
45
49
|
get_document as base_get_document,
|
|
50
|
+
)
|
|
51
|
+
from svc_infra.documents import (
|
|
46
52
|
list_documents as base_list_documents,
|
|
53
|
+
)
|
|
54
|
+
from svc_infra.documents import (
|
|
47
55
|
upload_document as base_upload_document,
|
|
48
56
|
)
|
|
49
57
|
|
|
@@ -64,15 +72,15 @@ if TYPE_CHECKING:
|
|
|
64
72
|
|
|
65
73
|
|
|
66
74
|
async def upload_document(
|
|
67
|
-
storage:
|
|
75
|
+
storage: StorageBackend,
|
|
68
76
|
user_id: str,
|
|
69
77
|
file: bytes,
|
|
70
|
-
document_type:
|
|
78
|
+
document_type: DocumentType,
|
|
71
79
|
filename: str,
|
|
72
|
-
metadata:
|
|
73
|
-
tax_year:
|
|
74
|
-
form_type:
|
|
75
|
-
) ->
|
|
80
|
+
metadata: dict | None = None,
|
|
81
|
+
tax_year: int | None = None,
|
|
82
|
+
form_type: str | None = None,
|
|
83
|
+
) -> FinancialDocument:
|
|
76
84
|
"""
|
|
77
85
|
Upload a financial document (delegates to svc-infra, adds financial fields).
|
|
78
86
|
|
|
@@ -140,7 +148,7 @@ async def upload_document(
|
|
|
140
148
|
return financial_doc
|
|
141
149
|
|
|
142
150
|
|
|
143
|
-
def get_document(document_id: str) ->
|
|
151
|
+
def get_document(document_id: str) -> FinancialDocument | None:
|
|
144
152
|
"""
|
|
145
153
|
Get financial document metadata by ID (delegates to svc-infra).
|
|
146
154
|
|
|
@@ -187,7 +195,7 @@ def get_document(document_id: str) -> Optional["FinancialDocument"]:
|
|
|
187
195
|
return financial_doc
|
|
188
196
|
|
|
189
197
|
|
|
190
|
-
async def download_document(storage:
|
|
198
|
+
async def download_document(storage: StorageBackend, document_id: str) -> bytes:
|
|
191
199
|
"""
|
|
192
200
|
Download a financial document by ID (delegates to svc-infra).
|
|
193
201
|
|
|
@@ -213,7 +221,7 @@ async def download_document(storage: "StorageBackend", document_id: str) -> byte
|
|
|
213
221
|
return await base_download_document(storage=storage, document_id=document_id)
|
|
214
222
|
|
|
215
223
|
|
|
216
|
-
async def delete_document(storage:
|
|
224
|
+
async def delete_document(storage: StorageBackend, document_id: str) -> bool:
|
|
217
225
|
"""
|
|
218
226
|
Delete a financial document and its metadata (delegates to svc-infra).
|
|
219
227
|
|
|
@@ -238,11 +246,11 @@ async def delete_document(storage: "StorageBackend", document_id: str) -> bool:
|
|
|
238
246
|
|
|
239
247
|
def list_documents(
|
|
240
248
|
user_id: str,
|
|
241
|
-
document_type:
|
|
242
|
-
tax_year:
|
|
249
|
+
document_type: DocumentType | None = None,
|
|
250
|
+
tax_year: int | None = None,
|
|
243
251
|
limit: int = 100,
|
|
244
252
|
offset: int = 0,
|
|
245
|
-
) -> list[
|
|
253
|
+
) -> list[FinancialDocument]:
|
|
246
254
|
"""
|
|
247
255
|
List user's financial documents with optional filters (delegates to svc-infra).
|
|
248
256
|
|
fin_infra/exceptions.py
CHANGED
|
@@ -23,7 +23,6 @@ from __future__ import annotations
|
|
|
23
23
|
import logging
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
# =============================================================================
|
|
28
27
|
# Logging Helper
|
|
29
28
|
# =============================================================================
|
fin_infra/goals/__init__.py
CHANGED
|
@@ -5,6 +5,14 @@ Provides comprehensive goal management with milestone tracking,
|
|
|
5
5
|
funding allocation, and progress monitoring.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from fin_infra.goals.add import add_goals
|
|
9
|
+
from fin_infra.goals.funding import (
|
|
10
|
+
get_account_allocations,
|
|
11
|
+
get_goal_funding_sources,
|
|
12
|
+
link_account_to_goal,
|
|
13
|
+
remove_account_from_goal,
|
|
14
|
+
update_account_allocation,
|
|
15
|
+
)
|
|
8
16
|
from fin_infra.goals.management import (
|
|
9
17
|
FinancialGoalTracker,
|
|
10
18
|
GoalProgressReport,
|
|
@@ -28,14 +36,6 @@ from fin_infra.goals.milestones import (
|
|
|
28
36
|
get_next_milestone,
|
|
29
37
|
trigger_milestone_notification,
|
|
30
38
|
)
|
|
31
|
-
from fin_infra.goals.funding import (
|
|
32
|
-
link_account_to_goal,
|
|
33
|
-
get_goal_funding_sources,
|
|
34
|
-
get_account_allocations,
|
|
35
|
-
update_account_allocation,
|
|
36
|
-
remove_account_from_goal,
|
|
37
|
-
)
|
|
38
|
-
from fin_infra.goals.add import add_goals
|
|
39
39
|
from fin_infra.goals.models import (
|
|
40
40
|
FundingSource,
|
|
41
41
|
Goal,
|
fin_infra/goals/add.py
CHANGED
|
@@ -29,30 +29,30 @@ add_goals(app)
|
|
|
29
29
|
|
|
30
30
|
import logging
|
|
31
31
|
from datetime import datetime
|
|
32
|
-
from typing import Any,
|
|
32
|
+
from typing import Any, cast
|
|
33
33
|
|
|
34
|
-
from fastapi import FastAPI, HTTPException,
|
|
34
|
+
from fastapi import Body, FastAPI, HTTPException, Query, status
|
|
35
35
|
from pydantic import BaseModel, Field
|
|
36
36
|
|
|
37
|
+
from fin_infra.goals.funding import (
|
|
38
|
+
get_goal_funding_sources,
|
|
39
|
+
link_account_to_goal,
|
|
40
|
+
remove_account_from_goal,
|
|
41
|
+
update_account_allocation,
|
|
42
|
+
)
|
|
37
43
|
from fin_infra.goals.management import (
|
|
38
44
|
create_goal,
|
|
39
|
-
list_goals,
|
|
40
|
-
get_goal,
|
|
41
|
-
update_goal,
|
|
42
45
|
delete_goal,
|
|
46
|
+
get_goal,
|
|
43
47
|
get_goal_progress,
|
|
48
|
+
list_goals,
|
|
49
|
+
update_goal,
|
|
44
50
|
)
|
|
45
51
|
from fin_infra.goals.milestones import (
|
|
46
52
|
add_milestone,
|
|
47
53
|
check_milestones,
|
|
48
54
|
get_milestone_progress,
|
|
49
55
|
)
|
|
50
|
-
from fin_infra.goals.funding import (
|
|
51
|
-
link_account_to_goal,
|
|
52
|
-
get_goal_funding_sources,
|
|
53
|
-
update_account_allocation,
|
|
54
|
-
remove_account_from_goal,
|
|
55
|
-
)
|
|
56
56
|
from fin_infra.goals.models import GoalStatus
|
|
57
57
|
|
|
58
58
|
logger = logging.getLogger(__name__)
|
|
@@ -63,7 +63,7 @@ logger = logging.getLogger(__name__)
|
|
|
63
63
|
# ============================================================================
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def parse_iso_date(date_str:
|
|
66
|
+
def parse_iso_date(date_str: str | None) -> datetime | None:
|
|
67
67
|
"""Parse ISO date string to datetime object."""
|
|
68
68
|
if date_str is None:
|
|
69
69
|
return None
|
|
@@ -85,24 +85,24 @@ class CreateGoalRequest(BaseModel):
|
|
|
85
85
|
name: str = Field(..., min_length=1, max_length=200, description="Goal name")
|
|
86
86
|
goal_type: str = Field(..., description="Goal type (savings, debt, investment, etc.)")
|
|
87
87
|
target_amount: float = Field(..., gt=0, description="Target amount")
|
|
88
|
-
deadline:
|
|
89
|
-
description:
|
|
90
|
-
current_amount:
|
|
91
|
-
auto_contribute:
|
|
92
|
-
tags:
|
|
88
|
+
deadline: str | None = Field(None, description="Deadline (ISO date)")
|
|
89
|
+
description: str | None = Field(None, description="Goal description")
|
|
90
|
+
current_amount: float | None = Field(0.0, ge=0, description="Current amount")
|
|
91
|
+
auto_contribute: bool | None = Field(False, description="Auto-contribute enabled")
|
|
92
|
+
tags: list[str] | None = Field(None, description="Goal tags")
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class UpdateGoalRequest(BaseModel):
|
|
96
96
|
"""Request body for updating a goal."""
|
|
97
97
|
|
|
98
|
-
name:
|
|
99
|
-
target_amount:
|
|
100
|
-
deadline:
|
|
101
|
-
description:
|
|
102
|
-
current_amount:
|
|
103
|
-
status:
|
|
104
|
-
auto_contribute:
|
|
105
|
-
tags:
|
|
98
|
+
name: str | None = Field(None, min_length=1, max_length=200)
|
|
99
|
+
target_amount: float | None = Field(None, gt=0)
|
|
100
|
+
deadline: str | None = None
|
|
101
|
+
description: str | None = None
|
|
102
|
+
current_amount: float | None = Field(None, ge=0)
|
|
103
|
+
status: GoalStatus | None = None
|
|
104
|
+
auto_contribute: bool | None = None
|
|
105
|
+
tags: list[str] | None = None
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
class AddMilestoneRequest(BaseModel):
|
|
@@ -110,7 +110,7 @@ class AddMilestoneRequest(BaseModel):
|
|
|
110
110
|
|
|
111
111
|
amount: float = Field(..., gt=0, description="Milestone amount")
|
|
112
112
|
description: str = Field(..., min_length=1, description="Milestone description")
|
|
113
|
-
target_date:
|
|
113
|
+
target_date: str | None = Field(None, description="Target date (ISO date)")
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
class LinkAccountRequest(BaseModel):
|
|
@@ -240,11 +240,11 @@ def add_goals(
|
|
|
240
240
|
|
|
241
241
|
@router.get("", response_model=list[dict])
|
|
242
242
|
async def list_goals_endpoint(
|
|
243
|
-
user_id:
|
|
243
|
+
user_id: str | None = Query(
|
|
244
244
|
None, description="User identifier (optional, returns all if not provided)"
|
|
245
245
|
),
|
|
246
|
-
goal_type:
|
|
247
|
-
status_filter:
|
|
246
|
+
goal_type: str | None = Query(None, description="Filter by goal type"),
|
|
247
|
+
status_filter: str | None = Query(None, alias="status", description="Filter by status"),
|
|
248
248
|
) -> list[dict]:
|
|
249
249
|
"""
|
|
250
250
|
List all goals for a user with optional filters.
|
|
@@ -469,7 +469,7 @@ def add_goals(
|
|
|
469
469
|
# Get all milestones from the goal (check_milestones only returns newly reached ones)
|
|
470
470
|
goal = get_goal(goal_id)
|
|
471
471
|
milestones = goal.get("milestones", [])
|
|
472
|
-
return cast(list[dict[Any, Any]], milestones)
|
|
472
|
+
return cast("list[dict[Any, Any]]", milestones)
|
|
473
473
|
except KeyError:
|
|
474
474
|
raise HTTPException(
|
|
475
475
|
status_code=status.HTTP_404_NOT_FOUND, detail=f"Goal {goal_id} not found"
|
fin_infra/goals/funding.py
CHANGED
|
@@ -22,8 +22,8 @@ Example:
|
|
|
22
22
|
>>> # Raises ValueError if total allocation > 100%
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
from fin_infra.goals.models import FundingSource
|
|
26
25
|
from fin_infra.goals.management import get_goal
|
|
26
|
+
from fin_infra.goals.models import FundingSource
|
|
27
27
|
|
|
28
28
|
# In-memory storage for funding allocations
|
|
29
29
|
# Structure: {account_id: {goal_id: allocation_percent}}
|