Spaces:
Running
Running
| """Finance Atlas — a Hugging Face Space by The Bit Trading Company. | |
| An index of finance models on the Hub: what they claim, what they document, | |
| and what nobody has checked. | |
| Architecture, in one paragraph: the visible layer is the design's own markup, | |
| rendered by `src/ui/shell.py` from a plain state dict. Gradio owns the | |
| transport and nothing that is seen -- one `gr.HTML` sink, a hidden textbox the | |
| click bridge writes into, and a hidden button it clicks. Every interaction is | |
| `state -> apply_action -> state -> render`, which keeps the whole UI a pure | |
| function of state and makes it testable without a browser. | |
| All reads come from the dataset repo. There is no Hub API call in the request | |
| path: `atlas.parquet` is loaded once at boot and every filter, sort and lineage | |
| walk is an in-memory operation. That is what keeps this usable on CPU basic. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import gradio as gr | |
| from src import atlas, suggest # noqa: F401 - sets the vendor path | |
| from src.atlas import growth | |
| from src.ui import chrome, shell | |
| from bit_ui import nav as bit_nav | |
| from bit_ui import bridge, theme | |
| logging.basicConfig(level=logging.INFO, | |
| format="%(asctime)s %(levelname)-7s %(name)s: %(message)s") | |
| log = logging.getLogger("atlas.app") | |
| ON_SPACE = bool(os.environ.get("SPACE_ID")) | |
| BACKTEST_URL = "https://huggingface.co/spaces/Bit-Trading-Company/bit-backtest-lab" | |
| DATASET_URL = f"https://huggingface.co/datasets/{atlas.DATASET_REPO}" | |
| ORG_URL = "https://huggingface.co/Bit-Trading-Company" | |
| # Only the destinations that exist are linked. The rest of the design's nav is | |
| # rendered but inert -- see `shell.sidebar`. | |
| LINKS = { | |
| "Backtest Lab": BACKTEST_URL, | |
| "Dataset": DATASET_URL, | |
| "Hugging Face org": ORG_URL, | |
| } | |
| TAPE_SIZE = 12 | |
| TRENDING_SIZE = 5 | |
| # How many rows are rendered into the table at once. The design's table is a | |
| # scroll container, not a paginated list, so this is a payload bound rather | |
| # than a UX choice. | |
| # | |
| # Measured over the real index: 300 rows is ~1.1 MB of HTML, which gzips to | |
| # ~35 KB because the design's inline styles repeat heavily (32:1). Rendering | |
| # takes ~5 ms. The count the UI reports is always the true number of matches, | |
| # never this cap -- see `build_view`. | |
| MAX_ROWS = 300 | |
| # -------------------------------------------------------------------------- | |
| # Boot | |
| # -------------------------------------------------------------------------- | |
| # `ATLAS_LOCAL_DIR` points the app at a dataset tree on disk instead of the Hub. | |
| # Used for local development and by the tests; unset in production. | |
| INDEX = atlas.build_index(local_dir=os.environ.get("ATLAS_LOCAL_DIR") or None) | |
| # The navigation is shared config, not code: it lives in a dataset every BTC | |
| # Space reads at boot. Flipping a module from "coming soon" to live is one edit | |
| # there and reaches every Space on its next restart -- no redeploy, no | |
| # submodule bump. `nav.load` falls back to the built-in nav if it cannot read | |
| # it, so an unreachable config never stops the Space from booting. | |
| NAV = bit_nav.load(local_path=os.environ.get("ATLAS_LOCAL_NAV") or None) | |
| # The renderers need to link back to the dataset; carrying it on the index | |
| # keeps the shell from importing app config. | |
| INDEX.dataset_repo = atlas.DATASET_REPO | |
| SUGGESTIONS = suggest.Suggestions( | |
| local_dir=os.environ.get("ATLAS_SUGGEST_DIR", "/tmp/atlas-suggestions"), | |
| ) | |
| # -------------------------------------------------------------------------- | |
| # Derived view | |
| # -------------------------------------------------------------------------- | |
| def tape_rows(index): | |
| """The header ticker: the most downloaded models, with real change.""" | |
| top = sorted(index.rows, key=lambda r: int(r.get("downloads_30d") or 0), | |
| reverse=True)[:TAPE_SIZE] | |
| out = [] | |
| for row in top: | |
| series = index.trends.get(row["id"]) | |
| out.append({ | |
| "id": row["id"], | |
| "downloads_30d": row.get("downloads_30d"), | |
| "series": series, | |
| "change": growth(series), | |
| }) | |
| return out | |
| def trending_rows(index): | |
| """Top real movers. Empty until two snapshots exist, by design.""" | |
| scored = [] | |
| for model_id, series in index.trends.items(): | |
| change = growth(series) | |
| if change is None or change <= 0: | |
| continue | |
| scored.append((model_id, change, series)) | |
| scored.sort(key=lambda item: item[1], reverse=True) | |
| return scored[:TRENDING_SIZE] | |
| def palette_groups(index, query: str): | |
| """Results for the ⌘K palette, ranked and grouped. | |
| Three kinds of thing are searchable, in the order someone is most likely | |
| to want them: a specific model, a filter that narrows the table, and the | |
| other Spaces. Matching uses the same fields the table's own search uses, | |
| so the palette and the table never disagree about what "finbert" means. | |
| """ | |
| from bit_ui import palette as bit_palette | |
| query = (query or "").strip().lower() | |
| labels = index.taxonomy.get("task_labels", {}) | |
| assets = index.taxonomy.get("asset_labels", {}) | |
| groups = [] | |
| # -- models -------------------------------------------------------- | |
| if query: | |
| scored = [] | |
| for row in index.rows: | |
| model_id = (row.get("id") or "") | |
| lowered = model_id.lower() | |
| position = lowered.find(query) | |
| if position < 0: | |
| if query not in (row.get("author") or "").lower(): | |
| continue | |
| position = 50 | |
| # Earlier match first, then popularity. An exact prefix on the | |
| # model name beats a big download count on a loose match. | |
| scored.append((position, -int(row.get("downloads_30d") or 0), row)) | |
| scored.sort(key=lambda item: (item[0], item[1])) | |
| results = [] | |
| for _, _, row in scored[:40]: | |
| results.append(bit_palette.Result( | |
| label=row["id"], | |
| action=f"open:{row['id']}", | |
| sub=labels.get(row.get("task"), row.get("task") or ""), | |
| meta=f"{int(row.get('downloads_30d') or 0):,}", | |
| glyph=shell.task_glyph(row.get("task")), | |
| badge="✓" if row.get("verified") else "", | |
| )) | |
| groups.append(bit_palette.Group("Models", results)) | |
| # -- filters ------------------------------------------------------- | |
| filters = [] | |
| for key in index.taxonomy.get("task_chips", ()): | |
| label = labels.get(key, key) | |
| if query and query not in label.lower(): | |
| continue | |
| filters.append(bit_palette.Result( | |
| label=f"Task: {label}", action=f"task:{key}", | |
| meta=f"{index.count_by('task', key):,}", | |
| glyph=shell.task_glyph(key))) | |
| for key in index.taxonomy.get("asset_chips", ()): | |
| label = assets.get(key, key) | |
| if query and query not in label.lower(): | |
| continue | |
| filters.append(bit_palette.Result( | |
| label=f"Asset: {label}", action=f"asset:{key}", | |
| meta=f"{index.count_by('asset_class', key):,}", | |
| glyph=shell.asset_glyph(key))) | |
| if not query or "verified" in query: | |
| filters.append(bit_palette.Result( | |
| label="Only human-verified models", action="verified:toggle", | |
| meta=f"{index.verified_count:,}", glyph="✓")) | |
| if not query or "graveyard" in query or "unmaintained" in query: | |
| filters.append(bit_palette.Result( | |
| label="Show the graveyard (unmaintained)", action="graveyard:", | |
| meta=f"{index.unmaintained_count:,}", glyph="†")) | |
| if filters: | |
| groups.append(bit_palette.Group("Filters", filters)) | |
| # -- other Spaces -------------------------------------------------- | |
| spaces = [] | |
| for group in NAV.get("groups") or (): | |
| for item in group.get("items") or (): | |
| if item.get("status") != "live" or not item.get("url"): | |
| continue | |
| if item["name"] == "Finance Atlas": | |
| continue | |
| if query and query not in item["name"].lower(): | |
| continue | |
| spaces.append(bit_palette.Result( | |
| label=item["name"], href=item["url"], glyph="↗", meta="OPEN")) | |
| if spaces: | |
| groups.append(bit_palette.Group("Go to", spaces)) | |
| return groups | |
| def build_view(index, state) -> dict: | |
| """Everything derived from state, computed once per render. | |
| `matched` is the true number of rows passing the filters; `rows` is the | |
| capped slice actually rendered. Both are carried because the UI must | |
| report the real count -- a table that says "300 shown" when 800 matched | |
| is telling the user their filter is narrower than it is. | |
| """ | |
| matched = index.sorted(index.filtered(state), state.get("sort", "Downloads")) | |
| return { | |
| "rows": matched[:MAX_ROWS], | |
| "matched": len(matched), | |
| "truncated": max(0, len(matched) - MAX_ROWS), | |
| "hidden": index.hidden_by_maintained(state), | |
| "tape": tape_rows(index), | |
| "trending": trending_rows(index), | |
| "links": LINKS, | |
| "nav": NAV, | |
| # Only computed when the palette is actually open -- it scans every row. | |
| "palette_groups": (palette_groups(index, state.get("palette_q")) | |
| if state.get("palette_open") else []), | |
| } | |
| def render(state) -> str: | |
| return shell.page(INDEX, state, build_view(INDEX, state)) | |
| # -------------------------------------------------------------------------- | |
| # Actions | |
| # -------------------------------------------------------------------------- | |
| def _toggle_in(state, key, value, allowed): | |
| """Add/remove `value` in a list-valued filter, validated against `allowed`. | |
| Actions arrive from the DOM and are user input. An unknown value is | |
| dropped rather than stored, so a hand-crafted click cannot inject a filter | |
| term the UI never offered. | |
| """ | |
| if value not in allowed: | |
| return state | |
| current = list(state.get(key) or ()) | |
| if value in current: | |
| current.remove(value) | |
| else: | |
| current.append(value) | |
| state[key] = current | |
| return state | |
| def apply_action(state: dict, raw: str) -> dict: | |
| """Fold one bridge action into the state. Unknown actions change nothing.""" | |
| action = chrome.parse_action(raw) | |
| if action is None or action.is_noop: | |
| return state | |
| state = dict(state) | |
| key, value = action.key, action.value | |
| taxonomy = INDEX.taxonomy | |
| if key == "task": | |
| state = _toggle_in(state, "tasks", value, | |
| set(taxonomy.get("task_chips", ()))) | |
| elif key == "asset": | |
| state = _toggle_in(state, "assets", value, | |
| set(taxonomy.get("asset_chips", ()))) | |
| elif key == "lic": | |
| state = _toggle_in(state, "lic", value, | |
| set(taxonomy.get("license_buckets", ()))) | |
| elif key == "verified": | |
| state["verified_only"] = not state.get("verified_only") | |
| elif key == "maintained": | |
| state["maintained_only"] = not state.get("maintained_only") | |
| elif key == "haseval": | |
| state["has_eval"] = not state.get("has_eval") | |
| elif key == "hasdata": | |
| state["has_data"] = not state.get("has_data") | |
| elif key == "sort": | |
| if value in atlas.SORTS: | |
| state["sort"] = value | |
| elif key == "clear": | |
| state.update(q="", tasks=[], assets=[], lic=[], verified_only=False, | |
| has_eval=False, has_data=False) | |
| elif key == "graveyard": | |
| state["maintained_only"] = False | |
| elif key == "open": | |
| # Only ids actually in the index can open a drawer. | |
| state["sel"] = value if value in INDEX.by_id else None | |
| elif key == "close": | |
| state["sel"] = None | |
| elif key == "q": | |
| state["q"] = value[:120] | |
| elif key == "suggestbox": | |
| state["suggest"] = value[:120] | |
| state["suggest_note"] = "" | |
| state["suggest_ok"] = False | |
| elif key == "suggest": | |
| state = _submit_suggestion(state) | |
| # ---- shared chrome ------------------------------------------------ | |
| elif key == "palette": | |
| if value == "open": | |
| # Seed the palette with whatever is already in the search box, so | |
| # ⌘K continues a search rather than discarding it. | |
| state.update(palette_open=True, | |
| palette_q=state.get("palette_q") or state.get("q") or "") | |
| else: | |
| state["palette_open"] = False | |
| elif key == "pq": | |
| state["palette_q"] = value[:120] | |
| elif key == "soon": | |
| if value == "close": | |
| state.update(soon_module="", notify_note="") | |
| elif bit_nav.find(NAV, value): | |
| # Only modules the shared nav actually declares. A hand-crafted | |
| # click cannot invent a product. | |
| state.update(soon_module=value, notify_note="", palette_open=False) | |
| elif key == "nemail": | |
| state["notify_email"] = value[:160] | |
| elif key == "notify": | |
| state = _register_interest(state, value) | |
| elif key == "contact": | |
| if value == "open": | |
| state.update(contact_open=True, contact_note="", contact_ok=False, | |
| palette_open=False, soon_module="") | |
| elif value == "close": | |
| state.update(contact_open=False, contact_form_open=False) | |
| elif value == "form": | |
| state["contact_form_open"] = not state.get("contact_form_open") | |
| elif value == "send": | |
| state = _send_contact(state) | |
| elif key == "topic": | |
| topics = (NAV.get("contact") or {}).get("topics") or [] | |
| if value in topics: | |
| state.update(contact_topic=value, contact_note="", contact_ok=False) | |
| elif key == "cname": | |
| state["contact_name"] = value[:120] | |
| elif key == "cemail": | |
| state["contact_email"] = value[:160] | |
| elif key == "cmsg": | |
| state["contact_message"] = value[:4000] | |
| return state | |
| def _register_interest(state: dict, module: str) -> dict: | |
| """"Notify me when this launches" from the coming-soon dialog. | |
| Written to the dataset alongside suggestions. It says RECORDED rather than | |
| "you're on the list", because there is no mailing list yet -- only a file | |
| somebody has to read. | |
| """ | |
| if not bit_nav.find(NAV, module): | |
| return state | |
| email = (state.get("notify_email") or "").strip() | |
| try: | |
| SUGGESTIONS.submit_interest(email, module) | |
| except suggest.SuggestionError as exc: | |
| state["notify_note"] = str(exc).upper() | |
| return state | |
| state["notify_note"] = "RECORDED — WE HAVE NO MAILING LIST YET" | |
| return state | |
| def _send_contact(state: dict) -> dict: | |
| """Append a contact message to the dataset. | |
| Deliberately not "SENT · TICKET #4821 OPENED" as the design writes it: | |
| there is no ticketing system behind this, and inventing a ticket number | |
| would be the most confidently false thing on the page. | |
| """ | |
| try: | |
| SUGGESTIONS.submit_contact( | |
| name=state.get("contact_name", ""), | |
| email=state.get("contact_email", ""), | |
| topic=state.get("contact_topic") | |
| or ((NAV.get("contact") or {}).get("topics") or ["Other"])[0], | |
| message=state.get("contact_message", ""), | |
| ) | |
| except suggest.SuggestionError as exc: | |
| state.update(contact_note=str(exc).upper(), contact_ok=False) | |
| return state | |
| state.update( | |
| contact_note=("RECEIVED" if SUGGESTIONS.syncing | |
| else "RECEIVED (NOT SYNCED)"), | |
| contact_ok=True, contact_message="", | |
| ) | |
| return state | |
| def _submit_suggestion(state: dict) -> dict: | |
| try: | |
| model_id = SUGGESTIONS.submit(state.get("suggest", "")) | |
| except suggest.SuggestionError as exc: | |
| state["suggest_note"] = str(exc).upper() | |
| state["suggest_ok"] = False | |
| return state | |
| if model_id in INDEX.by_id: | |
| state["suggest_note"] = "ALREADY INDEXED" | |
| elif SUGGESTIONS.syncing: | |
| state["suggest_note"] = "QUEUED FOR NEXT CRAWL" | |
| else: | |
| # Honest about the difference: accepted locally is not the same as | |
| # committed to the dataset. | |
| state["suggest_note"] = "RECEIVED (NOT SYNCED)" | |
| state["suggest_ok"] = True | |
| state["suggest"] = "" | |
| return state | |
| def on_action(raw, state): | |
| state = apply_action(state or atlas.default_state(), raw or "") | |
| return render(state), state | |
| # -------------------------------------------------------------------------- | |
| # App | |
| # -------------------------------------------------------------------------- | |
| def build_app() -> gr.Blocks: | |
| with gr.Blocks( | |
| title="Finance Atlas — The Bit Trading Company", | |
| theme=theme.bit_theme(), | |
| # chrome.full_css(), not theme.full_css(): the latter is bit-ui's | |
| # shared CSS only and silently drops this Space's own -- the table | |
| # row styling, the sticky header, the responsive breakpoints. | |
| css=chrome.full_css(), | |
| analytics_enabled=False, | |
| fill_width=True, | |
| ) as demo: | |
| state = gr.State(atlas.default_state()) | |
| # The one visible component. Everything the user sees is inside it. | |
| view = gr.HTML(render(atlas.default_state()), elem_id="bit-view") | |
| # The bridge's transport. Both must be rendered -- `visible=False` | |
| # removes an element from the DOM entirely, and the delegated listener | |
| # would then be writing into nothing. They are hidden in CSS instead. | |
| action = gr.Textbox("", elem_id=bridge.ACTION_ELEMENT_ID, | |
| label="", show_label=False, container=False, | |
| interactive=True) | |
| trigger = gr.Button("", elem_id=bridge.TRIGGER_ELEMENT_ID) | |
| # Two paths for one click: the textbox's own change event, and the | |
| # hidden button. Gradio always honours a real click on a real Button, | |
| # and the nonce on every action means a duplicate delivery folds to the | |
| # same state rather than acting twice. | |
| action.change(on_action, [action, state], [view, state], | |
| show_progress="hidden") | |
| # `api_name` keeps this reachable from `gradio_client`, which is how CI | |
| # smoke-tests the deployed Space: a green build only proves the image | |
| # started, not that an action still renders a page. | |
| trigger.click(on_action, [action, state], [view, state], | |
| show_progress="hidden", api_name="action") | |
| # Installed via its own `load`, with no outputs: Gradio treats a `js=` | |
| # return value as the output values, so sharing a load that has outputs | |
| # would wipe them. Spaces also drops `gr.Blocks(head=...)`, which is why | |
| # this is the path that actually works once deployed. | |
| demo.load(fn=None, inputs=None, outputs=None, js=bridge.BRIDGE_LOAD_JS) | |
| return demo | |
| demo = build_app() | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0" if ON_SPACE else "127.0.0.1", | |
| server_port=int(os.environ.get("PORT", 7860)), | |
| show_api=False, | |
| allowed_paths=theme.static_paths(), | |
| # Gradio 5 defaults to SSR. The page then renders server-side and only | |
| # becomes interactive once the client hydrates -- and when hydration | |
| # does not complete, every handler stays unbound: the markup is all | |
| # there, buttons look fine, and nothing does anything. That is exactly | |
| # what happened here, and it is a silent failure with no console error. | |
| # This app renders its own markup and needs only the event wiring, so | |
| # there is nothing for SSR to win and a whole failure mode to avoid. | |
| ssr_mode=False, | |
| ) | |