File size: 18,448 Bytes
9a96811
 
 
8a9db0e
b6190b3
9a96811
4c86203
b6190b3
8a9db0e
 
 
9a96811
4c86203
 
87b80cc
9a96811
4c86203
 
8a9db0e
4c86203
 
8a9db0e
9a96811
 
 
 
 
 
 
 
 
 
 
 
4c86203
 
 
 
 
 
 
 
 
 
 
 
 
 
87b80cc
 
 
 
8a9db0e
9a96811
4c86203
 
9a96811
 
 
 
 
 
 
 
 
 
 
4c86203
 
 
 
 
 
 
 
 
 
 
9a96811
 
 
4c86203
 
 
 
 
8a9db0e
bf29377
 
 
 
4c86203
 
 
 
 
 
9a96811
4c86203
 
 
 
 
 
 
 
 
 
 
 
b6190b3
 
 
 
 
 
 
 
4c86203
 
 
 
 
 
 
 
 
 
 
 
 
 
9a96811
 
8a9db0e
 
4c86203
 
87b80cc
 
 
9a96811
 
4c86203
9a96811
 
 
 
4c86203
 
9a96811
b6190b3
4c86203
8a9db0e
bf29377
9a96811
8a9db0e
9a96811
bf29377
8a9db0e
 
bf29377
8a9db0e
bf29377
8a9db0e
 
bf29377
 
4c86203
9a96811
 
b6190b3
9a96811
4c86203
 
 
bf29377
4c86203
 
9a96811
4c86203
9a96811
4c86203
9a96811
4c86203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6190b3
4c86203
 
 
 
9a96811
4c86203
9a96811
4c86203
 
 
bf29377
 
4c86203
bf29377
4c86203
bf29377
 
 
4c86203
 
bf29377
 
 
 
 
4c86203
 
 
 
 
 
b6190b3
4c86203
 
bf29377
 
4c86203
bf29377
 
 
4c86203
 
bf29377
 
 
 
4c86203
 
 
 
 
 
b6190b3
4c86203
 
bf29377
 
4c86203
bf29377
 
4c86203
 
bf29377
 
4c86203
 
 
 
 
 
b6190b3
4c86203
 
bf29377
 
4c86203
bf29377
4c86203
 
bf29377
4c86203
 
 
 
 
 
b6190b3
4c86203
 
bf29377
8a9db0e
 
 
 
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
from functools import partial, lru_cache

import duckdb
import gradio as gr
import json
import pandas as pd
import pyarrow as pa
import pyarrow.compute as pc
import requests
from huggingface_hub import HfApi

READ_PARQUET_FUNCTIONS = ("dd.read_parquet", "pd.read_parquet")
EMPTY_TABLE = pa.Table.from_pylist([{str(i): "" for i in range(4)}] * 10)
EMPTY_DF: pd.DataFrame = EMPTY_TABLE.to_pandas()
NUM_ROWS = 20
MAX_NUM_COLUMNS = 20
NUM_TRENDING_DATASETS = 10
NUM_USER_DATASETS = 10
css = """
.transparent-dropdown, .transparent-dropdown .container .wrap, .transparent-accordion  {
    background: var(--body-background-fill);
}
input {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;          
}
.cell-menu-button {
    z-index: -1;
}
thead {
    display: none;
}
.secondary-wrap:has(input[aria-expanded="true"]) {
    background: var(--table-odd-background-fill);
}
.secondary-wrap:has(input[aria-expanded="true"])::after {
    content: '↡';
    margin-right: var(--size-10);
    border-width: 1px;
    border-color: var(--block-border-color);
    border-radius: .23rem;
    background-color: #141c2e;
    padding-left: 2px;
    font-size: .75rem;
    color: var(--block-title-text-color);
}
.gradio-container {
    padding: var(--size-4) 0 !important;
    max-width: 98% !important;
}
"""
js = """
function load() {
    // Set DataFrame readonly
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
    var observer = new MutationObserver(function(mutations, observer) {
        // fired when a mutation occurs
        document.querySelectorAll('.readonly-dataframe div .table-wrap button svelte-virtual-table-viewport table tbody tr td .cell-wrap input').forEach(i => i.setAttribute("readonly", "true"));
    });
    // define what element should be observed by the observer
    // and what types of mutations trigger the callback
    observer.observe(document, {
        subtree: true,
        childList: true
    });

    // Run query on Enter in transform dropdown
    document.querySelectorAll("input").forEach(i => {
        if (i.parentElement.parentElement.parentElement.parentElement.parentElement.classList.contains("transform_dropdown")) {
            i.onkeydown = (event) => {
                if (event.code == "Enter") {
                    document.getElementById("run_button").click();
                }
            }
        }
    })
}
"""
text_functions_df = pd.read_csv("text_functions.tsv", delimiter="\t")
date_functions_df = pd.read_csv("date_functions.tsv", delimiter="\t")
list_functions_df = pd.read_csv("list_functions.tsv", delimiter="\t")
numeric_functions_df = pd.read_csv("numeric_functions.tsv", delimiter="\t")
time_functions_df = pd.read_csv("time_functions.tsv", delimiter="\t")
timestamp_functions_df = pd.read_csv("timestamp_functions.tsv", delimiter="\t")

