jeffrey1963 commited on
Commit
e08de86
·
verified ·
1 Parent(s): 5236497

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -0
app.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import os, io, json
3
+ from datetime import datetime, timezone
4
+ from dateutil import tz
5
+ import pandas as pd
6
+ import gradio as gr
7
+
8
+ from huggingface_hub import (
9
+ HfApi,
10
+ HfFolder,
11
+ hf_hub_download, # anonymous reads for public datasets
12
+ )
13
+
14
+ # --------------------
15
+ # Config
16
+ # --------------------
17
+ DATA_REPO = os.environ.get("DATA_REPO", "jeffrey1963/Farm_Sim_Data")
18
+ DATA_REPO_TYPE = "dataset" # don't change
19
+
20
+ # --------------------
21
+ # HF helpers
22
+ # --------------------
23
+ def _api_readonly() -> HfApi:
24
+ """OK without token for public dataset reads."""
25
+ tok = HfFolder.get_token()
26
+ return HfApi(token=tok) if tok else HfApi()
27
+
28
+ def _api_rw() -> HfApi:
29
+ """Require token for uploads."""
30
+ tok = HfFolder.get_token()
31
+ if not tok:
32
+ raise RuntimeError("Missing HF_TOKEN (add a Space secret named HF_TOKEN).")
33
+ return HfApi(token=tok)
34
+
35
+ def _candidate_purchase_paths(student_id: str) -> list[str]:
36
+ """
37
+ Return all jsonl paths for this student.
38
+ Supports both:
39
+ - purchases/{id}.jsonl
40
+ - purchases/{id}-TIMESTAMP.jsonl (your current store behavior)
41
+ """
42
+ api = _api_readonly()
43
+ try:
44
+ files = api.list_repo_files(repo_id=DATA_REPO, repo_type=DATA_REPO_TYPE)
45
+ except Exception:
46
+ return []
47
+ prefix = f"purchases/{student_id}"
48
+ out = []
49
+ for f in files:
50
+ if not f.startswith("purchases/") or not f.endswith(".jsonl"):
51
+ continue
52
+ if f == f"{prefix}.jsonl" or f.startswith(prefix + "-"):
53
+ out.append(f)
54
+ return sorted(out) # oldest first
55
+
56
+ def _download_to_local(path_in_repo: str) -> str | None:
57
+ try:
58
+ return hf_hub_download(
59
+ repo_id=DATA_REPO,
60
+ repo_type=DATA_REPO_TYPE,
61
+ filename=path_in_repo,
62
+ )
63
+ except Exception:
64
+ return None
65
+
66
+ # --------------------
67
+ # Core
68
+ # --------------------
69
+ def fetch_student_assets(student_id: str):
70
+ """Return list of assets purchased by this student (aggregate all files)."""
71
+ sid = (student_id or "").strip()
72
+ if not sid:
73
+ return []
74
+
75
+ paths = _candidate_purchase_paths(sid)
76
+ if not paths:
77
+ return []
78
+
79
+ rows = []
80
+ for p in paths:
81
+ local = _download_to_local(p)
82
+ if not local:
83
+ continue
84
+ with open(local, "r", encoding="utf-8") as f:
85
+ for line in f:
86
+ s = line.strip()
87
+ if not s:
88
+ continue
89
+ try:
90
+ rows.append(json.loads(s))
91
+ except Exception:
92
+ pass
93
+
94
+ assets = []
95
+ for r in rows:
96
+ try:
97
+ ts = int(r["ts"])
98
+ dt = datetime.fromtimestamp(ts, tz=timezone.utc).astimezone(tz.tzlocal())
99
+ asset_id = f'{r.get("catalog_id","unknown")}-{ts}'
100
+ snap = r.get("snapshot", {}) or {}
101
+ assets.append({
102
+ "asset_id": asset_id,
103
+ "label": r.get("label", "(unknown)"),
104
+ "catalog_id": r.get("catalog_id", ""),
105
+ "purchase_ts": ts,
106
+ "purchase_date": dt.strftime("%Y-%m-%d"),
107
+ "cost": float(r.get("price", 0.0)),
108
+ "expected_life_years": snap.get("expected_life_years") or 10,
109
+ "salvage_value_pct": snap.get("salvage_value_pct") or 0.2,
110
+ "dep_method_default": (snap.get("dep_method_default") or "SL").upper(),
111
+ })
112
+ except Exception:
113
+ continue
114
+
115
+ return assets
116
+
117
+ def straight_line_schedule(cost: float, life_years: int, salvage_value: float, start_year: int):
118
+ dep = (cost - salvage_value) / life_years
119
+ rows = []
120
+ beg = cost
121
+ acc = 0.0
122
+ for i in range(1, life_years + 1):
123
+ year = start_year + (i - 1)
124
+ end = max(salvage_value, beg - dep)
125
+ d = beg - end
126
+ acc += d
127
+ rows.append({
128
+ "Year": year,
129
+ "Begin BV": round(beg, 2),
130
+ "Depreciation": round(d, 2),
131
+ "Accum Dep": round(acc, 2),
132
+ "End BV": round(end, 2),
133
+ })
134
+ beg = end
135
+ return pd.DataFrame(rows)
136
+
137
+ def build_schedule(student_id, asset_pick, life_years, salvage_pct):
138
+ if not student_id:
139
+ return None, "Enter Student ID."
140
+
141
+ assets = fetch_student_assets(student_id)
142
+ idx = {a["asset_id"]: a for a in assets}
143
+ if asset_pick not in idx:
144
+ if not assets:
145
+ return None, f"No purchases found for '{student_id}'."
146
+ return None, "Pick an asset."
147
+
148
+ a = idx[asset_pick]
149
+ life = int(life_years) if life_years else int(a["expected_life_years"])
150
+ spct = float(salvage_pct) if salvage_pct not in (None, "") else float(a["salvage_value_pct"])
151
+ salvage_val = spct * a["cost"]
152
+
153
+ df = straight_line_schedule(
154
+ cost=a["cost"],
155
+ life_years=life,
156
+ salvage_value=salvage_val,
157
+ start_year=int(a["purchase_date"][:4]),
158
+ )
159
+
160
+ # write schedule CSV back to dataset
161
+ csv_bytes = df.to_csv(index=False).encode()
162
+ api = _api_rw()
163
+ path_in_repo = f"schedules/{student_id}/{asset_pick}.csv"
164
+ api.upload_file(
165
+ path_or_fileobj=io.BytesIO(csv_bytes),
166
+ path_in_repo=path_in_repo,
167
+ repo_id=DATA_REPO,
168
+ repo_type=DATA_REPO_TYPE,
169
+ )
170
+ note = f"📄 Wrote schedule to dataset at {path_in_repo}"
171
+ return df, f"✅ Schedule generated for {a['label']} — {note}"
172
+
173
+ def load_assets_for_ui(student_id):
174
+ if not student_id:
175
+ return gr.update(choices=[], value=None), "Enter Student ID then click “🔍 Load my assets”."
176
+ assets = fetch_student_assets(student_id)
177
+ choices = [(f"{a['label']} ({a['purchase_date']}) — ${a['cost']:,.0f}", a["asset_id"]) for a in assets]
178
+ if not choices:
179
+ # helpful hint: show what we looked for
180
+ paths = _candidate_purchase_paths(student_id)
181
+ hint = " none" if not paths else ("\n".join(f"- {p}" for p in paths))
182
+ return gr.update(choices=[], value=None), f"No purchases found. Looked for:\n{hint}"
183
+ return gr.update(choices=choices, value=choices[0][1]), f"Loaded {len(choices)} asset(s)."
184
+
185
+ # --------------------
186
+ # UI
187
+ # --------------------
188
+ with gr.Blocks(theme=gr.themes.Soft()) as app:
189
+ gr.Markdown("## 🧮 Jerry — Depreciation Helper")
190
+
191
+ with gr.Row():
192
+ student_id = gr.Textbox(label="Student ID", placeholder="e.g., jeff")
193
+ load_btn = gr.Button("🔍 Load my assets")
194
+
195
+ asset_dd = gr.Dropdown(label="Select an asset")
196
+
197
+ with gr.Row():
198
+ life_years = gr.Number(label="Service life (years)", value=None, precision=0)
199
+ salvage_pct = gr.Slider(label="Salvage % of cost", minimum=0, maximum=0.9, step=0.01, value=None)
200
+
201
+ build_btn = gr.Button("Build Straight-Line Schedule")
202
+ out_tbl = gr.Dataframe(label="Depreciation Schedule", interactive=False)
203
+ status = gr.Markdown()
204
+
205
+ load_btn.click(load_assets_for_ui, inputs=[student_id], outputs=[asset_dd, status])
206
+ build_btn.click(build_schedule, inputs=[student_id, asset_dd, life_years, salvage_pct],
207
+ outputs=[out_tbl, status])
208
+
209
+ # allow prefill via URL like ?student_id=jeff
210
+ def prefill(req: gr.Request):
211
+ sid = req.query_params.get("student_id")
212
+ return sid or ""
213
+
214
+ app.load(prefill, inputs=None, outputs=[student_id])
215
+
216
+ if __name__ == "__main__":
217
+ app.launch()