Theflame47 commited on
Commit
0eafdf2
·
verified ·
1 Parent(s): 7334ae4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -9
app.py CHANGED
@@ -1,8 +1,8 @@
1
-
2
- import os, time, html, logging, requests
3
  from typing import Any, Dict, List, Optional
4
  from fastapi import FastAPI, HTTPException
5
- from fastapi.responses import HTMLResponse
6
 
7
  # ----- Logging -----
8
  logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
@@ -25,6 +25,10 @@ app = FastAPI()
25
  _pages: List[Dict[str, Any]] = [] # each: {"results": [...], "next": str|None}
26
  _next_cursor_url: Optional[str] = f"{BASE}/models" # where to fetch next page from
27
 
 
 
 
 
28
  # ---------- Helpers ----------
29
 
30
  def _get_with_backoff(url: str) -> Dict[str, Any]:
@@ -59,6 +63,34 @@ def _ensure_page_loaded(target_index: int) -> None:
59
  })
60
  _next_cursor_url = payload.get("next")
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def _model_id(x: Dict[str, Any]) -> str:
63
  owner = x.get("owner") or x.get("username") or ""
64
  name = x.get("name") or ""
@@ -104,15 +136,41 @@ def _render_pagination(current_page: int) -> str:
104
  links.append(f"<span class='page current'>{i}</span>")
105
  else:
106
  links.append(f"<a class='page' href='/page/{i}'>{i}</a>")
107
- # If we believe there are more pages (next cursor still present), show a link to the next index
108
  if _next_cursor_url:
109
  links.append(f"<a class='page next' href='/page/{total_known + 1}'>Load {total_known + 1}</a>")
110
  return "<div class='pagination'>" + " ".join(links) + "</div>"
111
 