@lru_cache(maxsize=3)
def duckdb_sql(query: str) -> duckdb.DuckDBPyRelation:
    return duckdb.sql(query)

def prepare_function(func: str, placeholders: list[str], column_name: str) -> str:
    prepared_func = func.split("(", 1)
    for placeholder in placeholders:
        if placeholder in prepared_func[-1]:
            prepared_func[-1] = prepared_func[-1].replace(placeholder, column_name, 1)
            return "(".join(prepared_func)
    else:
        return None

def prettify_df(df: pd.DataFrame):
    return df.apply(lambda s: s.apply(str))

def get_prepared_functions_from_table(table: pa.Table) -> dict[str, list[str]]:
    prepared_functions = {}
    for field in table.schema:
        if pa.types.is_integer(field.type) or pa.types.is_floating(field.type):
            prepared_functions[field.name] = [prepare_function(numeric_func, ["x"], field.name) for numeric_func in numeric_functions_df.Name]
        elif pa.types.is_string(field.type):
            prepared_functions[field.name] = [prepare_function(text_func, ["string"], field.name) for text_func in text_functions_df.Name]
            # try parsing json
            if pc.all(pc.starts_with(table[field.name], "{")).as_py() or pc.all(pc.starts_with(table[field.name], "[")).as_py():
                try:
                    json_parsed_table = pa.Table.from_pylist([{field.name: json.loads(row)} for row in table[field.name].to_pylist()])
                    parsed_type = str(duckdb.from_arrow(json_parsed_table).dtypes[0])
                    prepared_functions[field.name] = [f"CAST({field.name} as {parsed_type})"] + prepared_functions[field.name]
                except Exception:
                    pass
        elif pa.types.is_date(field.type):
            prepared_functions[field.name] = [prepare_function(date_func, ["startdate", "date"], field.name) for date_func in date_functions_df.Name]
        elif pa.types.is_list(field.type):
            prepared_functions[field.name] = [prepare_function(list_func, ["list"], field.name) for list_func in list_functions_df.Name]
        elif pa.types.is_time(field.type):
            prepared_functions[field.name] = [prepare_function(time_func, ["starttime", "time"], field.name) for time_func in time_functions_df.Name]
        elif pa.types.is_timestamp(field.type):
            prepared_functions[field.name] = [prepare_function(timestamp_func, ["startdate", "timestamp"], field.name) for timestamp_func in timestamp_functions_df.Name]
        elif pa.types.is_struct(field.type):
            prepared_functions[field.name] = [f"{field.name}.{subfield.name}" for subfield in field.type.fields]
        else:
            prepared_functions[field.name] = []
        prepared_functions[field.name] = [prepared_function for prepared_function in prepared_functions[field.name] if prepared_function]
    return prepared_functions

