ramanna commited on
Commit
865e7c1
·
verified ·
1 Parent(s): e3f2cf6

Upload 2 files

Browse files
Files changed (2) hide show
  1. update_streamlit_panel.py +130 -0
  2. user_management.py +302 -0
update_streamlit_panel.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # update_streamlit_panel.py
2
+ import sys, json, subprocess
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ import streamlit as st
6
+
7
+ UPDATE_CMD = [sys.executable, "update_data.py"]
8
+ OUTPUTS = [Path("data/known_bills_visualize.json")]
9
+ LOCKFILE = Path(".update.lock")
10
+ LOGFILE = Path("pipeline_last_run.log")
11
+
12
+ def _is_lfs_pointer(path: Path) -> bool:
13
+ try:
14
+ with path.open("r", encoding="utf-8", errors="ignore") as f:
15
+ head = "".join([next(f) for _ in range(3)])
16
+ return head.startswith("version https://git-lfs.github.com/spec/v1")
17
+ except Exception:
18
+ return False
19
+
20
+ def _read_tail(path: Path, max_bytes=20000) -> str:
21
+ if not path.exists():
22
+ return ""
23
+ data = path.read_bytes()
24
+ return data[-max_bytes:].decode("utf-8", errors="ignore")
25
+
26
+ def _inspect_json(path: Path) -> tuple[bool, str]:
27
+ if not path.exists():
28
+ return False, f"Not found: {path}"
29
+ if _is_lfs_pointer(path):
30
+ return False, (
31
+ f"`{path}` looks like a Git-LFS pointer (not real data).\n"
32
+ " Fix locally:\n"
33
+ " git lfs install\n"
34
+ " git lfs pull\n"
35
+ " git lfs checkout"
36
+ )
37
+ try:
38
+ with path.open("r", encoding="utf-8") as f:
39
+ data = json.load(f)
40
+ count = len(data) if isinstance(data, list) else len(data.get("data", [])) if isinstance(data, dict) else 0
41
+ mtime = datetime.fromtimestamp(path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
42
+ return True, f"`{path}` • rows: **{count}** • saved: **{mtime}**"
43
+ except Exception as e:
44
+ return False, f"`{path}` failed to parse as JSON: {e}"
45
+
46
+ def _run_update(answer: str):
47
+ if LOCKFILE.exists():
48
+ st.error("Another update appears to be running (.update.lock exists). If stuck, delete that file and try again.")
49
+ return
50
+
51
+ try:
52
+ LOCKFILE.write_text(datetime.utcnow().isoformat())
53
+
54
+ with st.status("Running update_data.py …", expanded=True) as status:
55
+ st.write(f"Command: `{UPDATE_CMD}`")
56
+ with LOGFILE.open("w", encoding="utf-8") as lf:
57
+ proc = subprocess.Popen(
58
+ UPDATE_CMD,
59
+ stdout=subprocess.PIPE,
60
+ stderr=subprocess.STDOUT,
61
+ stdin=subprocess.PIPE,
62
+ text=True,
63
+ bufsize=1,
64
+ )
65
+ # forward user's choice to update_data.py
66
+ try:
67
+ proc.stdin.write(answer)
68
+ proc.stdin.flush()
69
+ except Exception:
70
+ pass
71
+
72
+ for line in proc.stdout:
73
+ line = line.rstrip("\n")
74
+ st.write(line)
75
+ lf.write(line + "\n")
76
+ rc = proc.wait()
77
+
78
+ if rc == 0:
79
+ status.update(label="✅ Update finished", state="complete")
80
+ all_ok = True
81
+ for p in OUTPUTS:
82
+ ok, msg = _inspect_json(p)
83
+ (st.success if ok else st.error)(msg)
84
+ all_ok &= ok
85
+
86
+ st.cache_data.clear()
87
+ if all_ok:
88
+ st.toast("Data updated and cache cleared.")
89
+ st.rerun()
90
+ else:
91
+ st.warning("Update completed, but one or more outputs failed validation.")
92
+ else:
93
+ status.update(label="Update failed", state="error")
94
+ st.error(f"`update_data.py` exited with code {rc}. Log tail below:")
95
+ st.code(_read_tail(LOGFILE), language="bash")
96
+ finally:
97
+ try: LOCKFILE.unlink()
98
+ except Exception: pass
99
+
100
+ def render_update_panel():
101
+ st.title("Admin — Data Update Panel")
102
+ st.caption("Hidden page (URL-only).")
103
+
104
+ c1, c2, c3 = st.columns(3)
105
+ run_btn = c1.button("Run update_data.py")
106
+ refresh_btn = c2.button("Clear app cache")
107
+ tail_btn = c3.button("Show last log tail")
108
+
109
+ if refresh_btn:
110
+ st.cache_data.clear()
111
+ st.success("Cache cleared — new data will load on next run.")
112
+ st.rerun()
113
+
114
+ if tail_btn:
115
+ st.code(_read_tail(LOGFILE) or "No previous log.", language="bash")
116
+
117
+ st.markdown("**Current local datasets**")
118
+ for p in OUTPUTS:
119
+ ok, msg = _inspect_json(p)
120
+ (st.success if ok else st.warning)(msg)
121
+
122
+ if run_btn:
123
+ choice = st.radio(
124
+ "Pull new data from LegiScan?",
125
+ ["No (use existing data)", "Yes (pull new data)"],
126
+ horizontal=True,
127
+ )
128
+ confirm = st.button("Confirm and Start Update", type="primary")
129
+ if confirm:
130
+ _run_update("y\n" if "Yes" in choice else "n\n")
user_management.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ User Management Module for HuggingFace-backed authentication
3
+ Stores user credentials in HuggingFace dataset for persistence
4
+ """
5
+
6
+ import json
7
+ import requests
8
+ from huggingface_hub import HfApi
9
+ import streamlit as st
10
+ from pathlib import Path
11
+
12
+
13
+ class HuggingFaceUserManager:
14
+ """Manage users stored in HuggingFace dataset"""
15
+
16
+ def __init__(self):
17
+ """Initialize the user manager"""
18
+ if "huggingface" not in st.secrets:
19
+ raise ValueError("HuggingFace configuration missing from secrets")
20
+
21
+ self.token = st.secrets["huggingface"]["token"]
22
+ self.repo_id = st.secrets["huggingface"]["dataset_repo"]
23
+ self.users_file = "users.json"
24
+ self.api = HfApi()
25
+
26
+ def get_users_url(self):
27
+ """Get the URL to the users.json file"""
28
+ return f"https://huggingface.co/datasets/{self.repo_id}/resolve/main/{self.users_file}"
29
+
30
+ def load_users(self, use_cache=True):
31
+ """
32
+ Load users from HuggingFace dataset
33
+
34
+ Args:
35
+ use_cache (bool): Whether to use cached data
36
+
37
+ Returns:
38
+ dict: User credentials in the format:
39
+ {
40
+ 'usernames': {
41
+ 'username': {
42
+ 'email': 'email@example.com',
43
+ 'name': 'Full Name',
44
+ 'password': '$2b$12$hash...'
45
+ }
46
+ }
47
+ }
48
+ """
49
+ try:
50
+ url = self.get_users_url()
51
+ if not use_cache:
52
+ import time
53
+ url = f"{url}?t={int(time.time())}"
54
+
55
+ response = requests.get(url, timeout=30)
56
+ response.raise_for_status()
57
+
58
+ users_data = response.json()
59
+
60
+ if 'usernames' not in users_data:
61
+ users_data = {'usernames': {}}
62
+
63
+ return users_data
64
+
65
+ except requests.exceptions.HTTPError as e:
66
+ if e.response.status_code == 404:
67
+ print("users.json not found on HuggingFace, creating new structure")
68
+ return {'usernames': {}}
69
+ else:
70
+ raise
71
+ except Exception as e:
72
+ print(f"Error loading users from HuggingFace: {e}")
73
+ return {'usernames': {}}
74
+
75
+ def save_users(self, users_data):
76
+ """
77
+ Save users to HuggingFace dataset
78
+
79
+ Args:
80
+ users_data (dict): User credentials dictionary
81
+
82
+ Returns:
83
+ str: Commit URL
84
+ """
85
+ try:
86
+ temp_file = Path("temp_users.json")
87
+ with temp_file.open('w', encoding='utf-8') as f:
88
+ json.dump(users_data, f, indent=2, ensure_ascii=False)
89
+
90
+ # Upload to HuggingFace
91
+ commit_info = self.api.upload_file(
92
+ path_or_fileobj=str(temp_file),
93
+ path_in_repo=self.users_file,
94
+ repo_id=self.repo_id,
95
+ repo_type="dataset",
96
+ token=self.token,
97
+ commit_message="Update user credentials"
98
+ )
99
+
100
+ temp_file.unlink()
101
+
102
+ if isinstance(commit_info, str):
103
+ return commit_info
104
+ elif hasattr(commit_info, 'commit_url'):
105
+ return commit_info.commit_url
106
+ else:
107
+ return f"https://huggingface.co/datasets/{self.repo_id}"
108
+
109
+ except Exception as e:
110
+ if temp_file.exists():
111
+ temp_file.unlink()
112
+ raise Exception(f"Failed to save users to HuggingFace: {str(e)}")
113
+
114
+ def add_user(self, username, email, name, hashed_password):
115
+ """
116
+ Add a new user
117
+
118
+ Args:
119
+ username (str): Username
120
+ email (str): Email address
121
+ name (str): Full name
122
+ hashed_password (str): Bcrypt hashed password
123
+
124
+ Returns:
125
+ tuple: (success: bool, message: str, commit_url: str|None)
126
+ """
127
+ try:
128
+ # Load current users
129
+ users_data = self.load_users()
130
+
131
+ # Check if username already exists
132
+ if username in users_data['usernames']:
133
+ return False, f"Username '{username}' already exists", None
134
+
135
+ # Add new user
136
+ users_data['usernames'][username] = {
137
+ 'email': email,
138
+ 'name': name,
139
+ 'password': hashed_password
140
+ }
141
+
142
+ # Save to HuggingFace
143
+ commit_url = self.save_users(users_data)
144
+
145
+ return True, f"User '{username}' added successfully", commit_url
146
+
147
+ except Exception as e:
148
+ return False, f"Error adding user: {str(e)}", None
149
+
150
+ def remove_user(self, username):
151
+ """
152
+ Remove a user
153
+
154
+ Args:
155
+ username (str): Username to remove
156
+
157
+ Returns:
158
+ tuple: (success: bool, message: str, commit_url: str|None)
159
+ """
160
+ try:
161
+ users_data = self.load_users()
162
+
163
+ if username not in users_data['usernames']:
164
+ return False, f"Username '{username}' not found", None
165
+
166
+ # Remove user
167
+ del users_data['usernames'][username]
168
+
169
+ # Save to HuggingFace
170
+ commit_url = self.save_users(users_data)
171
+
172
+ return True, f"User '{username}' removed successfully", commit_url
173
+
174
+ except Exception as e:
175
+ return False, f"Error removing user: {str(e)}", None
176
+
177
+ def update_user_password(self, username, new_hashed_password):
178
+ """
179
+ Update a user's password
180
+
181
+ Args:
182
+ username (str): Username
183
+ new_hashed_password (str): New bcrypt hashed password
184
+
185
+ Returns:
186
+ tuple: (success: bool, message: str, commit_url: str|None)
187
+ """
188
+ try:
189
+ # Load current users
190
+ users_data = self.load_users()
191
+
192
+ # Check if user exists
193
+ if username not in users_data['usernames']:
194
+ return False, f"Username '{username}' not found", None
195
+
196
+ # Update password
197
+ users_data['usernames'][username]['password'] = new_hashed_password
198
+
199
+ # Save to HuggingFace
200
+ commit_url = self.save_users(users_data)
201
+
202
+ return True, f"Password updated for '{username}'", commit_url
203
+
204
+ except Exception as e:
205
+ return False, f"Error updating password: {str(e)}", None
206
+
207
+ def update_user(self, username, new_email=None, new_name=None, new_password=None):
208
+ """
209
+ Update a user's details
210
+
211
+ Args:
212
+ username (str): Username
213
+ new_email (str, optional): New email address
214
+ new_name (str, optional): New full name
215
+ new_password (str, optional): New bcrypt hashed password
216
+
217
+ Returns:
218
+ tuple: (success: bool, message: str, commit_url: str|None)
219
+ """
220
+ try:
221
+ # Load current users
222
+ users_data = self.load_users()
223
+
224
+ # Check if user exists
225
+ if username not in users_data['usernames']:
226
+ return False, f"Username '{username}' not found", None
227
+
228
+ # Update fields if provided
229
+ if new_email is not None:
230
+ users_data['usernames'][username]['email'] = new_email
231
+ if new_name is not None:
232
+ users_data['usernames'][username]['name'] = new_name
233
+ if new_password is not None:
234
+ users_data['usernames'][username]['password'] = new_password
235
+
236
+ # Save to HuggingFace
237
+ commit_url = self.save_users(users_data)
238
+
239
+ return True, f"User '{username}' updated successfully", commit_url
240
+
241
+ except Exception as e:
242
+ return False, f"Error updating user: {str(e)}", None
243
+
244
+ def get_config_for_authenticator(self):
245
+ """
246
+ Get user data formatted for streamlit-authenticator
247
+
248
+ Returns:
249
+ dict: Configuration dictionary for Authenticate()
250
+ """
251
+ users_data = self.load_users()
252
+
253
+ # Get cookie config from secrets
254
+ cookie_config = {
255
+ 'name': st.secrets.get('auth', {}).get('cookie', {}).get('name', 'auth_cookie'),
256
+ 'key': st.secrets.get('auth', {}).get('cookie', {}).get('key', 'random_key'),
257
+ 'expiry_days': int(st.secrets.get('auth', {}).get('cookie', {}).get('expiry_days', 30))
258
+ }
259
+
260
+ return {
261
+ 'credentials': users_data,
262
+ 'cookie': cookie_config,
263
+ 'preauthorized': {'emails': []}
264
+ }
265
+
266
+
267
+ @st.cache_data(ttl=300)
268
+ def load_user_config():
269
+ """
270
+ Load user configuration from HuggingFace or fallback to secrets.toml
271
+
272
+ Returns:
273
+ tuple: (config: dict, using_hf: bool)
274
+ """
275
+ try:
276
+ if "huggingface" in st.secrets:
277
+ manager = HuggingFaceUserManager()
278
+ config = manager.get_config_for_authenticator()
279
+ return config, True
280
+ except Exception as e:
281
+ print(f"Could not load from HuggingFace: {e}")
282
+
283
+ if "auth" in st.secrets:
284
+ def _secrets_to_dict(obj):
285
+ if hasattr(obj, 'to_dict'):
286
+ return obj.to_dict()
287
+ elif isinstance(obj, dict):
288
+ return {k: _secrets_to_dict(v) for k, v in obj.items()}
289
+ elif isinstance(obj, (list, tuple)):
290
+ return [_secrets_to_dict(item) for item in obj]
291
+ else:
292
+ return obj
293
+
294
+ auth_secrets = _secrets_to_dict(st.secrets["auth"])
295
+ config = {
296
+ 'credentials': auth_secrets.get('credentials', {}),
297
+ 'cookie': auth_secrets.get('cookie', {}),
298
+ 'preauthorized': auth_secrets.get('preauthorized', {'emails': []})
299
+ }
300
+ return config, False
301
+
302
+ return None, False