Spaces:
Running
Running
File size: 9,911 Bytes
22de727 | 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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | """Export the site database to Parquet for the Hugging Face release: one file per table of the 65 sites, the schema,
and the sites' static images. scripts/restore_site_db.py rebuilds the SQLite database from them.
Left out: session state (session_overlay, session_collection_replaced, sessions, event_log, which hold the writes of
every evaluation episode), the full-text indexes and the shadow tables SQLite keeps for them (rebuilt on restore),
and the tables of retired sites that are not among the 65.
python scripts/export_site_db.py --db $MINIWEB_DB --static data/static --out data/release/miniweb-v1-hf/site_db
"""
import argparse, json, os, re, sqlite3
import pyarrow as pa
import pyarrow.parquet as pq
import yaml
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SHADOW = ("_data", "_idx", "_content", "_docsize", "_config")
BATCH = 20000
def arrow_type(decl):
"""SQLite affinity of a declared type -> an Arrow type (column affinity rules, sqlite.org/datatype3.html)."""
d = (decl or "").upper()
if "INT" in d:
return pa.int64()
if any(k in d for k in ("CHAR", "CLOB", "TEXT")):
return pa.string()
if "BLOB" in d or not d:
return None # decided from the values
if any(k in d for k in ("REAL", "FLOA", "DOUB")):
return pa.float64()
return None # NUMERIC: decided from the values
def export_table(con, table, path):
cols = con.execute(f'PRAGMA table_info("{table}")').fetchall()
names = [c[1] for c in cols]
types = {c[1]: arrow_type(c[2]) for c in cols}
# a column whose stored values do not all fit its declared type (SQLite is dynamically typed) becomes text, or
# binary if it holds blobs; untyped columns take the type of their values
for n in names:
kinds = {k for (k,) in con.execute(f'SELECT DISTINCT typeof("{n}") FROM "{table}"')} - {"null"}
want = types[n]
if "blob" in kinds:
types[n] = pa.binary() if kinds == {"blob"} else pa.string()
elif want is None or (want == pa.int64() and kinds - {"integer"}) or \
(want == pa.float64() and kinds - {"real", "integer"}):
types[n] = pa.int64() if kinds == {"integer"} else pa.float64() if kinds <= {"real", "integer"} and kinds \
else pa.string()
schema = pa.schema([(n, types[n]) for n in names])
cast = {n: (str if types[n] == pa.string() else None) for n in names}
cur = con.execute(f'SELECT * FROM "{table}"')
writer, rows = pq.ParquetWriter(path, schema, compression="zstd"), 0
while True:
chunk = cur.fetchmany(BATCH)
if not chunk:
break
data = {n: [None if r[i] is None else (cast[n](r[i]) if cast[n] else r[i]) for r in chunk]
for i, n in enumerate(names)}
writer.write_table(pa.table(data, schema=schema))
rows += len(chunk)
if rows == 0:
writer.write_table(pa.table({n: [] for n in names}, schema=schema))
writer.close()
return rows
def export_media(media_dir, out, shard_mb=480):
"""The local copy of the sites' external media (scripts/fetch_site_media.py): Parquet shards of (url, path,
content_type, bytes) under media/, and the manifest of every URL tried (status included) as media_manifest.parquet."""
rows = [json.loads(line) for line in open(os.path.join(media_dir, "manifest.jsonl"))]
pq.write_table(pa.table({k: [r.get(k) for r in rows] for k in ("url", "path", "bytes", "content_type")} |
{"status": [str(r.get("status")) for r in rows]}),
os.path.join(out, "media_manifest.parquet"))
have = [r for r in rows if r.get("path") and os.path.isfile(os.path.join(media_dir, r["path"]))]
os.makedirs(os.path.join(out, "media"), exist_ok=True)
schema = pa.schema([("url", pa.string()), ("path", pa.string()), ("content_type", pa.string()), ("bytes", pa.binary())])
shard, size, buf, files = 0, 0, [], 0
def flush():
nonlocal shard, size, buf
if buf:
pq.write_table(pa.Table.from_pylist(buf, schema=schema),
os.path.join(out, "media", f"media-{shard:05d}.parquet"), compression="zstd")
shard, size, buf = shard + 1, 0, []
for r in have:
data = open(os.path.join(media_dir, r["path"]), "rb").read()
buf.append({"url": r["url"], "path": r["path"], "content_type": r.get("content_type"), "bytes": data})
size += len(data); files += 1
if size > shard_mb * 2 ** 20:
flush()
flush()
print(f"media: {files} files in {shard} shards; manifest of {len(rows)} URLs")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--db", default=os.environ.get("MINIWEB_DB"))
ap.add_argument("--static", default=os.path.join(ROOT, "data", "static"))
ap.add_argument("--sites", default=os.path.join(ROOT, "data", "sites.yaml"))
ap.add_argument("--out", default=os.path.join(ROOT, "data", "release", "miniweb-v1-hf", "site_db"))
ap.add_argument("--media", default=os.path.join(ROOT, "data", "media"),
help="the local media copy to pack (scripts/fetch_site_media.py); skipped if absent")
ap.add_argument("--media-only", action="store_true")
a = ap.parse_args()
if a.media_only:
export_media(a.media, a.out)
return
con = sqlite3.connect(f"file:{a.db}?mode=ro", uri=True)
sites = [s["id"] for s in yaml.safe_load(open(a.sites))["sites"]]
prefixes = sorted(((s, s.replace("-", "_") + "_") for s in sites), key=lambda kv: -len(kv[1]))
owner = lambda t: next((s for s, p in prefixes if t.startswith(p)), None) # noqa: E731
master = con.execute("SELECT type, name, tbl_name, sql FROM sqlite_master WHERE sql IS NOT NULL").fetchall()
virtual = {n for t, n, _, s in master if t == "table" and s.upper().startswith("CREATE VIRTUAL")}
shadow = {v + suf for v in virtual for suf in SHADOW}
external = {n for t, n, _, s in master if n in virtual and re.search(r"content\s*=\s*\[?\w", s, re.I)
and not re.search(r"content\s*=\s*''", s, re.I)}
keep = {n for t, n, _, s in master if t == "table" and n not in shadow and n != "sqlite_sequence"
and (owner(n) or n in ("filesystem_files", "site_registry"))
and not (n in virtual and n.startswith("fts_") and owner(n[4:]) is None)}
keep -= {n for n in keep if n.startswith("fts_") and owner(n[4:]) is None}
# the full-text index of a kept table is rebuilt from its table; a full-text table with its own content is exported
fts_of_kept = {n for n in virtual if n.startswith("fts_") and owner(n[4:])}
data_tables = sorted((keep - fts_of_kept - (external & virtual)) | {"site_registry", "filesystem_files"})
os.makedirs(a.out, exist_ok=True)
order = {"table": 0, "index": 1, "trigger": 2}
schema = []
for t, n, tbl, s in sorted(master, key=lambda r: (order.get(r[0], 3), r[1])):
base = tbl if t in ("index", "trigger") else n
if base in keep or base in fts_of_kept:
if t == "table" or base not in shadow:
schema.append({"type": t, "name": n, "table": tbl, "sql": s, "virtual": n in virtual,
"rebuild": n in virtual and n in external})
# some full-text indexes were built before rows were added to their tables and never updated; record which rows
# such an index holds, so the restore can drop the others and search behaves exactly as when the tasks were made
for o in schema:
if o["type"] == "table" and o["rebuild"]:
m = re.search(r"content\s*=\s*\[?(\w+)\]?", o["sql"], re.I)
held = [r for (r,) in con.execute(f'SELECT id FROM "{o["name"]}_docsize" ORDER BY id')]
if len(held) != con.execute(f'SELECT COUNT(*) FROM "{m.group(1)}"').fetchone()[0]:
path = os.path.join(a.out, "fts_rowids", f'{o["name"]}.parquet')
os.makedirs(os.path.dirname(path), exist_ok=True)
pq.write_table(pa.table({"rowid": pa.array(held, pa.int64())}), path)
o["rowids"] = os.path.relpath(path, a.out)
json.dump(schema, open(os.path.join(a.out, "schema.json"), "w"), indent=1)
manifest = []
for t in data_tables:
site = owner(t) or "_shared"
d = os.path.join(a.out, "tables", site)
os.makedirs(d, exist_ok=True)
if t == "site_registry":
path = os.path.join(d, f"{t}.parquet")
rows = [r for r in con.execute("SELECT site, collection, table_name, pk_column FROM site_registry")
if r[2] in keep]
pq.write_table(pa.table({k: [r[i] for r in rows] for i, k in
enumerate(("site", "collection", "table_name", "pk_column"))}), path)
n = len(rows)
else:
n = export_table(con, t, os.path.join(d, f"{t}.parquet"))
manifest.append({"table": t, "site": site, "rows": n, "path": f"tables/{site}/{t}.parquet"})
print(f"{t}: {n}", flush=True)
json.dump(manifest, open(os.path.join(a.out, "tables.json"), "w"), indent=1)
# the sites' static images (generated pictures, avatars, thumbnails), served from /static/<sub>/...
files = [os.path.join(dp, f) for dp, _, fs in os.walk(a.static) for f in fs]
pq.write_table(pa.table({"path": [os.path.relpath(f, a.static) for f in files],
"bytes": [open(f, "rb").read() for f in files]}), os.path.join(a.out, "static.parquet"))
print(f"{len(data_tables)} tables, {sum(m['rows'] for m in manifest)} rows; {len(files)} static files; "
f"schema of {len(schema)} objects")
if os.path.isfile(os.path.join(a.media, "manifest.jsonl")):
export_media(a.media, a.out)
if __name__ == "__main__":
main()
|