fin-infra 0.1.62__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 +30 -32
- fin_infra/analytics/cash_flow.py +6 -5
- fin_infra/analytics/ease.py +19 -20
- fin_infra/analytics/portfolio.py +19 -26
- fin_infra/analytics/projections.py +1 -3
- fin_infra/analytics/rebalancing.py +2 -4
- fin_infra/analytics/savings.py +1 -1
- fin_infra/analytics/spending.py +15 -11
- fin_infra/banking/__init__.py +33 -31
- fin_infra/banking/history.py +11 -12
- fin_infra/banking/utils.py +116 -110
- fin_infra/brokerage/__init__.py +27 -27
- 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 +4 -5
- fin_infra/cashflows/__init__.py +8 -10
- fin_infra/cashflows/core.py +1 -1
- fin_infra/categorization/__init__.py +1 -1
- fin_infra/categorization/add.py +17 -19
- fin_infra/categorization/ease.py +3 -4
- fin_infra/categorization/engine.py +21 -18
- fin_infra/categorization/llm_layer.py +10 -10
- fin_infra/categorization/models.py +1 -1
- fin_infra/categorization/rules.py +2 -4
- fin_infra/categorization/taxonomy.py +2 -2
- fin_infra/chat/__init__.py +13 -22
- fin_infra/chat/planning.py +57 -1
- fin_infra/cli/cmds/scaffold_cmds.py +11 -12
- 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 +7 -6
- fin_infra/credit/add.py +7 -7
- fin_infra/credit/experian/auth.py +3 -2
- fin_infra/credit/experian/client.py +2 -2
- fin_infra/credit/experian/provider.py +19 -19
- fin_infra/crypto/__init__.py +8 -10
- fin_infra/crypto/insights.py +5 -6
- fin_infra/documents/add.py +11 -13
- fin_infra/documents/analysis.py +9 -9
- fin_infra/documents/ease.py +18 -17
- fin_infra/documents/models.py +7 -7
- fin_infra/documents/ocr.py +8 -8
- fin_infra/documents/storage.py +23 -14
- fin_infra/exceptions.py +1 -2
- 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 +6 -7
- fin_infra/goals/milestones.py +2 -3
- fin_infra/goals/models.py +7 -11
- fin_infra/insights/__init__.py +12 -10
- fin_infra/insights/aggregator.py +1 -1
- fin_infra/investments/__init__.py +14 -9
- fin_infra/investments/add.py +53 -73
- fin_infra/investments/ease.py +16 -13
- fin_infra/investments/models.py +135 -69
- fin_infra/investments/providers/base.py +9 -15
- fin_infra/investments/providers/plaid.py +70 -55
- fin_infra/investments/providers/snaptrade.py +35 -53
- fin_infra/markets/__init__.py +16 -11
- fin_infra/models/__init__.py +10 -10
- fin_infra/models/accounts.py +2 -1
- 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 +4 -4
- fin_infra/net_worth/__init__.py +7 -0
- fin_infra/net_worth/add.py +8 -5
- fin_infra/net_worth/aggregator.py +9 -6
- fin_infra/net_worth/calculator.py +8 -6
- fin_infra/net_worth/ease.py +36 -15
- fin_infra/net_worth/insights.py +4 -5
- fin_infra/net_worth/models.py +237 -116
- fin_infra/normalization/__init__.py +17 -15
- fin_infra/normalization/providers/exchangerate.py +5 -5
- fin_infra/obs/classifier.py +3 -3
- fin_infra/providers/banking/plaid_client.py +23 -22
- fin_infra/providers/banking/teller_client.py +14 -7
- fin_infra/providers/base.py +131 -14
- fin_infra/providers/brokerage/alpaca.py +7 -7
- fin_infra/providers/credit/experian.py +5 -0
- fin_infra/providers/market/alphavantage.py +6 -11
- fin_infra/providers/market/ccxt_crypto.py +25 -4
- 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 +8 -8
- fin_infra/providers/tax/taxbit.py +1 -1
- fin_infra/recurring/__init__.py +6 -6
- fin_infra/recurring/add.py +24 -12
- fin_infra/recurring/detector.py +8 -8
- fin_infra/recurring/detectors_llm.py +14 -13
- fin_infra/recurring/ease.py +3 -5
- fin_infra/recurring/insights.py +20 -19
- fin_infra/recurring/models.py +3 -3
- fin_infra/recurring/normalizer.py +3 -2
- fin_infra/recurring/normalizers.py +11 -10
- fin_infra/recurring/summary.py +13 -15
- fin_infra/scaffold/__init__.py +1 -1
- 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/security/pii_patterns.py +1 -1
- fin_infra/security/token_store.py +3 -1
- fin_infra/settings.py +2 -1
- fin_infra/tax/__init__.py +2 -2
- fin_infra/tax/add.py +3 -2
- fin_infra/tax/tlh.py +5 -5
- fin_infra/utils/http.py +5 -3
- fin_infra/utils/retry.py +2 -1
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
- fin_infra-0.1.82.dist-info/RECORD +180 -0
- fin_infra-0.1.62.dist-info/RECORD +0 -180
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
- {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
fin_infra/recurring/summary.py
CHANGED
|
@@ -33,14 +33,12 @@ Integration with svc-infra:
|
|
|
33
33
|
|
|
34
34
|
from __future__ import annotations
|
|
35
35
|
|
|
36
|
-
from typing import List, Dict, Optional
|
|
37
|
-
from datetime import datetime
|
|
38
36
|
from collections import defaultdict
|
|
37
|
+
from datetime import datetime
|
|
39
38
|
|
|
40
|
-
from pydantic import BaseModel,
|
|
41
|
-
|
|
42
|
-
from fin_infra.recurring.models import RecurringPattern, PatternType
|
|
39
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
43
40
|
|
|
41
|
+
from fin_infra.recurring.models import PatternType, RecurringPattern
|
|
44
42
|
|
|
45
43
|
__all__ = [
|
|
46
44
|
"RecurringItem",
|
|
@@ -121,16 +119,16 @@ class RecurringSummary(BaseModel):
|
|
|
121
119
|
user_id: str = Field(..., description="User identifier")
|
|
122
120
|
total_monthly_cost: float = Field(..., description="Total monthly recurring expenses")
|
|
123
121
|
total_monthly_income: float = Field(0.0, description="Total monthly recurring income")
|
|
124
|
-
subscriptions:
|
|
122
|
+
subscriptions: list[RecurringItem] = Field(
|
|
125
123
|
default_factory=list, description="List of recurring expense items"
|
|
126
124
|
)
|
|
127
|
-
recurring_income:
|
|
125
|
+
recurring_income: list[RecurringItem] = Field(
|
|
128
126
|
default_factory=list, description="List of recurring income items"
|
|
129
127
|
)
|
|
130
|
-
by_category:
|
|
128
|
+
by_category: dict[str, float] = Field(
|
|
131
129
|
default_factory=dict, description="Monthly cost grouped by category"
|
|
132
130
|
)
|
|
133
|
-
cancellation_opportunities:
|
|
131
|
+
cancellation_opportunities: list[CancellationOpportunity] = Field(
|
|
134
132
|
default_factory=list, description="Potential subscriptions to cancel"
|
|
135
133
|
)
|
|
136
134
|
generated_at: str = Field(
|
|
@@ -165,8 +163,8 @@ def _calculate_monthly_cost(amount: float, cadence: str) -> float:
|
|
|
165
163
|
|
|
166
164
|
|
|
167
165
|
def _identify_cancellation_opportunities(
|
|
168
|
-
subscriptions:
|
|
169
|
-
) ->
|
|
166
|
+
subscriptions: list[RecurringItem],
|
|
167
|
+
) -> list[CancellationOpportunity]:
|
|
170
168
|
"""Identify potential cancellation opportunities from subscriptions.
|
|
171
169
|
|
|
172
170
|
Looks for:
|
|
@@ -183,7 +181,7 @@ def _identify_cancellation_opportunities(
|
|
|
183
181
|
opportunities = []
|
|
184
182
|
|
|
185
183
|
# Group by category
|
|
186
|
-
by_category:
|
|
184
|
+
by_category: dict[str, list[RecurringItem]] = defaultdict(list)
|
|
187
185
|
for sub in subscriptions:
|
|
188
186
|
by_category[sub.category].append(sub)
|
|
189
187
|
|
|
@@ -253,8 +251,8 @@ def _identify_cancellation_opportunities(
|
|
|
253
251
|
|
|
254
252
|
def get_recurring_summary(
|
|
255
253
|
user_id: str,
|
|
256
|
-
patterns:
|
|
257
|
-
category_map:
|
|
254
|
+
patterns: list[RecurringPattern],
|
|
255
|
+
category_map: dict[str, str] | None = None,
|
|
258
256
|
) -> RecurringSummary:
|
|
259
257
|
"""Generate a comprehensive recurring transaction summary for a user.
|
|
260
258
|
|
|
@@ -283,7 +281,7 @@ def get_recurring_summary(
|
|
|
283
281
|
"""
|
|
284
282
|
subscriptions = []
|
|
285
283
|
recurring_income = []
|
|
286
|
-
by_category:
|
|
284
|
+
by_category: dict[str, float] = defaultdict(float)
|
|
287
285
|
|
|
288
286
|
for pattern in patterns:
|
|
289
287
|
# Determine amount (use fixed amount or average of range)
|
fin_infra/scaffold/__init__.py
CHANGED
fin_infra/scaffold/budgets.py
CHANGED
|
@@ -19,10 +19,10 @@ Typical usage:
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Any
|
|
22
|
+
from typing import Any
|
|
23
23
|
|
|
24
24
|
# Use svc-infra's scaffold utilities to avoid duplication
|
|
25
|
-
from svc_infra.utils import render_template, write
|
|
25
|
+
from svc_infra.utils import ensure_init_py, render_template, write
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def scaffold_budgets_core(
|
|
@@ -31,10 +31,10 @@ def scaffold_budgets_core(
|
|
|
31
31
|
include_soft_delete: bool = False,
|
|
32
32
|
with_repository: bool = True,
|
|
33
33
|
overwrite: bool = False,
|
|
34
|
-
models_filename:
|
|
35
|
-
schemas_filename:
|
|
36
|
-
repository_filename:
|
|
37
|
-
) ->
|
|
34
|
+
models_filename: str | None = None,
|
|
35
|
+
schemas_filename: str | None = None,
|
|
36
|
+
repository_filename: str | None = None,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
38
|
"""Generate budget persistence code from templates.
|
|
39
39
|
|
|
40
40
|
Args:
|
|
@@ -72,7 +72,7 @@ def scaffold_budgets_core(
|
|
|
72
72
|
subs = _generate_substitutions(include_tenant, include_soft_delete)
|
|
73
73
|
|
|
74
74
|
# Track all file operations
|
|
75
|
-
files:
|
|
75
|
+
files: list[dict[str, Any]] = []
|
|
76
76
|
|
|
77
77
|
# Render and write models
|
|
78
78
|
models_content = render_template("fin_infra.budgets.scaffold_templates", "models.py.tmpl", subs)
|
|
@@ -114,7 +114,7 @@ def scaffold_budgets_core(
|
|
|
114
114
|
def _generate_substitutions(
|
|
115
115
|
include_tenant: bool,
|
|
116
116
|
include_soft_delete: bool,
|
|
117
|
-
) ->
|
|
117
|
+
) -> dict[str, str]:
|
|
118
118
|
"""Generate template variable substitutions for budgets.
|
|
119
119
|
|
|
120
120
|
Args:
|
|
@@ -229,7 +229,7 @@ def _tenant_field_schema_read() -> str:
|
|
|
229
229
|
def _generate_init_content(
|
|
230
230
|
models_file: str,
|
|
231
231
|
schemas_file: str,
|
|
232
|
-
repo_file:
|
|
232
|
+
repo_file: str | None,
|
|
233
233
|
) -> str:
|
|
234
234
|
"""Generate __init__.py content with re-exports.
|
|
235
235
|
|
fin_infra/scaffold/goals.py
CHANGED
|
@@ -17,19 +17,19 @@ Typical usage:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
21
21
|
|
|
22
22
|
from svc_infra.utils import (
|
|
23
|
+
ensure_init_py,
|
|
23
24
|
render_template,
|
|
24
25
|
write,
|
|
25
|
-
ensure_init_py,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _generate_substitutions(
|
|
30
30
|
include_tenant: bool = False,
|
|
31
31
|
include_soft_delete: bool = False,
|
|
32
|
-
) ->
|
|
32
|
+
) -> dict[str, str]:
|
|
33
33
|
"""
|
|
34
34
|
Generate template substitutions for goals domain.
|
|
35
35
|
|
|
@@ -49,7 +49,7 @@ def _generate_substitutions(
|
|
|
49
49
|
Returns:
|
|
50
50
|
Dict mapping variable names to their substitution values
|
|
51
51
|
"""
|
|
52
|
-
subs:
|
|
52
|
+
subs: dict[str, str] = {
|
|
53
53
|
"Entity": "Goal",
|
|
54
54
|
"entity": "goal",
|
|
55
55
|
"table_name": "goals",
|
|
@@ -173,7 +173,7 @@ def scaffold_goals_core(
|
|
|
173
173
|
models_filename: str = "goal.py",
|
|
174
174
|
schemas_filename: str = "goal_schemas.py",
|
|
175
175
|
repository_filename: str = "goal_repository.py",
|
|
176
|
-
) ->
|
|
176
|
+
) -> dict[str, Any]:
|
|
177
177
|
"""
|
|
178
178
|
Scaffold goals domain files: models, schemas, repository (optional), and __init__.py.
|
|
179
179
|
|
fin_infra/security/__init__.py
CHANGED
|
@@ -8,25 +8,25 @@ Provides:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from .add import add_financial_security, generate_encryption_key
|
|
11
|
-
from .audit import
|
|
11
|
+
from .audit import clear_audit_logs, get_audit_logs, log_pii_access
|
|
12
12
|
from .encryption import ProviderTokenEncryption
|
|
13
|
-
from .models import
|
|
13
|
+
from .models import PIIAccessLog, ProviderTokenMetadata
|
|
14
14
|
from .pii_filter import FinancialPIIFilter
|
|
15
15
|
from .pii_patterns import (
|
|
16
|
-
SSN_PATTERN,
|
|
17
16
|
ACCOUNT_PATTERN,
|
|
18
|
-
ROUTING_PATTERN,
|
|
19
17
|
CARD_PATTERN,
|
|
20
18
|
CVV_PATTERN,
|
|
21
19
|
EIN_PATTERN,
|
|
22
|
-
|
|
20
|
+
ROUTING_PATTERN,
|
|
21
|
+
SSN_PATTERN,
|
|
23
22
|
is_valid_routing_number,
|
|
23
|
+
luhn_checksum,
|
|
24
24
|
)
|
|
25
25
|
from .token_store import (
|
|
26
|
-
store_provider_token,
|
|
27
|
-
get_provider_token,
|
|
28
|
-
delete_provider_token,
|
|
29
26
|
ProviderToken,
|
|
27
|
+
delete_provider_token,
|
|
28
|
+
get_provider_token,
|
|
29
|
+
store_provider_token,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
__all__ = [
|
fin_infra/security/encryption.py
CHANGED
|
@@ -7,7 +7,7 @@ Encrypt/decrypt financial provider API tokens at rest.
|
|
|
7
7
|
import base64
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, cast
|
|
11
11
|
|
|
12
12
|
from cryptography.fernet import Fernet, InvalidToken
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ class ProviderTokenEncryption:
|
|
|
37
37
|
>>> token = encryption.decrypt(encrypted, context={"user_id": "user123", "provider": "plaid"})
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
def __init__(self, key:
|
|
40
|
+
def __init__(self, key: bytes | None = None):
|
|
41
41
|
"""
|
|
42
42
|
Initialize token encryption.
|
|
43
43
|
|
|
@@ -64,7 +64,7 @@ class ProviderTokenEncryption:
|
|
|
64
64
|
raise ValueError(f"Invalid encryption key: {e}") from e
|
|
65
65
|
|
|
66
66
|
def encrypt(
|
|
67
|
-
self, token: str, context:
|
|
67
|
+
self, token: str, context: dict[str, Any] | None = None, key_id: str | None = None
|
|
68
68
|
) -> str:
|
|
69
69
|
"""
|
|
70
70
|
Encrypt provider token with optional context.
|
|
@@ -104,7 +104,7 @@ class ProviderTokenEncryption:
|
|
|
104
104
|
def decrypt(
|
|
105
105
|
self,
|
|
106
106
|
encrypted_token: str,
|
|
107
|
-
context:
|
|
107
|
+
context: dict[str, Any] | None = None,
|
|
108
108
|
verify_context: bool = True,
|
|
109
109
|
) -> str:
|
|
110
110
|
"""
|
|
@@ -144,7 +144,7 @@ class ProviderTokenEncryption:
|
|
|
144
144
|
"Token may have been tampered with or used for wrong user/provider."
|
|
145
145
|
)
|
|
146
146
|
|
|
147
|
-
return data["token"]
|
|
147
|
+
return cast("str", data["token"])
|
|
148
148
|
|
|
149
149
|
except InvalidToken as e:
|
|
150
150
|
raise ValueError(
|
|
@@ -154,7 +154,7 @@ class ProviderTokenEncryption:
|
|
|
154
154
|
raise ValueError(f"Decryption failed: {e}") from e
|
|
155
155
|
|
|
156
156
|
def rotate_key(
|
|
157
|
-
self, encrypted_token: str, new_key: bytes, context:
|
|
157
|
+
self, encrypted_token: str, new_key: bytes, context: dict[str, Any] | None = None
|
|
158
158
|
) -> str:
|
|
159
159
|
"""
|
|
160
160
|
Re-encrypt token with new key (for key rotation).
|
fin_infra/security/models.py
CHANGED
|
@@ -5,7 +5,7 @@ Pydantic models for security-related operations.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
11
|
|
|
@@ -15,12 +15,12 @@ class ProviderTokenMetadata(BaseModel):
|
|
|
15
15
|
user_id: str = Field(..., description="User ID who owns the token")
|
|
16
16
|
provider: str = Field(..., description="Provider name (plaid, alpaca, alphavantage, etc.)")
|
|
17
17
|
encrypted_token: str = Field(..., description="Encrypted token (base64-encoded)")
|
|
18
|
-
key_id:
|
|
18
|
+
key_id: str | None = Field(None, description="Key ID for key rotation")
|
|
19
19
|
created_at: datetime = Field(
|
|
20
20
|
default_factory=datetime.utcnow, description="Token creation timestamp"
|
|
21
21
|
)
|
|
22
|
-
expires_at:
|
|
23
|
-
last_used_at:
|
|
22
|
+
expires_at: datetime | None = Field(None, description="Token expiration timestamp")
|
|
23
|
+
last_used_at: datetime | None = Field(None, description="Last time token was used")
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class PIIAccessLog(BaseModel):
|
|
@@ -30,8 +30,8 @@ class PIIAccessLog(BaseModel):
|
|
|
30
30
|
pii_type: str = Field(..., description="Type of PII (ssn, account, card, etc.)")
|
|
31
31
|
action: str = Field(..., description="Action performed (read, write, delete)")
|
|
32
32
|
resource: str = Field(..., description="Resource accessed (e.g., user:123, account:456)")
|
|
33
|
-
ip_address:
|
|
34
|
-
user_agent:
|
|
33
|
+
ip_address: str | None = Field(None, description="IP address of requester")
|
|
34
|
+
user_agent: str | None = Field(None, description="User agent string")
|
|
35
35
|
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Access timestamp")
|
|
36
36
|
success: bool = Field(True, description="Whether access was successful")
|
|
37
|
-
error_message:
|
|
37
|
+
error_message: str | None = Field(None, description="Error message if failed")
|
fin_infra/security/pii_filter.py
CHANGED
|
@@ -9,21 +9,21 @@ import re
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from .pii_patterns import (
|
|
12
|
-
ACCOUNT_PATTERN,
|
|
13
12
|
ACCOUNT_CONTEXT,
|
|
13
|
+
ACCOUNT_PATTERN,
|
|
14
14
|
CARD_PATTERN,
|
|
15
|
-
CVV_PATTERN,
|
|
16
15
|
CVV_CONTEXT,
|
|
16
|
+
CVV_PATTERN,
|
|
17
17
|
EIN_PATTERN,
|
|
18
18
|
EMAIL_PATTERN,
|
|
19
19
|
PHONE_PATTERN,
|
|
20
|
-
ROUTING_PATTERN,
|
|
21
20
|
ROUTING_CONTEXT,
|
|
22
|
-
|
|
23
|
-
SSN_NO_DASH,
|
|
21
|
+
ROUTING_PATTERN,
|
|
24
22
|
SSN_CONTEXT,
|
|
25
|
-
|
|
23
|
+
SSN_NO_DASH,
|
|
24
|
+
SSN_PATTERN,
|
|
26
25
|
is_valid_routing_number,
|
|
26
|
+
luhn_checksum,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
|
|
@@ -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 = (
|
fin_infra/settings.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from functools import lru_cache
|
|
4
|
+
|
|
4
5
|
from pydantic import Field
|
|
5
6
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
7
|
|
|
@@ -33,5 +34,5 @@ class Settings(BaseSettings):
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
@lru_cache
|
|
36
|
-
def get_settings() ->
|
|
37
|
+
def get_settings() -> Settings:
|
|
37
38
|
return Settings()
|
fin_infra/tax/__init__.py
CHANGED
|
@@ -33,7 +33,7 @@ Example:
|
|
|
33
33
|
import os
|
|
34
34
|
|
|
35
35
|
from fin_infra.providers.base import TaxProvider
|
|
36
|
-
from fin_infra.providers.tax import
|
|
36
|
+
from fin_infra.providers.tax import IRSProvider, MockTaxProvider, TaxBitProvider
|
|
37
37
|
from fin_infra.tax.add import add_tax_data
|
|
38
38
|
from fin_infra.tax.tlh import (
|
|
39
39
|
TLHOpportunity,
|
|
@@ -144,7 +144,7 @@ def easy_tax(provider: str | TaxProvider = "mock", **config) -> TaxProvider:
|
|
|
144
144
|
|
|
145
145
|
else:
|
|
146
146
|
raise ValueError(
|
|
147
|
-
f"Unknown tax provider: {provider}.
|
|
147
|
+
f"Unknown tax provider: {provider}. Supported providers: 'mock', 'irs', 'taxbit'"
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
|
fin_infra/tax/add.py
CHANGED
|
@@ -17,7 +17,8 @@ Example:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
from decimal import Decimal
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
from fastapi import Body, FastAPI, Query
|
|
21
22
|
from pydantic import BaseModel
|
|
22
23
|
|
|
23
24
|
from fin_infra.providers.base import TaxProvider
|
|
@@ -89,8 +90,8 @@ def add_tax_data(
|
|
|
89
90
|
>>> # POST /tax/tax-liability
|
|
90
91
|
"""
|
|
91
92
|
# Use svc-infra user_router for authentication (tax data is user-specific and sensitive)
|
|
92
|
-
from svc_infra.api.fastapi.dual.protected import user_router
|
|
93
93
|
from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
|
|
94
|
+
from svc_infra.api.fastapi.dual.protected import user_router
|
|
94
95
|
|
|
95
96
|
# Initialize provider
|
|
96
97
|
if provider is None:
|
fin_infra/tax/tlh.py
CHANGED
|
@@ -55,7 +55,7 @@ Cost Considerations:
|
|
|
55
55
|
|
|
56
56
|
from __future__ import annotations
|
|
57
57
|
|
|
58
|
-
from datetime import
|
|
58
|
+
from datetime import UTC, datetime
|
|
59
59
|
from decimal import Decimal
|
|
60
60
|
from typing import TYPE_CHECKING
|
|
61
61
|
|
|
@@ -412,9 +412,9 @@ def _assess_wash_sale_risk(symbol: str, last_purchase_date: datetime | None) ->
|
|
|
412
412
|
return "none"
|
|
413
413
|
|
|
414
414
|
# Calculate days since last purchase
|
|
415
|
-
now = datetime.now(
|
|
415
|
+
now = datetime.now(UTC)
|
|
416
416
|
if last_purchase_date.tzinfo is None:
|
|
417
|
-
last_purchase_date = last_purchase_date.replace(tzinfo=
|
|
417
|
+
last_purchase_date = last_purchase_date.replace(tzinfo=UTC)
|
|
418
418
|
|
|
419
419
|
days_since = (now - last_purchase_date).days
|
|
420
420
|
|
|
@@ -506,8 +506,8 @@ def _generate_tlh_recommendations(
|
|
|
506
506
|
recommendations = []
|
|
507
507
|
|
|
508
508
|
# Timing recommendations
|
|
509
|
-
now = datetime.now(
|
|
510
|
-
days_until_year_end = (datetime(now.year, 12, 31, tzinfo=
|
|
509
|
+
now = datetime.now(UTC)
|
|
510
|
+
days_until_year_end = (datetime(now.year, 12, 31, tzinfo=UTC) - now).days
|
|
511
511
|
|
|
512
512
|
if days_until_year_end < 30:
|
|
513
513
|
recommendations.append(
|
fin_infra/utils/http.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
3
5
|
import httpx
|
|
4
|
-
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
6
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
|
|
5
7
|
|
|
6
8
|
_DEFAULT_TIMEOUT = httpx.Timeout(20.0)
|
|
7
9
|
|
|
@@ -12,8 +14,8 @@ _DEFAULT_TIMEOUT = httpx.Timeout(20.0)
|
|
|
12
14
|
retry=retry_if_exception_type(httpx.HTTPError),
|
|
13
15
|
reraise=True,
|
|
14
16
|
)
|
|
15
|
-
async def aget_json(url: str, **kwargs) -> dict:
|
|
17
|
+
async def aget_json(url: str, **kwargs) -> dict[Any, Any]:
|
|
16
18
|
async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
17
19
|
r = await client.get(url, **kwargs)
|
|
18
20
|
r.raise_for_status()
|
|
19
|
-
return r.json()
|
|
21
|
+
return cast("dict[Any, Any]", r.json())
|
fin_infra/utils/retry.py
CHANGED
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import random
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
6
|
+
from typing import TypeVar
|
|
6
7
|
|
|
7
8
|
from fin_infra.exceptions import RetryError
|
|
8
9
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fin-infra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.82
|
|
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
|
|
@@ -19,25 +19,30 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
19
19
|
Classifier: Topic :: Office/Business :: Financial
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Provides-Extra: banking
|
|
24
|
+
Provides-Extra: crypto
|
|
25
|
+
Provides-Extra: markets
|
|
26
|
+
Provides-Extra: plaid
|
|
27
|
+
Provides-Extra: yahoo
|
|
22
28
|
Requires-Dist: ai-infra (>=0.1.142)
|
|
23
29
|
Requires-Dist: cashews[redis] (>=7.0)
|
|
24
|
-
Requires-Dist: ccxt (>=4.0.0)
|
|
30
|
+
Requires-Dist: ccxt (>=4.0.0) ; extra == "markets" or extra == "crypto" or extra == "all"
|
|
25
31
|
Requires-Dist: httpx (>=0.25.0)
|
|
26
32
|
Requires-Dist: loguru (>=0.7.0)
|
|
27
33
|
Requires-Dist: numpy (>=1.24.0)
|
|
28
34
|
Requires-Dist: numpy-financial (>=1.0.0)
|
|
29
|
-
Requires-Dist: plaid-python (>=25.0.0)
|
|
35
|
+
Requires-Dist: plaid-python (>=25.0.0) ; extra == "plaid" or extra == "banking" or extra == "all"
|
|
30
36
|
Requires-Dist: pydantic (>=2.0)
|
|
31
37
|
Requires-Dist: pydantic-settings (>=2.0)
|
|
32
38
|
Requires-Dist: python-dotenv (>=1.0.0)
|
|
33
|
-
Requires-Dist: svc-infra (>=0.1.0)
|
|
34
39
|
Requires-Dist: tenacity (>=8.0.0)
|
|
35
40
|
Requires-Dist: typing-extensions (>=4.0)
|
|
36
|
-
Requires-Dist: yahooquery (>=2.3.0)
|
|
37
|
-
Project-URL: Documentation, https://
|
|
38
|
-
Project-URL: Homepage, https://github.com/
|
|
39
|
-
Project-URL: Issues, https://github.com/
|
|
40
|
-
Project-URL: Repository, https://github.com/
|
|
41
|
+
Requires-Dist: yahooquery (>=2.3.0) ; extra == "markets" or extra == "yahoo" or extra == "all"
|
|
42
|
+
Project-URL: Documentation, https://nfrax.com/fin-infra
|
|
43
|
+
Project-URL: Homepage, https://github.com/nfraxlab/fin-infra
|
|
44
|
+
Project-URL: Issues, https://github.com/nfraxlab/fin-infra/issues
|
|
45
|
+
Project-URL: Repository, https://github.com/nfraxlab/fin-infra
|
|
41
46
|
Description-Content-Type: text/markdown
|
|
42
47
|
|
|
43
48
|
<div align="center">
|