ysharma HF Staff commited on
Commit
499d63f
Β·
verified Β·
1 Parent(s): 251be85

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -0
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Space Keeper - Keeps HuggingFace Spaces alive during hackathon evaluation
3
+ Pings all Spaces in an organization on a schedule to prevent them from sleeping.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import threading
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ import gradio as gr
13
+ import requests
14
+ from huggingface_hub import HfApi
15
+ from apscheduler.schedulers.background import BackgroundScheduler
16
+ from apscheduler.triggers.interval import IntervalTrigger
17
+
18
+ # Configuration
19
+ ORG_NAME = os.environ.get("ORG_NAME", "mcp-hackathon")
20
+ PING_INTERVAL_HOURS = int(os.environ.get("PING_INTERVAL_HOURS", "6"))
21
+ REQUEST_TIMEOUT = int(os.environ.get("REQUEST_TIMEOUT", "30"))
22
+ LOG_FILE = Path("run_logs.json")
23
+ MAX_LOG_ENTRIES = 100 # Keep last N runs
24
+
25
+ # Global state
26
+ scheduler = BackgroundScheduler()
27
+ api = HfApi()
28
+
29
+
30
+ def load_logs() -> list:
31
+ """Load logs from file."""
32
+ if LOG_FILE.exists():
33
+ try:
34
+ with open(LOG_FILE, "r") as f:
35
+ return json.load(f)
36
+ except Exception:
37
+ return []
38
+ return []
39
+
40
+
41
+ def save_logs(logs: list):
42
+ """Save logs to file, keeping only the most recent entries."""
43
+ logs = logs[-MAX_LOG_ENTRIES:]
44
+ with open(LOG_FILE, "w") as f:
45
+ json.dump(logs, f, indent=2)
46
+
47
+
48
+ def ping_space(space_id: str) -> dict:
49
+ """Ping a single Space and return the result."""
50
+ url = f"https://huggingface.co/spaces/{space_id}"
51
+ try:
52
+ response = requests.get(url, timeout=REQUEST_TIMEOUT)
53
+ return {
54
+ "space_id": space_id,
55
+ "status": "success",
56
+ "status_code": response.status_code,
57
+ "error": None
58
+ }
59
+ except requests.Timeout:
60
+ return {
61
+ "space_id": space_id,
62
+ "status": "timeout",
63
+ "status_code": None,
64
+ "error": f"Request timed out after {REQUEST_TIMEOUT}s"
65
+ }
66
+ except Exception as e:
67
+ return {
68
+ "space_id": space_id,
69
+ "status": "error",
70
+ "status_code": None,
71
+ "error": str(e)
72
+ }
73
+
74
+
75
+ def run_ping_job(triggered_by: str = "scheduler") -> dict:
76
+ """Run the ping job for all Spaces in the org."""
77
+ start_time = datetime.now(timezone.utc)
78
+
79
+ # Get all Spaces in the org
80
+ try:
81
+ spaces = list(api.list_spaces(author=ORG_NAME))
82
+ except Exception as e:
83
+ run_result = {
84
+ "timestamp": start_time.isoformat(),
85
+ "triggered_by": triggered_by,
86
+ "status": "error",
87
+ "error": f"Failed to list Spaces: {str(e)}",
88
+ "total_spaces": 0,
89
+ "successful": 0,
90
+ "failed": 0,
91
+ "duration_seconds": 0,
92
+ "results": []
93
+ }
94
+ logs = load_logs()
95
+ logs.append(run_result)
96
+ save_logs(logs)
97
+ return run_result
98
+
99
+ # Ping each Space
100
+ results = []
101
+ for space in spaces:
102
+ result = ping_space(space.id)
103
+ results.append(result)
104
+
105
+ end_time = datetime.now(timezone.utc)
106
+ duration = (end_time - start_time).total_seconds()
107
+
108
+ successful = sum(1 for r in results if r["status"] == "success")
109
+ failed = len(results) - successful
110
+
111
+ run_result = {
112
+ "timestamp": start_time.isoformat(),
113
+ "triggered_by": triggered_by,
114
+ "status": "completed",
115
+ "error": None,
116
+ "total_spaces": len(results),
117
+ "successful": successful,
118
+ "failed": failed,
119
+ "duration_seconds": round(duration, 2),
120
+ "results": results
121
+ }
122
+
123
+ # Save to logs
124
+ logs = load_logs()
125
+ logs.append(run_result)
126
+ save_logs(logs)
127
+
128
+ return run_result
129
+
130
+
131
+ def scheduled_job():
132
+ """Wrapper for the scheduled job."""
133
+ print(f"[{datetime.now(timezone.utc).isoformat()}] Running scheduled ping job...")
134
+ run_ping_job(triggered_by="scheduler")
135
+
136
+
137
+ def manual_trigger():
138
+ """Manually trigger a ping run."""
139
+ result = run_ping_job(triggered_by="manual")
140
+ return format_run_result(result), get_logs_display()
141
+
142
+
143
+ def format_run_result(result: dict) -> str:
144
+ """Format a single run result for display."""
145
+ if result["status"] == "error":
146
+ return f"""## ❌ Run Failed
147
+
148
+ **Time:** {result['timestamp']}
149
+ **Triggered by:** {result['triggered_by']}
150
+ **Error:** {result['error']}
151
+ """
152
+
153
+ status_emoji = "βœ…" if result["failed"] == 0 else "⚠️"
154
+
155
+ output = f"""## {status_emoji} Run Completed
156
+
157
+ **Time:** {result['timestamp']}
158
+ **Triggered by:** {result['triggered_by']}
159
+ **Duration:** {result['duration_seconds']}s
160
+ **Spaces pinged:** {result['total_spaces']} ({result['successful']} successful, {result['failed']} failed)
161
+ """
162
+
163
+ if result["failed"] > 0:
164
+ output += "\n### Failed Spaces:\n"
165
+ for r in result["results"]:
166
+ if r["status"] != "success":
167
+ output += f"- `{r['space_id']}`: {r['error']}\n"
168
+
169
+ return output
170
+
171
+
172
+ def get_logs_display() -> str:
173
+ """Get formatted logs for display."""
174
+ logs = load_logs()
175
+
176
+ if not logs:
177
+ return "No runs recorded yet. Click 'Run Now' to trigger a manual run."
178
+
179
+ # Reverse to show most recent first
180
+ logs = list(reversed(logs))
181
+
182
+ output = "# Run History\n\n"
183
+
184
+ for i, run in enumerate(logs[:20]): # Show last 20 runs
185
+ timestamp = run["timestamp"]
186
+ triggered_by = run["triggered_by"]
187
+
188
+ if run["status"] == "error":
189
+ output += f"### ❌ {timestamp}\n"
190
+ output += f"Triggered by: {triggered_by} | Error: {run['error']}\n\n"
191
+ else:
192
+ status_emoji = "βœ…" if run["failed"] == 0 else "⚠️"
193
+ output += f"### {status_emoji} {timestamp}\n"
194
+ output += f"Triggered by: {triggered_by} | "
195
+ output += f"Spaces: {run['total_spaces']} | "
196
+ output += f"Success: {run['successful']} | "
197
+ output += f"Failed: {run['failed']} | "
198
+ output += f"Duration: {run['duration_seconds']}s\n"
199
+
200
+ if run["failed"] > 0:
201
+ failed_spaces = [r["space_id"] for r in run["results"] if r["status"] != "success"]
202
+ output += f"Failed: {', '.join(failed_spaces)}\n"
203
+ output += "\n"
204
+
205
+ if len(logs) > 20:
206
+ output += f"\n*...and {len(logs) - 20} more runs (showing last 20)*\n"
207
+
208
+ return output
209
+
210
+
211
+ def get_status() -> str:
212
+ """Get current scheduler status."""
213
+ next_run = scheduler.get_jobs()[0].next_run_time if scheduler.get_jobs() else None
214
+
215
+ return f"""## Space Keeper Status
216
+
217
+ **Organization:** `{ORG_NAME}`
218
+ **Ping Interval:** Every {PING_INTERVAL_HOURS} hours
219
+ **Request Timeout:** {REQUEST_TIMEOUT} seconds
220
+ **Next Scheduled Run:** {next_run.strftime('%Y-%m-%d %H:%M:%S UTC') if next_run else 'Not scheduled'}
221
+
222
+ ---
223
+
224
+ This Space automatically pings all Spaces in the `{ORG_NAME}` organization to keep them from sleeping during the evaluation period.
225
+ """
226
+
227
+
228
+ def refresh_logs():
229
+ """Refresh the logs display."""
230
+ return get_logs_display()
231
+
232
+
233
+ # Build the Gradio interface
234
+ with gr.Blocks(title="Space Keeper", theme=gr.themes.Soft()) as demo:
235
+ gr.Markdown("# πŸ”„ Space Keeper")
236
+ gr.Markdown("Keeps HuggingFace Spaces alive by pinging them on a schedule.")
237
+
238
+ with gr.Row():
239
+ with gr.Column(scale=1):
240
+ status_display = gr.Markdown(get_status())
241
+
242
+ with gr.Row():
243
+ run_btn = gr.Button("πŸš€ Run Now", variant="primary", size="lg")
244
+ refresh_btn = gr.Button("πŸ”„ Refresh Logs", size="lg")
245
+
246
+ last_run_display = gr.Markdown("Click 'Run Now' to trigger a manual ping run.")
247
+
248
+ with gr.Column(scale=2):
249
+ logs_display = gr.Markdown(get_logs_display())
250
+
251
+ # Event handlers
252
+ run_btn.click(
253
+ fn=manual_trigger,
254
+ outputs=[last_run_display, logs_display]
255
+ )
256
+
257
+ refresh_btn.click(
258
+ fn=refresh_logs,
259
+ outputs=[logs_display]
260
+ )
261
+
262
+
263
+ # Start the scheduler
264
+ scheduler.add_job(
265
+ scheduled_job,
266
+ trigger=IntervalTrigger(hours=PING_INTERVAL_HOURS),
267
+ id="ping_job",
268
+ name="Ping all Spaces",
269
+ replace_existing=True
270
+ )
271
+ scheduler.start()
272
+
273
+ print(f"Space Keeper started for org: {ORG_NAME}")
274
+ print(f"Ping interval: {PING_INTERVAL_HOURS} hours")
275
+
276
+ if __name__ == "__main__":
277
+ demo.launch()