File size: 3,577 Bytes
a1a01f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""Data loading: published HF parquet samples and the small aggregate bundles committed under data/."""
from __future__ import annotations

import io
import json
import os
import sys
import urllib.request
from pathlib import Path

import pandas as pd
import streamlit as st

ROOT = Path(__file__).resolve().parents[1]
META = json.loads((ROOT / "common" / "meta.json").read_text(encoding="utf-8"))
# For offline tests: a directory holding <key>.parquet copies of the published files.
LOCAL_DIR = os.environ.get("FLIAR_PARQUET_DIR")
# True when running in the browser (stlite / Pyodide). There is no socket networking there, so files are
# downloaded with a synchronous XMLHttpRequest (allowed inside the Web Worker stlite runs Python in), and parquet is
# read with fastparquet, which stlite already loads (pyarrow would be a large extra download).
IS_PYODIDE = sys.platform == "emscripten" or "pyodide" in sys.modules
# Optional JSON map {dataset key: URL} that overrides the published parquet URL, e.g. a column-trimmed copy
# served next to a static page. Ignored when unset.
URL_OVERRIDES = json.loads(os.environ.get("FLIAR_PARQUET_URLS") or "{}")


def meta(key: str) -> dict:
    return META[key]


def _browser_get(url: str) -> bytes:
    from js import XMLHttpRequest  # type: ignore  # Pyodide only

    xhr = XMLHttpRequest.new()
    xhr.open("GET", url, False)
    xhr.responseType = "arraybuffer"
    xhr.send(None)
    if xhr.status != 200:
        raise RuntimeError(f"Download failed ({xhr.status}): {url}")
    return xhr.response.to_bytes()


def _fetch(key: str) -> bytes | str:
    if LOCAL_DIR and (Path(LOCAL_DIR) / f"{key}.parquet").exists():
        return str(Path(LOCAL_DIR) / f"{key}.parquet")
    url = URL_OVERRIDES.get(key) or META[key]["parquet"]
    if IS_PYODIDE:
        return _browser_get(url)
    req = urllib.request.Request(url, headers={"User-Agent": "fliar-streamlit-apps/1.0"})
    with urllib.request.urlopen(req, timeout=120) as r:
        return r.read()


def _shrink(df: pd.DataFrame) -> pd.DataFrame:
    """Low-cardinality text columns become categoricals to keep memory small."""
    for c in df.columns:
        if df[c].dtype == object or str(df[c].dtype).startswith("string"):
            if df[c].nunique(dropna=True) < max(50, len(df) // 4):
                df[c] = df[c].astype("category")
            else:
                df[c] = df[c].astype(object)
    return df


@st.cache_data(show_spinner="Loading the published sample from Hugging Face ...", ttl=24 * 3600, max_entries=8)
def load(key: str, columns: tuple[str, ...]) -> pd.DataFrame:
    """Read only the needed columns of a published dataset sample."""
    src = _fetch(key)
    f = io.BytesIO(src) if isinstance(src, bytes) else src
    if IS_PYODIDE:
        import fastparquet

        pf = fastparquet.ParquetFile(f)
        df = pf.to_pandas(columns=[c for c in columns if c in pf.columns])
    else:
        import pyarrow.parquet as pq

        pf = pq.ParquetFile(f)
        df = pf.read(columns=[c for c in columns if c in pf.schema_arrow.names]).to_pandas()
    del src
    return _shrink(df)


@st.cache_data(show_spinner=False, max_entries=32)
def load_bundle(theme: str, name: str) -> pd.DataFrame:
    """Read an aggregate parquet committed under data/<theme>/<name>.parquet."""
    return pd.read_parquet(ROOT / "data" / theme / f"{name}.parquet", engine="fastparquet" if IS_PYODIDE else "auto")


def bundle_names(theme: str) -> list[str]:
    d = ROOT / "data" / theme
    return sorted(p.stem for p in d.glob("*.parquet")) if d.exists() else []