with gr.Blocks(css=css, js=js) as demo:
    loading_codes_json = gr.JSON(visible=False)
    dataset_subset_split_textbox = gr.Textbox(visible=False)
    input_table_state = gr.State()
    run_button = gr.Button(visible=False, elem_id="run_button")
    with gr.Row():
        with gr.Column():
            gr.Markdown("# <p style='text-align:center;'>πŸ₯ DuckDB Spreadsheets πŸ“</p>\n\n<p style='text-align:center;'>Edit any dataset on Hugging Face (full list <a href='https://huggingface.co/datasets' target='_blank'>here</a>) using DuckDB functions (documentation <a href='https://duckdb.org/docs/sql/functions/overview' target='_blank'>here</a>)</p>")
    with gr.Group():
        with gr.Row():
            dataset_dropdown = gr.Dropdown(label="Dataset", allow_custom_value=True, scale=10)
            subset_dropdown = gr.Dropdown(info="Subset", allow_custom_value=True, show_label=False, visible=False, elem_classes="transparent-dropdown")
            split_dropdown = gr.Dropdown(info="Split", allow_custom_value=True, show_label=False, visible=False, elem_classes="transparent-dropdown")
            gr.LoginButton()
        with gr.Row():
            transform_dropdowns = [gr.Dropdown(choices=[column_name] + [prepare_function(text_func, "string", column_name) for text_func in text_functions_df.Name if "string" in text_func], value=column_name, container=False, interactive=True, allow_custom_value=True, visible=True, elem_classes="transform_dropdown") for column_name in EMPTY_DF.columns]
            transform_dropdowns += [gr.Dropdown(choices=[None], value=None, container=False, interactive=True, allow_custom_value=True, visible=False, elem_classes="transform_dropdown") for _ in range(MAX_NUM_COLUMNS - len(transform_dropdowns))]
        dataframe = gr.DataFrame(EMPTY_DF, column_widths=[f"{1/len(EMPTY_DF.columns):.0%}"] * len(EMPTY_DF.columns), interactive=True, elem_classes="readonly-dataframe")
        with gr.Accordion("Show DuckDB SQL command", open=False, elem_classes="transparent-accordion"):
            code_markdown = gr.Markdown()

    def show_subset_dropdown(dataset: str):
        if dataset and "/" not in dataset.strip().strip("/"):
            return []
        resp = requests.get(f"https://datasets-server.huggingface.co/compatible-libraries?dataset={dataset}", timeout=3).json()
        loading_codes = ([lib["loading_codes"] for lib in resp.get("libraries", []) if lib["function"] in READ_PARQUET_FUNCTIONS] or [[]])[0] or []
        subsets = [loading_code["config_name"] for loading_code in loading_codes]
        subset = (subsets or [""])[0]
        return dict(choices=subsets, value=subset, visible=len(subsets) > 1, key=hash(str(loading_codes))), loading_codes

    def show_split_dropdown(subset: str, loading_codes: list[dict]):
        splits = ([list(loading_code["arguments"]["splits"]) for loading_code in loading_codes if loading_code["config_name"] == subset] or [[]])[0]
        split = (splits or [""])[0]
        return dict(choices=splits, value=split, visible=len(splits) > 1, key=hash(str(loading_codes) + subset))

    def show_input_dataframe(dataset: str, subset: str, split: str, loading_codes: list[dict]):
        pattern = ([loading_code["arguments"]["splits"][split] for loading_code in loading_codes if loading_code["config_name"] == subset] or [None])[0]
        if dataset and subset and split and pattern:
            table = duckdb_sql(f"SELECT * FROM 'hf://datasets/{dataset}/{pattern}' LIMIT {NUM_ROWS}").arrow()
        else:
            table = EMPTY_TABLE
        prepared_functions = get_prepared_functions_from_table(table)
        new_transform_dropdowns = [dict(choices=[column_name] + prepared_functions[column_name], value=column_name, container=False, interactive=True, allow_custom_value=True, visible=True) for column_name in table.column_names]
        new_transform_dropdowns += [dict(choices=[None], value=None, container=False, interactive=True, allow_custom_value=True, visible=False) for _ in range(MAX_NUM_COLUMNS - len(new_transform_dropdowns))]
        df = table.to_pandas()
        return [table, dict(value=prettify_df(df), column_widths=[f"{1/len(df.columns):.0%}"] * len(df.columns))] + new_transform_dropdowns
    
    def set_dataframe(dataset: str, subset: str, split: str, loading_codes: list[dict], input_table: pa.Table, df: pd.DataFrame, *transforms, show_warning=True):
        try:
            table = duckdb.sql(f"SELECT {', '.join(transform for transform in transforms if transform)} FROM input_table;").arrow()
        except Exception as e:
            if show_warning:
                gr.Warning(f"{type(e).__name__}: {e}")
            return {
                dataframe: df
            }
        prepared_functions = get_prepared_functions_from_table(table)
        new_transform_dropdowns = [dict(choices=list({original_column_name: None, column_name: None}) + prepared_functions[column_name], value=column_name, container=False, interactive=True, allow_custom_value=True, visible=True) for original_column_name, column_name in zip(input_table.column_names, table.column_names)]
        new_transform_dropdowns += [dict(choices=[None], value=None, container=False, interactive=True, allow_custom_value=True, visible=False) for _ in range(MAX_NUM_COLUMNS - len(new_transform_dropdowns))]
        pattern = ([loading_code["arguments"]["splits"][split] for loading_code in loading_codes if loading_code["config_name"] == subset] or [None])[0]
        return {
            dataframe: prettify_df(table.to_pandas()),
            **dict(zip(transform_dropdowns, [gr.Dropdown(**new_transform_dropdown) for new_transform_dropdown in new_transform_dropdowns])),
            code_markdown: (
                "```sql\n"
                + f"SELECT {', '.join(new_transform_dropdown['value'] for new_transform_dropdown in new_transform_dropdowns if new_transform_dropdown['value'])} "
                + f"FROM 'hf://datasets/{dataset}/{pattern}' LIMIT {NUM_ROWS};"
                + "\n```"
            ) if pattern else "",
        }

    for column_index, transform_dropdown in enumerate(transform_dropdowns):
        transform_dropdown.select(partial(set_dataframe, show_warning=False), inputs=[dataset_dropdown, subset_dropdown, split_dropdown, loading_codes_json, input_table_state, dataframe] + transform_dropdowns, outputs=[dataframe, code_markdown] + transform_dropdowns)

    run_button.click(set_dataframe, inputs=[dataset_dropdown, subset_dropdown, split_dropdown, loading_codes_json, input_table_state, dataframe] + transform_dropdowns, outputs=[dataframe, code_markdown] + transform_dropdowns)

    @demo.load(outputs=[dataset_dropdown, loading_codes_json, subset_dropdown, split_dropdown, input_table_state, dataframe, code_markdown] + transform_dropdowns)
    def _fetch_datasets(request: gr.Request, oauth_token: gr.OAuthToken | None):
        api = HfApi(token=oauth_token.token if oauth_token else None)
        datasets = list(api.list_datasets(limit=NUM_TRENDING_DATASETS, sort="trendingScore", direction=-1, filter=["format:parquet"]))
        if oauth_token and (user := api.whoami().get("name")):
            datasets += list(api.list_datasets(limit=NUM_USER_DATASETS, sort="trendingScore", direction=-1, filter=["format:parquet"], author=user))
        dataset = request.query_params.get("dataset") or datasets[0].id
        subsets, loading_codes = show_subset_dropdown(dataset)
        splits = show_split_dropdown(subsets["value"], loading_codes)
        input_table, input_dataframe, *new_transform_dropdowns = show_input_dataframe(dataset, subsets["value"], splits["value"], loading_codes)
        pattern = ([loading_code["arguments"]["splits"][splits["value"]] for loading_code in loading_codes if loading_code["config_name"] == subsets["value"]] or [None])[0]
        return {
            dataset_dropdown: gr.Dropdown(choices=[dataset.id for dataset in datasets], value=dataset),
            loading_codes_json: loading_codes,
            subset_dropdown: gr.Dropdown(**subsets),
            split_dropdown: gr.Dropdown(**splits),
            input_table_state: input_table,
            dataframe: gr.DataFrame(**input_dataframe),
            **dict(zip(transform_dropdowns, [gr.Dropdown(**new_transform_dropdown) for new_transform_dropdown in new_transform_dropdowns])),
            code_markdown: (
                "```sql\n"
                + f"SELECT {', '.join(new_transform_dropdown['value'] for new_transform_dropdown in new_transform_dropdowns if new_transform_dropdown['value'])} "
                + f"FROM 'hf://datasets/{dataset}/{pattern}' LIMIT {NUM_ROWS};"
                + "\n```"
            ) if pattern else "",
        }
    
    @dataset_dropdown.select(inputs=dataset_dropdown, outputs=[loading_codes_json, subset_dropdown, split_dropdown, input_table_state, dataframe, code_markdown] + transform_dropdowns)
    def _show_subset_dropdown(dataset: str):
        subsets, loading_codes = show_subset_dropdown(dataset)
        splits = show_split_dropdown(subsets["value"], loading_codes)
        input_table, input_dataframe, *new_transform_dropdowns = show_input_dataframe(dataset, subsets["value"], splits["value"], loading_codes)
        pattern = ([loading_code["arguments"]["splits"][splits["value"]] for loading_code in loading_codes if loading_code["config_name"] == subsets["value"]] or [None])[0]
        return {
            loading_codes_json: loading_codes,
            subset_dropdown: gr.Dropdown(**subsets),
            split_dropdown: gr.Dropdown(**splits),
            input_table_state: input_table,
            dataframe: gr.DataFrame(**input_dataframe),
            **dict(zip(transform_dropdowns, [gr.Dropdown(**new_transform_dropdown) for new_transform_dropdown in new_transform_dropdowns])),
            code_markdown: (
                "```sql\n"
                + f"SELECT {', '.join(new_transform_dropdown['value'] for new_transform_dropdown in new_transform_dropdowns if new_transform_dropdown['value'])} "
                + f"FROM 'hf://datasets/{dataset}/{pattern}' LIMIT {NUM_ROWS};"
                + "\n```"
            ) if pattern else "",
        }
    
    @subset_dropdown.select(inputs=[dataset_dropdown, subset_dropdown, loading_codes_json], outputs=[split_dropdown, input_table_state, dataframe, code_markdown] + transform_dropdowns)
    def _show_split_dropdown(dataset: str, subset: str, loading_codes: list[dict]):
        splits = show_split_dropdown(subset, loading_codes)
        input_table, input_dataframe, *new_transform_dropdowns = show_input_dataframe(dataset, subset, splits["value"], loading_codes)
        pattern = ([loading_code["arguments"]["splits"][splits["value"]] for loading_code in loading_codes if loading_code["config_name"] == subset] or [None])[0]
        return {
            split_dropdown: gr.Dropdown(**splits),
            input_table_state: input_table,
            dataframe: gr.DataFrame(**input_dataframe),
            **dict(zip(transform_dropdowns, [gr.Dropdown(**new_transform_dropdown) for new_transform_dropdown in new_transform_dropdowns])),
            code_markdown: (
                "```sql\n"
                + f"SELECT {', '.join(new_transform_dropdown['value'] for new_transform_dropdown in new_transform_dropdowns if new_transform_dropdown['value'])} "
                + f"FROM 'hf://datasets/{dataset}/{pattern}' LIMIT {NUM_ROWS};"
                + "\n```"
            ) if pattern else "",
        }
    
    @split_dropdown.select(inputs=[dataset_dropdown, subset_dropdown, split_dropdown, loading_codes_json], outputs=[input_table_state, dataframe, code_markdown] + transform_dropdowns)
    def _show_input_dataframe(dataset: str, subset: str, split: str, loading_codes: list[dict]) -> pd.DataFrame:
        input_table, input_dataframe, *new_transform_dropdowns = show_input_dataframe(dataset, subset, split, loading_codes)
        pattern = ([loading_code["arguments"]["splits"][split] for loading_code in loading_codes if loading_code["config_name"] == subset] or [None])[0]
        return {
            input_table_state: input_table,
            dataframe: gr.DataFrame(**input_dataframe),
            **dict(zip(transform_dropdowns, [gr.Dropdown(**new_transform_dropdown) for new_transform_dropdown in new_transform_dropdowns])),
            code_markdown: (
                "```sql\n"
                + f"SELECT {', '.join(new_transform_dropdown['value'] for new_transform_dropdown in new_transform_dropdowns if new_transform_dropdown['value'])} "
                + f"FROM 'hf://datasets/{dataset}/{pattern}' LIMIT {NUM_ROWS};"
                + "\n```"
            ) if pattern else "",
        }


if __name__ == "__main__":
    demo.launch()