gl29/kervent-projections
0
1import json2import os3 4from huggingface_hub import hf_hub_download, list_repo_files, upload_file5import streamlit as st6 7TOKEN = os.environ.get("HF_TOKEN")8REPO_ID = os.environ.get("DATASET_ID")9DEFAULT_FILENAME = "defaults.json"10 11 12def _profile_filename(name: str) -> str:13 if name == "Default":14 return DEFAULT_FILENAME15 slug = name.strip().lower().replace(" ", "_")16 return f"profile_{slug}.json"17 18 19def list_profiles() -> list[str]:20 """Discover available profiles from HF dataset (or local fallback)."""21 profiles = ["Default"]22 try:23 files = list_repo_files(REPO_ID, repo_type="dataset", token=TOKEN)24 for f in sorted(files):25 if f.startswith("profile_") and f.endswith(".json"):26 display = f[8:-5].replace("_", " ").title()27 profiles.append(display)28 except Exception:29 for f in sorted(os.listdir(".")):30 if f.startswith("profile_") and f.endswith(".json"):31 display = f[8:-5].replace("_", " ").title()32 profiles.append(display)33 return profiles34 35 36def _load_bundled_defaults() -> dict:37 """Load the defaults.json shipped with the app (the schema source of truth)."""38 if os.path.exists(DEFAULT_FILENAME):39 with open(DEFAULT_FILENAME, "r") as f:40 return json.load(f)41 return {}42 43 44def load_profile(name: str) -> dict:45 """Load a profile by display name. Falls back to local file.46 47 When loading the Default profile, any top-level keys present in the48 bundled defaults.json but missing from the stored copy are back-filled.49 This keeps the app working after schema changes even if the HF dataset50 still has an older format.51 """52 filename = _profile_filename(name)53 result = None54 try:55 path = hf_hub_download(56 repo_id=REPO_ID,57 filename=filename,58 repo_type="dataset",59 token=TOKEN,60 )61 with open(path, "r") as f:62 result = json.load(f)63 except Exception:64 if os.path.exists(filename):65 with open(filename, "r") as f:66 result = json.load(f)67 elif filename != DEFAULT_FILENAME and os.path.exists(DEFAULT_FILENAME):68 with open(DEFAULT_FILENAME, "r") as f:69 result = json.load(f)70 71 if result is None:72 result = _load_bundled_defaults()73 74 # Back-fill missing sections from the bundled defaults so that an older75 # saved profile doesn't hide newly-added config sections. We go two76 # levels deep so that e.g. global.global_debt is filled even when the77 # stored data already has a "global" key from an older schema.78 bundled = _load_bundled_defaults()79 for key, default_val in bundled.items():80 if key not in result:81 result[key] = default_val82 elif isinstance(default_val, dict) and isinstance(result[key], dict):83 for sub_key, sub_val in default_val.items():84 if sub_key not in result[key]:85 result[key][sub_key] = sub_val86 87 return result88 89 90def save_profile(name: str, data: dict):91 """Save a profile to HF dataset (and locally)."""92 filename = _profile_filename(name)93 json_str = json.dumps(data, indent=4)94 with open(filename, "w") as f:95 f.write(json_str)96 try:97 upload_file(98 path_or_fileobj=filename,99 path_in_repo=filename,100 repo_id=REPO_ID,101 repo_type="dataset",102 token=TOKEN,103 )104 st.success(f"โ
Profile **{name}** saved permanently!")105 except Exception as e:106 st.warning(f"Saved locally but HF upload failed: {e}")107 108 109def delete_profile(name: str):110 """Delete a non-default profile from HF dataset and locally."""111 if name == "Default":112 return113 filename = _profile_filename(name)114 if os.path.exists(filename):115 os.remove(filename)116 try:117 from huggingface_hub import delete_file118 delete_file(119 path_in_repo=filename,120 repo_id=REPO_ID,121 repo_type="dataset",122 token=TOKEN,123 )124 st.success(f"๐๏ธ Profile **{name}** deleted.")125 except Exception as e:126 st.warning(f"Local delete OK but HF delete failed: {e}")127 128 129# ============================================================130# Research Tasks persistence131# ============================================================132 133RESEARCH_TASKS_FILENAME = "research_tasks.json"134RESEARCH_TASKS_BUNDLED = "research_tasks_default.json"135 136 137def load_research_tasks() -> dict:138 """Load the interactive research tracker from HF dataset, falling back to the bundled seed."""139 try:140 path = hf_hub_download(141 repo_id=REPO_ID,142 filename=RESEARCH_TASKS_FILENAME,143 repo_type="dataset",144 token=TOKEN,145 )146 with open(path, "r") as f:147 return json.load(f)148 except Exception:149 pass150 151 if os.path.exists(RESEARCH_TASKS_FILENAME):152 with open(RESEARCH_TASKS_FILENAME, "r") as f:153 return json.load(f)154 155 if os.path.exists(RESEARCH_TASKS_BUNDLED):156 with open(RESEARCH_TASKS_BUNDLED, "r") as f:157 return json.load(f)158 159 return {"sections": []}160 161 162def save_research_tasks(data: dict):163 """Save the research tracker state to HF dataset (and locally)."""164 json_str = json.dumps(data, indent=4)165 with open(RESEARCH_TASKS_FILENAME, "w") as f:166 f.write(json_str)167 try:168 upload_file(169 path_or_fileobj=RESEARCH_TASKS_FILENAME,170 path_in_repo=RESEARCH_TASKS_FILENAME,171 repo_id=REPO_ID,172 repo_type="dataset",173 token=TOKEN,174 )175 st.success("Research tracker saved!")176 except Exception as e:177 st.warning(f"Saved locally but HF upload failed: {e}")178 179 180# Backward-compatible aliases181def load_from_persistence() -> dict:182 return load_profile("Default")183 184 185def save_to_persistence(data: dict):186 save_profile("Default", data)187 