taxforge 0.9.21__tar.gz → 0.9.23__tar.gz
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.
- {taxforge-0.9.21 → taxforge-0.9.23}/PKG-INFO +1 -1
- {taxforge-0.9.21 → taxforge-0.9.23}/pyproject.toml +1 -1
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/aitax/__init__.py +1 -1
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/aitax/aitax.py +2 -6
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/aitax/cli.py +1 -1
- taxforge-0.9.23/ui/aitax/persistence.py +197 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/aitax/state.py +22 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/taxforge.egg-info/PKG-INFO +1 -1
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/taxforge.egg-info/SOURCES.txt +1 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/README.md +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/setup.cfg +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/tests/test_fact_graph.py +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/tests/test_investments.py +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/tests/test_tax_engine.py +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/aitax/components.py +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/aitax/document_extractor.py +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/taxforge.egg-info/dependency_links.txt +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/taxforge.egg-info/entry_points.txt +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/taxforge.egg-info/requires.txt +0 -0
- {taxforge-0.9.21 → taxforge-0.9.23}/ui/taxforge.egg-info/top_level.txt +0 -0
|
@@ -617,11 +617,7 @@ def review_page() -> rx.Component:
|
|
|
617
617
|
{"background": "#E8EDFF"},
|
|
618
618
|
{},
|
|
619
619
|
),
|
|
620
|
-
on_click=
|
|
621
|
-
doc["status"] != "parsed",
|
|
622
|
-
lambda: TaxAppState.toggle_file_selection(doc["name"]),
|
|
623
|
-
lambda: None,
|
|
624
|
-
),
|
|
620
|
+
on_click=lambda: TaxAppState.toggle_file_selection(doc["name"]),
|
|
625
621
|
transition="all 0.15s ease",
|
|
626
622
|
),
|
|
627
623
|
),
|
|
@@ -1512,7 +1508,7 @@ app = rx.App(
|
|
|
1512
1508
|
)
|
|
1513
1509
|
|
|
1514
1510
|
# Register pages
|
|
1515
|
-
app.add_page(dashboard_page, route="/", title="TaxForge - Dashboard")
|
|
1511
|
+
app.add_page(dashboard_page, route="/", title="TaxForge - Dashboard", on_load=TaxAppState.load_saved_data)
|
|
1516
1512
|
app.add_page(upload_page, route="/upload", title="TaxForge - Upload")
|
|
1517
1513
|
app.add_page(review_page, route="/review", title="TaxForge - Review")
|
|
1518
1514
|
app.add_page(settings_page, route="/settings", title="TaxForge - Settings")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaxForge - Data Persistence
|
|
3
|
+
Save and load tax data to/from local JSON file.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
|
|
13
|
+
# Data directory
|
|
14
|
+
DATA_DIR = Path.home() / ".taxforge"
|
|
15
|
+
DATA_FILE = DATA_DIR / "tax_data.json"
|
|
16
|
+
BACKUP_DIR = DATA_DIR / "backups"
|
|
17
|
+
|
|
18
|
+
# Fields to persist (from TaxAppState)
|
|
19
|
+
PERSIST_FIELDS = [
|
|
20
|
+
# Personal info
|
|
21
|
+
"first_name", "last_name", "filing_status",
|
|
22
|
+
# API key (will be obfuscated)
|
|
23
|
+
"openai_api_key",
|
|
24
|
+
# Documents
|
|
25
|
+
"uploaded_files", "parsed_documents",
|
|
26
|
+
# Tax data
|
|
27
|
+
"w2_list", "form_1099_list", "form_1098_list", "form_5498_list",
|
|
28
|
+
# Manual entries
|
|
29
|
+
"rental_properties", "business_income", "other_income", "other_deductions",
|
|
30
|
+
"dependents", "child_care_expenses",
|
|
31
|
+
# Calculated values we want to persist
|
|
32
|
+
"total_wages", "total_interest", "total_dividends", "total_capital_gains",
|
|
33
|
+
"total_rental_income", "total_business_income", "total_other_income",
|
|
34
|
+
"hsa_deduction", "self_employment_tax", "mortgage_interest_deduction",
|
|
35
|
+
"adjusted_gross_income", "itemized_deductions", "standard_deduction",
|
|
36
|
+
"total_deductions", "taxable_income", "total_tax", "total_withholding",
|
|
37
|
+
"other_withholding", "refund_or_owed", "is_refund",
|
|
38
|
+
"child_tax_credit", "other_dependent_credit", "child_care_credit", "total_credits",
|
|
39
|
+
# Status
|
|
40
|
+
"return_status", "return_generated",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Fields that should be obfuscated (not encrypted, just not plain text)
|
|
44
|
+
SENSITIVE_FIELDS = ["openai_api_key"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _obfuscate(value: str) -> str:
|
|
48
|
+
"""Simple obfuscation for sensitive values (not real encryption)."""
|
|
49
|
+
if not value:
|
|
50
|
+
return ""
|
|
51
|
+
return base64.b64encode(value.encode()).decode()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _deobfuscate(value: str) -> str:
|
|
55
|
+
"""Reverse obfuscation."""
|
|
56
|
+
if not value:
|
|
57
|
+
return ""
|
|
58
|
+
try:
|
|
59
|
+
return base64.b64decode(value.encode()).decode()
|
|
60
|
+
except:
|
|
61
|
+
return value # Return as-is if can't decode
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_data_dir():
|
|
65
|
+
"""Ensure data directory exists."""
|
|
66
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def save_state(state) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Save state to JSON file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
state: TaxAppState instance
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if successful, False otherwise
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
ensure_data_dir()
|
|
82
|
+
|
|
83
|
+
# Extract fields to save
|
|
84
|
+
data = {
|
|
85
|
+
"_saved_at": datetime.now().isoformat(),
|
|
86
|
+
"_version": "0.9.21",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for field in PERSIST_FIELDS:
|
|
90
|
+
if hasattr(state, field):
|
|
91
|
+
value = getattr(state, field)
|
|
92
|
+
# Handle special types
|
|
93
|
+
if isinstance(value, (list, dict)):
|
|
94
|
+
data[field] = value
|
|
95
|
+
else:
|
|
96
|
+
data[field] = value
|
|
97
|
+
|
|
98
|
+
# Obfuscate sensitive fields
|
|
99
|
+
if field in SENSITIVE_FIELDS and value:
|
|
100
|
+
data[field] = _obfuscate(str(value))
|
|
101
|
+
|
|
102
|
+
# Create backup of existing file
|
|
103
|
+
if DATA_FILE.exists():
|
|
104
|
+
backup_name = f"tax_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
105
|
+
backup_path = BACKUP_DIR / backup_name
|
|
106
|
+
# Keep only last 5 backups
|
|
107
|
+
existing_backups = sorted(BACKUP_DIR.glob("tax_data_*.json"))
|
|
108
|
+
if len(existing_backups) >= 5:
|
|
109
|
+
for old_backup in existing_backups[:-4]:
|
|
110
|
+
old_backup.unlink()
|
|
111
|
+
|
|
112
|
+
# Write to file
|
|
113
|
+
with open(DATA_FILE, 'w') as f:
|
|
114
|
+
json.dump(data, f, indent=2, default=str)
|
|
115
|
+
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f"[TaxForge] Error saving state: {e}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load_state(state) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Load state from JSON file.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
state: TaxAppState instance to populate
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if data was loaded, False if no data or error
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
if not DATA_FILE.exists():
|
|
135
|
+
print("[TaxForge] No saved data found, starting fresh")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
with open(DATA_FILE, 'r') as f:
|
|
139
|
+
data = json.load(f)
|
|
140
|
+
|
|
141
|
+
print(f"[TaxForge] Loading saved data from {data.get('_saved_at', 'unknown')}")
|
|
142
|
+
|
|
143
|
+
# Restore fields
|
|
144
|
+
for field in PERSIST_FIELDS:
|
|
145
|
+
if field in data:
|
|
146
|
+
value = data[field]
|
|
147
|
+
|
|
148
|
+
# Deobfuscate sensitive fields
|
|
149
|
+
if field in SENSITIVE_FIELDS and value:
|
|
150
|
+
value = _deobfuscate(value)
|
|
151
|
+
|
|
152
|
+
# Set the attribute
|
|
153
|
+
if hasattr(state, field):
|
|
154
|
+
setattr(state, field, value)
|
|
155
|
+
|
|
156
|
+
print(f"[TaxForge] Loaded: {len(data.get('w2_list', []))} W-2s, "
|
|
157
|
+
f"{len(data.get('form_1099_list', []))} 1099s, "
|
|
158
|
+
f"{len(data.get('rental_properties', []))} rentals")
|
|
159
|
+
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"[TaxForge] Error loading state: {e}")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def clear_saved_data() -> bool:
|
|
168
|
+
"""Clear all saved data."""
|
|
169
|
+
try:
|
|
170
|
+
if DATA_FILE.exists():
|
|
171
|
+
DATA_FILE.unlink()
|
|
172
|
+
return True
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"[TaxForge] Error clearing data: {e}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_data_info() -> Dict[str, Any]:
|
|
179
|
+
"""Get info about saved data."""
|
|
180
|
+
info = {
|
|
181
|
+
"exists": DATA_FILE.exists(),
|
|
182
|
+
"path": str(DATA_FILE),
|
|
183
|
+
"size": 0,
|
|
184
|
+
"saved_at": None,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if DATA_FILE.exists():
|
|
188
|
+
info["size"] = DATA_FILE.stat().st_size
|
|
189
|
+
try:
|
|
190
|
+
with open(DATA_FILE, 'r') as f:
|
|
191
|
+
data = json.load(f)
|
|
192
|
+
info["saved_at"] = data.get("_saved_at")
|
|
193
|
+
info["version"] = data.get("_version")
|
|
194
|
+
except:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
return info
|
|
@@ -15,6 +15,7 @@ from .document_extractor import (
|
|
|
15
15
|
convert_1099_misc_to_other_income,
|
|
16
16
|
REQUEST_DELAY_SECONDS
|
|
17
17
|
)
|
|
18
|
+
from .persistence import save_state, load_state, clear_saved_data
|
|
18
19
|
import asyncio
|
|
19
20
|
|
|
20
21
|
|
|
@@ -269,6 +270,7 @@ class TaxAppState(rx.State):
|
|
|
269
270
|
self.openai_api_key = self.temp_api_key
|
|
270
271
|
self.show_api_modal = False
|
|
271
272
|
self.success_message = "API key saved!"
|
|
273
|
+
self._auto_save() # Persist API key
|
|
272
274
|
|
|
273
275
|
def check_api_key_for_upload(self):
|
|
274
276
|
"""Check if API key is set, open modal if not."""
|
|
@@ -328,6 +330,7 @@ class TaxAppState(rx.State):
|
|
|
328
330
|
if new_files > 0:
|
|
329
331
|
self.success_message = f"Uploaded {new_files} file(s). Go to Review to process with AI."
|
|
330
332
|
self.return_status = "in_progress"
|
|
333
|
+
self._auto_save() # Persist uploaded files list
|
|
331
334
|
except Exception as e:
|
|
332
335
|
self.error_message = f"Upload failed: {str(e)}"
|
|
333
336
|
|
|
@@ -1086,6 +1089,16 @@ class TaxAppState(rx.State):
|
|
|
1086
1089
|
|
|
1087
1090
|
if self.total_wages > 0 or total_income > 0:
|
|
1088
1091
|
self.return_status = "in_progress"
|
|
1092
|
+
|
|
1093
|
+
# Auto-save after recalculation
|
|
1094
|
+
self._auto_save()
|
|
1095
|
+
|
|
1096
|
+
def _auto_save(self):
|
|
1097
|
+
"""Auto-save state to disk."""
|
|
1098
|
+
try:
|
|
1099
|
+
save_state(self)
|
|
1100
|
+
except Exception as e:
|
|
1101
|
+
print(f"[TaxForge] Auto-save failed: {e}")
|
|
1089
1102
|
|
|
1090
1103
|
def _calculate_tax(self, income: float, brackets: list) -> float:
|
|
1091
1104
|
"""Calculate tax from brackets."""
|
|
@@ -1153,6 +1166,15 @@ class TaxAppState(rx.State):
|
|
|
1153
1166
|
self.return_generated = False
|
|
1154
1167
|
self.error_message = ""
|
|
1155
1168
|
self.success_message = ""
|
|
1169
|
+
# Also clear persisted data
|
|
1170
|
+
clear_saved_data()
|
|
1171
|
+
|
|
1172
|
+
def load_saved_data(self):
|
|
1173
|
+
"""Load previously saved data from disk."""
|
|
1174
|
+
if load_state(self):
|
|
1175
|
+
self.success_message = "Previous session restored!"
|
|
1176
|
+
else:
|
|
1177
|
+
self.success_message = ""
|
|
1156
1178
|
|
|
1157
1179
|
# ===== Computed Properties =====
|
|
1158
1180
|
@rx.var
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|