112
- def _shell(page_index_0: int, html_table: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  page_num = page_index_0 + 1
114
  known_total = len(_pages)
115
  more_flag = "yes" if _next_cursor_url else "no"
 
 
 
 
 
 
116
  return f"""
117
  <!doctype html>
118
  <html>
@@ -120,8 +178,8 @@ def _shell(page_index_0: int, html_table: str) -> str:
120
  <meta charset="utf-8" />
121
  <title>Replicate Catalog — Page {page_num}</title>
122
  <style>
123
- body {{ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; margin: 24px; }}
124
- .meta {{ margin-bottom: 16px; color: #444; }}
125
  .grid {{ width: 100%; border-collapse: collapse; }}
126
  .grid th, .grid td {{ border-bottom: 1px solid #eee; padding: 10px; vertical-align: top; }}
127
  .grid th {{ text-align: left; background: #fafafa; }}
@@ -135,15 +193,63 @@ def _shell(page_index_0: int, html_table: str) -> str:
135
  .page:hover {{ background: #fafafa; }}
136
  .page.current {{ background: #111; color: #fff; border-color: #111; cursor: default; }}
137
  .page.next {{ font-weight: 600; }}
 
 
 
 
138
  </style>
139
  </head>
140
  <body>
141
  <div class="meta">
142
- <div><strong>Replicate catalog</strong> — showing page {page_num} of {known_total} known; more pages available: {more_flag}</div>
143
- <div>Rows on this page: {len(_pages[page_index_0]["results"])}</div>
 
 
 
144
  </div>
 
 
 
 
 
 
 
145
  {html_table}
146
  {_render_pagination(page_num)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  </body>
148
  </html>
149
  """
@@ -175,3 +281,41 @@ def page(n: int):
175
  raise HTTPException(status_code=404, detail="No more pages.")
176
  models = _pages[idx]["results"]
177
  return HTMLResponse(_shell(idx, _render_table(models)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import os, time, html, logging, requests, threading
3
  from typing import Any, Dict, List, Optional
4
  from fastapi import FastAPI, HTTPException
5
+ from fastapi.responses import HTMLResponse, JSONResponse
6
 
7
  # ----- Logging -----
8
  logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
 
25
  _pages: List[Dict[str, Any]] = [] # each: {"results": [...], "next": str|None}
26
  _next_cursor_url: Optional[str] = f"{BASE}/models" # where to fetch next page from
27
 
28
+ # Build state
29
+ _BUILDING = False
30
+ _BUILD_COMPLETE = False
31
+
32
  # ---------- Helpers ----------
33
 
34
  def _get_with_backoff(url: str) -> Dict[str, Any]:
 
63
  })
64
  _next_cursor_url = payload.get("next")
65
 
66
+ def _build_all_pages():
67
+ """
68
+ Fetches every page exactly once, then marks complete.
69
+ Intended to be run in a background thread started after first render.
70
+ """
71
+ global _BUILDING, _BUILD_COMPLETE, _next_cursor_url
72
+ if _BUILD_COMPLETE or _BUILDING:
73
+ return
74
+ _BUILDING = True
75
+ try:
76
+ # If nothing loaded yet, load the first page to guarantee index 0 exists
77
+ if not _pages and _next_cursor_url:
78
+ _ensure_page_loaded(0)
79
+ # Continue until no 'next'
80
+ while _next_cursor_url:
81
+ payload = _get_with_backoff(_next_cursor_url)
82
+ _pages.append({
83
+ "results": payload.get("results", []) or [],
84
+ "next": payload.get("next")
85
+ })
86
+ _next_cursor_url = payload.get("next")
87
+ _BUILD_COMPLETE = True
88
+ log.info("Catalog build complete. Total pages: %d", len(_pages))
89
+ except Exception as e:
90
+ log.error("Build failed: %s", e)
91
+ finally:
92
+ _BUILDING = False
93
+
94
  def _model_id(x: Dict[str, Any]) -> str:
95
  owner = x.get("owner") or x.get("username") or ""
96
  name = x.get("name") or ""
 
136
  links.append(f"<span class='page current'>{i}</span>")
137
  else:
138
  links.append(f"<a class='page' href='/page/{i}'>{i}</a>")
139
+ # If more pages likely exist (cursor present), show a link to the next index
140
  if _next_cursor_url:
141
  links.append(f"<a class='page next' href='/page/{total_known + 1}'>Load {total_known + 1}</a>")
142
  return "<div class='pagination'>" + " ".join(links) + "</div>"
143
 
144
+ def _search_models(query: str) -> List[Dict[str, Any]]:
145
+ q = query.strip().lower()
146
+ if not q:
147
+ return []
148
+ results: List[Dict[str, Any]] = []
149
+ seen = set()
150
+ for page in _pages:
151
+ for m in page.get("results", []):
152
+ mid = _model_id(m)
153
+ hay = " ".join([
154
+ mid,
155
+ m.get("description") or "",
156
+ " ".join([str(t) for t in (m.get("tags") or m.get("categories") or [])])
157
+ ]).lower()
158
+ if q in hay:
159
+ if mid not in seen:
160
+ results.append(m)
161
+ seen.add(mid)
162
+ return results
163
+
164
+ def _shell(page_index_0: int, html_table: str, *, search_query: str = "") -> str:
165
  page_num = page_index_0 + 1
166
  known_total = len(_pages)
167
  more_flag = "yes" if _next_cursor_url else "no"
168
+ building = "yes" if _BUILDING else "no"
169
+ complete = "yes" if _BUILD_COMPLETE else "no"
170
+
171
+ search_disabled = "" if _BUILD_COMPLETE else "disabled"
172
+ search_opacity = "1.0" if _BUILD_COMPLETE else "0.45"
173
+
174
  return f"""
175
  <!doctype html>
176
  <html>
 
178
  <meta charset="utf-8" />
179
  <title>Replicate Catalog — Page {page_num}</title>
180
  <style>
181
+ body {{ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; margin: 24px; }}
182
+ .meta {{ margin-bottom: 12px; color: #444; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }}
183
  .grid {{ width: 100%; border-collapse: collapse; }}
184
  .grid th, .grid td {{ border-bottom: 1px solid #eee; padding: 10px; vertical-align: top; }}
185
  .grid th {{ text-align: left; background: #fafafa; }}
 
193
  .page:hover {{ background: #fafafa; }}
194
  .page.current {{ background: #111; color: #fff; border-color: #111; cursor: default; }}
195
  .page.next {{ font-weight: 600; }}
196
+ .searchbar {{ display:flex; gap:8px; align-items:center; }}
197
+ .searchbar input[type="text"] {{ width: 360px; padding:8px 10px; border:1px solid #ddd; border-radius:8px; opacity:{search_opacity}; }}
198
+ .searchbar button {{ padding:8px 12px; border:1px solid #111; background:#111; color:#fff; border-radius:8px; opacity:{search_opacity}; }}
199
+ .pill {{ display:inline-block; padding:2px 8px; border:1px solid #ddd; border-radius:999px; font-size:12px; color:#333; }}
200
  </style>
201
  </head>
202
  <body>
203
  <div class="meta">
204
+ <strong>Replicate catalog</strong>
205
+ <span class="pill">page {page_num} of {known_total} known</span>
206
+ <span class="pill">more pages available: {more_flag}</span>
207
+ <span class="pill">building: {building}</span>
208
+ <span class="pill">complete: {complete}</span>
209
  </div>
210
+
211
+ <form class="searchbar" method="GET" action="/search" onsubmit="return onSearchSubmit();" >
212
+ <input id="q" name="q" type="text" placeholder="Search models, descriptions, tags" value="{html.escape(search_query)}" {search_disabled} />
213
+ <button id="qbtn" type="submit" {search_disabled}>Search</button>
214
+ <span id="searchHint" style="color:#666; font-size:12px;">{'' if _BUILD_COMPLETE else 'Search available after catalog completes.'}</span>
215
+ </form>
216
+
217
  {html_table}
218
  {_render_pagination(page_num)}
219
+
220
+ <script>
221
+ // Start building after the first render completes
222
+ document.addEventListener("DOMContentLoaded", function() {{
223
+ fetch("/build_all", {{ method: "POST" }})
224
+ .catch(() => {{ /* ignore */ }});
225
+ // Poll status to update UI affordances
226
+ const poll = () => {{
227
+ fetch("/status").then(r => r.json()).then(s => {{
228
+ // When complete, enable search UI (no reload required)
229
+ if (s.complete) {{
230
+ const q = document.getElementById("q");
231
+ const b = document.getElementById("qbtn");
232
+ const hint = document.getElementById("searchHint");
233
+ if (q && b) {{
234
+ q.disabled = false; b.disabled = false;
235
+ q.style.opacity = "1.0"; b.style.opacity = "1.0";
236
+ if (hint) hint.textContent = "";
237
+ }}
238
+ }}
239
+ // Update the document title with progress (optional, lightweight)
240
+ document.title = "Replicate Catalog — Page {page_num} (" + s.pages + " known)";
241
+ // Keep polling until complete
242
+ if (!s.complete) setTimeout(poll, 1200);
243
+ }}).catch(() => setTimeout(poll, 2000));
244
+ }};
245
+ poll();
246
+ }});
247
+
248
+ function onSearchSubmit() {{
249
+ const q = document.getElementById("q");
250
+ return q && !q.disabled;
251
+ }}
252
+ </script>
253
  </body>
254
  </html>
255
  """
 
281
  raise HTTPException(status_code=404, detail="No more pages.")
282
  models = _pages[idx]["results"]
283
  return HTMLResponse(_shell(idx, _render_table(models)))
284
+
285
+ @app.post("/build_all")
286
+ def build_all():
287
+ """
288
+ Trigger a one-time, full-catalog build.
289
+ Starts only if not already building and not complete.
290
+ """
291
+ global _BUILDING, _BUILD_COMPLETE
292
+ if _BUILD_COMPLETE:
293
+ return JSONResponse({"started": False, "reason": "complete"}, status_code=200)
294
+ if _BUILDING:
295
+ return JSONResponse({"started": False, "reason": "already_building"}, status_code=200)
296
+ t = threading.Thread(target=_build_all_pages, daemon=True)
297
+ t.start()
298
+ return JSONResponse({"started": True}, status_code=202)
299
+
300
+ @app.get("/status")
301
+ def status():
302
+ return JSONResponse({
303
+ "pages": len(_pages),
304
+ "more": bool(_next_cursor_url),
305
+ "building": _BUILDING,
306
+ "complete": _BUILD_COMPLETE
307
+ })
308
+
309
+ @app.get("/search", response_class=HTMLResponse)
310
+ def search(q: str = ""):
311
+ # If not complete, keep the search UI disabled but render current page 1
312
+ idx = 0 if _pages else 0
313
+ base_models = _pages[idx]["results"] if _pages else []
314
+ if _BUILD_COMPLETE and q.strip():
315
+ results = _search_models(q)
316
+ content = _render_table(results)
317
+ return HTMLResponse(_shell(idx, content, search_query=q))
318
+ else:
319
+ # Show page 1 with disabled search and a hint
320
+ content = _render_table(base_models)
321
+ return HTMLResponse(_shell(idx, content, search_query=q))