From 157fda51513f62a447e491ce949f3fccba9c1dfb Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 02:58:13 +0000 Subject: [PATCH 01/44] feat: add evaluation summary dashboard with filtering and directory metrics to EvalBench viewer --- .dockerignore | 5 + pyproject.toml | 4 + uv.lock | 2 + viewer/main.py | 300 ++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 295 insertions(+), 16 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c5d724a4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.venv +.git +__pycache__ +*.pyc +results diff --git a/pyproject.toml b/pyproject.toml index fe0e074f..eb5caf21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,7 @@ members = [ dev = [ "pycodestyle>=2.14.0", ] + +[[tool.uv.index]] +url = "https://pypi.org/simple" +default = true diff --git a/uv.lock b/uv.lock index c570140c..38d8e77a 100644 --- a/uv.lock +++ b/uv.lock @@ -690,6 +690,7 @@ dependencies = [ { name = "google-cloud-bigtable" }, { name = "google-cloud-firestore" }, { name = "google-cloud-geminidataanalytics" }, + { name = "google-cloud-iam" }, { name = "google-cloud-secret-manager" }, { name = "google-cloud-spanner" }, { name = "google-genai" }, @@ -735,6 +736,7 @@ requires-dist = [ { name = "google-cloud-bigtable" }, { name = "google-cloud-firestore" }, { name = "google-cloud-geminidataanalytics", specifier = ">=0.11.0" }, + { name = "google-cloud-iam" }, { name = "google-cloud-secret-manager" }, { name = "google-cloud-spanner" }, { name = "google-genai" }, diff --git a/viewer/main.py b/viewer/main.py index dccdb479..917c9523 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -3,6 +3,7 @@ import pandas as pd import yaml import logging +import json logging.basicConfig(level=logging.INFO) @@ -61,6 +62,9 @@ class State: selected_directory: str selected_tab: str = "Dashboard" conversation_index: int = 0 + eval_summaries: str = "" + eval_id_filter: str = "" + product_filter: str = "" def get_results_dir(): @@ -78,6 +82,66 @@ def get_results_dir(): return results_dir_candidates[1] # Fallback to default +def get_eval_details(results_dir, dir_name): + details = {"product": "N/A", "exact_match": "N/A", "llmrater": "N/A", "trajectory_matcher": "N/A", "turn_count": "N/A", "executable": "N/A", "token_consumption": "N/A", "end_to_end_latency": "N/A"} + + # Get product + config_path = os.path.join(results_dir, dir_name, "configs.csv") + if os.path.exists(config_path): + try: + df = pd.read_csv(config_path) + # Check for both typo and correct spelling + row = df[df["config"].isin(["experiment_config.poduct_name", "experiment_config.product_name"])] + if not row.empty: + details["product"] = str(row["value"].iloc[0]) + except Exception: + pass + + # Get summary metrics + summary_path = os.path.join(results_dir, dir_name, "summary.csv") + if os.path.exists(summary_path): + try: + df = pd.read_csv(summary_path) + for _, row in df.iterrows(): + name = row.get("metric_name") + correct = row.get("correct_results_count", 0) + total = row.get("total_results_count", 0) + pct = (correct / total) * 100 if total > 0 else 0 + if name == "exact_match": + details["exact_match"] = f"{pct:.0f}%" + elif name == "llmrater": + details["llmrater"] = f"{pct:.0f}%" + elif name == "trajectory_matcher": + details["trajectory_matcher"] = f"{pct:.0f}%" + elif name == "turn_count": + details["turn_count"] = f"{correct:.1f}" + elif name == "executable": + details["executable"] = f"{pct:.0f}%" + elif name == "token_consumption": + details["token_consumption"] = f"{correct:.0f}" + elif name == "end_to_end_latency": + details["end_to_end_latency"] = f"{correct:.0f}" + except Exception: + pass + + return details + + +def get_color_for_pct(val_str): + if not val_str or not val_str.endswith("%"): + return "#334155" # Default color + try: + val = float(val_str.rstrip("%")) + if val >= 80: + return "#16a34a" # Green + elif val >= 40: + return "#ca8a04" # Yellow + else: + return "#dc2626" # Red + except Exception: + return "#334155" + + def on_load(e: me.LoadEvent): state = me.state(State) results_dir = get_results_dir() @@ -97,7 +161,7 @@ def on_load(e: me.LoadEvent): @me.page( path="/", - title="Evalbench", + title="EvalBench Viewer", on_load=on_load, security_policy=me.SecurityPolicy( dangerously_disable_trusted_types=True, @@ -121,8 +185,10 @@ def app(): if os.path.isdir(os.path.join(results_dir, d)) ] - def on_selection_change(e: me.SelectSelectionChangeEvent): - state.selected_directory = e.value + + + def on_title_click(e: me.ClickEvent): + state.selected_directory = "" state.conversation_index = 0 # Full-width header bar @@ -133,13 +199,19 @@ def on_selection_change(e: me.SelectSelectionChangeEvent): margin=me.Margin(bottom="24px"), ) ): - me.text( + me.button( "EvalBench Viewer", + on_click=on_title_click, style=me.Style( color="#f8fafc", font_size="22px", font_weight="700", letter_spacing="0.5px", + background="transparent", + padding=me.Padding.all("0px"), + margin=me.Margin.all("0px"), + border=me.Border.all(me.BorderSide(width="0px")), + text_align="left", ), ) @@ -153,18 +225,7 @@ def on_selection_change(e: me.SelectSelectionChangeEvent): gap="16px", ) ): - with me.box( - style=me.Style(width="100%", max_width="400px", margin=me.Margin(bottom="8px")) - ): - me.select( - label="Select a result directory", - options=[ - me.SelectOption(label=d, value=d) for d in sorted(directories) - ], - on_selection_change=on_selection_change, - value=state.selected_directory, - appearance="outline", - ) + if state.selected_directory: @@ -227,6 +288,8 @@ def on_next_conversation(e: me.ClickEvent): if os.path.exists(evals_path): try: df = pd.read_csv(evals_path) + details = get_eval_details(results_dir, state.selected_directory) + df.insert(0, "orchestrator", details["orchestrator"]) me.table(data_frame=df) except Exception as e: me.text(f"Error reading evals.csv: {e}") @@ -256,6 +319,211 @@ def on_next_conversation(e: me.ClickEvent): me.text(f"Error reading summary.csv: {e}") else: me.text(f"summary.csv not found in {state.selected_directory}") + else: + with me.box( + style=me.Style( + background="#ffffff", + padding=me.Padding.all("24px"), + border_radius="12px", + border=me.Border.all( + me.BorderSide(width="1px", color="#e5e7eb", style="solid") + ), + box_shadow="0 1px 3px rgba(0,0,0,0.06)", + text_align="center", + margin=me.Margin(top="16px"), + ) + ): + me.text( + "Welcome to EvalBench Viewer", + style=me.Style( + font_size="24px", + font_weight="700", + color="#1f2937", + margin=me.Margin(bottom="8px"), + ), + ) + me.text( + f"Found {len(directories)} evaluation runs. Click on an Eval ID in the table below to explore the results.", + style=me.Style( + font_size="16px", + color="#6b7280", + margin=me.Margin(bottom="16px"), + ), + ) + if directories: + # Compute summaries if empty + s = me.state(State) + summaries = [] + if s.eval_summaries: + try: + summaries = json.loads(s.eval_summaries) + except Exception: + summaries = [] + + if not summaries: + for d in sorted(directories): + details = get_eval_details(results_dir, d) + summaries.append({ + "id": d, + "product": details["product"], + "exact_match": details["exact_match"], + "llmrater": details["llmrater"], + "trajectory_matcher": details["trajectory_matcher"], + "turn_count": details["turn_count"], + "executable": details["executable"], + "token_consumption": details["token_consumption"], + "end_to_end_latency": details["end_to_end_latency"] + }) + s.eval_summaries = json.dumps(summaries) + + # Extract unique values for filters from ALL summaries + all_summaries = [] + if s.eval_summaries: + try: + all_summaries = json.loads(s.eval_summaries) + except Exception: + all_summaries = [] + + products = sorted(list(set(x["product"] for x in all_summaries if x["product"] != "N/A"))) + eval_ids = sorted([x["id"] for x in all_summaries]) + + # Apply filters + if state.eval_id_filter: + summaries = [x for x in summaries if x["id"] == state.eval_id_filter] + if state.product_filter: + summaries = [x for x in summaries if x["product"] == state.product_filter] + + # Render filters UI + with me.box( + style=me.Style( + display="flex", + flex_direction="row", + gap="16px", + margin=me.Margin(top="16px", bottom="16px"), + ) + ): + def on_eval_id_filter_change(e: me.SelectSelectionChangeEvent): + st = me.state(State) + st.eval_id_filter = e.value + + def on_product_filter_change(e: me.SelectSelectionChangeEvent): + st = me.state(State) + st.product_filter = e.value + + me.select( + label="Filter by Eval ID", + options=[me.SelectOption(label="All", value="")] + [me.SelectOption(label=d, value=d) for d in eval_ids], + on_selection_change=on_eval_id_filter_change, + value=state.eval_id_filter, + ) + me.select( + label="Filter by Product", + options=[me.SelectOption(label="All", value="")] + [me.SelectOption(label=p, value=p) for p in products], + on_selection_change=on_product_filter_change, + value=state.product_filter, + ) + + # Render custom table + with me.box( + style=me.Style( + max_height="600px", + overflow_y="auto", + margin=me.Margin(top="16px"), + display="table", + width="100%", + border=me.Border.all( + me.BorderSide(width="1px", color="#e5e7eb", style="solid") + ), + border_radius="8px", + ) + ): + # Header row + with me.box( + style=me.Style( + display="table-row", + background="#f8fafc", + font_weight="bold", + color="#475569", + font_size="12px", + text_transform="uppercase", + letter_spacing="0.05em", + ) + ): + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="36ch", white_space="nowrap")): + me.text("Eval ID") + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text("Product") + + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="18ch", white_space="nowrap")): + me.text("Trajectory Matcher") + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text("Turn Count") + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text("Executable") + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text("Token Consumption") + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text("End-to-End Latency (ms)") + + # Data rows + for idx, item in enumerate(summaries): + d = item["id"] + prod = item["product"] + traj = item.get("trajectory_matcher", "N/A") + turns = item.get("turn_count", "N/A") + exec_val = item.get("executable", "N/A") + tokens = item.get("token_consumption", "N/A") + latency = item.get("end_to_end_latency", "N/A") + + bg_color = "#ffffff" if idx % 2 == 0 else "#f8fafc" + + def make_on_click(dir_name): + def on_click(e: me.ClickEvent): + s = me.state(State) + s.selected_directory = dir_name + return on_click + + with me.box( + style=me.Style( + display="table-row", + background=bg_color, + ) + ): + # Eval ID as a link/button + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="36ch", white_space="nowrap")): + me.button( + d, + on_click=make_on_click(d), + style=me.Style( + text_align="center", + background="transparent", + color="#0284c7", + font_family="monospace", + font_size="14px", + padding=me.Padding.all("0px"), + margin=me.Margin.all("0px"), + border=me.Border.all(me.BorderSide(width="0px")), + font_weight="500", + width="100%", + ), + ) + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text(prod, style=me.Style(color="#334155")) + + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="18ch", white_space="nowrap")): + me.text(traj, style=me.Style(color=get_color_for_pct(traj))) + + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text(turns, style=me.Style(color="#334155")) + + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text(exec_val, style=me.Style(color=get_color_for_pct(exec_val))) + + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text(tokens, style=me.Style(color="#334155")) + + with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + me.text(latency, style=me.Style(color="#334155")) if __name__ == "__main__": From f82134e71aed390547a511d1fc92c21d68a3ed81 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 03:02:45 +0000 Subject: [PATCH 02/44] refactor: apply black formatting to viewer/main.py for improved code readability --- viewer/main.py | 410 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 358 insertions(+), 52 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 917c9523..071944c6 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -83,20 +83,36 @@ def get_results_dir(): def get_eval_details(results_dir, dir_name): - details = {"product": "N/A", "exact_match": "N/A", "llmrater": "N/A", "trajectory_matcher": "N/A", "turn_count": "N/A", "executable": "N/A", "token_consumption": "N/A", "end_to_end_latency": "N/A"} - + details = { + "product": "N/A", + "exact_match": "N/A", + "llmrater": "N/A", + "trajectory_matcher": "N/A", + "turn_count": "N/A", + "executable": "N/A", + "token_consumption": "N/A", + "end_to_end_latency": "N/A", + } + # Get product config_path = os.path.join(results_dir, dir_name, "configs.csv") if os.path.exists(config_path): try: df = pd.read_csv(config_path) # Check for both typo and correct spelling - row = df[df["config"].isin(["experiment_config.poduct_name", "experiment_config.product_name"])] + row = df[ + df["config"].isin( + [ + "experiment_config.poduct_name", + "experiment_config.product_name", + ] + ) + ] if not row.empty: details["product"] = str(row["value"].iloc[0]) except Exception: pass - + # Get summary metrics summary_path = os.path.join(results_dir, dir_name, "summary.csv") if os.path.exists(summary_path): @@ -123,7 +139,7 @@ def get_eval_details(results_dir, dir_name): details["end_to_end_latency"] = f"{correct:.0f}" except Exception: pass - + return details @@ -185,8 +201,6 @@ def app(): if os.path.isdir(os.path.join(results_dir, d)) ] - - def on_title_click(e: me.ClickEvent): state.selected_directory = "" state.conversation_index = 0 @@ -226,7 +240,6 @@ def on_title_click(e: me.ClickEvent): ) ): - if state.selected_directory: def on_tab_change(e: me.ButtonToggleChangeEvent): @@ -235,8 +248,12 @@ def on_tab_change(e: me.ButtonToggleChangeEvent): me.button_toggle( value=state.selected_tab, buttons=[ - me.ButtonToggleButton(label="Dashboard", value="Dashboard"), - me.ButtonToggleButton(label="Configs", value="Configs"), + me.ButtonToggleButton( + label="Dashboard", value="Dashboard" + ), + me.ButtonToggleButton( + label="Configs", value="Configs" + ), # me.ButtonToggleButton(label="Evals", value="Evals"), # me.ButtonToggleButton(label="Scores", value="Scores"), me.ButtonToggleButton( @@ -280,7 +297,9 @@ def on_next_conversation(e: me.ClickEvent): except Exception as e: me.text(f"Error reading configs.csv: {e}") else: - me.text(f"configs.csv not found in {state.selected_directory}") + me.text( + f"configs.csv not found in {state.selected_directory}" + ) elif state.selected_tab == "Evals": evals_path = os.path.join( results_dir, state.selected_directory, "evals.csv" @@ -288,13 +307,17 @@ def on_next_conversation(e: me.ClickEvent): if os.path.exists(evals_path): try: df = pd.read_csv(evals_path) - details = get_eval_details(results_dir, state.selected_directory) + details = get_eval_details( + results_dir, state.selected_directory + ) df.insert(0, "orchestrator", details["orchestrator"]) me.table(data_frame=df) except Exception as e: me.text(f"Error reading evals.csv: {e}") else: - me.text(f"evals.csv not found in {state.selected_directory}") + me.text( + f"evals.csv not found in {state.selected_directory}" + ) elif state.selected_tab == "Scores": scores_path = os.path.join( results_dir, state.selected_directory, "scores.csv" @@ -306,7 +329,9 @@ def on_next_conversation(e: me.ClickEvent): except Exception as e: me.text(f"Error reading scores.csv: {e}") else: - me.text(f"scores.csv not found in {state.selected_directory}") + me.text( + f"scores.csv not found in {state.selected_directory}" + ) elif state.selected_tab == "Summary": summary_path = os.path.join( results_dir, state.selected_directory, "summary.csv" @@ -318,7 +343,9 @@ def on_next_conversation(e: me.ClickEvent): except Exception as e: me.text(f"Error reading summary.csv: {e}") else: - me.text(f"summary.csv not found in {state.selected_directory}") + me.text( + f"summary.csv not found in {state.selected_directory}" + ) else: with me.box( style=me.Style( @@ -326,7 +353,9 @@ def on_next_conversation(e: me.ClickEvent): padding=me.Padding.all("24px"), border_radius="12px", border=me.Border.all( - me.BorderSide(width="1px", color="#e5e7eb", style="solid") + me.BorderSide( + width="1px", color="#e5e7eb", style="solid" + ) ), box_shadow="0 1px 3px rgba(0,0,0,0.06)", text_align="center", @@ -343,7 +372,9 @@ def on_next_conversation(e: me.ClickEvent): ), ) me.text( - f"Found {len(directories)} evaluation runs. Click on an Eval ID in the table below to explore the results.", + f"Found {len(directories)} evaluation runs. " + "Click on an Eval ID in the table below to explore " + "the results.", style=me.Style( font_size="16px", color="#6b7280", @@ -359,20 +390,26 @@ def on_next_conversation(e: me.ClickEvent): summaries = json.loads(s.eval_summaries) except Exception: summaries = [] - + if not summaries: for d in sorted(directories): details = get_eval_details(results_dir, d) summaries.append({ - "id": d, + "id": d, "product": details["product"], "exact_match": details["exact_match"], "llmrater": details["llmrater"], - "trajectory_matcher": details["trajectory_matcher"], + "trajectory_matcher": details[ + "trajectory_matcher" + ], "turn_count": details["turn_count"], "executable": details["executable"], - "token_consumption": details["token_consumption"], - "end_to_end_latency": details["end_to_end_latency"] + "token_consumption": details[ + "token_consumption" + ], + "end_to_end_latency": details[ + "end_to_end_latency" + ] }) s.eval_summaries = json.dumps(summaries) @@ -383,15 +420,31 @@ def on_next_conversation(e: me.ClickEvent): all_summaries = json.loads(s.eval_summaries) except Exception: all_summaries = [] - - products = sorted(list(set(x["product"] for x in all_summaries if x["product"] != "N/A"))) + + products = sorted( + list( + set( + x["product"] + for x in all_summaries + if x["product"] != "N/A" + ) + ) + ) eval_ids = sorted([x["id"] for x in all_summaries]) # Apply filters if state.eval_id_filter: - summaries = [x for x in summaries if x["id"] == state.eval_id_filter] + summaries = [ + x + for x in summaries + if x["id"] == state.eval_id_filter + ] if state.product_filter: - summaries = [x for x in summaries if x["product"] == state.product_filter] + summaries = [ + x + for x in summaries + if x["product"] == state.product_filter + ] # Render filters UI with me.box( @@ -402,23 +455,35 @@ def on_next_conversation(e: me.ClickEvent): margin=me.Margin(top="16px", bottom="16px"), ) ): - def on_eval_id_filter_change(e: me.SelectSelectionChangeEvent): + def on_eval_id_filter_change( + e: me.SelectSelectionChangeEvent + ): st = me.state(State) st.eval_id_filter = e.value - def on_product_filter_change(e: me.SelectSelectionChangeEvent): + def on_product_filter_change( + e: me.SelectSelectionChangeEvent + ): st = me.state(State) st.product_filter = e.value me.select( label="Filter by Eval ID", - options=[me.SelectOption(label="All", value="")] + [me.SelectOption(label=d, value=d) for d in eval_ids], + options=[me.SelectOption(label="All", value="")] + + [ + me.SelectOption(label=d, value=d) + for d in eval_ids + ], on_selection_change=on_eval_id_filter_change, value=state.eval_id_filter, ) me.select( label="Filter by Product", - options=[me.SelectOption(label="All", value="")] + [me.SelectOption(label=p, value=p) for p in products], + options=[me.SelectOption(label="All", value="")] + + [ + me.SelectOption(label=p, value=p) + for p in products + ], on_selection_change=on_product_filter_change, value=state.product_filter, ) @@ -432,7 +497,9 @@ def on_product_filter_change(e: me.SelectSelectionChangeEvent): display="table", width="100%", border=me.Border.all( - me.BorderSide(width="1px", color="#e5e7eb", style="solid") + me.BorderSide( + width="1px", color="#e5e7eb", style="solid" + ) ), border_radius="8px", ) @@ -449,20 +516,129 @@ def on_product_filter_change(e: me.SelectSelectionChangeEvent): letter_spacing="0.05em", ) ): - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="36ch", white_space="nowrap")): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="36ch", + white_space="nowrap", + ) + ): me.text("Eval ID") - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): me.text("Product") - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="18ch", white_space="nowrap")): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="18ch", + white_space="nowrap", + ) + ): me.text("Trajectory Matcher") - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): me.text("Turn Count") - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): me.text("Executable") - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): me.text("Token Consumption") - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): me.text("End-to-End Latency (ms)") # Data rows @@ -490,7 +666,24 @@ def on_click(e: me.ClickEvent): ) ): # Eval ID as a link/button - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="36ch", white_space="nowrap")): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="36ch", + white_space="nowrap", + ) + ): me.button( d, on_click=make_on_click(d), @@ -502,28 +695,141 @@ def on_click(e: me.ClickEvent): font_size="14px", padding=me.Padding.all("0px"), margin=me.Margin.all("0px"), - border=me.Border.all(me.BorderSide(width="0px")), + border=me.Border.all( + me.BorderSide(width="0px") + ), font_weight="500", width="100%", ), ) - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): - me.text(prod, style=me.Style(color="#334155")) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + prod, style=me.Style(color="#334155") + ) - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")), width="18ch", white_space="nowrap")): - me.text(traj, style=me.Style(color=get_color_for_pct(traj))) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="18ch", + white_space="nowrap", + ) + ): + me.text( + traj, + style=me.Style( + color=get_color_for_pct(traj) + ), + ) - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): - me.text(turns, style=me.Style(color="#334155")) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + turns, style=me.Style(color="#334155") + ) - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): - me.text(exec_val, style=me.Style(color=get_color_for_pct(exec_val))) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + exec_val, + style=me.Style( + color=get_color_for_pct(exec_val) + ), + ) - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): - me.text(tokens, style=me.Style(color="#334155")) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + tokens, style=me.Style(color="#334155") + ) - with me.box(style=me.Style(display="table-cell", padding=me.Padding.symmetric(vertical="10px", horizontal="16px"), text_align="center", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0", style="solid")))): - me.text(latency, style=me.Style(color="#334155")) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + latency, + style=me.Style(color="#334155"), + ) if __name__ == "__main__": From bd3a3b0c8b3069efd4ed667c1dc56af41e1cd6cf Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 16:59:08 +0000 Subject: [PATCH 03/44] feat: add requester field, sorting, and UI styling to evaluation viewer dashboard --- viewer/main.py | 782 ++++++++++++++++++++++++++++++++------- viewer/static/custom.css | 24 ++ 2 files changed, 668 insertions(+), 138 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 071944c6..22ed415b 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -65,6 +65,10 @@ class State: eval_summaries: str = "" eval_id_filter: str = "" product_filter: str = "" + requester_filter: str = "" + sort_column: str = "date" + sort_descending: bool = True + open_dropdown: str = "" def get_results_dir(): @@ -85,6 +89,8 @@ def get_results_dir(): def get_eval_details(results_dir, dir_name): details = { "product": "N/A", + "date": "N/A", + "requester": "N/A", "exact_match": "N/A", "llmrater": "N/A", "trajectory_matcher": "N/A", @@ -110,6 +116,18 @@ def get_eval_details(results_dir, dir_name): ] if not row.empty: details["product"] = str(row["value"].iloc[0]) + + # Check for requester + req_row = df[ + df["config"].isin( + [ + "experiment_config.experiment_config.guitar_requester", + "experiment_config.guitar_requester", + ] + ) + ] + if not req_row.empty: + details["requester"] = str(req_row["value"].iloc[0]) except Exception: pass @@ -118,6 +136,8 @@ def get_eval_details(results_dir, dir_name): if os.path.exists(summary_path): try: df = pd.read_csv(summary_path) + if "run_time" in df.columns and not df.empty: + details["date"] = str(df["run_time"].iloc[0]) for _, row in df.iterrows(): name = row.get("metric_name") correct = row.get("correct_results_count", 0) @@ -189,6 +209,18 @@ def on_load(e: me.LoadEvent): ], ) def app(): + state = me.state(State) + with me.box( + style=me.Style( + background="#f8fafc", + min_height="100vh", + width="100%", + ) + ): + render_app_content() + + +def render_app_content(): state = me.state(State) results_dir = get_results_dir() @@ -211,6 +243,9 @@ def on_title_click(e: me.ClickEvent): background="#1e293b", padding=me.Padding.symmetric(vertical="16px", horizontal="5%"), margin=me.Margin(bottom="24px"), + display="flex", + justify_content="space-between", + align_items="center", ) ): me.button( @@ -229,6 +264,8 @@ def on_title_click(e: me.ClickEvent): ), ) + + # Centered content at 90% browser width with me.box( style=me.Style( @@ -237,6 +274,11 @@ def on_title_click(e: me.ClickEvent): display="flex", flex_direction="column", gap="16px", + background="#ffffff", + padding=me.Padding.all("24px"), + border_radius="8px", + box_shadow="0 4px 6px -1px rgb(0 0 0 / 0.1)", + color="#1e293b", ) ): @@ -354,7 +396,9 @@ def on_next_conversation(e: me.ClickEvent): border_radius="12px", border=me.Border.all( me.BorderSide( - width="1px", color="#e5e7eb", style="solid" + width="1px", + color="#e5e7eb", + style="solid", ) ), box_shadow="0 1px 3px rgba(0,0,0,0.06)", @@ -396,7 +440,9 @@ def on_next_conversation(e: me.ClickEvent): details = get_eval_details(results_dir, d) summaries.append({ "id": d, + "date": details.get("date", "N/A"), "product": details["product"], + "requester": details.get("requester", "N/A"), "exact_match": details["exact_match"], "llmrater": details["llmrater"], "trajectory_matcher": details[ @@ -413,6 +459,48 @@ def on_next_conversation(e: me.ClickEvent): }) s.eval_summaries = json.dumps(summaries) + # Sort by selected column + reverse = state.sort_descending + col = state.sort_column + + def get_sort_key(x): + val = x.get(col, "N/A") + + # Handle numbers and percentages + if col in [ + "exact_match", + "llmrater", + "trajectory_matcher", + "executable", + ]: + if val == "N/A": + return -1.0 if reverse else 101.0 + if val.endswith("%"): + try: + return float(val.rstrip("%")) + except ValueError: + return -1.0 if reverse else 101.0 + return -1.0 if reverse else 101.0 + + elif col in [ + "turn_count", + "token_consumption", + "end_to_end_latency", + ]: + if val == "N/A": + return -1.0 if reverse else 1e12 + try: + return float(val) + except ValueError: + return -1.0 if reverse else 1e12 + + # String columns (product, requester, id, date) + if val == "N/A": + return "" if reverse else "\xff\xff\xff\xff" + return str(val) + + summaries.sort(key=get_sort_key, reverse=reverse) + # Extract unique values for filters from ALL summaries all_summaries = [] if s.eval_summaries: @@ -430,6 +518,15 @@ def on_next_conversation(e: me.ClickEvent): ) ) ) + requesters = sorted( + list( + set( + x.get("requester", "N/A") + for x in all_summaries + if x.get("requester", "N/A") != "N/A" + ) + ) + ) eval_ids = sorted([x["id"] for x in all_summaries]) # Apply filters @@ -445,16 +542,39 @@ def on_next_conversation(e: me.ClickEvent): for x in summaries if x["product"] == state.product_filter ] + if state.requester_filter: + summaries = [ + x + for x in summaries + if x.get("requester", "N/A") + == state.requester_filter + ] # Render filters UI with me.box( style=me.Style( display="flex", flex_direction="row", - gap="16px", - margin=me.Margin(top="16px", bottom="16px"), + gap="24px", + margin=me.Margin(top="16px", bottom="24px"), + padding=me.Padding.all("16px"), + background="#ffffff", + border_radius="12px", + box_shadow=( + "0 1px 3px 0 rgb(0 0 0 / 0.1), " + "0 1px 2px -1px rgb(0 0 0 / 0.1)" + ), + align_items="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), ) ): + def on_eval_id_filter_change( e: me.SelectSelectionChangeEvent ): @@ -467,191 +587,518 @@ def on_product_filter_change( st = me.state(State) st.product_filter = e.value - me.select( - label="Filter by Eval ID", - options=[me.SelectOption(label="All", value="")] - + [ - me.SelectOption(label=d, value=d) - for d in eval_ids - ], - on_selection_change=on_eval_id_filter_change, - value=state.eval_id_filter, - ) - me.select( - label="Filter by Product", - options=[me.SelectOption(label="All", value="")] - + [ - me.SelectOption(label=p, value=p) - for p in products - ], - on_selection_change=on_product_filter_change, - value=state.product_filter, - ) + def on_requester_filter_change( + e: me.SelectSelectionChangeEvent + ): + st = me.state(State) + st.requester_filter = e.value + + def on_product_input(e: me.InputEvent): + st = me.state(State) + st.product_filter = e.value + + def on_requester_input(e: me.InputEvent): + st = me.state(State) + st.requester_filter = e.value + + def on_product_suggestion( + e: me.SelectSelectionChangeEvent + ): + st = me.state(State) + st.product_filter = e.value + + def on_requester_suggestion( + e: me.SelectSelectionChangeEvent + ): + st = me.state(State) + st.requester_filter = e.value + + def make_prod_handler(val, idx): + def handler(e: me.ClickEvent): + st = me.state(State) + st.product_filter = val + + handler.__name__ = f"click_prod_sugg_{idx}" + return handler + + def make_req_handler(val, idx): + def handler(e: me.ClickEvent): + st = me.state(State) + st.requester_filter = val + + handler.__name__ = f"click_req_sugg_{idx}" + return handler + + def toggle_eval_id_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "eval_id": + st.open_dropdown = "" + else: + st.open_dropdown = "eval_id" + + def make_eval_id_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.eval_id_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_eval_id_{val}" + return handler - # Render custom table - with me.box( - style=me.Style( - max_height="600px", - overflow_y="auto", - margin=me.Margin(top="16px"), - display="table", - width="100%", - border=me.Border.all( - me.BorderSide( - width="1px", color="#e5e7eb", style="solid" - ) - ), - border_radius="8px", - ) - ): - # Header row with me.box( style=me.Style( - display="table-row", - background="#f8fafc", - font_weight="bold", - color="#475569", - font_size="12px", - text_transform="uppercase", - letter_spacing="0.05em", + position="relative", + width="200px", ) ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", + background="#ffffff", border=me.Border.all( me.BorderSide( width="1px", color="#e2e8f0", - style="solid", ) ), - width="36ch", - white_space="nowrap", - ) + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_eval_id_dropdown, ): - me.text("Eval ID") - with me.box( - style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) + me.text( + state.eval_id_filter + if state.eval_id_filter + else "Select Eval ID", + style=me.Style( + color="#1f2937" ), ) - ): - me.text("Product") + # The Popup List + if state.open_dropdown == "eval_id": + with me.box( + style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + width="100%", + max_height="200px", + overflow_y="auto", + ) + ): + # All option + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=make_eval_id_handler(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" + ), + ) + + for d in eval_ids: + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=make_eval_id_handler(d), + ): + me.text( + d, + style=me.Style( + color="#1f2937" + ), + ) + + # Product Filter with Floating Autocomplete + def toggle_product_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "product": + st.open_dropdown = "" + else: + st.open_dropdown = "product" + + def make_prod_dropdown_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.product_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_prod_dd_{val}" + return handler + + mk_prod_dd = make_prod_dropdown_handler + + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", + background="#ffffff", border=me.Border.all( me.BorderSide( width="1px", color="#e2e8f0", - style="solid", ) ), - width="18ch", - white_space="nowrap", - ) + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_product_dropdown, ): - me.text("Trajectory Matcher") - with me.box( - style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) + me.text( + state.product_filter + if state.product_filter + else "Filter by Product", + style=me.Style( + color="#1f2937" ), ) - ): - me.text("Turn Count") + + # The Popup List + if state.open_dropdown == "product": + with me.box( + style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + width="100%", + max_height="200px", + overflow_y="auto", + ) + ): + # All option + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_prod_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" + ), + ) + + for p in products: + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_prod_dd(p), + ): + me.text( + p, + style=me.Style( + color="#1f2937" + ), + ) + + # Requester Filter with Floating Autocomplete + def toggle_requester_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "requester": + st.open_dropdown = "" + else: + st.open_dropdown = "requester" + + def make_req_dropdown_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.requester_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_req_dd_{val}" + return handler + + mk_req_dd = make_req_dropdown_handler + + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", + background="#ffffff", border=me.Border.all( me.BorderSide( width="1px", color="#e2e8f0", - style="solid", ) ), - ) + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_requester_dropdown, ): - me.text("Executable") - with me.box( - style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) + me.text( + state.requester_filter + if state.requester_filter + else "Filter by Requester", + style=me.Style( + color="#1f2937" ), ) - ): - me.text("Token Consumption") + + # The Popup List + if state.open_dropdown == "requester": + with me.box( + style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + width="100%", + max_height="200px", + overflow_y="auto", + ) + ): + # All option + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_req_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" + ), + ) + + for r in requesters: + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_req_dd(r), + ): + me.text( + r, + style=me.Style( + color="#1f2937" + ), + ) + + def on_sort_click(col_name): + s = me.state(State) + if s.sort_column == col_name: + s.sort_descending = not s.sort_descending + else: + s.sort_column = col_name + s.sort_descending = True + + def click_id(e): + on_sort_click("id") + + def click_date(e): + on_sort_click("date") + + def click_product(e): + on_sort_click("product") + + def click_requester(e): + on_sort_click("requester") + + def click_traj(e): + on_sort_click("trajectory_matcher") + + def click_turns(e): + on_sort_click("turn_count") + + def click_exec(e): + on_sort_click("executable") + + def click_tokens(e): + on_sort_click("token_consumption") + + def click_latency(e): + on_sort_click("end_to_end_latency") + + sort_handlers = { + "id": click_id, + "date": click_date, + "product": click_product, + "requester": click_requester, + "trajectory_matcher": click_traj, + "turn_count": click_turns, + "executable": click_exec, + "token_consumption": click_tokens, + "end_to_end_latency": click_latency, + } + + def render_header_cell(h_label, h_col, h_width): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + cursor="pointer", + width=h_width, + white_space="nowrap" if h_width else None, + background="#f8fafc", + ), + on_click=sort_handlers[h_col], + ): with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), + display="flex", + align_items="center", + justify_content="center", + color="#475569", ) ): - me.text("End-to-End Latency (ms)") + me.text(h_label) + s = me.state(State) + arrow = ( + " ↓" if s.sort_descending else " ↑" + ) + arrow_str = ( + arrow + if s.sort_column == h_col + else "" + ) + me.text( + arrow_str, + style=me.Style( + font_weight="bold", + color="#0284c7", + font_size="14px", + margin=me.Margin(left="4px"), + ), + ) + + with me.box( + style=me.Style( + max_height="600px", + overflow_y="auto", + margin=me.Margin(top="16px"), + display="table", + width="100%", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e5e7eb", + style="solid", + ) + ), + border_radius="8px", + background="#ffffff", + ) + ): + # Header row + with me.box( + style=me.Style( + display="table-row", + background="#f8fafc", + font_weight="bold", + color="#475569", + font_size="12px", + text_transform="uppercase", + letter_spacing="0.05em", + ) + ): + headers = [ + ("Eval ID", "id", "36ch"), + ("Date", "date", "24ch"), + ("Product", "product", None), + ("Requester", "requester", None), + ( + "Trajectory Matcher", + "trajectory_matcher", + "18ch", + ), + ("Turn Count", "turn_count", "12ch"), + ("Executable", "executable", "12ch"), + ( + "Token Consumption", + "token_consumption", + "16ch", + ), + ( + "End-to-End Latency", + "end_to_end_latency", + "20ch", + ), + ] + for label, col, width in headers: + render_header_cell(label, col, width) # Data rows for idx, item in enumerate(summaries): d = item["id"] + date_val = item.get("date", "N/A") prod = item["product"] + req_val = item.get("requester", "N/A") traj = item.get("trajectory_matcher", "N/A") turns = item.get("turn_count", "N/A") exec_val = item.get("executable", "N/A") tokens = item.get("token_consumption", "N/A") latency = item.get("end_to_end_latency", "N/A") - bg_color = "#ffffff" if idx % 2 == 0 else "#f8fafc" + bg_color = ( + "#ffffff" + if idx % 2 == 0 + else "#f8fafc" + ) def make_on_click(dir_name): def on_click(e: me.ClickEvent): @@ -702,6 +1149,54 @@ def on_click(e: me.ClickEvent): width="100%", ), ) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="24ch", + white_space="nowrap", + ) + ): + me.text( + date_val, + style=me.Style( + color="#334155", + font_family="monospace", + ), + ) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + prod, + style=me.Style( + color="#334155" + ), + ) + with me.box( style=me.Style( display="table-cell", @@ -719,7 +1214,10 @@ def on_click(e: me.ClickEvent): ) ): me.text( - prod, style=me.Style(color="#334155") + req_val, + style=me.Style( + color="#334155" + ), ) with me.box( @@ -764,7 +1262,10 @@ def on_click(e: me.ClickEvent): ) ): me.text( - turns, style=me.Style(color="#334155") + turns, + style=me.Style( + color="#334155" + ), ) with me.box( @@ -807,7 +1308,10 @@ def on_click(e: me.ClickEvent): ) ): me.text( - tokens, style=me.Style(color="#334155") + tokens, + style=me.Style( + color="#334155" + ), ) with me.box( @@ -828,7 +1332,9 @@ def on_click(e: me.ClickEvent): ): me.text( latency, - style=me.Style(color="#334155"), + style=me.Style( + color="#334155" + ), ) diff --git a/viewer/static/custom.css b/viewer/static/custom.css index 9fff3588..10dcd0f2 100644 --- a/viewer/static/custom.css +++ b/viewer/static/custom.css @@ -3,3 +3,27 @@ max-width: none !important; white-space: pre-wrap !important; } + +/* Style Material inputs */ +.mdc-text-field { + background-color: #ffffff !important; +} +.mdc-select__anchor { + background-color: #ffffff !important; +} +.mdc-select__selected-text { + color: #000000 !important; +} +.mdc-floating-label { + color: #6b7280 !important; +} +.mat-mdc-input-element { + color: #000000 !important; +} +.mat-mdc-select { + color: #000000 !important; +} +.mat-mdc-option { + background-color: #ffffff !important; + color: #000000 !important; +} From f393e179acb0b8724c5244026b672f9b042232ec Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 19:10:03 +0000 Subject: [PATCH 04/44] feat: configure min instances for Cloud Run and clean up unused filter handler functions in viewer UI --- Makefile | 1 + viewer/main.py | 42 +++--------------------------------------- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index b603c3a4..b19b1190 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,7 @@ deploy-corprun: --image=us-central1-docker.pkg.dev/evalbench-dev/cr-images/eval_server:latest \ --port=3000 \ --memory=2Gi \ + --min-instances=1 \ --service-account=crsvc-evalbench@evalbench-dev.iam.gserviceaccount.com \ --set-env-vars CLOUD_RUN=True,GOOGLE_CLOUD_PROJECT=evalbench-dev,MESOP_XSRF_CHECK=false \ --ingress=internal-and-cloud-load-balancing \ diff --git a/viewer/main.py b/viewer/main.py index 22ed415b..7bce9c21 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -277,7 +277,7 @@ def on_title_click(e: me.ClickEvent): background="#ffffff", padding=me.Padding.all("24px"), border_radius="8px", - box_shadow="0 4px 6px -1px rgb(0 0 0 / 0.1)", + box_shadow="0 4px 6px -1px rgba(0, 0, 0, 0.1)", color="#1e293b", ) ): @@ -561,8 +561,8 @@ def get_sort_key(x): background="#ffffff", border_radius="12px", box_shadow=( - "0 1px 3px 0 rgb(0 0 0 / 0.1), " - "0 1px 2px -1px rgb(0 0 0 / 0.1)" + "0 1px 3px 0 rgba(0, 0, 0, 0.1), " + "0 1px 2px -1px rgba(0, 0, 0, 0.1)" ), align_items="center", border=me.Border.all( @@ -575,43 +575,7 @@ def get_sort_key(x): ) ): - def on_eval_id_filter_change( - e: me.SelectSelectionChangeEvent - ): - st = me.state(State) - st.eval_id_filter = e.value - - def on_product_filter_change( - e: me.SelectSelectionChangeEvent - ): - st = me.state(State) - st.product_filter = e.value - def on_requester_filter_change( - e: me.SelectSelectionChangeEvent - ): - st = me.state(State) - st.requester_filter = e.value - - def on_product_input(e: me.InputEvent): - st = me.state(State) - st.product_filter = e.value - - def on_requester_input(e: me.InputEvent): - st = me.state(State) - st.requester_filter = e.value - - def on_product_suggestion( - e: me.SelectSelectionChangeEvent - ): - st = me.state(State) - st.product_filter = e.value - - def on_requester_suggestion( - e: me.SelectSelectionChangeEvent - ): - st = me.state(State) - st.requester_filter = e.value def make_prod_handler(val, idx): def handler(e: me.ClickEvent): From 3284eb4b1a41858f5e9ff17ce36ddb9b0f9027a4 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 19:18:12 +0000 Subject: [PATCH 05/44] chore: bump version to 1.2.0, add error logging to file readers, and remove unused state/handlers --- uv.lock | 2 +- viewer/main.py | 30 ++++-------------------------- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/uv.lock b/uv.lock index 38d8e77a..7b6bcd63 100644 --- a/uv.lock +++ b/uv.lock @@ -676,7 +676,7 @@ wheels = [ [[package]] name = "evalbench" -version = "1.1.0" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "absl-py" }, diff --git a/viewer/main.py b/viewer/main.py index 7bce9c21..162c8298 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -128,8 +128,8 @@ def get_eval_details(results_dir, dir_name): ] if not req_row.empty: details["requester"] = str(req_row["value"].iloc[0]) - except Exception: - pass + except Exception as e: + logging.warning(f"Error reading configs.csv for {dir_name}: {e}") # Get summary metrics summary_path = os.path.join(results_dir, dir_name, "summary.csv") @@ -157,8 +157,8 @@ def get_eval_details(results_dir, dir_name): details["token_consumption"] = f"{correct:.0f}" elif name == "end_to_end_latency": details["end_to_end_latency"] = f"{correct:.0f}" - except Exception: - pass + except Exception as e: + logging.warning(f"Error reading summary.csv for {dir_name}: {e}") return details @@ -209,7 +209,6 @@ def on_load(e: me.LoadEvent): ], ) def app(): - state = me.state(State) with me.box( style=me.Style( background="#f8fafc", @@ -264,8 +263,6 @@ def on_title_click(e: me.ClickEvent): ), ) - - # Centered content at 90% browser width with me.box( style=me.Style( @@ -574,25 +571,6 @@ def get_sort_key(x): ), ) ): - - - - def make_prod_handler(val, idx): - def handler(e: me.ClickEvent): - st = me.state(State) - st.product_filter = val - - handler.__name__ = f"click_prod_sugg_{idx}" - return handler - - def make_req_handler(val, idx): - def handler(e: me.ClickEvent): - st = me.state(State) - st.requester_filter = val - - handler.__name__ = f"click_req_sugg_{idx}" - return handler - def toggle_eval_id_dropdown(e: me.ClickEvent): st = me.state(State) if st.open_dropdown == "eval_id": From 7ccf0930175e789be2e6d7e7f1f7bb09cefe6c09 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 20:03:00 +0000 Subject: [PATCH 06/44] feat: add git commit version tracking to build process and display in viewer UI --- Makefile | 1 + viewer/main.py | 28 ++++++++++++++++++++++++++++ viewer/version.txt | 1 + 3 files changed, 30 insertions(+) create mode 100644 viewer/version.txt diff --git a/Makefile b/Makefile index b19b1190..81971a26 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ SHELL := /bin/bash TYPE != awk -F '=' '/GOOGLE_ROLE/ { print $$2 }' /etc/lsb-release build: + git rev-parse --short HEAD > viewer/version.txt || echo "unknown" > viewer/version.txt $(CONTAINER_ENGINE) build -t evalbench -f evalbench_service/Dockerfile . build-test: diff --git a/viewer/main.py b/viewer/main.py index 162c8298..55b154b3 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -4,6 +4,23 @@ import yaml import logging import json +import subprocess + +try: + # Try to read version from file (created during build) + version_file = os.path.join(os.path.dirname(__file__), "version.txt") + if os.path.exists(version_file): + with open(version_file, "r") as f: + GIT_VERSION = f.read().strip() + else: + # Fallback to git command (for local dev) + GIT_VERSION = subprocess.check_output( + ["git", "rev-parse", "--short", "HEAD"], + stderr=subprocess.DEVNULL + ).decode("utf-8").strip() +except Exception: + GIT_VERSION = "unknown" + logging.basicConfig(level=logging.INFO) @@ -263,6 +280,17 @@ def on_title_click(e: me.ClickEvent): ), ) + if GIT_VERSION != "unknown": + with me.box( + style=me.Style( + font_size="12px", + color="#94a3b8", + ) + ): + me.markdown( + f"[Git: {GIT_VERSION}](https://github.com/GoogleCloudPlatform/evalbench/commit/{GIT_VERSION})" + ) + # Centered content at 90% browser width with me.box( style=me.Style( diff --git a/viewer/version.txt b/viewer/version.txt new file mode 100644 index 00000000..e078ddab --- /dev/null +++ b/viewer/version.txt @@ -0,0 +1 @@ +3284eb4 From ae832d15b99aac0c2d0f4b86e031132ab83c4d36 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 20:04:35 +0000 Subject: [PATCH 07/44] chore: remove unused version.txt file --- viewer/version.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 viewer/version.txt diff --git a/viewer/version.txt b/viewer/version.txt deleted file mode 100644 index e078ddab..00000000 --- a/viewer/version.txt +++ /dev/null @@ -1 +0,0 @@ -3284eb4 From 4e36317708fe925eb0c9d765d6f497386169f061 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 20:05:46 +0000 Subject: [PATCH 08/44] chore: ignore autogenerated version file in viewer directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8c6adf06..622e9cad 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,6 @@ datasets/bird/db_connections/bird/thrombosis_prediction.sqlite datasets/bird/db_connections/bird/toxicology.sqlite evalbench/results/* evalbench/db_connections/bat/db_blog.db + +# Autogenerated version file +viewer/version.txt From 4ea1def477771fc1ddb3ff30cefec009122df613 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 23:03:24 +0000 Subject: [PATCH 09/44] feat: add background precomputation worker for trends and update UI styling --- Makefile | 1 + evalbench_service/entrypoint.sh | 6 +- evalbench_service/supervisord.conf | 10 + uv.lock | 784 ++++++++- viewer/background_worker.py | 25 + viewer/main.py | 1959 +++++++++++----------- viewer/precompute_trends.py | 130 ++ viewer/pyproject.toml | 3 + viewer/run_precompute.py | 19 + viewer/state.py | 16 + viewer/static/style.css | 15 + viewer/static/{custom.css => styles.css} | 0 viewer/trends.py | 233 +++ 13 files changed, 2253 insertions(+), 948 deletions(-) create mode 100644 viewer/background_worker.py create mode 100644 viewer/precompute_trends.py create mode 100644 viewer/run_precompute.py create mode 100644 viewer/state.py create mode 100644 viewer/static/style.css rename viewer/static/{custom.css => styles.css} (100%) create mode 100644 viewer/trends.py diff --git a/Makefile b/Makefile index 81971a26..95bce872 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,7 @@ deploy-corprun: --port=3000 \ --memory=2Gi \ --min-instances=1 \ + --no-cpu-throttling \ --service-account=crsvc-evalbench@evalbench-dev.iam.gserviceaccount.com \ --set-env-vars CLOUD_RUN=True,GOOGLE_CLOUD_PROJECT=evalbench-dev,MESOP_XSRF_CHECK=false \ --ingress=internal-and-cloud-load-balancing \ diff --git a/evalbench_service/entrypoint.sh b/evalbench_service/entrypoint.sh index 56520d32..73a013d4 100644 --- a/evalbench_service/entrypoint.sh +++ b/evalbench_service/entrypoint.sh @@ -2,7 +2,11 @@ # evalbench_service/entrypoint.sh if [[ "$CLOUD_RUN" == "True" ]]; then - echo "Cloud Run detected. Starting only gunicorn frontend on port ${PORT:-3000}..." + echo "Cloud Run detected. Starting gunicorn frontend and background precompute..." + + # Start background precomputation loop + python /evalbench/viewer/run_precompute.py & + # Ensure we are in the viewer directory for gunicorn to find main:me cd /evalbench/viewer exec gunicorn -w 4 -k gevent main:me --bind :${PORT:-3000} --forwarded-allow-ips="*" diff --git a/evalbench_service/supervisord.conf b/evalbench_service/supervisord.conf index 0d18da06..3ec67a56 100644 --- a/evalbench_service/supervisord.conf +++ b/evalbench_service/supervisord.conf @@ -24,3 +24,13 @@ stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 + +[program:precompute_trends] +command=python /evalbench/viewer/run_precompute.py +directory=/evalbench/viewer +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/uv.lock b/uv.lock index cfdb1221..189e1454 100644 --- a/uv.lock +++ b/uv.lock @@ -503,6 +503,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "choreographer" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "logistro" }, + { name = "simplejson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/47/64a035c6f764450ea9f902cbeba14c8c70316c2641125510066d8f912bfa/choreographer-1.2.1.tar.gz", hash = "sha256:022afd72b1e9b0bcb950420b134e70055a294c791b6f36cfb47d89745b701b5f", size = 43399, upload-time = "2025-11-09T23:04:44.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -558,6 +571,169 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + [[package]] name = "cryptography" version = "46.0.7" @@ -618,6 +794,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "db-dtypes" version = "1.4.4" @@ -676,7 +861,7 @@ wheels = [ [[package]] name = "evalbench" -version = "1.2.0" +version = "1.3.0" source = { editable = "." } dependencies = [ { name = "absl-py" }, @@ -810,6 +995,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1959,6 +2201,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -2104,6 +2355,155 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "kaleido" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "choreographer" }, + { name = "logistro" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pytest-timeout" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/76eec859b71eda803a88ea50ed3f270281254656bb23d19eb0a39aa706a0/kaleido-1.2.0.tar.gz", hash = "sha256:fa621a14423e8effa2895a2526be00af0cf21655be1b74b7e382c171d12e71ef", size = 64160, upload-time = "2025-11-04T21:24:23.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/97/f6de8d4af54d6401d6581a686cce3e3e2371a79ba459a449104e026c08bc/kaleido-1.2.0-py3-none-any.whl", hash = "sha256:c27ed82b51df6b923d0e656feac221343a0dbcd2fb9bc7e6b1db97f61e9a1513", size = 68997, upload-time = "2025-11-04T21:24:21.704Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "logistro" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/90/bfd7a6fab22bdfafe48ed3c4831713cb77b4779d18ade5e248d5dbc0ca22/logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047", size = 8398, upload-time = "2025-11-01T02:41:18.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/6aa79ba3570bddd1bf7e951c6123f806751e58e8cce736bad77b2cf348d7/logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb", size = 8555, upload-time = "2025-11-01T02:41:17.587Z" }, +] + [[package]] name = "mako" version = "1.3.10" @@ -2213,6 +2613,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + [[package]] name = "mcp" version = "1.26.0" @@ -2591,6 +3066,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "narwhals" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/1a/bd3317c0bdbcd9ffb710ddf5250b32898f8f2c240be99494fe137feb77a7/narwhals-2.19.0.tar.gz", hash = "sha256:14fd7040b5ff211d415a82e4827b9d04c354e213e72a6d0730205ffd72e3b7ff", size = 623698, upload-time = "2026-04-06T15:50:58.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/72/e61e3091e0e00fae9d3a8ef85ece9d2cd4b5966058e1f2901ce42679eebf/narwhals-2.19.0-py3-none-any.whl", hash = "sha256:1f8dfa4a33a6dbff878c3e9be4c3b455dfcaf2a9322f1357db00e4e92e95b84b", size = 446991, upload-time = "2026-04-06T15:50:57.046Z" }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -2906,6 +3390,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, ] +[[package]] +name = "orjson" +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/5d81f61fe3e4270da80c71442864c091cee3003cc8984c75f413fe742a07/orjson-3.11.8-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e6693ff90018600c72fd18d3d22fa438be26076cd3c823da5f63f7bab28c11cb", size = 229663, upload-time = "2026-03-31T16:14:30.708Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/85e06b0eb11de6fb424120fd5788a07035bd4c5e6bb7841ae9972a0526d1/orjson-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93de06bc920854552493c81f1f729fab7213b7db4b8195355db5fda02c7d1363", size = 132321, upload-time = "2026-03-31T16:14:32.317Z" }, + { url = "https://files.pythonhosted.org/packages/86/71/089338ee51b3132f050db0864a7df9bdd5e94c2a03820ab8a91e8f655618/orjson-3.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", size = 130658, upload-time = "2026-03-31T16:14:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/10/0d/f39d8802345d0ad65f7fd4374b29b9b59f98656dc30f21ca5c773265b2f0/orjson-3.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d823831105c01f6c8029faf297633dbeb30271892bd430e9c24ceae3734744", size = 135708, upload-time = "2026-03-31T16:14:35.224Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b5/40aae576b3473511696dcffea84fde638b2b64774eb4dcb8b2c262729f8a/orjson-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60c0423f15abb6cf78f56dff00168a1b582f7a1c23f114036e2bfc697814d5f", size = 147047, upload-time = "2026-03-31T16:14:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/778a84458d1fdaa634b2e572e51ce0b354232f580b2327e1f00a8d88c38c/orjson-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01928d0476b216ad2201823b0a74000440360cef4fed1912d297b8d84718f277", size = 133072, upload-time = "2026-03-31T16:14:37.715Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/1bbf2fc3ffcc4b829ade554b574af68cec898c9b5ad6420a923c75a073d3/orjson-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4a639049c44d36a6d1ae0f4a94b271605c745aee5647fa8ffaabcdc01b69a6", size = 133867, upload-time = "2026-03-31T16:14:39.356Z" }, + { url = "https://files.pythonhosted.org/packages/08/94/6413da22edc99a69a8d0c2e83bf42973b8aa94d83ef52a6d39ac85da00bc/orjson-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3222adff1e1ff0dce93c16146b93063a7793de6c43d52309ae321234cdaf0f4d", size = 142268, upload-time = "2026-03-31T16:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/aa5dbaa6136d7ba55f5461ac2e885efc6e6349424a428927fd46d68f4396/orjson-3.11.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3223665349bbfb68da234acd9846955b1a0808cbe5520ff634bf253a4407009b", size = 424008, upload-time = "2026-03-31T16:14:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/fa/aa/2c1962d108c7fe5e27aa03a354b378caf56d8eafdef15fd83dec081ce45a/orjson-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:61c9d357a59465736022d5d9ba06687afb7611dfb581a9d2129b77a6fcf78e59", size = 147942, upload-time = "2026-03-31T16:14:44.256Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/65f404f4c47eb1b0b4476f03ec838cac0c4aa933920ff81e5dda4dee14e7/orjson-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58fb9b17b4472c7b1dcf1a54583629e62e23779b2331052f09a9249edf81675b", size = 136640, upload-time = "2026-03-31T16:14:45.884Z" }, + { url = "https://files.pythonhosted.org/packages/90/5f/7b784aea98bdb125a2f2da7c27d6c2d2f6d943d96ef0278bae596d563f85/orjson-3.11.8-cp310-cp310-win32.whl", hash = "sha256:b43dc2a391981d36c42fa57747a49dae793ef1d2e43898b197925b5534abd10a", size = 132066, upload-time = "2026-03-31T16:14:47.397Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/2e284af8d6c9478df5ef938917743f61d68f4c70d17f1b6e82f7e3b8dba1/orjson-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:c98121237fea2f679480765abd566f7713185897f35c9e6c2add7e3a9900eb61", size = 127609, upload-time = "2026-03-31T16:14:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -3088,6 +3653,126 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/07/5fd183858dff4d24840f07fc845f213cd371a19958558607ba22035dadd7/pg8000-1.31.5-py3-none-any.whl", hash = "sha256:0af2c1926b153307639868d2ee5cef6cd3a7d07448e12736989b10e1d491e201", size = 57816, upload-time = "2025-09-14T09:16:47.798Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "plotly" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/7f/0f100df1172aadf88a929a9dbb902656b0880ba4b960fe5224867159d8f4/plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6", size = 6911286, upload-time = "2026-04-09T20:36:45.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0", size = 9898444, upload-time = "2026-04-09T20:36:39.812Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -3663,6 +4348,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -4034,6 +4749,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] +[[package]] +name = "simplejson" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/09/2bf3761de89ea2d91bdce6cf107dcd858892d0adc22c995684878826cc6b/simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5", size = 94039, upload-time = "2025-09-26T16:27:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/0f/33/c3277db8931f0ae9e54b9292668863365672d90fb0f632f4cf9829cb7d68/simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d", size = 75894, upload-time = "2025-09-26T16:27:30.378Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ea/ae47b04d03c7c8a7b7b1a8b39a6e27c3bd424e52f4988d70aca6293ff5e5/simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1", size = 76116, upload-time = "2025-09-26T16:27:31.42Z" }, + { url = "https://files.pythonhosted.org/packages/4b/42/6c9af551e5a8d0f171d6dce3d9d1260068927f7b80f1f09834e07887c8c4/simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6", size = 138827, upload-time = "2025-09-26T16:27:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/2b/22/5e268bbcbe9f75577491e406ec0a5536f5b2fa91a3b52031fea51cd83e1d/simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01", size = 146772, upload-time = "2025-09-26T16:27:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/b4/800f14728e2ad666f420dfdb57697ca128aeae7f991b35759c09356b829a/simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3", size = 134497, upload-time = "2025-09-26T16:27:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda", size = 138172, upload-time = "2025-09-26T16:27:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/09/36/4e282f5211b34620f1b2e4b51d9ddaab5af82219b9b7b78360a33f7e5387/simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2", size = 140272, upload-time = "2025-09-26T16:27:37.605Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/94ad2cf32f477c449e1f63c863d8a513e2408d651c4e58fe4b6a7434e168/simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4", size = 140468, upload-time = "2025-09-26T16:27:39.015Z" }, + { url = "https://files.pythonhosted.org/packages/e5/46/827731e4163be3f987cb8ee90f5d444161db8f540b5e735355faa098d9bc/simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8", size = 148700, upload-time = "2025-09-26T16:27:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/c32121064b1ec2fb7b5d872d9a1abda62df064d35e0160eddfa907118343/simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7", size = 141323, upload-time = "2025-09-26T16:27:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/46/b6/c897c54326fe86dd12d101981171a49361949f4728294f418c3b86a1af77/simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53", size = 74377, upload-time = "2025-09-26T16:27:42.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/87/a6e03d4d80cca99c1fee4e960f3440e2f21be9470e537970f960ca5547f1/simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476", size = 76081, upload-time = "2025-09-26T16:27:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633, upload-time = "2025-09-26T16:27:45.028Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309, upload-time = "2025-09-26T16:27:46.142Z" }, + { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308, upload-time = "2025-09-26T16:27:47.542Z" }, + { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733, upload-time = "2025-09-26T16:27:48.673Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397, upload-time = "2025-09-26T16:27:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654, upload-time = "2025-09-26T16:27:51.168Z" }, + { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913, upload-time = "2025-09-26T16:27:52.331Z" }, + { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568, upload-time = "2025-09-26T16:27:53.41Z" }, + { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239, upload-time = "2025-09-26T16:27:54.502Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497, upload-time = "2025-09-26T16:27:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069, upload-time = "2025-09-26T16:27:57.039Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158, upload-time = "2025-09-26T16:27:58.265Z" }, + { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911, upload-time = "2025-09-26T16:27:59.292Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523, upload-time = "2025-09-26T16:28:00.356Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844, upload-time = "2025-09-26T16:28:01.756Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655, upload-time = "2025-09-26T16:28:03.037Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335, upload-time = "2025-09-26T16:28:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519, upload-time = "2025-09-26T16:28:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571, upload-time = "2025-09-26T16:28:07.715Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367, upload-time = "2025-09-26T16:28:08.921Z" }, + { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205, upload-time = "2025-09-26T16:28:10.086Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823, upload-time = "2025-09-26T16:28:11.329Z" }, + { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997, upload-time = "2025-09-26T16:28:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367, upload-time = "2025-09-26T16:28:14.393Z" }, + { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285, upload-time = "2025-09-26T16:28:15.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969, upload-time = "2025-09-26T16:28:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530, upload-time = "2025-09-26T16:28:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846, upload-time = "2025-09-26T16:28:19.12Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661, upload-time = "2025-09-26T16:28:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579, upload-time = "2025-09-26T16:28:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797, upload-time = "2025-09-26T16:28:22.53Z" }, + { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851, upload-time = "2025-09-26T16:28:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598, upload-time = "2025-09-26T16:28:24.962Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498, upload-time = "2025-09-26T16:28:26.114Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129, upload-time = "2025-09-26T16:28:27.552Z" }, + { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359, upload-time = "2025-09-26T16:28:28.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717, upload-time = "2025-09-26T16:28:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289, upload-time = "2025-09-26T16:28:31.809Z" }, + { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972, upload-time = "2025-09-26T16:28:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -4340,9 +5116,12 @@ source = { virtual = "viewer" } dependencies = [ { name = "gevent" }, { name = "gunicorn" }, + { name = "kaleido" }, + { name = "matplotlib" }, { name = "mesop" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "plotly" }, { name = "pyaml" }, ] @@ -4350,8 +5129,11 @@ dependencies = [ requires-dist = [ { name = "gevent", specifier = ">=25.9.1" }, { name = "gunicorn", specifier = ">=25.1.0" }, + { name = "kaleido", specifier = ">=0.2.1" }, + { name = "matplotlib", specifier = ">=3.10.8" }, { name = "mesop", specifier = ">=1.2.1" }, { name = "pandas", specifier = ">=2.3.3" }, + { name = "plotly", specifier = ">=5.18.0" }, { name = "pyaml", specifier = ">=26.2.1" }, ] diff --git a/viewer/background_worker.py b/viewer/background_worker.py new file mode 100644 index 00000000..70ee3b68 --- /dev/null +++ b/viewer/background_worker.py @@ -0,0 +1,25 @@ +import time +import sys +import os +import logging + +# Add current dir to path +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(current_dir) + +import precompute_trends + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def main(): + while True: + logging.info("Starting precomputation...") + try: + precompute_trends.precompute() + except Exception as e: + logging.error(f"Error during precomputation: {e}") + logging.info("Precomputation finished. Sleeping for 5 minutes...") + time.sleep(300) + +if __name__ == "__main__": + main() diff --git a/viewer/main.py b/viewer/main.py index 55b154b3..6b83c052 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -5,6 +5,7 @@ import logging import json import subprocess +from state import State try: # Try to read version from file (created during build) @@ -74,21 +75,15 @@ def df_to_config(df: pd.DataFrame) -> dict: return original_dict -@me.stateclass -class State: - selected_directory: str - selected_tab: str = "Dashboard" - conversation_index: int = 0 - eval_summaries: str = "" - eval_id_filter: str = "" - product_filter: str = "" - requester_filter: str = "" - sort_column: str = "date" - sort_descending: bool = True - open_dropdown: str = "" + def get_results_dir(): + # Try to read from environment variable + res_dir = os.environ.get("RESULTS_DIR") + if res_dir: + return res_dir + # Check multiple locations for results directory results_dir_candidates = [ "/tmp_session_files/results", @@ -212,328 +207,154 @@ def on_load(e: me.LoadEvent): state.selected_directory = job_id -@me.page( - path="/", - title="EvalBench Viewer", - on_load=on_load, - security_policy=me.SecurityPolicy( - dangerously_disable_trusted_types=True, - cross_origin_opener_policy="same-origin", - ), - stylesheets=[ - "data:", - "/static/custom.css", - ], -) -def app(): - with me.box( - style=me.Style( - background="#f8fafc", - min_height="100vh", - width="100%", - ) - ): - render_app_content() - -def render_app_content(): +def list_view_component(): + global json + import os state = me.state(State) results_dir = get_results_dir() - directories = [] if os.path.exists(results_dir): - # List directories only directories = [ d for d in os.listdir(results_dir) if os.path.isdir(os.path.join(results_dir, d)) ] - - def on_title_click(e: me.ClickEvent): - state.selected_directory = "" - state.conversation_index = 0 - - # Full-width header bar - with me.box( - style=me.Style( - background="#1e293b", - padding=me.Padding.symmetric(vertical="16px", horizontal="5%"), - margin=me.Margin(bottom="24px"), - display="flex", - justify_content="space-between", - align_items="center", - ) - ): - me.button( - "EvalBench Viewer", - on_click=on_title_click, + with me.box( style=me.Style( - color="#f8fafc", - font_size="22px", - font_weight="700", - letter_spacing="0.5px", - background="transparent", - padding=me.Padding.all("0px"), - margin=me.Margin.all("0px"), - border=me.Border.all(me.BorderSide(width="0px")), - text_align="left", - ), - ) - - if GIT_VERSION != "unknown": - with me.box( + background="#ffffff", + padding=me.Padding.all("24px"), + border_radius="12px", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e5e7eb", + style="solid", + ) + ), + box_shadow="0 1px 3px rgba(0,0,0,0.06)", + text_align="center", + margin=me.Margin(top="16px"), + ) + ): + me.text( + "Welcome to EvalBench Viewer", style=me.Style( - font_size="12px", - color="#94a3b8", - ) - ): - me.markdown( - f"[Git: {GIT_VERSION}](https://github.com/GoogleCloudPlatform/evalbench/commit/{GIT_VERSION})" - ) - - # Centered content at 90% browser width - with me.box( - style=me.Style( - width="90%", - margin=me.Margin.symmetric(horizontal="auto"), - display="flex", - flex_direction="column", - gap="16px", - background="#ffffff", - padding=me.Padding.all("24px"), - border_radius="8px", - box_shadow="0 4px 6px -1px rgba(0, 0, 0, 0.1)", - color="#1e293b", - ) - ): - - if state.selected_directory: - - def on_tab_change(e: me.ButtonToggleChangeEvent): - state.selected_tab = e.value - - me.button_toggle( - value=state.selected_tab, - buttons=[ - me.ButtonToggleButton( - label="Dashboard", value="Dashboard" - ), - me.ButtonToggleButton( - label="Configs", value="Configs" - ), - # me.ButtonToggleButton(label="Evals", value="Evals"), - # me.ButtonToggleButton(label="Scores", value="Scores"), - me.ButtonToggleButton( - label="Conversations", value="Conversations" - ), - # me.ButtonToggleButton(label="Summary", value="Summary"), - ], - on_change=on_tab_change, + font_size="24px", + font_weight="700", + color="#1f2937", + margin=me.Margin(bottom="8px"), + ), ) - - if state.selected_tab == "Dashboard": - dashboard.dashboard_component( - os.path.join(results_dir, state.selected_directory) - ) - elif state.selected_tab == "Conversations": - - def on_prev_conversation(e: me.ClickEvent): - s = me.state(State) - if s.conversation_index > 0: - s.conversation_index -= 1 - - def on_next_conversation(e: me.ClickEvent): - s = me.state(State) - s.conversation_index += 1 - - conversations.conversations_component( - os.path.join(results_dir, state.selected_directory), - conversation_index=state.conversation_index, - on_prev=on_prev_conversation, - on_next=on_next_conversation, - ) - elif state.selected_tab == "Configs": - config_path = os.path.join( - results_dir, state.selected_directory, "configs.csv" - ) - if os.path.exists(config_path): - try: - df = pd.read_csv(config_path) - config = df_to_config(df) - me.code(yaml.dump(config)) - except Exception as e: - me.text(f"Error reading configs.csv: {e}") - else: - me.text( - f"configs.csv not found in {state.selected_directory}" - ) - elif state.selected_tab == "Evals": - evals_path = os.path.join( - results_dir, state.selected_directory, "evals.csv" - ) - if os.path.exists(evals_path): - try: - df = pd.read_csv(evals_path) - details = get_eval_details( - results_dir, state.selected_directory - ) - df.insert(0, "orchestrator", details["orchestrator"]) - me.table(data_frame=df) - except Exception as e: - me.text(f"Error reading evals.csv: {e}") - else: - me.text( - f"evals.csv not found in {state.selected_directory}" - ) - elif state.selected_tab == "Scores": - scores_path = os.path.join( - results_dir, state.selected_directory, "scores.csv" - ) - if os.path.exists(scores_path): - try: - df = pd.read_csv(scores_path) - me.table(data_frame=df) - except Exception as e: - me.text(f"Error reading scores.csv: {e}") - else: - me.text( - f"scores.csv not found in {state.selected_directory}" - ) - elif state.selected_tab == "Summary": - summary_path = os.path.join( - results_dir, state.selected_directory, "summary.csv" - ) - if os.path.exists(summary_path): - try: - df = pd.read_csv(summary_path) - me.table(data_frame=df) - except Exception as e: - me.text(f"Error reading summary.csv: {e}") - else: - me.text( - f"summary.csv not found in {state.selected_directory}" - ) - else: - with me.box( + me.text( + f"Found {len(directories)} evaluation runs. " + "Click on an Eval ID in the table below to explore " + "the results.", style=me.Style( - background="#ffffff", - padding=me.Padding.all("24px"), - border_radius="12px", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e5e7eb", - style="solid", - ) - ), - box_shadow="0 1px 3px rgba(0,0,0,0.06)", - text_align="center", - margin=me.Margin(top="16px"), - ) - ): - me.text( - "Welcome to EvalBench Viewer", - style=me.Style( - font_size="24px", - font_weight="700", - color="#1f2937", - margin=me.Margin(bottom="8px"), - ), - ) - me.text( - f"Found {len(directories)} evaluation runs. " - "Click on an Eval ID in the table below to explore " - "the results.", - style=me.Style( - font_size="16px", - color="#6b7280", - margin=me.Margin(bottom="16px"), - ), - ) - if directories: - # Compute summaries if empty - s = me.state(State) - summaries = [] - if s.eval_summaries: - try: - summaries = json.loads(s.eval_summaries) - except Exception: - summaries = [] - - if not summaries: - for d in sorted(directories): - details = get_eval_details(results_dir, d) - summaries.append({ - "id": d, - "date": details.get("date", "N/A"), - "product": details["product"], - "requester": details.get("requester", "N/A"), - "exact_match": details["exact_match"], - "llmrater": details["llmrater"], - "trajectory_matcher": details[ - "trajectory_matcher" - ], - "turn_count": details["turn_count"], - "executable": details["executable"], - "token_consumption": details[ - "token_consumption" - ], - "end_to_end_latency": details[ - "end_to_end_latency" - ] - }) - s.eval_summaries = json.dumps(summaries) - - # Sort by selected column - reverse = state.sort_descending - col = state.sort_column - - def get_sort_key(x): - val = x.get(col, "N/A") - - # Handle numbers and percentages - if col in [ - "exact_match", - "llmrater", - "trajectory_matcher", - "executable", - ]: - if val == "N/A": - return -1.0 if reverse else 101.0 - if val.endswith("%"): - try: - return float(val.rstrip("%")) - except ValueError: - return -1.0 if reverse else 101.0 + font_size="16px", + color="#6b7280", + margin=me.Margin(bottom="16px"), + ), + ) + if directories: + # Compute summaries if empty + s = me.state(State) + summaries = [] + if s.eval_summaries: + try: + summaries = json.loads(s.eval_summaries) + except Exception: + summaries = [] + + if not summaries: + for d in sorted(directories): + details = get_eval_details(results_dir, d) + summaries.append({ + "id": d, + "date": details.get("date", "N/A"), + "product": details["product"], + "requester": details.get("requester", "N/A"), + "exact_match": details["exact_match"], + "llmrater": details["llmrater"], + "trajectory_matcher": details[ + "trajectory_matcher" + ], + "turn_count": details["turn_count"], + "executable": details["executable"], + "token_consumption": details[ + "token_consumption" + ], + "end_to_end_latency": details[ + "end_to_end_latency" + ] + }) + s.eval_summaries = json.dumps(summaries) + + # Sort by selected column + reverse = state.sort_descending + col = state.sort_column + + def get_sort_key(x): + val = x.get(col, "N/A") + + # Handle numbers and percentages + if col in [ + "exact_match", + "llmrater", + "trajectory_matcher", + "executable", + ]: + if val == "N/A": return -1.0 if reverse else 101.0 - - elif col in [ - "turn_count", - "token_consumption", - "end_to_end_latency", - ]: - if val == "N/A": - return -1.0 if reverse else 1e12 + if val.endswith("%"): try: - return float(val) + return float(val.rstrip("%")) except ValueError: - return -1.0 if reverse else 1e12 - - # String columns (product, requester, id, date) + return -1.0 if reverse else 101.0 + return -1.0 if reverse else 101.0 + + elif col in [ + "turn_count", + "token_consumption", + "end_to_end_latency", + ]: if val == "N/A": - return "" if reverse else "\xff\xff\xff\xff" - return str(val) - - summaries.sort(key=get_sort_key, reverse=reverse) - - # Extract unique values for filters from ALL summaries - all_summaries = [] - if s.eval_summaries: + return -1.0 if reverse else 1e12 try: - all_summaries = json.loads(s.eval_summaries) - except Exception: - all_summaries = [] - + return float(val) + except ValueError: + return -1.0 if reverse else 1e12 + + # String columns (product, requester, id, date) + if val == "N/A": + return "" if reverse else "\xff\xff\xff\xff" + return str(val) + + summaries.sort(key=get_sort_key, reverse=reverse) + + # Extract unique values for filters from ALL summaries + all_summaries = [] + if s.eval_summaries: + try: + all_summaries = json.loads(s.eval_summaries) + except Exception: + all_summaries = [] + + filters_file = os.path.join(results_dir, "filters_cache.json") + if os.path.exists(filters_file): + try: + with open(filters_file, "r") as f: + filters_data = json.load(f) + products = filters_data.get("products", []) + requesters = filters_data.get("requesters", []) + eval_ids = filters_data.get("eval_ids", []) + except Exception as e: + import logging + logging.error(f"Error reading filters cache: {e}") + products = [] + requesters = [] + eval_ids = [] + else: products = sorted( list( set( @@ -553,77 +374,107 @@ def get_sort_key(x): ) ) eval_ids = sorted([x["id"] for x in all_summaries]) - - # Apply filters - if state.eval_id_filter: - summaries = [ - x - for x in summaries - if x["id"] == state.eval_id_filter - ] - if state.product_filter: - summaries = [ - x - for x in summaries - if x["product"] == state.product_filter - ] - if state.requester_filter: - summaries = [ - x - for x in summaries - if x.get("requester", "N/A") - == state.requester_filter - ] - - # Render filters UI + + # Apply filters + if state.eval_id_filter: + summaries = [ + x + for x in summaries + if x["id"] == state.eval_id_filter + ] + if state.product_filter: + summaries = [ + x + for x in summaries + if x["product"] == state.product_filter + ] + if state.requester_filter: + summaries = [ + x + for x in summaries + if x.get("requester", "N/A") + == state.requester_filter + ] + + # Render filters UI + with me.box( + style=me.Style( + display="flex", + flex_direction="row", + gap="24px", + margin=me.Margin(top="16px", bottom="24px"), + padding=me.Padding.all("16px"), + background="#ffffff", + border_radius="12px", + box_shadow=( + "0 1px 3px 0 rgba(0, 0, 0, 0.1), " + "0 1px 2px -1px rgba(0, 0, 0, 0.1)" + ), + align_items="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + def toggle_eval_id_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "eval_id": + st.open_dropdown = "" + else: + st.open_dropdown = "eval_id" + + def make_eval_id_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.eval_id_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_eval_id_{val}" + return handler + with me.box( style=me.Style( - display="flex", - flex_direction="row", - gap="24px", - margin=me.Margin(top="16px", bottom="24px"), - padding=me.Padding.all("16px"), - background="#ffffff", - border_radius="12px", - box_shadow=( - "0 1px 3px 0 rgba(0, 0, 0, 0.1), " - "0 1px 2px -1px rgba(0, 0, 0, 0.1)" - ), - align_items="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), + position="relative", + width="200px", ) ): - def toggle_eval_id_dropdown(e: me.ClickEvent): - st = me.state(State) - if st.open_dropdown == "eval_id": - st.open_dropdown = "" - else: - st.open_dropdown = "eval_id" - - def make_eval_id_handler(val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.eval_id_filter = val - st.open_dropdown = "" - - handler.__name__ = f"click_eval_id_{val}" - return handler - + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - position="relative", - width="200px", - ) + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_eval_id_dropdown, ): - # The Box acting as Dropdown Trigger + me.text( + state.eval_id_filter + if state.eval_id_filter + else "Select Eval ID", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "eval_id": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all( me.BorderSide( @@ -632,99 +483,99 @@ def handler(e: me.ClickEvent): ) ), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_eval_id_dropdown, - ): - me.text( - state.eval_id_filter - if state.eval_id_filter - else "Select Eval ID", - style=me.Style( - color="#1f2937" - ), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # The Popup List - if state.open_dropdown == "eval_id": + ): + # All option with me.box( style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - ) + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=make_eval_id_handler(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" ), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", ) - ): - # All option + + for d in eval_ids: with me.box( style=me.Style( padding=me.Padding.all("8px"), cursor="pointer", ), - on_click=make_eval_id_handler(""), + on_click=make_eval_id_handler(d), ): me.text( - "All", + d, style=me.Style( color="#1f2937" ), ) - - for d in eval_ids: - with me.box( - style=me.Style( - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=make_eval_id_handler(d), - ): - me.text( - d, - style=me.Style( - color="#1f2937" - ), - ) - - # Product Filter with Floating Autocomplete - def toggle_product_dropdown(e: me.ClickEvent): + + # Product Filter with Floating Autocomplete + def toggle_product_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "product": + st.open_dropdown = "" + else: + st.open_dropdown = "product" + + def make_prod_dropdown_handler(val): + def handler(e: me.ClickEvent): st = me.state(State) - if st.open_dropdown == "product": - st.open_dropdown = "" - else: - st.open_dropdown = "product" - - def make_prod_dropdown_handler(val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.product_filter = val - st.open_dropdown = "" - - handler.__name__ = f"click_prod_dd_{val}" - return handler - - mk_prod_dd = make_prod_dropdown_handler - + st.product_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_prod_dd_{val}" + return handler + + mk_prod_dd = make_prod_dropdown_handler + + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - position="relative", - width="200px", - ) + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_product_dropdown, ): - # The Box acting as Dropdown Trigger + me.text( + state.product_filter + if state.product_filter + else "Filter by Product", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "product": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all( me.BorderSide( @@ -733,99 +584,99 @@ def handler(e: me.ClickEvent): ) ), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_product_dropdown, - ): - me.text( - state.product_filter - if state.product_filter - else "Filter by Product", - style=me.Style( - color="#1f2937" - ), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # The Popup List - if state.open_dropdown == "product": + ): + # All option with me.box( style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - ) + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_prod_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" ), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", ) - ): - # All option + + for p in products: with me.box( style=me.Style( padding=me.Padding.all("8px"), cursor="pointer", ), - on_click=mk_prod_dd(""), + on_click=mk_prod_dd(p), ): me.text( - "All", + p, style=me.Style( color="#1f2937" ), ) - - for p in products: - with me.box( - style=me.Style( - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=mk_prod_dd(p), - ): - me.text( - p, - style=me.Style( - color="#1f2937" - ), - ) - - # Requester Filter with Floating Autocomplete - def toggle_requester_dropdown(e: me.ClickEvent): + + # Requester Filter with Floating Autocomplete + def toggle_requester_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "requester": + st.open_dropdown = "" + else: + st.open_dropdown = "requester" + + def make_req_dropdown_handler(val): + def handler(e: me.ClickEvent): st = me.state(State) - if st.open_dropdown == "requester": - st.open_dropdown = "" - else: - st.open_dropdown = "requester" - - def make_req_dropdown_handler(val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.requester_filter = val - st.open_dropdown = "" - - handler.__name__ = f"click_req_dd_{val}" - return handler - - mk_req_dd = make_req_dropdown_handler - + st.requester_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_req_dd_{val}" + return handler + + mk_req_dd = make_req_dropdown_handler + + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - position="relative", - width="200px", - ) + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_requester_dropdown, ): - # The Box acting as Dropdown Trigger + me.text( + state.requester_filter + if state.requester_filter + else "Filter by Requester", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "requester": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all( me.BorderSide( @@ -834,479 +685,695 @@ def handler(e: me.ClickEvent): ) ), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_requester_dropdown, - ): - me.text( - state.requester_filter - if state.requester_filter - else "Filter by Requester", - style=me.Style( - color="#1f2937" - ), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # The Popup List - if state.open_dropdown == "requester": + ): + # All option with me.box( style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - ) + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_req_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" ), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", ) - ): - # All option + + for r in requesters: with me.box( style=me.Style( padding=me.Padding.all("8px"), cursor="pointer", ), - on_click=mk_req_dd(""), + on_click=mk_req_dd(r), ): me.text( - "All", + r, style=me.Style( color="#1f2937" ), ) - - for r in requesters: - with me.box( - style=me.Style( - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=mk_req_dd(r), - ): - me.text( - r, - style=me.Style( - color="#1f2937" - ), - ) - - def on_sort_click(col_name): - s = me.state(State) - if s.sort_column == col_name: - s.sort_descending = not s.sort_descending - else: - s.sort_column = col_name - s.sort_descending = True - - def click_id(e): - on_sort_click("id") - - def click_date(e): - on_sort_click("date") - - def click_product(e): - on_sort_click("product") - - def click_requester(e): - on_sort_click("requester") - - def click_traj(e): - on_sort_click("trajectory_matcher") - - def click_turns(e): - on_sort_click("turn_count") - - def click_exec(e): - on_sort_click("executable") - - def click_tokens(e): - on_sort_click("token_consumption") - - def click_latency(e): - on_sort_click("end_to_end_latency") - - sort_handlers = { - "id": click_id, - "date": click_date, - "product": click_product, - "requester": click_requester, - "trajectory_matcher": click_traj, - "turn_count": click_turns, - "executable": click_exec, - "token_consumption": click_tokens, - "end_to_end_latency": click_latency, - } - - def render_header_cell(h_label, h_col, h_width): + + def on_sort_click(col_name): + s = me.state(State) + if s.sort_column == col_name: + s.sort_descending = not s.sort_descending + else: + s.sort_column = col_name + s.sort_descending = True + + def click_id(e): + on_sort_click("id") + + def click_date(e): + on_sort_click("date") + + def click_product(e): + on_sort_click("product") + + def click_requester(e): + on_sort_click("requester") + + def click_traj(e): + on_sort_click("trajectory_matcher") + + def click_turns(e): + on_sort_click("turn_count") + + def click_exec(e): + on_sort_click("executable") + + def click_tokens(e): + on_sort_click("token_consumption") + + def click_latency(e): + on_sort_click("end_to_end_latency") + + sort_handlers = { + "id": click_id, + "date": click_date, + "product": click_product, + "requester": click_requester, + "trajectory_matcher": click_traj, + "turn_count": click_turns, + "executable": click_exec, + "token_consumption": click_tokens, + "end_to_end_latency": click_latency, + } + + def render_header_cell(h_label, h_col, h_width): + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + cursor="pointer", + width=h_width, + white_space="nowrap" if h_width else None, + background="#f8fafc", + ), + on_click=sort_handlers[h_col], + ): with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - cursor="pointer", - width=h_width, - white_space="nowrap" if h_width else None, - background="#f8fafc", - ), - on_click=sort_handlers[h_col], + display="flex", + align_items="center", + justify_content="center", + color="#475569", + ) ): - with me.box( + me.text(h_label) + s = me.state(State) + arrow = ( + " ↓" if s.sort_descending else " ↑" + ) + arrow_str = ( + arrow + if s.sort_column == h_col + else "" + ) + me.text( + arrow_str, style=me.Style( - display="flex", - align_items="center", - justify_content="center", - color="#475569", - ) - ): - me.text(h_label) - s = me.state(State) - arrow = ( - " ↓" if s.sort_descending else " ↑" - ) - arrow_str = ( - arrow - if s.sort_column == h_col - else "" - ) - me.text( - arrow_str, - style=me.Style( - font_weight="bold", - color="#0284c7", - font_size="14px", - margin=me.Margin(left="4px"), - ), - ) - + font_weight="bold", + color="#0284c7", + font_size="14px", + margin=me.Margin(left="4px"), + ), + ) + + with me.box( + style=me.Style( + max_height="600px", + overflow_y="auto", + margin=me.Margin(top="16px"), + display="table", + width="100%", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e5e7eb", + style="solid", + ) + ), + border_radius="8px", + background="#ffffff", + ) + ): + # Header row with me.box( style=me.Style( - max_height="600px", - overflow_y="auto", - margin=me.Margin(top="16px"), - display="table", - width="100%", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e5e7eb", - style="solid", - ) - ), - border_radius="8px", - background="#ffffff", + display="table-row", + background="#f8fafc", + font_weight="bold", + color="#475569", + font_size="12px", + text_transform="uppercase", + letter_spacing="0.05em", ) ): - # Header row + headers = [ + ("Eval ID", "id", "36ch"), + ("Date", "date", "24ch"), + ("Product", "product", None), + ("Requester", "requester", None), + ( + "Trajectory Matcher", + "trajectory_matcher", + "18ch", + ), + ("Turn Count", "turn_count", "12ch"), + ("Executable", "executable", "12ch"), + ( + "Token Consumption", + "token_consumption", + "16ch", + ), + ( + "End-to-End Latency", + "end_to_end_latency", + "20ch", + ), + ] + for label, col, width in headers: + render_header_cell(label, col, width) + + # Data rows + for idx, item in enumerate(summaries): + d = item["id"] + date_val = item.get("date", "N/A") + prod = item["product"] + req_val = item.get("requester", "N/A") + traj = item.get("trajectory_matcher", "N/A") + turns = item.get("turn_count", "N/A") + exec_val = item.get("executable", "N/A") + tokens = item.get("token_consumption", "N/A") + latency = item.get("end_to_end_latency", "N/A") + + bg_color = ( + "#ffffff" + if idx % 2 == 0 + else "#f8fafc" + ) + + def make_on_click(dir_name): + def on_click(e: me.ClickEvent): + import logging + logging.info(f"CLICKED EVAL ID: {dir_name}") + s = me.state(State) + s.selected_directory = dir_name + return on_click + with me.box( style=me.Style( display="table-row", - background="#f8fafc", - font_weight="bold", - color="#475569", - font_size="12px", - text_transform="uppercase", - letter_spacing="0.05em", + background=bg_color, ) ): - headers = [ - ("Eval ID", "id", "36ch"), - ("Date", "date", "24ch"), - ("Product", "product", None), - ("Requester", "requester", None), - ( - "Trajectory Matcher", - "trajectory_matcher", - "18ch", - ), - ("Turn Count", "turn_count", "12ch"), - ("Executable", "executable", "12ch"), - ( - "Token Consumption", - "token_consumption", - "16ch", - ), - ( - "End-to-End Latency", - "end_to_end_latency", - "20ch", - ), - ] - for label, col, width in headers: - render_header_cell(label, col, width) - - # Data rows - for idx, item in enumerate(summaries): - d = item["id"] - date_val = item.get("date", "N/A") - prod = item["product"] - req_val = item.get("requester", "N/A") - traj = item.get("trajectory_matcher", "N/A") - turns = item.get("turn_count", "N/A") - exec_val = item.get("executable", "N/A") - tokens = item.get("token_consumption", "N/A") - latency = item.get("end_to_end_latency", "N/A") - - bg_color = ( - "#ffffff" - if idx % 2 == 0 - else "#f8fafc" - ) - - def make_on_click(dir_name): - def on_click(e: me.ClickEvent): - s = me.state(State) - s.selected_directory = dir_name - return on_click - + # Eval ID as a link/button with me.box( style=me.Style( - display="table-row", - background=bg_color, + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="36ch", + white_space="nowrap", ) ): - # Eval ID as a link/button with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - width="36ch", - white_space="nowrap", + display="flex", + justify_content="center", + width="100%", ) ): - me.button( - d, - on_click=make_on_click(d), - style=me.Style( - text_align="center", - background="transparent", - color="#0284c7", - font_family="monospace", - font_size="14px", - padding=me.Padding.all("0px"), - margin=me.Margin.all("0px"), - border=me.Border.all( - me.BorderSide(width="0px") - ), - font_weight="500", - width="100%", - ), - ) - with me.box( + me.markdown(f'{d}') + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="24ch", + white_space="nowrap", + ) + ): + me.text( + date_val, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - width="24ch", - white_space="nowrap", - ) - ): - me.text( - date_val, - style=me.Style( - color="#334155", - font_family="monospace", - ), - ) - with me.box( + color="#334155", + font_family="monospace", + ), + ) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + prod, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - prod, - style=me.Style( - color="#334155" - ), - ) - - with me.box( + color="#334155" + ), + ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + req_val, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - req_val, - style=me.Style( - color="#334155" - ), - ) - - with me.box( + color="#334155" + ), + ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="18ch", + white_space="nowrap", + ) + ): + me.text( + traj, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - width="18ch", - white_space="nowrap", - ) - ): - me.text( - traj, - style=me.Style( - color=get_color_for_pct(traj) - ), - ) - - with me.box( + color=get_color_for_pct(traj) + ), + ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + turns, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - turns, - style=me.Style( - color="#334155" - ), - ) - - with me.box( + color="#334155" + ), + ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + exec_val, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - exec_val, - style=me.Style( - color=get_color_for_pct(exec_val) - ), - ) - - with me.box( + color=get_color_for_pct(exec_val) + ), + ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + tokens, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - tokens, - style=me.Style( - color="#334155" - ), - ) - - with me.box( + color="#334155" + ), + ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + latency, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - latency, - style=me.Style( - color="#334155" - ), - ) + color="#334155" + ), + ) + + +@me.page( + path="/", + title="EvalBench Viewer", + on_load=on_load, + security_policy=me.SecurityPolicy( + dangerously_disable_trusted_types=True, + cross_origin_opener_policy="same-origin", + ), + stylesheets=["/static/style.css"], +) +def app(): + with me.box( + style=me.Style( + background="#f8fafc", + min_height="100vh", + width="100%", + ) + ): + render_app_content() + + +def render_app_content(): + try: + state = me.state(State) + results_dir = get_results_dir() + import logging + logging.info(f"render_app_content: selected_directory='{state.selected_directory}'") + + directories = [] + if os.path.exists(results_dir): + # List directories only + directories = [ + d + for d in os.listdir(results_dir) + if os.path.isdir(os.path.join(results_dir, d)) + ] + + def on_title_click(e: me.ClickEvent): + state.selected_directory = "" + state.conversation_index = 0 + me.navigate("/") + + # Full-width header bar + with me.box( + style=me.Style( + background="#1e293b", + padding=me.Padding.symmetric(vertical="16px", horizontal="5%"), + margin=me.Margin(bottom="24px"), + display="flex", + justify_content="space-between", + align_items="center", + ) + ): + me.button( + "EvalBench Viewer", + on_click=on_title_click, + style=me.Style( + color="#f8fafc", + font_size="22px", + font_weight="700", + letter_spacing="0.5px", + background="transparent", + padding=me.Padding.all("0px"), + margin=me.Margin.all("0px"), + border=me.Border.all(me.BorderSide(width="0px")), + text_align="left", + ), + ) + + if GIT_VERSION != "unknown": + with me.box( + style=me.Style( + font_size="12px", + color="#94a3b8", + ) + ): + me.markdown( + f"[Git: {GIT_VERSION}](https://github.com/GoogleCloudPlatform/evalbench/commit/{GIT_VERSION})" + ) + + # Centered content at 90% browser width + with me.box( + style=me.Style( + width="90%", + margin=me.Margin.symmetric(horizontal="auto"), + display="flex", + flex_direction="column", + gap="16px", + background="#ffffff", + padding=me.Padding.all("24px"), + border_radius="8px", + box_shadow="0 4px 6px -1px rgba(0, 0, 0, 0.1)", + color="#1e293b", + ) + ): + # Cache freshness indicator + import time + cache_file = os.path.join(results_dir, "trends_cache.csv") + cache_status = "Not Ready" + cache_color = "#ef4444" # Red + + if os.path.exists(cache_file): + mtime = os.path.getmtime(cache_file) + elapsed = time.time() - mtime + if elapsed < 600: # 10 minutes + cache_status = "Fresh" + cache_color = "#10b981" # Green + else: + cache_status = "Stale" + cache_color = "#f59e0b" # Yellow + + with me.box(style=me.Style(display="flex", align_items="center", gap="8px", font_size="12px", margin=me.Margin(bottom="8px"))): + me.box(style=me.Style(width="12px", height="12px", border_radius="50%", background=cache_color)) + me.text(f"Cache Status: {cache_status}", style=me.Style(font_weight="600", color="#64748b")) + + if state.selected_directory: + + def on_tab_change(e: me.ButtonToggleChangeEvent): + state.selected_tab = e.value + + me.button_toggle( + value=state.selected_tab, + buttons=[ + me.ButtonToggleButton( + label="Dashboard", value="Dashboard" + ), + me.ButtonToggleButton( + label="Configs", value="Configs" + ), + # me.ButtonToggleButton(label="Evals", value="Evals"), + # me.ButtonToggleButton(label="Scores", value="Scores"), + me.ButtonToggleButton( + label="Conversations", value="Conversations" + ), + # me.ButtonToggleButton(label="Summary", value="Summary"), + ], + on_change=on_tab_change, + ) + + if state.selected_tab == "Dashboard": + dashboard.dashboard_component( + os.path.join(results_dir, state.selected_directory) + ) + elif state.selected_tab == "Conversations": + + def on_prev_conversation(e: me.ClickEvent): + s = me.state(State) + if s.conversation_index > 0: + s.conversation_index -= 1 + + def on_next_conversation(e: me.ClickEvent): + s = me.state(State) + s.conversation_index += 1 + + conversations.conversations_component( + os.path.join(results_dir, state.selected_directory), + conversation_index=state.conversation_index, + on_prev=on_prev_conversation, + on_next=on_next_conversation, + ) + elif state.selected_tab == "Configs": + config_path = os.path.join( + results_dir, state.selected_directory, "configs.csv" + ) + if os.path.exists(config_path): + try: + df = pd.read_csv(config_path) + config = df_to_config(df) + me.code(yaml.dump(config)) + except Exception as e: + me.text(f"Error reading configs.csv: {e}") + else: + me.text( + f"configs.csv not found in {state.selected_directory}" + ) + elif state.selected_tab == "Evals": + evals_path = os.path.join( + results_dir, state.selected_directory, "evals.csv" + ) + if os.path.exists(evals_path): + try: + df = pd.read_csv(evals_path) + details = get_eval_details( + results_dir, state.selected_directory + ) + df.insert(0, "orchestrator", details["orchestrator"]) + me.table(data_frame=df) + except Exception as e: + me.text(f"Error reading evals.csv: {e}") + else: + me.text( + f"evals.csv not found in {state.selected_directory}" + ) + elif state.selected_tab == "Scores": + scores_path = os.path.join( + results_dir, state.selected_directory, "scores.csv" + ) + if os.path.exists(scores_path): + try: + df = pd.read_csv(scores_path) + me.table(data_frame=df) + except Exception as e: + me.text(f"Error reading scores.csv: {e}") + else: + me.text( + f"scores.csv not found in {state.selected_directory}" + ) + elif state.selected_tab == "Summary": + summary_path = os.path.join( + results_dir, state.selected_directory, "summary.csv" + ) + if os.path.exists(summary_path): + try: + df = pd.read_csv(summary_path) + me.table(data_frame=df) + except Exception as e: + me.text(f"Error reading summary.csv: {e}") + else: + me.text( + f"summary.csv not found in {state.selected_directory}" + ) + else: + from trends import trends_component + state = me.state(State) + + def on_main_tab_change(e: me.ButtonToggleChangeEvent): + me.state(State).selected_main_tab = e.value + + with me.box(style=me.Style(margin=me.Margin(bottom="16px"))): + me.button_toggle( + value=state.selected_main_tab, + buttons=[ + me.ButtonToggleButton(label="List", value="List"), + me.ButtonToggleButton(label="Charts", value="Charts"), + ], + on_change=on_main_tab_change, + ) + + if state.selected_main_tab == "List": + try: + list_view_component() + except Exception as e: + import logging + logging.exception("Failed to call list_view_component") + me.text(f"Error: {e}") + elif state.selected_main_tab == "Charts": + trends_component() + + except Exception as e: + import logging + logging.exception("render_app_content failed") + me.text(f"Fatal Error: {e}") if __name__ == "__main__": me.run(app) diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py new file mode 100644 index 00000000..d717aa8e --- /dev/null +++ b/viewer/precompute_trends.py @@ -0,0 +1,130 @@ +import io +import os +import logging +import json +import pandas as pd + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def get_results_dir(): + # Try to read from environment variable + res_dir = os.environ.get("RESULTS_DIR") + if res_dir: + return res_dir + + # Check multiple locations for results directory + results_dir_candidates = [ + "/tmp_session_files/results", + os.path.join(os.path.dirname(os.path.dirname(__file__)), "results"), + os.path.join(os.getcwd(), "results"), + ] + + for candidate in results_dir_candidates: + if os.path.exists(candidate) and os.path.isdir(candidate): + return candidate + + return results_dir_candidates[1] # Fallback to default + +def precompute(): + results_dir = get_results_dir() + logging.info(f"Reading results from {results_dir}") + + if not os.path.exists(results_dir): + logging.warning(f"Results directory not found at {results_dir}") + return + + directories = [ + d + for d in os.listdir(results_dir) + if os.path.isdir(os.path.join(results_dir, d)) + ] + + data = [] + products = set() + requesters = set() + eval_ids = [] + + total_dirs = len(directories) + logging.info(f"Found {total_dirs} directories to process.") + + for i, d in enumerate(directories): + eval_ids.append(d) + + if (i + 1) % 10 == 0 or i == total_dirs - 1: + logging.info(f"Precompute progress: {(i + 1) / total_dirs * 100:.1f}% ({i + 1}/{total_dirs})") + run_dir = os.path.join(results_dir, d) + configs_file = os.path.join(run_dir, "configs.csv") + summary_file = os.path.join(run_dir, "summary.csv") + + if os.path.exists(configs_file) and os.path.exists(summary_file): + try: + # Read configs + configs_df = pd.read_csv(configs_file) + + # Extract requester and product + requester_row = configs_df[configs_df['config'].str.contains('guitar_requester', na=False)] + product_row = configs_df[configs_df['config'].isin(['experiment_config.product_name', 'experiment_config.poduct_name'])] + + requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" + product = product_row['value'].values[0] if not product_row.empty else "unknown" + + if product != "unknown" and str(product).strip() != "": + products.add(product) + if requester != "unknown" and str(requester).strip() != "": + requesters.add(requester) + + # Read summary + summary_df = pd.read_csv(summary_file) + + # Extract metrics + latency_row = summary_df[summary_df['metric_name'] == 'end_to_end_latency'] + token_row = summary_df[summary_df['metric_name'] == 'token_consumption'] + trajectory_row = summary_df[summary_df['metric_name'] == 'trajectory_matcher'] + + latency = float(latency_row['metric_score'].values[0]) if not latency_row.empty else 0.0 + tokens = float(token_row['metric_score'].values[0]) if not token_row.empty else 0.0 + trajectory = float(trajectory_row['metric_score'].values[0]) if not trajectory_row.empty else 0.0 + + run_time = summary_df['run_time'].values[0] if not summary_df.empty else "unknown" + if run_time != "unknown": + try: + run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') + except: + pass + + data.append({ + 'run_time': run_time, + 'requester': requester, + 'product': product, + 'latency': latency, + 'tokens': tokens, + 'trajectory': trajectory, + 'job_id': d + }) + except Exception as e: + print(f"Error reading data from {d}: {e}") + + if not data: + logging.warning("No data found in any run directory.") + return + + df = pd.DataFrame(data) + + # Save trends cache + cache_file = os.path.join(results_dir, "trends_cache.csv") + df.to_csv(cache_file, index=False) + logging.info(f"Precomputed trends data saved to {cache_file}") + + # Save filters cache + filters_file = os.path.join(results_dir, "filters_cache.json") + filters_data = { + "products": sorted(list(products)), + "requesters": sorted(list(requesters)), + "eval_ids": sorted(eval_ids) + } + with open(filters_file, "w") as f: + json.dump(filters_data, f, indent=2) + logging.info(f"Precomputed filter values saved to {filters_file}") + +if __name__ == "__main__": + precompute() diff --git a/viewer/pyproject.toml b/viewer/pyproject.toml index 1579fa3f..2267c954 100644 --- a/viewer/pyproject.toml +++ b/viewer/pyproject.toml @@ -7,7 +7,10 @@ requires-python = ">=3.10" dependencies = [ "gevent>=25.9.1", "gunicorn>=25.1.0", + "matplotlib>=3.10.8", "mesop>=1.2.1", "pandas>=2.3.3", "pyaml>=26.2.1", + "plotly>=5.18.0", + "kaleido>=0.2.1", ] diff --git a/viewer/run_precompute.py b/viewer/run_precompute.py new file mode 100644 index 00000000..a2db33e1 --- /dev/null +++ b/viewer/run_precompute.py @@ -0,0 +1,19 @@ +import time +import sys +import os + +# Add the directory containing precompute_trends to the path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +import precompute_trends + +def main(): + while True: + try: + precompute_trends.precompute() + except Exception as e: + print(f"Error in precompute: {e}") + time.sleep(300) + +if __name__ == "__main__": + main() diff --git a/viewer/state.py b/viewer/state.py new file mode 100644 index 00000000..66f9229e --- /dev/null +++ b/viewer/state.py @@ -0,0 +1,16 @@ +import mesop as me + +@me.stateclass +class State: + selected_directory: str = "" + selected_tab: str = "Dashboard" + conversation_index: int = 0 + eval_summaries: str = "" + eval_id_filter: str = "" + product_filter: str = "" + requester_filter: str = "" + sort_column: str = "date" + sort_descending: bool = True + open_dropdown: str = "" + selected_main_tab: str = "List" + trends_product_filter: str = "" diff --git a/viewer/static/style.css b/viewer/static/style.css new file mode 100644 index 00000000..f277031f --- /dev/null +++ b/viewer/static/style.css @@ -0,0 +1,15 @@ +.pill-link { + display: inline-flex; + align-items: center; + background-color: #e0f2fe; + color: #0369a1; + border-radius: 9999px; + padding: 4px 12px; + text-decoration: none; + font-weight: 600; + font-size: 12px; + font-family: monospace; +} +.pill-link:hover { + background-color: #bae6fd; +} diff --git a/viewer/static/custom.css b/viewer/static/styles.css similarity index 100% rename from viewer/static/custom.css rename to viewer/static/styles.css diff --git a/viewer/trends.py b/viewer/trends.py new file mode 100644 index 00000000..e5f712f5 --- /dev/null +++ b/viewer/trends.py @@ -0,0 +1,233 @@ +import io +import os +import logging +import mesop as me +import pandas as pd +import plotly.express as px +from state import State + +def get_results_dir(): + # Try to read from environment variable + res_dir = os.environ.get("RESULTS_DIR") + if res_dir: + return res_dir + + # Check multiple locations for results directory + results_dir_candidates = [ + "/tmp_session_files/results", + os.path.join(os.path.dirname(os.path.dirname(__file__)), "results"), + os.path.join(os.getcwd(), "results"), + ] + + for candidate in results_dir_candidates: + if os.path.exists(candidate) and os.path.isdir(candidate): + return candidate + + return results_dir_candidates[1] # Fallback to default + +def generate_plotly_chart(df, x_col, y_col, hue_col, title, ylabel): + df_sorted = df.sort_values(by=x_col) + + fig = px.line( + df_sorted, + x=x_col, + y=y_col, + color=hue_col, + title=title, + labels={y_col: ylabel, x_col: "Run Time"}, + markers=True + ) + + fig.update_layout( + autosize=True, + width=None, + height=500, + margin=dict(l=40, r=40, t=60, b=40), + legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) + ) + + return fig.to_html(full_html=True, include_plotlyjs='cdn') + +def trends_component(): + results_dir = get_results_dir() + + if not os.path.exists(results_dir): + me.text(f"Results directory not found at {results_dir}") + return + + cache_file = os.path.join(results_dir, "trends_cache.csv") + + df = None + + # Try to load from cache + if os.path.exists(cache_file): + try: + df = pd.read_csv(cache_file) + logging.info("Loaded trends data from cache.") + except Exception as e: + logging.error(f"Error reading cache file: {e}") + + # Fallback to computing on the fly if cache is missing or failed + if df is None: + directories = [ + d + for d in os.listdir(results_dir) + if os.path.isdir(os.path.join(results_dir, d)) + ] + + data = [] + + for d in directories: + run_dir = os.path.join(results_dir, d) + configs_file = os.path.join(run_dir, "configs.csv") + summary_file = os.path.join(run_dir, "summary.csv") + + if os.path.exists(configs_file) and os.path.exists(summary_file): + try: + configs_df = pd.read_csv(configs_file) + + requester_row = configs_df[configs_df['config'].str.contains('guitar_requester', na=False)] + product_row = configs_df[configs_df['config'].isin(['experiment_config.product_name', 'experiment_config.poduct_name'])] + + requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" + product = product_row['value'].values[0] if not product_row.empty else "unknown" + + summary_df = pd.read_csv(summary_file) + + latency_row = summary_df[summary_df['metric_name'] == 'end_to_end_latency'] + token_row = summary_df[summary_df['metric_name'] == 'token_consumption'] + trajectory_row = summary_df[summary_df['metric_name'] == 'trajectory_matcher'] + + latency = float(latency_row['metric_score'].values[0]) if not latency_row.empty else 0.0 + tokens = float(token_row['metric_score'].values[0]) if not token_row.empty else 0.0 + trajectory = float(trajectory_row['metric_score'].values[0]) if not trajectory_row.empty else 0.0 + + run_time = summary_df['run_time'].values[0] if not summary_df.empty else "unknown" + if run_time != "unknown": + try: + run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') + except: + pass + + data.append({ + 'run_time': run_time, + 'requester': requester, + 'product': product, + 'latency': latency, + 'tokens': tokens, + 'trajectory': trajectory, + 'job_id': d + }) + except Exception as e: + logging.error(f"Error reading data from {d}: {e}") + + if not data: + me.text("No data found in any run directory.") + return + + df = pd.DataFrame(data) + + # Filter by requester + df = df[df['requester'] == 'cloud-db-nl2sql-testing-jobs'] + + # Filter by product (remove unknown or empty) + df = df[df['product'].notna() & (df['product'] != 'unknown') & (df['product'].str.strip() != '')] + + # Extract unique products for dropdown + all_products = sorted(df['product'].unique().tolist()) + + state = me.state(State) + + # Apply filter if selected + if state.trends_product_filter: + df = df[df['product'] == state.trends_product_filter] + + if df.empty: + me.text("No data found for selected filters.") + return + + # Generate charts + latency_chart = generate_plotly_chart(df, 'run_time', 'latency', 'product', 'Latency Trend', 'Latency (ms)') + token_chart = generate_plotly_chart(df, 'run_time', 'tokens', 'product', 'Token Consumption Trend', 'Tokens') + trajectory_chart = generate_plotly_chart(df, 'run_time', 'trajectory', 'product', 'Trajectory Score Trend', 'Score (%)') + + # Render charts + with me.box(style=me.Style(display="flex", flex_direction="column", gap="24px", padding=me.Padding.all("24px"), width="100%")): + me.text("Trends for cloud-db-nl2sql-testing-jobs", style=me.Style(font_size="20px", font_weight="700")) + + # Render custom dropdown + def toggle_trends_product_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "trends_product": + st.open_dropdown = "" + else: + st.open_dropdown = "trends_product" + + def make_product_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.trends_product_filter = val + st.open_dropdown = "" + handler.__name__ = f"click_trends_product_{val}" + return handler + + with me.box(style=me.Style(display="flex", align_items="center", gap="8px", margin=me.Margin(bottom="16px"))): + me.text("Filter by Product:", style=me.Style(font_weight="600")) + + with me.box(style=me.Style(position="relative", width="200px")): + # Trigger + with me.box( + style=me.Style( + background="#ffffff", + border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_trends_product_dropdown, + ): + me.text( + state.trends_product_filter if state.trends_product_filter else "All Products", + style=me.Style(color="#1f2937"), + ) + + # Popup + if state.open_dropdown == "trends_product": + with me.box( + style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, + background="#ffffff", + border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), + border_radius="4px", + width="100%", + max_height="200px", + overflow_y="auto", + ) + ): + # All option + with me.box( + style=me.Style(padding=me.Padding.all("8px"), cursor="pointer"), + on_click=make_product_handler(""), + ): + me.text("All Products", style=me.Style(color="#1f2937")) + + # Product options + for p in all_products: + with me.box( + style=me.Style(padding=me.Padding.all("8px"), cursor="pointer"), + on_click=make_product_handler(p), + ): + me.text(p, style=me.Style(color="#1f2937")) + + with me.box(style=me.Style(display="flex", flex_direction="column", gap="16px", width="100%")): + me.text("Latency", style=me.Style(font_size="16px", font_weight="600")) + me.html(latency_chart, mode="sandboxed", style=me.Style(width="100%", height="550px")) + + me.text("Token Consumption", style=me.Style(font_size="16px", font_weight="600")) + me.html(token_chart, mode="sandboxed", style=me.Style(width="100%", height="550px")) + + me.text("Trajectory Score", style=me.Style(font_size="16px", font_weight="600")) + me.html(trajectory_chart, mode="sandboxed", style=me.Style(width="100%", height="550px")) From 610385ebc75b18bc59e2041930f1fcd0cf0b2d21 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 23:16:05 +0000 Subject: [PATCH 10/44] feat: implement incremental precomputation of trends with persistent cache and directory state tracking --- viewer/main.py | 1481 +++++++++++++++++------------------ viewer/precompute_trends.py | 79 +- 2 files changed, 801 insertions(+), 759 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 6b83c052..82fa1720 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -208,243 +208,263 @@ def on_load(e: me.LoadEvent): -def list_view_component(): - global json - import os +def list_view_component(directories, results_dir): state = me.state(State) - results_dir = get_results_dir() - directories = [] - if os.path.exists(results_dir): - directories = [ - d - for d in os.listdir(results_dir) - if os.path.isdir(os.path.join(results_dir, d)) - ] - with me.box( + with me.box( + style=me.Style( + background="#ffffff", + padding=me.Padding.all("24px"), + border_radius="12px", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e5e7eb", + style="solid", + ) + ), + box_shadow="0 1px 3px rgba(0,0,0,0.06)", + text_align="center", + margin=me.Margin(top="16px"), + ) + ): + me.text( + "Welcome to EvalBench Viewer", style=me.Style( - background="#ffffff", - padding=me.Padding.all("24px"), - border_radius="12px", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e5e7eb", - style="solid", - ) - ), - box_shadow="0 1px 3px rgba(0,0,0,0.06)", - text_align="center", - margin=me.Margin(top="16px"), - ) - ): - me.text( - "Welcome to EvalBench Viewer", - style=me.Style( - font_size="24px", - font_weight="700", - color="#1f2937", - margin=me.Margin(bottom="8px"), - ), - ) - me.text( - f"Found {len(directories)} evaluation runs. " - "Click on an Eval ID in the table below to explore " - "the results.", - style=me.Style( - font_size="16px", - color="#6b7280", - margin=me.Margin(bottom="16px"), - ), - ) - if directories: - # Compute summaries if empty - s = me.state(State) - summaries = [] - if s.eval_summaries: - try: - summaries = json.loads(s.eval_summaries) - except Exception: - summaries = [] - - if not summaries: - for d in sorted(directories): - details = get_eval_details(results_dir, d) - summaries.append({ - "id": d, - "date": details.get("date", "N/A"), - "product": details["product"], - "requester": details.get("requester", "N/A"), - "exact_match": details["exact_match"], - "llmrater": details["llmrater"], - "trajectory_matcher": details[ - "trajectory_matcher" - ], - "turn_count": details["turn_count"], - "executable": details["executable"], - "token_consumption": details[ - "token_consumption" - ], - "end_to_end_latency": details[ - "end_to_end_latency" - ] - }) - s.eval_summaries = json.dumps(summaries) - - # Sort by selected column - reverse = state.sort_descending - col = state.sort_column - - def get_sort_key(x): - val = x.get(col, "N/A") - - # Handle numbers and percentages - if col in [ - "exact_match", - "llmrater", - "trajectory_matcher", - "executable", - ]: - if val == "N/A": - return -1.0 if reverse else 101.0 - if val.endswith("%"): - try: - return float(val.rstrip("%")) - except ValueError: - return -1.0 if reverse else 101.0 + font_size="24px", + font_weight="700", + color="#1f2937", + margin=me.Margin(bottom="8px"), + ), + ) + me.text( + f"Found {len(directories)} evaluation runs. " + "Click on an Eval ID in the table below to explore " + "the results.", + style=me.Style( + font_size="16px", + color="#6b7280", + margin=me.Margin(bottom="16px"), + ), + ) + if directories: + # Compute summaries if empty + s = me.state(State) + summaries = [] + if s.eval_summaries: + try: + summaries = json.loads(s.eval_summaries) + except Exception: + summaries = [] + + if not summaries: + for d in sorted(directories): + details = get_eval_details(results_dir, d) + summaries.append({ + "id": d, + "date": details.get("date", "N/A"), + "product": details["product"], + "requester": details.get("requester", "N/A"), + "exact_match": details["exact_match"], + "llmrater": details["llmrater"], + "trajectory_matcher": details[ + "trajectory_matcher" + ], + "turn_count": details["turn_count"], + "executable": details["executable"], + "token_consumption": details[ + "token_consumption" + ], + "end_to_end_latency": details[ + "end_to_end_latency" + ] + }) + s.eval_summaries = json.dumps(summaries) + + # Sort by selected column + reverse = state.sort_descending + col = state.sort_column + + def get_sort_key(x): + val = x.get(col, "N/A") + + # Handle numbers and percentages + if col in [ + "exact_match", + "llmrater", + "trajectory_matcher", + "executable", + ]: + if val == "N/A": return -1.0 if reverse else 101.0 - - elif col in [ - "turn_count", - "token_consumption", - "end_to_end_latency", - ]: - if val == "N/A": - return -1.0 if reverse else 1e12 + if val.endswith("%"): try: - return float(val) + return float(val.rstrip("%")) except ValueError: - return -1.0 if reverse else 1e12 + return -1.0 if reverse else 101.0 + return -1.0 if reverse else 101.0 - # String columns (product, requester, id, date) + elif col in [ + "turn_count", + "token_consumption", + "end_to_end_latency", + ]: if val == "N/A": - return "" if reverse else "\xff\xff\xff\xff" - return str(val) - - summaries.sort(key=get_sort_key, reverse=reverse) - - # Extract unique values for filters from ALL summaries - all_summaries = [] - if s.eval_summaries: - try: - all_summaries = json.loads(s.eval_summaries) - except Exception: - all_summaries = [] - - filters_file = os.path.join(results_dir, "filters_cache.json") - if os.path.exists(filters_file): + return -1.0 if reverse else 1e12 try: - with open(filters_file, "r") as f: - filters_data = json.load(f) - products = filters_data.get("products", []) - requesters = filters_data.get("requesters", []) - eval_ids = filters_data.get("eval_ids", []) - except Exception as e: - import logging - logging.error(f"Error reading filters cache: {e}") - products = [] - requesters = [] - eval_ids = [] - else: - products = sorted( - list( - set( - x["product"] - for x in all_summaries - if x["product"] != "N/A" - ) + return float(val) + except ValueError: + return -1.0 if reverse else 1e12 + + # String columns (product, requester, id, date) + if val == "N/A": + return "" if reverse else "\xff\xff\xff\xff" + return str(val) + + summaries.sort(key=get_sort_key, reverse=reverse) + + # Extract unique values for filters from ALL summaries + all_summaries = [] + if s.eval_summaries: + try: + all_summaries = json.loads(s.eval_summaries) + except Exception: + all_summaries = [] + + filters_file = os.path.join(results_dir, "filters_cache.json") + if os.path.exists(filters_file): + try: + with open(filters_file, "r") as f: + filters_data = json.load(f) + products = filters_data.get("products", []) + requesters = filters_data.get("requesters", []) + eval_ids = filters_data.get("eval_ids", []) + except Exception as e: + import logging + logging.error(f"Error reading filters cache: {e}") + products = [] + requesters = [] + eval_ids = [] + else: + products = sorted( + list( + set( + x["product"] + for x in all_summaries + if x["product"] != "N/A" ) ) - requesters = sorted( - list( - set( - x.get("requester", "N/A") - for x in all_summaries - if x.get("requester", "N/A") != "N/A" - ) + ) + requesters = sorted( + list( + set( + x.get("requester", "N/A") + for x in all_summaries + if x.get("requester", "N/A") != "N/A" ) ) - eval_ids = sorted([x["id"] for x in all_summaries]) - - # Apply filters - if state.eval_id_filter: - summaries = [ - x - for x in summaries - if x["id"] == state.eval_id_filter - ] - if state.product_filter: - summaries = [ - x - for x in summaries - if x["product"] == state.product_filter - ] - if state.requester_filter: - summaries = [ - x - for x in summaries - if x.get("requester", "N/A") - == state.requester_filter - ] + ) + eval_ids = sorted([x["id"] for x in all_summaries]) + + # Apply filters + if state.eval_id_filter: + summaries = [ + x + for x in summaries + if x["id"] == state.eval_id_filter + ] + if state.product_filter: + summaries = [ + x + for x in summaries + if x["product"] == state.product_filter + ] + if state.requester_filter: + summaries = [ + x + for x in summaries + if x.get("requester", "N/A") + == state.requester_filter + ] + + # Render filters UI + with me.box( + style=me.Style( + display="flex", + flex_direction="row", + gap="24px", + margin=me.Margin(top="16px", bottom="24px"), + padding=me.Padding.all("16px"), + background="#ffffff", + border_radius="12px", + box_shadow=( + "0 1px 3px 0 rgba(0, 0, 0, 0.1), " + "0 1px 2px -1px rgba(0, 0, 0, 0.1)" + ), + align_items="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + def toggle_eval_id_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "eval_id": + st.open_dropdown = "" + else: + st.open_dropdown = "eval_id" + + def make_eval_id_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.eval_id_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_eval_id_{val}" + return handler - # Render filters UI with me.box( style=me.Style( - display="flex", - flex_direction="row", - gap="24px", - margin=me.Margin(top="16px", bottom="24px"), - padding=me.Padding.all("16px"), - background="#ffffff", - border_radius="12px", - box_shadow=( - "0 1px 3px 0 rgba(0, 0, 0, 0.1), " - "0 1px 2px -1px rgba(0, 0, 0, 0.1)" - ), - align_items="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), + position="relative", + width="200px", ) ): - def toggle_eval_id_dropdown(e: me.ClickEvent): - st = me.state(State) - if st.open_dropdown == "eval_id": - st.open_dropdown = "" - else: - st.open_dropdown = "eval_id" - - def make_eval_id_handler(val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.eval_id_filter = val - st.open_dropdown = "" - - handler.__name__ = f"click_eval_id_{val}" - return handler - + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - position="relative", - width="200px", - ) + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_eval_id_dropdown, ): - # The Box acting as Dropdown Trigger + me.text( + state.eval_id_filter + if state.eval_id_filter + else "Select Eval ID", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "eval_id": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all( me.BorderSide( @@ -453,99 +473,99 @@ def handler(e: me.ClickEvent): ) ), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_eval_id_dropdown, - ): - me.text( - state.eval_id_filter - if state.eval_id_filter - else "Select Eval ID", - style=me.Style( - color="#1f2937" - ), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # The Popup List - if state.open_dropdown == "eval_id": + ): + # All option with me.box( style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - ) + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=make_eval_id_handler(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" ), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", ) - ): - # All option + + for d in eval_ids: with me.box( style=me.Style( padding=me.Padding.all("8px"), cursor="pointer", ), - on_click=make_eval_id_handler(""), + on_click=make_eval_id_handler(d), ): me.text( - "All", + d, style=me.Style( color="#1f2937" ), ) - for d in eval_ids: - with me.box( - style=me.Style( - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=make_eval_id_handler(d), - ): - me.text( - d, - style=me.Style( - color="#1f2937" - ), - ) - - # Product Filter with Floating Autocomplete - def toggle_product_dropdown(e: me.ClickEvent): - st = me.state(State) - if st.open_dropdown == "product": - st.open_dropdown = "" - else: - st.open_dropdown = "product" + # Product Filter with Floating Autocomplete + def toggle_product_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "product": + st.open_dropdown = "" + else: + st.open_dropdown = "product" - def make_prod_dropdown_handler(val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.product_filter = val - st.open_dropdown = "" + def make_prod_dropdown_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.product_filter = val + st.open_dropdown = "" - handler.__name__ = f"click_prod_dd_{val}" - return handler + handler.__name__ = f"click_prod_dd_{val}" + return handler - mk_prod_dd = make_prod_dropdown_handler + mk_prod_dd = make_prod_dropdown_handler + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - position="relative", - width="200px", - ) + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_product_dropdown, ): - # The Box acting as Dropdown Trigger + me.text( + state.product_filter + if state.product_filter + else "Filter by Product", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "product": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all( me.BorderSide( @@ -554,99 +574,99 @@ def handler(e: me.ClickEvent): ) ), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_product_dropdown, - ): - me.text( - state.product_filter - if state.product_filter - else "Filter by Product", - style=me.Style( - color="#1f2937" - ), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # The Popup List - if state.open_dropdown == "product": + ): + # All option with me.box( style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - ) + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_prod_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" ), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", ) - ): - # All option + + for p in products: with me.box( style=me.Style( padding=me.Padding.all("8px"), cursor="pointer", ), - on_click=mk_prod_dd(""), + on_click=mk_prod_dd(p), ): me.text( - "All", + p, style=me.Style( color="#1f2937" ), ) - for p in products: - with me.box( - style=me.Style( - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=mk_prod_dd(p), - ): - me.text( - p, - style=me.Style( - color="#1f2937" - ), - ) - - # Requester Filter with Floating Autocomplete - def toggle_requester_dropdown(e: me.ClickEvent): - st = me.state(State) - if st.open_dropdown == "requester": - st.open_dropdown = "" - else: - st.open_dropdown = "requester" + # Requester Filter with Floating Autocomplete + def toggle_requester_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "requester": + st.open_dropdown = "" + else: + st.open_dropdown = "requester" - def make_req_dropdown_handler(val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.requester_filter = val - st.open_dropdown = "" + def make_req_dropdown_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.requester_filter = val + st.open_dropdown = "" - handler.__name__ = f"click_req_dd_{val}" - return handler + handler.__name__ = f"click_req_dd_{val}" + return handler - mk_req_dd = make_req_dropdown_handler + mk_req_dd = make_req_dropdown_handler + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger with me.box( style=me.Style( - position="relative", - width="200px", - ) + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_requester_dropdown, ): - # The Box acting as Dropdown Trigger + me.text( + state.requester_filter + if state.requester_filter + else "Filter by Requester", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "requester": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all( me.BorderSide( @@ -655,470 +675,440 @@ def handler(e: me.ClickEvent): ) ), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_requester_dropdown, - ): - me.text( - state.requester_filter - if state.requester_filter - else "Filter by Requester", - style=me.Style( - color="#1f2937" - ), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # The Popup List - if state.open_dropdown == "requester": + ): + # All option with me.box( style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - ) + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_req_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" ), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", ) - ): - # All option + + for r in requesters: with me.box( style=me.Style( padding=me.Padding.all("8px"), cursor="pointer", ), - on_click=mk_req_dd(""), + on_click=mk_req_dd(r), ): me.text( - "All", + r, style=me.Style( color="#1f2937" ), ) - for r in requesters: - with me.box( - style=me.Style( - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=mk_req_dd(r), - ): - me.text( - r, - style=me.Style( - color="#1f2937" - ), - ) - - def on_sort_click(col_name): - s = me.state(State) - if s.sort_column == col_name: - s.sort_descending = not s.sort_descending - else: - s.sort_column = col_name - s.sort_descending = True + def on_sort_click(col_name): + s = me.state(State) + if s.sort_column == col_name: + s.sort_descending = not s.sort_descending + else: + s.sort_column = col_name + s.sort_descending = True - def click_id(e): - on_sort_click("id") + def click_id(e): + on_sort_click("id") - def click_date(e): - on_sort_click("date") + def click_date(e): + on_sort_click("date") - def click_product(e): - on_sort_click("product") + def click_product(e): + on_sort_click("product") - def click_requester(e): - on_sort_click("requester") + def click_requester(e): + on_sort_click("requester") - def click_traj(e): - on_sort_click("trajectory_matcher") + def click_traj(e): + on_sort_click("trajectory_matcher") - def click_turns(e): - on_sort_click("turn_count") + def click_turns(e): + on_sort_click("turn_count") - def click_exec(e): - on_sort_click("executable") + def click_exec(e): + on_sort_click("executable") - def click_tokens(e): - on_sort_click("token_consumption") + def click_tokens(e): + on_sort_click("token_consumption") - def click_latency(e): - on_sort_click("end_to_end_latency") + def click_latency(e): + on_sort_click("end_to_end_latency") - sort_handlers = { - "id": click_id, - "date": click_date, - "product": click_product, - "requester": click_requester, - "trajectory_matcher": click_traj, - "turn_count": click_turns, - "executable": click_exec, - "token_consumption": click_tokens, - "end_to_end_latency": click_latency, - } - - def render_header_cell(h_label, h_col, h_width): - with me.box( - style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="12px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - cursor="pointer", - width=h_width, - white_space="nowrap" if h_width else None, - background="#f8fafc", - ), - on_click=sort_handlers[h_col], - ): - with me.box( - style=me.Style( - display="flex", - align_items="center", - justify_content="center", - color="#475569", - ) - ): - me.text(h_label) - s = me.state(State) - arrow = ( - " ↓" if s.sort_descending else " ↑" - ) - arrow_str = ( - arrow - if s.sort_column == h_col - else "" - ) - me.text( - arrow_str, - style=me.Style( - font_weight="bold", - color="#0284c7", - font_size="14px", - margin=me.Margin(left="4px"), - ), - ) + sort_handlers = { + "id": click_id, + "date": click_date, + "product": click_product, + "requester": click_requester, + "trajectory_matcher": click_traj, + "turn_count": click_turns, + "executable": click_exec, + "token_consumption": click_tokens, + "end_to_end_latency": click_latency, + } + def render_header_cell(h_label, h_col, h_width): with me.box( style=me.Style( - max_height="600px", - overflow_y="auto", - margin=me.Margin(top="16px"), - display="table", - width="100%", + display="table-cell", + padding=me.Padding.symmetric( + vertical="12px", horizontal="16px" + ), + text_align="center", border=me.Border.all( me.BorderSide( width="1px", - color="#e5e7eb", + color="#e2e8f0", style="solid", ) ), - border_radius="8px", - background="#ffffff", - ) + cursor="pointer", + width=h_width, + white_space="nowrap" if h_width else None, + background="#f8fafc", + ), + on_click=sort_handlers[h_col], ): - # Header row with me.box( style=me.Style( - display="table-row", - background="#f8fafc", - font_weight="bold", + display="flex", + align_items="center", + justify_content="center", color="#475569", - font_size="12px", - text_transform="uppercase", - letter_spacing="0.05em", ) ): - headers = [ - ("Eval ID", "id", "36ch"), - ("Date", "date", "24ch"), - ("Product", "product", None), - ("Requester", "requester", None), - ( - "Trajectory Matcher", - "trajectory_matcher", - "18ch", - ), - ("Turn Count", "turn_count", "12ch"), - ("Executable", "executable", "12ch"), - ( - "Token Consumption", - "token_consumption", - "16ch", - ), - ( - "End-to-End Latency", - "end_to_end_latency", - "20ch", + me.text(h_label) + s = me.state(State) + arrow = ( + " ↓" if s.sort_descending else " ↑" + ) + arrow_str = ( + arrow + if s.sort_column == h_col + else "" + ) + me.text( + arrow_str, + style=me.Style( + font_weight="bold", + color="#0284c7", + font_size="14px", + margin=me.Margin(left="4px"), ), - ] - for label, col, width in headers: - render_header_cell(label, col, width) - - # Data rows - for idx, item in enumerate(summaries): - d = item["id"] - date_val = item.get("date", "N/A") - prod = item["product"] - req_val = item.get("requester", "N/A") - traj = item.get("trajectory_matcher", "N/A") - turns = item.get("turn_count", "N/A") - exec_val = item.get("executable", "N/A") - tokens = item.get("token_consumption", "N/A") - latency = item.get("end_to_end_latency", "N/A") - - bg_color = ( - "#ffffff" - if idx % 2 == 0 - else "#f8fafc" ) - def make_on_click(dir_name): - def on_click(e: me.ClickEvent): - import logging - logging.info(f"CLICKED EVAL ID: {dir_name}") - s = me.state(State) - s.selected_directory = dir_name - return on_click + with me.box( + style=me.Style( + max_height="600px", + overflow_y="auto", + margin=me.Margin(top="16px"), + display="table", + width="100%", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e5e7eb", + style="solid", + ) + ), + border_radius="8px", + background="#ffffff", + ) + ): + # Header row + with me.box( + style=me.Style( + display="table-row", + background="#f8fafc", + font_weight="bold", + color="#475569", + font_size="12px", + text_transform="uppercase", + letter_spacing="0.05em", + ) + ): + headers = [ + ("Eval ID", "id", "36ch"), + ("Date", "date", "24ch"), + ("Product", "product", None), + ("Requester", "requester", None), + ( + "Trajectory Matcher", + "trajectory_matcher", + "18ch", + ), + ("Turn Count", "turn_count", "12ch"), + ("Executable", "executable", "12ch"), + ( + "Token Consumption", + "token_consumption", + "16ch", + ), + ( + "End-to-End Latency", + "end_to_end_latency", + "20ch", + ), + ] + for label, col, width in headers: + render_header_cell(label, col, width) + + # Data rows + for idx, item in enumerate(summaries): + d = item["id"] + date_val = item.get("date", "N/A") + prod = item["product"] + req_val = item.get("requester", "N/A") + traj = item.get("trajectory_matcher", "N/A") + turns = item.get("turn_count", "N/A") + exec_val = item.get("executable", "N/A") + tokens = item.get("token_consumption", "N/A") + latency = item.get("end_to_end_latency", "N/A") + + bg_color = ( + "#ffffff" + if idx % 2 == 0 + else "#f8fafc" + ) + def make_on_click(dir_name): + def on_click(e: me.ClickEvent): + import logging + logging.info(f"CLICKED EVAL ID: {dir_name}") + s = me.state(State) + s.selected_directory = dir_name + return on_click + + with me.box( + style=me.Style( + display="table-row", + background=bg_color, + ) + ): + # Eval ID as a link/button with me.box( style=me.Style( - display="table-row", - background=bg_color, + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="36ch", + white_space="nowrap", ) ): - # Eval ID as a link/button with me.box( style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - width="36ch", - white_space="nowrap", + display="flex", + justify_content="center", + width="100%", ) ): - with me.box( - style=me.Style( - display="flex", - justify_content="center", - width="100%", + me.markdown(f'{d}') + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", ) - ): - me.markdown(f'{d}') - with me.box( + ), + width="24ch", + white_space="nowrap", + ) + ): + me.text( + date_val, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - width="24ch", - white_space="nowrap", - ) - ): - me.text( - date_val, - style=me.Style( - color="#334155", - font_family="monospace", - ), - ) - with me.box( + color="#334155", + font_family="monospace", + ), + ) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + prod, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - prod, - style=me.Style( - color="#334155" - ), - ) + color="#334155" + ), + ) - with me.box( + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + req_val, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - req_val, - style=me.Style( - color="#334155" - ), - ) + color="#334155" + ), + ) - with me.box( + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="18ch", + white_space="nowrap", + ) + ): + me.text( + traj, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - width="18ch", - white_space="nowrap", - ) - ): - me.text( - traj, - style=me.Style( - color=get_color_for_pct(traj) - ), - ) + color=get_color_for_pct(traj) + ), + ) - with me.box( + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + turns, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - turns, - style=me.Style( - color="#334155" - ), - ) + color="#334155" + ), + ) - with me.box( + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + exec_val, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - exec_val, - style=me.Style( - color=get_color_for_pct(exec_val) - ), - ) + color=get_color_for_pct(exec_val) + ), + ) - with me.box( + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + tokens, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - tokens, - style=me.Style( - color="#334155" - ), - ) + color="#334155" + ), + ) - with me.box( + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + latency, style=me.Style( - display="table-cell", - padding=me.Padding.symmetric( - vertical="10px", horizontal="16px" - ), - text_align="center", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e2e8f0", - style="solid", - ) - ), - ) - ): - me.text( - latency, - style=me.Style( - color="#334155" - ), - ) + color="#334155" + ), + ) @@ -1363,13 +1353,14 @@ def on_main_tab_change(e: me.ButtonToggleChangeEvent): if state.selected_main_tab == "List": try: - list_view_component() + list_view_component(directories, results_dir) except Exception as e: import logging logging.exception("Failed to call list_view_component") me.text(f"Error: {e}") elif state.selected_main_tab == "Charts": trends_component() + except Exception as e: import logging diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index d717aa8e..2757250d 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -33,25 +33,60 @@ def precompute(): logging.warning(f"Results directory not found at {results_dir}") return - directories = [ + # Load processed directories + processed_dirs_file = os.path.join(results_dir, "processed_dirs.json") + processed_dirs = set() + if os.path.exists(processed_dirs_file): + try: + with open(processed_dirs_file, "r") as f: + processed_dirs = set(json.load(f)) + logging.info(f"Loaded {len(processed_dirs)} processed directories from state.") + except Exception as e: + logging.error(f"Error reading processed dirs file: {e}") + + # Load existing trends data + cache_file = os.path.join(results_dir, "trends_cache.csv") + existing_df = pd.DataFrame() + if os.path.exists(cache_file): + try: + existing_df = pd.read_csv(cache_file) + logging.info(f"Loaded {len(existing_df)} rows of existing trends data.") + except Exception as e: + logging.error(f"Error reading trends cache: {e}") + + all_directories = [ d for d in os.listdir(results_dir) if os.path.isdir(os.path.join(results_dir, d)) ] + # Filter for new directories + new_directories = [d for d in all_directories if d not in processed_dirs] + + total_new = len(new_directories) + logging.info(f"Found {len(all_directories)} total directories. {total_new} are new.") + + if total_new == 0: + logging.info("No new directories to process.") + return + data = [] products = set() requesters = set() - eval_ids = [] + eval_ids = all_directories - total_dirs = len(directories) - logging.info(f"Found {total_dirs} directories to process.") + # If we have existing data, populate products and requesters from it + if not existing_df.empty: + if 'product' in existing_df.columns: + products.update(existing_df['product'].dropna().unique()) + if 'requester' in existing_df.columns: + requesters.update(existing_df['requester'].dropna().unique()) + + successfully_processed = [] - for i, d in enumerate(directories): - eval_ids.append(d) - - if (i + 1) % 10 == 0 or i == total_dirs - 1: - logging.info(f"Precompute progress: {(i + 1) / total_dirs * 100:.1f}% ({i + 1}/{total_dirs})") + for i, d in enumerate(new_directories): + if (i + 1) % 10 == 0 or i == total_new - 1: + logging.info(f"Precompute progress: {(i + 1) / total_new * 100:.1f}% ({i + 1}/{total_new})") run_dir = os.path.join(results_dir, d) configs_file = os.path.join(run_dir, "configs.csv") summary_file = os.path.join(run_dir, "summary.csv") @@ -101,17 +136,26 @@ def precompute(): 'trajectory': trajectory, 'job_id': d }) + successfully_processed.append(d) except Exception as e: print(f"Error reading data from {d}: {e}") - if not data: - logging.warning("No data found in any run directory.") + if not data and existing_df.empty: + logging.warning("No data found in any run directory and no existing cache.") return - df = pd.DataFrame(data) + new_df = pd.DataFrame(data) + + # Combine with existing data + if not existing_df.empty: + df = pd.concat([existing_df, new_df], ignore_index=True) + else: + df = new_df + + # Drop duplicates just in case (based on job_id) + df = df.drop_duplicates(subset=['job_id'], keep='last') # Save trends cache - cache_file = os.path.join(results_dir, "trends_cache.csv") df.to_csv(cache_file, index=False) logging.info(f"Precomputed trends data saved to {cache_file}") @@ -120,11 +164,18 @@ def precompute(): filters_data = { "products": sorted(list(products)), "requesters": sorted(list(requesters)), - "eval_ids": sorted(eval_ids) + "eval_ids": sorted(list(eval_ids)) } with open(filters_file, "w") as f: json.dump(filters_data, f, indent=2) logging.info(f"Precomputed filter values saved to {filters_file}") + + # Save processed directories list + new_processed_dirs = processed_dirs.union(set(successfully_processed)) + with open(processed_dirs_file, "w") as f: + json.dump(list(new_processed_dirs), f, indent=2) + logging.info(f"Saved {len(new_processed_dirs)} processed directories to state.") + if __name__ == "__main__": precompute() From dc943bbc187cef4d5e407d7a08ff1d2d9d6ac059 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 9 Apr 2026 23:30:44 +0000 Subject: [PATCH 11/44] feat: add experimental configuration summary dashboard panel to the viewer --- viewer/main.py | 66 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 82fa1720..b85f8da2 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -1251,6 +1251,46 @@ def on_tab_change(e: me.ButtonToggleChangeEvent): ], on_change=on_tab_change, ) + + # Read configs first to get interesting attributes + config_path = os.path.join( + results_dir, state.selected_directory, "configs.csv" + ) + interesting_configs = {} + if os.path.exists(config_path): + try: + config_df = pd.read_csv(config_path) + + def get_val(cfg_name): + row = config_df[config_df['config'] == cfg_name] + if not row.empty: + return row['value'].values[0] + return None + + product = get_val('experiment_config.product_name') + requester = get_val('experiment_config.experiment_config.guitar_requester') + cli_version = get_val('model_config.gemini_cli_version') + orchestrator = get_val('experiment_config.orchestrator') + eval_group = get_val('experiment_config.eval_group') + + if product: interesting_configs['Product'] = product + if requester: interesting_configs['Requester'] = requester + if cli_version: interesting_configs['CLI Version'] = cli_version + if orchestrator: interesting_configs['Orchestrator'] = orchestrator + if eval_group: interesting_configs['Eval Group'] = eval_group + + except Exception as e: + import logging + logging.warning(f"Error reading configs for summary: {e}") + + if interesting_configs: + with me.box(style=me.Style(display="flex", flex_wrap="wrap", gap="24px", padding=me.Padding.all("16px"), background="#f8fafc", border_radius="8px", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), margin=me.Margin(bottom="16px"))): + for k, v in interesting_configs.items(): + with me.box(style=me.Style(display="flex", flex_direction="column", gap="4px")): + me.text(k, style=me.Style(font_size="16px", color="#64748b", font_weight="600", text_transform="uppercase")) + me.text(str(v), style=me.Style(font_size="20px", color="#0f172a", font_weight="500")) + + if state.selected_tab == "Dashboard": dashboard.dashboard_component( @@ -1321,19 +1361,21 @@ def on_next_conversation(e: me.ClickEvent): f"scores.csv not found in {state.selected_directory}" ) elif state.selected_tab == "Summary": - summary_path = os.path.join( - results_dir, state.selected_directory, "summary.csv" - ) - if os.path.exists(summary_path): - try: - df = pd.read_csv(summary_path) - me.table(data_frame=df) - except Exception as e: - me.text(f"Error reading summary.csv: {e}") - else: - me.text( - f"summary.csv not found in {state.selected_directory}" + summary_path = os.path.join( + results_dir, state.selected_directory, "summary.csv" ) + if os.path.exists(summary_path): + try: + df = pd.read_csv(summary_path) + me.table(data_frame=df) + except Exception as e: + me.text(f"Error reading summary.csv: {e}") + else: + me.text( + f"summary.csv not found in {state.selected_directory}" + ) + + else: from trends import trends_component state = me.state(State) From 4053d3adbbe0ea2f227ed64142c2ade2b38e58f7 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 02:30:53 +0000 Subject: [PATCH 12/44] refactor: replace plotly charts with custom d3.js implementation for improved interactivity --- viewer/static/chart.js | 166 +++++++++++++++++++++++++++++++++++++++++ viewer/trends.py | 91 ++++++++++++++++------ 2 files changed, 235 insertions(+), 22 deletions(-) create mode 100644 viewer/static/chart.js diff --git a/viewer/static/chart.js b/viewer/static/chart.js new file mode 100644 index 00000000..97cb4bf3 --- /dev/null +++ b/viewer/static/chart.js @@ -0,0 +1,166 @@ +function drawChart() { + const data = window.chartData; + const xCol = window.chartConfig.xCol; + const yCol = window.chartConfig.yCol; + const hueCol = window.chartConfig.hueCol; + const title = window.chartConfig.title; + const ylabel = window.chartConfig.ylabel; + + const margin = {top: 60, right: 180, bottom: 60, left: 60}; + const container = document.getElementById('chart-container'); + if (!container) return; + + const width = container.clientWidth - margin.left - margin.right; + const height = 500 - margin.top - margin.bottom; + + // Clear previous SVG + d3.select("#chart").selectAll("*").remove(); + + const svg = d3.select("#chart") + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // X axis + const x = d3.scalePoint() + .domain(data.map(d => d[xCol])) + .range([0, width]); + + // Y axis + const y = d3.scaleLinear() + .domain([0, d3.max(data, d => d[yCol]) * 1.1]) + .range([height, 0]); + + // Grid lines + svg.append("g") + .attr("class", "grid") + .attr("transform", `translate(0,${height})`) + .call(d3.axisBottom(x).tickSize(-height).tickFormat("")); + + svg.append("g") + .attr("class", "grid") + .call(d3.axisLeft(y).tickSize(-width).tickFormat("")); + + // Axes + svg.append("g") + .attr("transform", `translate(0,${height})`) + .call(d3.axisBottom(x)) + .selectAll("text") + .attr("transform", "rotate(-45)") + .style("text-anchor", "end") + .attr("class", "axis-label"); + + svg.append("g") + .call(d3.axisLeft(y)) + .selectAll("text") + .attr("class", "axis-label"); + + // Color scale - Premium palette + const products = [...new Set(data.map(d => d[hueCol]))]; + const colors = ['#6366f1', '#10b981', '#f43f5e', '#f59e0b', '#8b5cf6']; + const color = d3.scaleOrdinal() + .domain(products) + .range(colors); + + // Group data by product + const dataByProduct = d3.group(data, d => d[hueCol]); + + // Draw smooth lines and areas + dataByProduct.forEach((productData, product) => { + // Area + svg.append("path") + .datum(productData) + .attr("class", "area") + .attr("d", d3.area() + .x(d => x(d[xCol])) + .y0(height) + .y1(d => y(d[yCol])) + .curve(d3.curveCardinal) + ) + .style("fill", color(product)); + + // Line + svg.append("path") + .datum(productData) + .attr("class", "line") + .attr("d", d3.line() + .x(d => x(d[xCol])) + .y(d => y(d[yCol])) + .curve(d3.curveCardinal) + ) + .style("stroke", color(product)); + }); + + // Add dots and tooltips + const tooltip = d3.select("#tooltip"); + + data.forEach(d => { + svg.append("circle") + .attr("cx", x(d[xCol])) + .attr("cy", y(d[yCol])) + .attr("r", 5) + .attr("fill", color(d[hueCol])) + .attr("class", "dot") + .on("mouseover", function(event) { + d3.select(this).attr("r", 8).style("stroke-width", "3px"); + tooltip.style("opacity", 1) + .html(`Product: ${d[hueCol]}
Time: ${d[xCol]}
Value: ${d[yCol]}`); + }) + .on("mousemove", function(event) { + tooltip.style("left", (event.pageX + 15) + "px") + .style("top", (event.pageY - 28) + "px"); + }) + .on("mouseout", function() { + d3.select(this).attr("r", 5).style("stroke-width", "2px"); + tooltip.style("opacity", 0); + }); + }); + + // Add Title + svg.append("text") + .attr("x", width / 2) + .attr("y", -margin.top / 2) + .attr("text-anchor", "middle") + .attr("class", "chart-title") + .text(title); + + // Add Y axis label + svg.append("text") + .attr("transform", "rotate(-90)") + .attr("y", -margin.left + 20) + .attr("x", -height / 2) + .attr("text-anchor", "middle") + .style("font-size", "12px") + .style("fill", "#64748b") + .style("font-weight", "600") + .text(ylabel); + + // Add Legend + const legend = svg.selectAll(".legend") + .data(products) + .enter().append("g") + .attr("class", "legend") + .attr("transform", (d, i) => `translate(${width + 20}, ${i * 25})`); + + legend.append("rect") + .attr("x", 0) + .attr("width", 12) + .attr("height", 12) + .attr("rx", 3) + .style("fill", color); + + legend.append("text") + .attr("x", 20) + .attr("y", 6) + .attr("dy", ".35em") + .style("text-anchor", "start") + .text(d => d); +} + +// Initial draw +drawChart(); + +// Redraw on resize +window.addEventListener('resize', drawChart); diff --git a/viewer/trends.py b/viewer/trends.py index e5f712f5..cabd944c 100644 --- a/viewer/trends.py +++ b/viewer/trends.py @@ -25,28 +25,74 @@ def get_results_dir(): return results_dir_candidates[1] # Fallback to default -def generate_plotly_chart(df, x_col, y_col, hue_col, title, ylabel): +def generate_d3_chart(df, x_col, y_col, hue_col, title, ylabel): df_sorted = df.sort_values(by=x_col) - fig = px.line( - df_sorted, - x=x_col, - y=y_col, - color=hue_col, - title=title, - labels={y_col: ylabel, x_col: "Run Time"}, - markers=True - ) + # Convert dataframe to records for JSON + data_records = df_sorted.to_dict(orient='records') + import json + data_json = json.dumps(data_records) - fig.update_layout( - autosize=True, - width=None, - height=500, - margin=dict(l=40, r=40, t=60, b=40), - legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) - ) - - return fig.to_html(full_html=True, include_plotlyjs='cdn') + html = f""" + + + + + + + +
+
+
+
+ + + + + + """ + return html + + + def trends_component(): results_dir = get_results_dir() @@ -147,9 +193,10 @@ def trends_component(): return # Generate charts - latency_chart = generate_plotly_chart(df, 'run_time', 'latency', 'product', 'Latency Trend', 'Latency (ms)') - token_chart = generate_plotly_chart(df, 'run_time', 'tokens', 'product', 'Token Consumption Trend', 'Tokens') - trajectory_chart = generate_plotly_chart(df, 'run_time', 'trajectory', 'product', 'Trajectory Score Trend', 'Score (%)') + latency_chart = generate_d3_chart(df, 'run_time', 'latency', 'product', 'Latency Trend', 'Latency (ms)') + token_chart = generate_d3_chart(df, 'run_time', 'tokens', 'product', 'Token Consumption Trend', 'Tokens') + trajectory_chart = generate_d3_chart(df, 'run_time', 'trajectory', 'product', 'Trajectory Score Trend', 'Score (%)') + # Render charts with me.box(style=me.Style(display="flex", flex_direction="column", gap="24px", padding=me.Padding.all("24px"), width="100%")): From ceb814c04f8243fe88ea5fb90b4cefce868963c5 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 02:44:49 +0000 Subject: [PATCH 13/44] refactor: remove unused make_on_click helper function from main viewer component --- viewer/main.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index b85f8da2..20d694d4 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -881,13 +881,7 @@ def render_header_cell(h_label, h_col, h_width): else "#f8fafc" ) - def make_on_click(dir_name): - def on_click(e: me.ClickEvent): - import logging - logging.info(f"CLICKED EVAL ID: {dir_name}") - s = me.state(State) - s.selected_directory = dir_name - return on_click + with me.box( style=me.Style( From 08df159790d5ddf0999d29b9f6d3f17d2859a743 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 02:59:09 +0000 Subject: [PATCH 14/44] refactor: remove redundant inline logging imports in main.py --- viewer/main.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 20d694d4..48b96d80 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -339,7 +339,6 @@ def get_sort_key(x): requesters = filters_data.get("requesters", []) eval_ids = filters_data.get("eval_ids", []) except Exception as e: - import logging logging.error(f"Error reading filters cache: {e}") products = [] requesters = [] @@ -1132,7 +1131,6 @@ def render_app_content(): try: state = me.state(State) results_dir = get_results_dir() - import logging logging.info(f"render_app_content: selected_directory='{state.selected_directory}'") directories = [] @@ -1274,7 +1272,6 @@ def get_val(cfg_name): if eval_group: interesting_configs['Eval Group'] = eval_group except Exception as e: - import logging logging.warning(f"Error reading configs for summary: {e}") if interesting_configs: @@ -1391,7 +1388,6 @@ def on_main_tab_change(e: me.ButtonToggleChangeEvent): try: list_view_component(directories, results_dir) except Exception as e: - import logging logging.exception("Failed to call list_view_component") me.text(f"Error: {e}") elif state.selected_main_tab == "Charts": @@ -1399,7 +1395,6 @@ def on_main_tab_change(e: me.ButtonToggleChangeEvent): except Exception as e: - import logging logging.exception("render_app_content failed") me.text(f"Fatal Error: {e}") if __name__ == "__main__": From 6a3afb72eb9ba4cb9bae54f23cc40ecdb0173ef2 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 03:15:17 +0000 Subject: [PATCH 15/44] refactor: remove unused imports, improve exception handling, and add evalproto to pycodestyle ignore list --- .pycodestyle | 2 ++ viewer/precompute_trends.py | 3 +-- viewer/trends.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.pycodestyle b/.pycodestyle index cfda8f39..09815ebf 100644 --- a/.pycodestyle +++ b/.pycodestyle @@ -3,3 +3,5 @@ count = False ignore = E402, E501, W503, W504 max-line-length = 160 statistics = True +exclude = evalbench/evalproto + diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index 2757250d..a31b0c8d 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -1,4 +1,3 @@ -import io import os import logging import json @@ -124,7 +123,7 @@ def precompute(): if run_time != "unknown": try: run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') - except: + except Exception: pass data.append({ diff --git a/viewer/trends.py b/viewer/trends.py index cabd944c..56d0bcac 100644 --- a/viewer/trends.py +++ b/viewer/trends.py @@ -1,9 +1,7 @@ -import io import os import logging import mesop as me import pandas as pd -import plotly.express as px from state import State def get_results_dir(): @@ -152,7 +150,7 @@ def trends_component(): if run_time != "unknown": try: run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') - except: + except Exception: pass data.append({ From 4e9e498e763d8115e732ac4e3a1054403150dd1a Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 03:22:52 +0000 Subject: [PATCH 16/44] fix: add logging for failed run_time date parsing in trend precomputation and viewer scripts --- viewer/precompute_trends.py | 4 ++-- viewer/trends.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index a31b0c8d..ef3da898 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -123,8 +123,8 @@ def precompute(): if run_time != "unknown": try: run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') - except Exception: - pass + except Exception as e: + logging.warning(f"Failed to parse run_time '{run_time}': {e}") data.append({ 'run_time': run_time, diff --git a/viewer/trends.py b/viewer/trends.py index 56d0bcac..0f8586a5 100644 --- a/viewer/trends.py +++ b/viewer/trends.py @@ -150,8 +150,8 @@ def trends_component(): if run_time != "unknown": try: run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') - except Exception: - pass + except Exception as e: + logging.warning(f"Failed to parse run_time '{run_time}': {e}") data.append({ 'run_time': run_time, From a4b7847ff41f3562486c1afb4b1b646e261f0928 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 22:40:53 +0000 Subject: [PATCH 17/44] refactor: consolidate State class, introduce status dashboard, and update metric tracking logic --- Makefile | 2 +- viewer/main.py | 306 ++++++++++++++++++++++++++++++------ viewer/precompute_trends.py | 22 ++- viewer/state.py | 16 -- viewer/trends.py | 2 +- 5 files changed, 277 insertions(+), 71 deletions(-) delete mode 100644 viewer/state.py diff --git a/Makefile b/Makefile index 95bce872..541a9a1a 100644 --- a/Makefile +++ b/Makefile @@ -155,7 +155,7 @@ test: @nox style: - @pycodestyle --exclude=evalbench/lib,evalbench/lib64,evalproto evalbench + @pycodestyle --config=.pycodestyle --exclude=evalbench/lib,evalbench/lib64,evalproto evalbench run: @./run_service.sh diff --git a/viewer/main.py b/viewer/main.py index 48b96d80..b3925b44 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -5,7 +5,21 @@ import logging import json import subprocess -from state import State + +@me.stateclass +class State: + selected_directory: str = "" + selected_tab: str = "Dashboard" + conversation_index: int = 0 + eval_summaries: str = "" + eval_id_filter: str = "" + product_filter: str = "" + requester_filter: str = "" + sort_column: str = "date" + sort_descending: bool = True + open_dropdown: str = "" + selected_main_tab: str = "Status" + trends_product_filter: str = "" try: # Try to read version from file (created during build) @@ -208,6 +222,181 @@ def on_load(e: me.LoadEvent): +def status_component(): + results_dir = get_results_dir() + directories = [] + if os.path.exists(results_dir): + directories = [ + d + for d in os.listdir(results_dir) + if os.path.isdir(os.path.join(results_dir, d)) + ] + + with me.box( + style=me.Style( + background="#ffffff", + padding=me.Padding.all("24px"), + border_radius="12px", + border=me.Border.all( + me.BorderSide(width="1px", style="solid", color="#e0e0e0") + ), + box_shadow="0 2px 4px rgba(0,0,0,0.05)", + ) + ): + me.text("Product status", type="headline-5") + me.box(style=me.Style(height="16px")) + me.text(f"Total Evaluation Jobs: {len(directories)}") + + me.box(style=me.Style(height="24px")) + me.text("Product Performance (Latest Eval per Product)", type="headline-6") + me.box(style=me.Style(height="8px")) + + # Build summary data from precomputed trends cache + data = [] + cache_file = os.path.join(results_dir, "trends_cache.csv") + if os.path.exists(cache_file): + try: + cache_df = pd.read_csv(cache_file) + for _, row in cache_df.iterrows(): + data.append({ + 'Product': row['product'], + 'Trajectory Matcher': row['trajectory'], + 'Turn Count': row['turn_count'], + 'Executable': row['executable'], + 'Token Consumption': row['tokens'], + 'End-to-End Latency': row['latency'], + 'Run Time': row['run_time'] + }) + except Exception as e: + logging.error(f"Error reading trends cache: {e}") + else: + logging.warning(f"Trends cache file not found at {cache_file}") + + # Add requested default products if not present in data + default_products = ['spanner', 'bigtable', 'alloydb', 'memorystore', 'dms', 'datastream'] + products_in_data = [d['Product'] for d in data] + for p in default_products: + if p not in products_in_data: + data.append({ + 'Product': p, + 'Trajectory Matcher': None, + 'Turn Count': None, + 'Executable': None, + 'Token Consumption': None, + 'End-to-End Latency': None, + 'Run Time': None + }) + + if data: + df = pd.DataFrame(data) + # Filter out unknown products + df = df[df['Product'] != 'unknown'] + + if not df.empty: + # Sort by Run Time descending to get the latest + df['Run Time'] = pd.to_datetime(df['Run Time']) + df = df.sort_values('Run Time', ascending=False, na_position='last') + + # Group by Product and take the first (latest) + summary_df = df.groupby("Product").first().reset_index() + + # Render table similar to lists tab + with me.box( + style=me.Style( + display="table", + width="100%", + border=me.Border.all( + me.BorderSide(width="1px", color="#e5e7eb", style="solid") + ), + border_radius="8px", + background="#ffffff", + margin=me.Margin(top="16px"), + ) + ): + # Header row + with me.box( + style=me.Style( + display="table-row", + background="#f8fafc", + font_weight="bold", + color="#475569", + font_size="12px", + text_transform="uppercase", + letter_spacing="0.05em", + ) + ): + headers = [ + "Product", + "Trajectory Matcher", + "Turn Count", + "Executable", + "Token Consumption", + "End-to-End Latency" + ] + for label in headers: + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), + text_align="center", + border=me.Border.all( + me.BorderSide(width="1px", color="#e2e8f0", style="solid") + ), + background="#f8fafc", + ) + ): + me.text(label) + + # Data rows + for idx, row in summary_df.iterrows(): + is_na = pd.isna(row['Trajectory Matcher']) + + with me.box( + style=me.Style( + display="table-row", + background="#ffffff" if idx % 2 == 0 else "#f9fafb", + ) + ): + def render_cell(text, color="#334155", cell_bg=None): + style = me.Style( + display="table-cell", + padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), + text_align="center", + border=me.Border.all( + me.BorderSide(width="1px", color="#e2e8f0", style="solid") + ), + ) + if cell_bg: + style.background = cell_bg + with me.box(style=style): + me.text(text, style=me.Style(color=color)) + + render_cell(str(row['Product'])) + + if is_na: + # Make cells gray for products with no data + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + else: + traj_str = f"{row['Trajectory Matcher']:.0f}%" + render_cell(traj_str, get_color_for_pct(traj_str)) + + render_cell(f"{row['Turn Count']:.1f}") + + exec_str = f"{row['Executable']:.0f}%" + render_cell(exec_str, get_color_for_pct(exec_str)) + + render_cell(f"{row['Token Consumption']:.0f}") + render_cell(f"{row['End-to-End Latency']:.0f}") + else: + me.text("No evaluation data found for known products.") + else: + me.text("No evaluation data found in results directories.") + + def list_view_component(directories, results_dir): state = me.state(State) with me.box( @@ -257,28 +446,29 @@ def list_view_component(directories, results_dir): summaries = [] if not summaries: - for d in sorted(directories): - details = get_eval_details(results_dir, d) - summaries.append({ - "id": d, - "date": details.get("date", "N/A"), - "product": details["product"], - "requester": details.get("requester", "N/A"), - "exact_match": details["exact_match"], - "llmrater": details["llmrater"], - "trajectory_matcher": details[ - "trajectory_matcher" - ], - "turn_count": details["turn_count"], - "executable": details["executable"], - "token_consumption": details[ - "token_consumption" - ], - "end_to_end_latency": details[ - "end_to_end_latency" - ] - }) - s.eval_summaries = json.dumps(summaries) + cache_file = os.path.join(results_dir, "trends_cache.csv") + if os.path.exists(cache_file): + try: + cache_df = pd.read_csv(cache_file) + for _, row in cache_df.iterrows(): + summaries.append({ + "id": str(row['job_id']), + "date": str(row['run_time']) if not pd.isna(row['run_time']) else "N/A", + "product": str(row['product']) if not pd.isna(row['product']) else "N/A", + "requester": str(row['requester']) if not pd.isna(row['requester']) else "N/A", + "exact_match": f"{row['exact_match']:.0f}%" if not pd.isna(row['exact_match']) else "N/A", + "llmrater": f"{row['llmrater']:.0f}%" if not pd.isna(row['llmrater']) else "N/A", + "trajectory_matcher": f"{row['trajectory']:.0f}%" if not pd.isna(row['trajectory']) else "N/A", + "turn_count": f"{row['turn_count']:.1f}" if not pd.isna(row['turn_count']) else "N/A", + "executable": f"{row['executable']:.0f}%" if not pd.isna(row['executable']) else "N/A", + "token_consumption": f"{row['tokens']:.0f}" if not pd.isna(row['tokens']) else "N/A", + "end_to_end_latency": f"{row['latency']:.0f}" if not pd.isna(row['latency']) else "N/A" + }) + s.eval_summaries = json.dumps(summaries) + except Exception as e: + logging.error(f"Error reading trends cache: {e}") + else: + logging.warning(f"Trends cache file not found at {cache_file}") # Sort by selected column reverse = state.sort_descending @@ -1174,16 +1364,43 @@ def on_title_click(e: me.ClickEvent): ), ) - if GIT_VERSION != "unknown": - with me.box( - style=me.Style( - font_size="12px", - color="#94a3b8", - ) - ): - me.markdown( - f"[Git: {GIT_VERSION}](https://github.com/GoogleCloudPlatform/evalbench/commit/{GIT_VERSION})" - ) + import time + cache_file = os.path.join(results_dir, "trends_cache.csv") + cache_status = "Not Ready" + cache_color = "#ef4444" # Red + + if os.path.exists(cache_file): + mtime = os.path.getmtime(cache_file) + elapsed = time.time() - mtime + if elapsed < 600: # 10 minutes + cache_status = "Fresh" + cache_color = "#10b981" # Green + else: + cache_status = "Stale" + cache_color = "#f59e0b" # Yellow + + with me.box( + style=me.Style( + display="flex", + flex_direction="column", + align_items="flex-end", + gap="4px", + ) + ): + if GIT_VERSION != "unknown": + with me.box( + style=me.Style( + font_size="12px", + color="#94a3b8", + ) + ): + me.markdown( + f"[Git: {GIT_VERSION}](https://github.com/GoogleCloudPlatform/evalbench/commit/{GIT_VERSION})" + ) + + with me.box(style=me.Style(display="flex", align_items="center", gap="6px", font_size="12px")): + me.box(style=me.Style(width="8px", height="8px", border_radius="50%", background=cache_color)) + me.text(f"Cache: {cache_status}", style=me.Style(font_weight="500", color="#94a3b8")) # Centered content at 90% browser width with me.box( @@ -1200,25 +1417,7 @@ def on_title_click(e: me.ClickEvent): color="#1e293b", ) ): - # Cache freshness indicator - import time - cache_file = os.path.join(results_dir, "trends_cache.csv") - cache_status = "Not Ready" - cache_color = "#ef4444" # Red - - if os.path.exists(cache_file): - mtime = os.path.getmtime(cache_file) - elapsed = time.time() - mtime - if elapsed < 600: # 10 minutes - cache_status = "Fresh" - cache_color = "#10b981" # Green - else: - cache_status = "Stale" - cache_color = "#f59e0b" # Yellow - - with me.box(style=me.Style(display="flex", align_items="center", gap="8px", font_size="12px", margin=me.Margin(bottom="8px"))): - me.box(style=me.Style(width="12px", height="12px", border_radius="50%", background=cache_color)) - me.text(f"Cache Status: {cache_status}", style=me.Style(font_weight="600", color="#64748b")) + if state.selected_directory: @@ -1378,6 +1577,7 @@ def on_main_tab_change(e: me.ButtonToggleChangeEvent): me.button_toggle( value=state.selected_main_tab, buttons=[ + me.ButtonToggleButton(label="Status", value="Status"), me.ButtonToggleButton(label="List", value="List"), me.ButtonToggleButton(label="Charts", value="Charts"), ], @@ -1392,6 +1592,8 @@ def on_main_tab_change(e: me.ButtonToggleChangeEvent): me.text(f"Error: {e}") elif state.selected_main_tab == "Charts": trends_component() + elif state.selected_main_tab == "Status": + status_component() except Exception as e: diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index ef3da898..04354cd0 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -114,10 +114,26 @@ def precompute(): latency_row = summary_df[summary_df['metric_name'] == 'end_to_end_latency'] token_row = summary_df[summary_df['metric_name'] == 'token_consumption'] trajectory_row = summary_df[summary_df['metric_name'] == 'trajectory_matcher'] + executable_row = summary_df[summary_df['metric_name'] == 'executable'] + turn_count_row = summary_df[summary_df['metric_name'] == 'turn_count'] + exact_match_row = summary_df[summary_df['metric_name'] == 'exact_match'] + llmrater_row = summary_df[summary_df['metric_name'] == 'llmrater'] latency = float(latency_row['metric_score'].values[0]) if not latency_row.empty else 0.0 tokens = float(token_row['metric_score'].values[0]) if not token_row.empty else 0.0 - trajectory = float(trajectory_row['metric_score'].values[0]) if not trajectory_row.empty else 0.0 + turn_count = float(turn_count_row['metric_score'].values[0]) if not turn_count_row.empty else 0.0 + + def get_metric_pct(row): + if not row.empty: + correct = float(row['correct_results_count'].values[0]) + total = float(row['total_results_count'].values[0]) + return (correct / total) * 100 if total > 0 else 0.0 + return 0.0 + + trajectory = get_metric_pct(trajectory_row) + executable = get_metric_pct(executable_row) + exact_match = get_metric_pct(exact_match_row) + llmrater = get_metric_pct(llmrater_row) run_time = summary_df['run_time'].values[0] if not summary_df.empty else "unknown" if run_time != "unknown": @@ -133,6 +149,10 @@ def precompute(): 'latency': latency, 'tokens': tokens, 'trajectory': trajectory, + 'executable': executable, + 'turn_count': turn_count, + 'exact_match': exact_match, + 'llmrater': llmrater, 'job_id': d }) successfully_processed.append(d) diff --git a/viewer/state.py b/viewer/state.py deleted file mode 100644 index 66f9229e..00000000 --- a/viewer/state.py +++ /dev/null @@ -1,16 +0,0 @@ -import mesop as me - -@me.stateclass -class State: - selected_directory: str = "" - selected_tab: str = "Dashboard" - conversation_index: int = 0 - eval_summaries: str = "" - eval_id_filter: str = "" - product_filter: str = "" - requester_filter: str = "" - sort_column: str = "date" - sort_descending: bool = True - open_dropdown: str = "" - selected_main_tab: str = "List" - trends_product_filter: str = "" diff --git a/viewer/trends.py b/viewer/trends.py index 0f8586a5..38d2378a 100644 --- a/viewer/trends.py +++ b/viewer/trends.py @@ -2,7 +2,7 @@ import logging import mesop as me import pandas as pd -from state import State +from main import State def get_results_dir(): # Try to read from environment variable From 9f0402b6738b64f4e95d64657677b913cfba4b73 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 10 Apr 2026 23:14:29 +0000 Subject: [PATCH 18/44] feat: add dataset column to eval viewer, enable product click-to-filter, and simplify chart line rendering --- viewer/main.py | 62 +++++++++++++++++++++++++++++++++++-- viewer/precompute_trends.py | 6 +++- viewer/static/chart.js | 2 -- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index b3925b44..adf3be4e 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -357,7 +357,7 @@ def status_component(): background="#ffffff" if idx % 2 == 0 else "#f9fafb", ) ): - def render_cell(text, color="#334155", cell_bg=None): + def render_cell(text, color="#334155", cell_bg=None, on_click=None): style = me.Style( display="table-cell", padding=me.Padding.symmetric(vertical="12px", horizontal="16px"), @@ -369,9 +369,35 @@ def render_cell(text, color="#334155", cell_bg=None): if cell_bg: style.background = cell_bg with me.box(style=style): - me.text(text, style=me.Style(color=color)) + if on_click: + me.button( + text, + on_click=on_click, + style=me.Style( + color=color, + background="transparent", + border=me.Border.all(me.BorderSide(width="0px")), + padding=me.Padding.all("0px"), + margin=me.Margin.all("0px"), + font_size="inherit", + font_weight="500", + cursor="pointer", + ) + ) + else: + me.text(text, style=me.Style(color=color)) - render_cell(str(row['Product'])) + product_val = str(row['Product']) + + def make_click_handler(p_val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.selected_main_tab = "List" + st.product_filter = p_val + handler.__name__ = f"click_product_status_{p_val}" + return handler + + render_cell(product_val, color="#2563eb", on_click=make_click_handler(product_val)) if is_na: # Make cells gray for products with no data @@ -456,6 +482,7 @@ def list_view_component(directories, results_dir): "date": str(row['run_time']) if not pd.isna(row['run_time']) else "N/A", "product": str(row['product']) if not pd.isna(row['product']) else "N/A", "requester": str(row['requester']) if not pd.isna(row['requester']) else "N/A", + "dataset": str(row['dataset']) if 'dataset' in row and not pd.isna(row['dataset']) else "N/A", "exact_match": f"{row['exact_match']:.0f}%" if not pd.isna(row['exact_match']) else "N/A", "llmrater": f"{row['llmrater']:.0f}%" if not pd.isna(row['llmrater']) else "N/A", "trajectory_matcher": f"{row['trajectory']:.0f}%" if not pd.isna(row['trajectory']) else "N/A", @@ -928,6 +955,9 @@ def click_turns(e): def click_exec(e): on_sort_click("executable") + def click_dataset(e): + on_sort_click("dataset") + def click_tokens(e): on_sort_click("token_consumption") @@ -939,6 +969,7 @@ def click_latency(e): "date": click_date, "product": click_product, "requester": click_requester, + "dataset": click_dataset, "trajectory_matcher": click_traj, "turn_count": click_turns, "executable": click_exec, @@ -1031,6 +1062,7 @@ def render_header_cell(h_label, h_col, h_width): ("Date", "date", "24ch"), ("Product", "product", None), ("Requester", "requester", None), + ("Dataset", "dataset", None), ( "Trajectory Matcher", "trajectory_matcher", @@ -1058,6 +1090,7 @@ def render_header_cell(h_label, h_col, h_width): date_val = item.get("date", "N/A") prod = item["product"] req_val = item.get("requester", "N/A") + dataset_val = item.get("dataset", "N/A") traj = item.get("trajectory_matcher", "N/A") turns = item.get("turn_count", "N/A") exec_val = item.get("executable", "N/A") @@ -1175,6 +1208,29 @@ def render_header_cell(h_label, h_col, h_width): color="#334155" ), ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + ) + ): + me.text( + dataset_val, + style=me.Style( + color="#334155" + ), + ) with me.box( style=me.Style( diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index 04354cd0..bd2a5a00 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -95,12 +95,15 @@ def precompute(): # Read configs configs_df = pd.read_csv(configs_file) - # Extract requester and product + # Extract requester, product and dataset requester_row = configs_df[configs_df['config'].str.contains('guitar_requester', na=False)] product_row = configs_df[configs_df['config'].isin(['experiment_config.product_name', 'experiment_config.poduct_name'])] + dataset_row = configs_df[configs_df['config'] == 'experiment_config.dataset_config'] requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" product = product_row['value'].values[0] if not product_row.empty else "unknown" + dataset_path = dataset_row['value'].values[0] if not dataset_row.empty else "unknown" + dataset = os.path.basename(dataset_path) if dataset_path != "unknown" else "unknown" if product != "unknown" and str(product).strip() != "": products.add(product) @@ -146,6 +149,7 @@ def get_metric_pct(row): 'run_time': run_time, 'requester': requester, 'product': product, + 'dataset': dataset, 'latency': latency, 'tokens': tokens, 'trajectory': trajectory, diff --git a/viewer/static/chart.js b/viewer/static/chart.js index 97cb4bf3..b954c0ad 100644 --- a/viewer/static/chart.js +++ b/viewer/static/chart.js @@ -77,7 +77,6 @@ function drawChart() { .x(d => x(d[xCol])) .y0(height) .y1(d => y(d[yCol])) - .curve(d3.curveCardinal) ) .style("fill", color(product)); @@ -88,7 +87,6 @@ function drawChart() { .attr("d", d3.line() .x(d => x(d[xCol])) .y(d => y(d[yCol])) - .curve(d3.curveCardinal) ) .style("stroke", color(product)); }); From 082a5be7ec47fc5ed4dd6b358b0f02e95b558156 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Sat, 11 Apr 2026 03:28:40 +0000 Subject: [PATCH 19/44] feat: add goal completion fallback to precompute and display in viewer --- evalbench/reporting/analyzer.py | 3 +- viewer/conversations.py | 19 ++++ viewer/dashboard.py | 51 +++++++++ viewer/main.py | 194 +++++++++++++++++++++++++++++++- viewer/precompute_trends.py | 36 +++++- 5 files changed, 294 insertions(+), 9 deletions(-) diff --git a/evalbench/reporting/analyzer.py b/evalbench/reporting/analyzer.py index b066477d..8d8d5a1f 100644 --- a/evalbench/reporting/analyzer.py +++ b/evalbench/reporting/analyzer.py @@ -105,7 +105,8 @@ def analyze_result(scores, experiment_config: dict[str, str]): logging.info(f"Error: {row['comparison_error']}") else: logging.info("No analysis provided.") - continue + if metric_name != "goal_completion": + continue summary = analyze_one_metric( df=df, diff --git a/viewer/conversations.py b/viewer/conversations.py index f34d57b7..5d679cef 100644 --- a/viewer/conversations.py +++ b/viewer/conversations.py @@ -190,6 +190,25 @@ def conversations_component( gap="8px", ) ): + # Conversation Plan + scenario_str = df["scenario"].iloc[idx] if "scenario" in df.columns else "" + conversation_plan = "" + if scenario_str and pd.notna(scenario_str): + try: + scenario_data = json.loads(scenario_str) + conversation_plan = scenario_data.get("conversation_plan", "") + except Exception: + try: + import ast + scenario_data = ast.literal_eval(scenario_str) + conversation_plan = scenario_data.get("conversation_plan", "") + except Exception as e: + logging.warning(f"Failed to parse scenario: {e}") + + if conversation_plan: + with me.expansion_panel(title="Conversation Plan", expanded=True): + me.markdown(conversation_plan) + scores_path = os.path.join(results_dir, "scores.csv") if os.path.exists(scores_path): try: diff --git a/viewer/dashboard.py b/viewer/dashboard.py index 3a3883cc..007997a7 100644 --- a/viewer/dashboard.py +++ b/viewer/dashboard.py @@ -74,3 +74,54 @@ def dashboard_component(results_dir: str): color=color, ), ) + + # Fallback for goal_completion if not in summary.csv + metrics_in_summary = df['metric_name'].tolist() if 'metric_name' in df.columns else [] + if 'goal_completion' not in metrics_in_summary: + scores_path = os.path.join(results_dir, "scores.csv") + if os.path.exists(scores_path): + try: + scores_df = pd.read_csv(scores_path) + gc_df = scores_df[scores_df['comparator'] == 'goal_completion'] + if not gc_df.empty: + total = len(gc_df) + correct = len(gc_df[gc_df['score'] >= 100.0]) + + pct = (correct / total) * 100 if total > 0 else 0 + color = ( + "#10b981" + if pct >= 80 + else ("#ef4444" if pct < 40 else "#f59e0b") + ) + + with me.box( + style=me.Style( + width="calc(33.333% - 11px)", + background="#ffffff", + border_radius="10px", + border=me.Border.all( + me.BorderSide(width="1px", color="#e5e7eb", style="solid") + ), + padding=me.Padding.all("12px"), + box_shadow="0 1px 3px rgba(0,0,0,0.06)", + ) + ): + me.text( + "goal_completion", + style=me.Style( + font_weight="600", + font_size="12px", + color="#374151", + margin=me.Margin(bottom="4px"), + ), + ) + me.text( + f"{correct}/{total}", + style=me.Style( + font_weight="700", + font_size="22px", + color=color, + ), + ) + except Exception as e: + print(f"Error reading scores.csv for dashboard: {e}") diff --git a/viewer/main.py b/viewer/main.py index adf3be4e..fa39c157 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -15,6 +15,7 @@ class State: eval_id_filter: str = "" product_filter: str = "" requester_filter: str = "" + dataset_filter: str = "" sort_column: str = "date" sort_descending: bool = True open_dropdown: str = "" @@ -260,7 +261,9 @@ def status_component(): for _, row in cache_df.iterrows(): data.append({ 'Product': row['product'], + 'Dataset': row['dataset'] if 'dataset' in row and not pd.isna(row['dataset']) else "N/A", 'Trajectory Matcher': row['trajectory'], + 'Goal Completion': row['goal_completion'] if 'goal_completion' in row else None, 'Turn Count': row['turn_count'], 'Executable': row['executable'], 'Token Consumption': row['tokens'], @@ -279,6 +282,7 @@ def status_component(): if p not in products_in_data: data.append({ 'Product': p, + 'Dataset': 'N/A', 'Trajectory Matcher': None, 'Turn Count': None, 'Executable': None, @@ -297,8 +301,8 @@ def status_component(): df['Run Time'] = pd.to_datetime(df['Run Time']) df = df.sort_values('Run Time', ascending=False, na_position='last') - # Group by Product and take the first (latest) - summary_df = df.groupby("Product").first().reset_index() + # Group by Product and Dataset and take the first (latest) + summary_df = df.groupby(["Product", "Dataset"]).first().reset_index() # Render table similar to lists tab with me.box( @@ -327,7 +331,9 @@ def status_component(): ): headers = [ "Product", + "Dataset", "Trajectory Matcher", + "Goal Completion", "Turn Count", "Executable", "Token Consumption", @@ -388,16 +394,20 @@ def render_cell(text, color="#334155", cell_bg=None, on_click=None): me.text(text, style=me.Style(color=color)) product_val = str(row['Product']) + dataset_val = str(row['Dataset']) - def make_click_handler(p_val): + def make_click_handler(p_val, d_val): def handler(e: me.ClickEvent): st = me.state(State) st.selected_main_tab = "List" st.product_filter = p_val - handler.__name__ = f"click_product_status_{p_val}" + st.dataset_filter = d_val + handler.__name__ = f"click_status_row_{p_val}_{d_val}" return handler - render_cell(product_val, color="#2563eb", on_click=make_click_handler(product_val)) + click_handler = make_click_handler(product_val, dataset_val) + render_cell(product_val, color="#2563eb", on_click=click_handler) + render_cell(dataset_val, color="#2563eb", on_click=click_handler) if is_na: # Make cells gray for products with no data @@ -406,10 +416,17 @@ def handler(e: me.ClickEvent): render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") else: traj_str = f"{row['Trajectory Matcher']:.0f}%" render_cell(traj_str, get_color_for_pct(traj_str)) + if pd.isna(row['Goal Completion']): + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + else: + goal_str = f"{row['Goal Completion']:.0f}%" + render_cell(goal_str, get_color_for_pct(goal_str)) + render_cell(f"{row['Turn Count']:.1f}") exec_str = f"{row['Executable']:.0f}%" @@ -486,6 +503,7 @@ def list_view_component(directories, results_dir): "exact_match": f"{row['exact_match']:.0f}%" if not pd.isna(row['exact_match']) else "N/A", "llmrater": f"{row['llmrater']:.0f}%" if not pd.isna(row['llmrater']) else "N/A", "trajectory_matcher": f"{row['trajectory']:.0f}%" if not pd.isna(row['trajectory']) else "N/A", + "goal_completion": f"{row['goal_completion']:.0f}%" if 'goal_completion' in row and not pd.isna(row['goal_completion']) else "N/A", "turn_count": f"{row['turn_count']:.1f}" if not pd.isna(row['turn_count']) else "N/A", "executable": f"{row['executable']:.0f}%" if not pd.isna(row['executable']) else "N/A", "token_consumption": f"{row['tokens']:.0f}" if not pd.isna(row['tokens']) else "N/A", @@ -555,11 +573,13 @@ def get_sort_key(x): products = filters_data.get("products", []) requesters = filters_data.get("requesters", []) eval_ids = filters_data.get("eval_ids", []) + datasets = filters_data.get("datasets", []) except Exception as e: logging.error(f"Error reading filters cache: {e}") products = [] requesters = [] eval_ids = [] + datasets = [] else: products = sorted( list( @@ -580,6 +600,27 @@ def get_sort_key(x): ) ) eval_ids = sorted([x["id"] for x in all_summaries]) + datasets = sorted( + list( + set( + x.get("dataset", "N/A") + for x in all_summaries + if x.get("dataset", "N/A") != "N/A" + ) + ) + ) + + # Fallback for datasets if empty from cache + if not datasets and all_summaries: + datasets = sorted( + list( + set( + x.get("dataset", "N/A") + for x in all_summaries + if x.get("dataset", "N/A") != "N/A" + ) + ) + ) # Apply filters if state.eval_id_filter: @@ -601,6 +642,13 @@ def get_sort_key(x): if x.get("requester", "N/A") == state.requester_filter ] + if state.dataset_filter: + summaries = [ + x + for x in summaries + if x.get("dataset", "N/A") + == state.dataset_filter + ] # Render filters UI with me.box( @@ -926,6 +974,107 @@ def handler(e: me.ClickEvent): ), ) + # Dataset Filter with Floating Autocomplete + def toggle_dataset_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "dataset": + st.open_dropdown = "" + else: + st.open_dropdown = "dataset" + + def make_dataset_dropdown_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.dataset_filter = val + st.open_dropdown = "" + + handler.__name__ = f"click_dataset_dd_{val}" + return handler + + mk_dataset_dd = make_dataset_dropdown_handler + + with me.box( + style=me.Style( + position="relative", + width="200px", + ) + ): + # The Box acting as Dropdown Trigger + with me.box( + style=me.Style( + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_dataset_dropdown, + ): + me.text( + state.dataset_filter + if state.dataset_filter + else "Filter by Dataset", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "dataset": + with me.box( + style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + width="100%", + max_height="200px", + overflow_y="auto", + ) + ): + # All option + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_dataset_dd(""), + ): + me.text( + "All", + style=me.Style( + color="#1f2937" + ), + ) + + for d in datasets: + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=mk_dataset_dd(d), + ): + me.text( + d, + style=me.Style( + color="#1f2937" + ), + ) + def on_sort_click(col_name): s = me.state(State) if s.sort_column == col_name: @@ -964,6 +1113,9 @@ def click_tokens(e): def click_latency(e): on_sort_click("end_to_end_latency") + def click_goal_comp(e): + on_sort_click("goal_completion") + sort_handlers = { "id": click_id, "date": click_date, @@ -971,6 +1123,7 @@ def click_latency(e): "requester": click_requester, "dataset": click_dataset, "trajectory_matcher": click_traj, + "goal_completion": click_goal_comp, "turn_count": click_turns, "executable": click_exec, "token_consumption": click_tokens, @@ -1068,6 +1221,11 @@ def render_header_cell(h_label, h_col, h_width): "trajectory_matcher", "18ch", ), + ( + "Goal Completion", + "goal_completion", + "16ch", + ), ("Turn Count", "turn_count", "12ch"), ("Executable", "executable", "12ch"), ( @@ -1092,6 +1250,7 @@ def render_header_cell(h_label, h_col, h_width): req_val = item.get("requester", "N/A") dataset_val = item.get("dataset", "N/A") traj = item.get("trajectory_matcher", "N/A") + goal_comp = item.get("goal_completion", "N/A") turns = item.get("turn_count", "N/A") exec_val = item.get("executable", "N/A") tokens = item.get("token_consumption", "N/A") @@ -1257,6 +1416,31 @@ def render_header_cell(h_label, h_col, h_width): ), ) + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="16ch", + white_space="nowrap", + ) + ): + me.text( + goal_comp, + style=me.Style( + color=get_color_for_pct(goal_comp) + ), + ) + with me.box( style=me.Style( display="table-cell", diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index bd2a5a00..14e961d4 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -72,6 +72,7 @@ def precompute(): data = [] products = set() requesters = set() + datasets = set() eval_ids = all_directories # If we have existing data, populate products and requesters from it @@ -98,13 +99,15 @@ def precompute(): # Extract requester, product and dataset requester_row = configs_df[configs_df['config'].str.contains('guitar_requester', na=False)] product_row = configs_df[configs_df['config'].isin(['experiment_config.product_name', 'experiment_config.poduct_name'])] - dataset_row = configs_df[configs_df['config'] == 'experiment_config.dataset_config'] requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" product = product_row['value'].values[0] if not product_row.empty else "unknown" - dataset_path = dataset_row['value'].values[0] if not dataset_row.empty else "unknown" + dataset_path = configs_df[configs_df['config'] == 'experiment_config.dataset_config']['value'].values[0] if 'experiment_config.dataset_config' in configs_df['config'].values else "unknown" dataset = os.path.basename(dataset_path) if dataset_path != "unknown" else "unknown" + if dataset != "unknown": + datasets.add(dataset) + if product != "unknown" and str(product).strip() != "": products.add(product) if requester != "unknown" and str(requester).strip() != "": @@ -121,6 +124,7 @@ def precompute(): turn_count_row = summary_df[summary_df['metric_name'] == 'turn_count'] exact_match_row = summary_df[summary_df['metric_name'] == 'exact_match'] llmrater_row = summary_df[summary_df['metric_name'] == 'llmrater'] + goal_completion_row = summary_df[summary_df['metric_name'] == 'goal_completion'] latency = float(latency_row['metric_score'].values[0]) if not latency_row.empty else 0.0 tokens = float(token_row['metric_score'].values[0]) if not token_row.empty else 0.0 @@ -137,6 +141,30 @@ def get_metric_pct(row): executable = get_metric_pct(executable_row) exact_match = get_metric_pct(exact_match_row) llmrater = get_metric_pct(llmrater_row) + goal_completion = get_metric_pct(goal_completion_row) + if goal_completion == 0.0 and goal_completion_row.empty: + # Fallback to results.csv or scores.csv if goal_completion is missing from summary.csv + results_file = os.path.join(results_dir, d, "results.csv") + scores_file = os.path.join(results_dir, d, "scores.csv") + + file_to_read = None + if os.path.exists(results_file): + file_to_read = results_file + elif os.path.exists(scores_file): + file_to_read = scores_file + + if file_to_read: + try: + df = pd.read_csv(file_to_read) + if 'comparator' in df.columns and 'score' in df.columns: + gc_scores = df[df['comparator'] == 'goal_completion'] + if not gc_scores.empty: + correct = len(gc_scores[gc_scores['score'] == 100.0]) + total = len(gc_scores) + goal_completion = (correct / total) * 100 if total > 0 else 0.0 + logging.info(f"Computed goal_completion from {os.path.basename(file_to_read)} for {d}: {goal_completion}") + except Exception as e: + logging.warning(f"Error reading {os.path.basename(file_to_read)} for {d}: {e}") run_time = summary_df['run_time'].values[0] if not summary_df.empty else "unknown" if run_time != "unknown": @@ -157,6 +185,7 @@ def get_metric_pct(row): 'turn_count': turn_count, 'exact_match': exact_match, 'llmrater': llmrater, + 'goal_completion': goal_completion, 'job_id': d }) successfully_processed.append(d) @@ -187,7 +216,8 @@ def get_metric_pct(row): filters_data = { "products": sorted(list(products)), "requesters": sorted(list(requesters)), - "eval_ids": sorted(list(eval_ids)) + "eval_ids": sorted(list(eval_ids)), + "datasets": sorted(list(datasets)) } with open(filters_file, "w") as f: json.dump(filters_data, f, indent=2) From 2e5a2029b19e9b2e61668323123532bc79078e0a Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Sat, 11 Apr 2026 04:11:34 +0000 Subject: [PATCH 20/44] feat: add cache clearing functionality and increase Gunicorn timeout to 120s --- evalbench_service/supervisord.conf | 2 +- viewer/main.py | 38 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/evalbench_service/supervisord.conf b/evalbench_service/supervisord.conf index 3ec67a56..43200999 100644 --- a/evalbench_service/supervisord.conf +++ b/evalbench_service/supervisord.conf @@ -16,7 +16,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:evalbench_frontend] -command=gunicorn -w 4 -k gevent main:me --bind :3000 --forwarded-allow-ips="*" +command=gunicorn -w 4 -k gevent main:me --bind :3000 --forwarded-allow-ips="*" --timeout 120 directory=/evalbench/viewer autostart=true autorestart=true diff --git a/viewer/main.py b/viewer/main.py index fa39c157..0e163a48 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -5,6 +5,7 @@ import logging import json import subprocess +import precompute_trends @me.stateclass class State: @@ -21,6 +22,7 @@ class State: open_dropdown: str = "" selected_main_tab: str = "Status" trends_product_filter: str = "" + cache_cleared_message: str = "" try: # Try to read version from file (created during build) @@ -1576,6 +1578,30 @@ def on_title_click(e: me.ClickEvent): state.selected_directory = "" state.conversation_index = 0 me.navigate("/") + + def on_clear_cache_click(e: me.ClickEvent): + results_dir = get_results_dir() + processed_dirs_file = os.path.join(results_dir, "processed_dirs.json") + trends_cache_file = os.path.join(results_dir, "trends_cache.csv") + filters_cache_file = os.path.join(results_dir, "filters_cache.json") + + try: + if os.path.exists(processed_dirs_file): + os.remove(processed_dirs_file) + if os.path.exists(trends_cache_file): + os.remove(trends_cache_file) + if os.path.exists(filters_cache_file): + os.remove(filters_cache_file) + + logging.info("Cleared precomputed files. Triggering precompute...") + + import threading + threading.Thread(target=precompute_trends.precompute).start() + + state.cache_cleared_message = "Cache cleared. Precompute triggered in background." + except Exception as ex: + logging.error(f"Error clearing cache: {ex}") + state.cache_cleared_message = f"Error clearing cache: {ex}" # Full-width header bar with me.box( @@ -1641,6 +1667,18 @@ def on_title_click(e: me.ClickEvent): with me.box(style=me.Style(display="flex", align_items="center", gap="6px", font_size="12px")): me.box(style=me.Style(width="8px", height="8px", border_radius="50%", background=cache_color)) me.text(f"Cache: {cache_status}", style=me.Style(font_weight="500", color="#94a3b8")) + # me.button( + # "Clear Cache", + # on_click=on_clear_cache_click, + # style=me.Style( + # color="#f8fafc", + # font_size="10px", + # background="#ef4444", + # padding=me.Padding.symmetric(vertical="2px", horizontal="4px"), + # border_radius="3px", + # margin=me.Margin(left="10px"), + # ) + # ) # Centered content at 90% browser width with me.box( From bb8f83a6610ad96896b2a33f610561e5d5a3e359 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Sun, 12 Apr 2026 03:35:41 +0000 Subject: [PATCH 21/44] feat: update UI, increase gunicorn worker count to 12, and bump package version to 1.3.1 --- evalbench_service/entrypoint.sh | 2 +- evalbench_service/supervisord.conf | 2 +- uv.lock | 2 +- viewer/conversations.py | 70 ++++++++++++++++++------------ viewer/main.py | 24 +++++----- 5 files changed, 58 insertions(+), 42 deletions(-) diff --git a/evalbench_service/entrypoint.sh b/evalbench_service/entrypoint.sh index 73a013d4..ac3a43c7 100644 --- a/evalbench_service/entrypoint.sh +++ b/evalbench_service/entrypoint.sh @@ -9,7 +9,7 @@ if [[ "$CLOUD_RUN" == "True" ]]; then # Ensure we are in the viewer directory for gunicorn to find main:me cd /evalbench/viewer - exec gunicorn -w 4 -k gevent main:me --bind :${PORT:-3000} --forwarded-allow-ips="*" + exec gunicorn -w 12 -k gevent main:me --bind :${PORT:-3000} --forwarded-allow-ips="*" --timeout 120 else echo "Starting supervisord to manage multiple processes..." exec /usr/bin/supervisord -c /evalbench/supervisord.conf diff --git a/evalbench_service/supervisord.conf b/evalbench_service/supervisord.conf index 0c8eed1f..940abe7c 100644 --- a/evalbench_service/supervisord.conf +++ b/evalbench_service/supervisord.conf @@ -16,7 +16,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:evalbench_frontend] -command=gunicorn -w 4 -k gevent main:me --bind :3000 --forwarded-allow-ips="*" --timeout 120 +command=gunicorn -w 12 -k gevent main:me --bind :3000 --forwarded-allow-ips="*" --timeout 120 directory=/evalbench/viewer autostart=true autorestart=true diff --git a/uv.lock b/uv.lock index 189e1454..b6626a0b 100644 --- a/uv.lock +++ b/uv.lock @@ -861,7 +861,7 @@ wheels = [ [[package]] name = "evalbench" -version = "1.3.0" +version = "1.3.1" source = { editable = "." } dependencies = [ { name = "absl-py" }, diff --git a/viewer/conversations.py b/viewer/conversations.py index 5d679cef..ed01a3a6 100644 --- a/viewer/conversations.py +++ b/viewer/conversations.py @@ -145,37 +145,51 @@ def conversations_component( width="100%", ) ): - with me.tooltip(message=stats_str, position="right", disabled=not bool(stats_str)): - with me.box( + with me.box( + style=me.Style( + background="#ffffff", + color="#1f2937", + padding=me.Padding.symmetric( + vertical="12px", + horizontal="16px", + ), + border_radius="12px", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e5e7eb", + style="solid", + ) + ), + max_width="80%", + box_shadow="0 1px 2px 0 rgba(0,0,0,0.05)", + overflow_x="auto", + ) + ): + me.text( + "Agent", style=me.Style( - background="#ffffff", - color="#1f2937", - padding=me.Padding.symmetric( - vertical="12px", - horizontal="16px", - ), - border_radius="12px", - border=me.Border.all( - me.BorderSide( - width="1px", - color="#e5e7eb", - style="solid", - ) - ), - max_width="80%", - box_shadow="0 1px 2px 0 rgba(0,0,0,0.05)", - ) - ): + font_weight="bold", + font_size="12px", + color="#6b7280", + margin=me.Margin(bottom="4px"), + ), + ) + if agent_content: + me.markdown(agent_content) + else: me.text( - "Agent", + "Empty response", style=me.Style( - font_weight="bold", - font_size="12px", - color="#6b7280", - margin=me.Margin(bottom="4px"), + color="#94a3b8", + font_style="italic", + font_size="14px", ), ) - me.markdown(agent_content) + + if stats_str: + with me.expansion_panel(title="Stats", expanded=False): + me.code(stats_str) except Exception as parse_e: me.text(f"Error parsing JSON: {parse_e}") me.code(history_str) @@ -207,7 +221,9 @@ def conversations_component( if conversation_plan: with me.expansion_panel(title="Conversation Plan", expanded=True): - me.markdown(conversation_plan) + if isinstance(conversation_plan, list): + conversation_plan = "\n".join([str(x) for x in conversation_plan]) + me.markdown(str(conversation_plan)) scores_path = os.path.join(results_dir, "scores.csv") if os.path.exists(scores_path): diff --git a/viewer/main.py b/viewer/main.py index 0e163a48..6ee768b8 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -1667,18 +1667,18 @@ def on_clear_cache_click(e: me.ClickEvent): with me.box(style=me.Style(display="flex", align_items="center", gap="6px", font_size="12px")): me.box(style=me.Style(width="8px", height="8px", border_radius="50%", background=cache_color)) me.text(f"Cache: {cache_status}", style=me.Style(font_weight="500", color="#94a3b8")) - # me.button( - # "Clear Cache", - # on_click=on_clear_cache_click, - # style=me.Style( - # color="#f8fafc", - # font_size="10px", - # background="#ef4444", - # padding=me.Padding.symmetric(vertical="2px", horizontal="4px"), - # border_radius="3px", - # margin=me.Margin(left="10px"), - # ) - # ) + me.button( + "Clear Cache", + on_click=on_clear_cache_click, + style=me.Style( + color="transparent", + font_size="10px", + background="transparent", + padding=me.Padding.symmetric(vertical="2px", horizontal="4px"), + border_radius="3px", + margin=me.Margin(left="10px"), + ) + ) # Centered content at 90% browser width with me.box( From 615f3c70bab4a4669163a6e9d5d0a0b23d8ea7ba Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Mon, 13 Apr 2026 03:33:33 +0000 Subject: [PATCH 22/44] feat: integrate AI-driven evaluation scoring and summary generation into the viewer dashboard. --- Makefile | 23 +- docs/summarizer_documentation.md | 69 ++ uv.lock | 782 --------------------- viewer/analyzer.md | 22 + viewer/config/gemini_summarizer_model.yaml | 5 + viewer/config/summarizer_config.yaml | 1 + viewer/main.py | 128 +++- viewer/precompute_trends.py | 242 ++++--- viewer/pyproject.toml | 6 +- viewer/summarizer.py | 121 ++++ 10 files changed, 492 insertions(+), 907 deletions(-) create mode 100644 docs/summarizer_documentation.md create mode 100644 viewer/analyzer.md create mode 100644 viewer/config/gemini_summarizer_model.yaml create mode 100644 viewer/config/summarizer_config.yaml create mode 100644 viewer/summarizer.py diff --git a/Makefile b/Makefile index 541a9a1a..98571686 100644 --- a/Makefile +++ b/Makefile @@ -99,7 +99,8 @@ deploy-corprun: --region=us-central1 \ --image=us-central1-docker.pkg.dev/evalbench-dev/cr-images/eval_server:latest \ --port=3000 \ - --memory=2Gi \ + --cpu=4 \ + --memory=8Gi \ --min-instances=1 \ --no-cpu-throttling \ --service-account=crsvc-evalbench@evalbench-dev.iam.gserviceaccount.com \ @@ -111,6 +112,26 @@ deploy-corprun: --add-volume=name=session-files,type=cloud-storage,bucket=evalbench-sessions-cloud-db-nl2sql \ --add-volume-mount=volume=session-files,mount-path=/tmp_session_files +create-precompute-job: + gcloud run jobs create precompute-job \ + --project=evalbench-dev \ + --region=us-central1 \ + --image=us-central1-docker.pkg.dev/evalbench-dev/cr-images/eval_server:latest \ + --cpu=4 \ + --memory=8Gi \ + --service-account=crsvc-evalbench@evalbench-dev.iam.gserviceaccount.com \ + --set-env-vars CLOUD_RUN=True,GOOGLE_CLOUD_PROJECT=evalbench-dev \ + --network=cr-infra-vpc-network \ + --subnet=cr-infra-subnetwork \ + --vpc-egress=all-traffic \ + --add-volume=name=session-files,type=cloud-storage,bucket=evalbench-sessions-cloud-db-nl2sql \ + --add-volume-mount=volume=session-files,mount-path=/tmp_session_files \ + --command=python3 \ + --args=viewer/precompute_trends.py + +run-precompute-job: + gcloud run jobs execute precompute-job --project=evalbench-dev --region=us-central1 + undeploy: gcloud container clusters get-credentials evalbench-directpath-cluster --zone us-central1-c --project cloud-db-nl2sql kubectl delete -f evalbench_service/k8s/evalbench.yaml diff --git a/docs/summarizer_documentation.md b/docs/summarizer_documentation.md new file mode 100644 index 00000000..5072d243 --- /dev/null +++ b/docs/summarizer_documentation.md @@ -0,0 +1,69 @@ +# EvalBench Summarizer Logic and Formula Rationale + +## Summarizer Logic + +The EvalBench summarization pipeline has been optimized to handle large numbers of evaluation runs efficiently and reliably. + +### 1. Parallel Processing +To reduce latency, `viewer/precompute_trends.py` uses a `ThreadPoolExecutor` with **10 workers** to process evaluation directories in parallel. This allows multiple AI summaries to be generated concurrently. + +### 2. Asynchronous Cloud Run Job +Heavy precomputation workloads have been moved off the main web service to a dedicated **Cloud Run Job** (`precompute-job`). This prevents the web service from blocking or timing out during heavy processing. + +### 3. Dual Authentication Support +The summarizer supports two modes of operation depending on available credentials: +- **API Key Mode (Preferred for Dev)**: If the `GOOGLE_API_KEY` environment variable is set, the summarizer bypasses the default Vertex AI configuration and uses the `google-genai` SDK directly to call **`gemini-2.5-flash`** via Google AI Studio. This is faster and avoids IAM permission complexities. +- **Vertex AI Mode (Default Fallback)**: If no API key is set, it falls back to the project's default generator (`gcp_vertex_gemini`) using **`gemini-2.5-pro`** via Vertex AI, relying on Service Account IAM permissions (`roles/aiplatform.user`). + +### 4. Rate Limit Resilience +To handle high concurrency without failing, the summarizer implements **exponential backoff retry logic** for `429 RESOURCE_EXHAUSTED` (Rate Limit) errors. It will retry up to 5 times with increasing delays. + +--- + +## General Score Formula Rationale + +The **General Score** is a weighted composite metric designed to provide a quick, high-level assessment of an evaluation run's success. It ranges from **0 to 100**. + +### The Formula +``` +General Score = 0.4 * goal_completion + 0.2 * trajectory_matcher + 0.2 * behavioral_metrics + 0.2 * parameter_analysis +``` + +### Rationale for Weights + +1. **Goal Completion (40%)**: This is the most critical factor. Did the agent achieve what it was asked to do? Because of its primary importance, it receives the highest weight. +2. **Trajectory Matcher (20%)**: Measures how closely the agent followed the expected path or sequence of actions. This helps assess efficiency and adherence to protocols. +3. **Behavioral Metrics (20%)**: Evaluates the quality of the agent's behavior (e.g., politeness, responsiveness, safety). +4. **Parameter Analysis (20%)**: Checks if the agent used the correct parameters and constraints in its execution. + +By combining these dimensions, the score balances *what* was achieved (Goal Completion) with *how* it was achieved (Trajectory, Behavior, Parameters). + +--- + +## Example Scores and Analysis + +Here are three hypothetical examples demonstrating how different runs might be scored and analyzed. + +### Example 1: High Performer +- **Goal Completion**: 100 +- **Trajectory Matcher**: 90 +- **Behavioral Metrics**: 95 +- **Parameter Analysis**: 100 +- **Calculated General Score**: `(0.4 * 100) + (0.2 * 90) + (0.2 * 95) + (0.2 * 100)` = `40 + 18 + 19 + 20` = **97.0** +- **Analysis**: This run is near perfect. The agent achieved the goal efficiently, followed the expected path closely, exhibited excellent behavior, and adhered to all parameter constraints. + +### Example 2: The "Brute Force" Agent +- **Goal Completion**: 100 +- **Trajectory Matcher**: 40 +- **Behavioral Metrics**: 70 +- **Parameter Analysis**: 60 +- **Calculated General Score**: `(0.4 * 100) + (0.2 * 40) + (0.2 * 70) + (0.2 * 60)` = `40 + 8 + 14 + 12` = **74.0** +- **Analysis**: The agent successfully completed the goal (100%), but it was highly inefficient (low trajectory score) and violated some parameter constraints. It got the job done, but not in the desired manner. + +### Example 3: Failed but Well-Behaved +- **Goal Completion**: 0 +- **Trajectory Matcher**: 30 +- **Behavioral Metrics**: 90 +- **Parameter Analysis**: 80 +- **Calculated General Score**: `(0.4 * 0) + (0.2 * 30) + (0.2 * 90) + (0.2 * 80)` = `0 + 6 + 18 + 16` = **40.0** +- **Analysis**: The agent failed to complete the goal. However, it didn't fail catastrophically in terms of behavior or parameter usage. It likely got stuck or stopped early while maintaining good protocol. The low score correctly reflects the failure to achieve the main objective. diff --git a/uv.lock b/uv.lock index b6626a0b..bf98965f 100644 --- a/uv.lock +++ b/uv.lock @@ -503,19 +503,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "choreographer" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "logistro" }, - { name = "simplejson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/47/64a035c6f764450ea9f902cbeba14c8c70316c2641125510066d8f912bfa/choreographer-1.2.1.tar.gz", hash = "sha256:022afd72b1e9b0bcb950420b134e70055a294c791b6f36cfb47d89745b701b5f", size = 43399, upload-time = "2025-11-09T23:04:44.749Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" }, -] - [[package]] name = "click" version = "8.3.1" @@ -571,169 +558,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - -[[package]] -name = "contourpy" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, - { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, - { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, - { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, - { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, - { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, - { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, - { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, - { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, - { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, - { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, - { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, - { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, - { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, - { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, - { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, -] - [[package]] name = "cryptography" version = "46.0.7" @@ -794,15 +618,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, -] - [[package]] name = "db-dtypes" version = "1.4.4" @@ -995,63 +810,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] -[[package]] -name = "fonttools" -version = "4.62.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, - { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, - { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, - { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, - { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, - { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, - { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, - { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, - { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, - { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, - { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, - { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, - { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, - { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, - { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, - { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, - { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, - { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, - { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, - { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, - { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, - { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -2201,15 +1959,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - [[package]] name = "itsdangerous" version = "2.2.0" @@ -2355,155 +2104,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] -[[package]] -name = "kaleido" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "choreographer" }, - { name = "logistro" }, - { name = "orjson" }, - { name = "packaging" }, - { name = "pytest-timeout" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/76eec859b71eda803a88ea50ed3f270281254656bb23d19eb0a39aa706a0/kaleido-1.2.0.tar.gz", hash = "sha256:fa621a14423e8effa2895a2526be00af0cf21655be1b74b7e382c171d12e71ef", size = 64160, upload-time = "2025-11-04T21:24:23.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/97/f6de8d4af54d6401d6581a686cce3e3e2371a79ba459a449104e026c08bc/kaleido-1.2.0-py3-none-any.whl", hash = "sha256:c27ed82b51df6b923d0e656feac221343a0dbcd2fb9bc7e6b1db97f61e9a1513", size = 68997, upload-time = "2025-11-04T21:24:21.704Z" }, -] - -[[package]] -name = "kiwisolver" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, - { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, - { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, - { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, - { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, - { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, - { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, - { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, - { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, - { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, - { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, - { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, - { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, - { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, - { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, - { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, - { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, - { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, - { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, - { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, - { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, - { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, - { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, - { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, - { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, - { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, - { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, - { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, - { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, - { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, - { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, - { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, - { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, - { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, - { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, - { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, - { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, - { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, - { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, - { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, - { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, - { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, - { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, - { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, - { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, - { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, - { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, - { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, -] - -[[package]] -name = "logistro" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/90/bfd7a6fab22bdfafe48ed3c4831713cb77b4779d18ade5e248d5dbc0ca22/logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047", size = 8398, upload-time = "2025-11-01T02:41:18.81Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/6aa79ba3570bddd1bf7e951c6123f806751e58e8cce736bad77b2cf348d7/logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb", size = 8555, upload-time = "2025-11-01T02:41:17.587Z" }, -] - [[package]] name = "mako" version = "1.3.10" @@ -2613,81 +2213,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "matplotlib" -version = "3.10.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, - { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, - { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, - { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, - { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, - { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, - { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, - { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, - { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, - { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, - { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, -] - [[package]] name = "mcp" version = "1.26.0" @@ -3066,15 +2591,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "narwhals" -version = "2.19.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/1a/bd3317c0bdbcd9ffb710ddf5250b32898f8f2c240be99494fe137feb77a7/narwhals-2.19.0.tar.gz", hash = "sha256:14fd7040b5ff211d415a82e4827b9d04c354e213e72a6d0730205ffd72e3b7ff", size = 623698, upload-time = "2026-04-06T15:50:58.786Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/72/e61e3091e0e00fae9d3a8ef85ece9d2cd4b5966058e1f2901ce42679eebf/narwhals-2.19.0-py3-none-any.whl", hash = "sha256:1f8dfa4a33a6dbff878c3e9be4c3b455dfcaf2a9322f1357db00e4e92e95b84b", size = 446991, upload-time = "2026-04-06T15:50:57.046Z" }, -] - [[package]] name = "numpy" version = "2.2.6" @@ -3390,87 +2906,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, ] -[[package]] -name = "orjson" -version = "3.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/90/5d81f61fe3e4270da80c71442864c091cee3003cc8984c75f413fe742a07/orjson-3.11.8-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e6693ff90018600c72fd18d3d22fa438be26076cd3c823da5f63f7bab28c11cb", size = 229663, upload-time = "2026-03-31T16:14:30.708Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ef/85e06b0eb11de6fb424120fd5788a07035bd4c5e6bb7841ae9972a0526d1/orjson-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93de06bc920854552493c81f1f729fab7213b7db4b8195355db5fda02c7d1363", size = 132321, upload-time = "2026-03-31T16:14:32.317Z" }, - { url = "https://files.pythonhosted.org/packages/86/71/089338ee51b3132f050db0864a7df9bdd5e94c2a03820ab8a91e8f655618/orjson-3.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", size = 130658, upload-time = "2026-03-31T16:14:33.935Z" }, - { url = "https://files.pythonhosted.org/packages/10/0d/f39d8802345d0ad65f7fd4374b29b9b59f98656dc30f21ca5c773265b2f0/orjson-3.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d823831105c01f6c8029faf297633dbeb30271892bd430e9c24ceae3734744", size = 135708, upload-time = "2026-03-31T16:14:35.224Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b5/40aae576b3473511696dcffea84fde638b2b64774eb4dcb8b2c262729f8a/orjson-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60c0423f15abb6cf78f56dff00168a1b582f7a1c23f114036e2bfc697814d5f", size = 147047, upload-time = "2026-03-31T16:14:36.489Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f0/778a84458d1fdaa634b2e572e51ce0b354232f580b2327e1f00a8d88c38c/orjson-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01928d0476b216ad2201823b0a74000440360cef4fed1912d297b8d84718f277", size = 133072, upload-time = "2026-03-31T16:14:37.715Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d3/1bbf2fc3ffcc4b829ade554b574af68cec898c9b5ad6420a923c75a073d3/orjson-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4a639049c44d36a6d1ae0f4a94b271605c745aee5647fa8ffaabcdc01b69a6", size = 133867, upload-time = "2026-03-31T16:14:39.356Z" }, - { url = "https://files.pythonhosted.org/packages/08/94/6413da22edc99a69a8d0c2e83bf42973b8aa94d83ef52a6d39ac85da00bc/orjson-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3222adff1e1ff0dce93c16146b93063a7793de6c43d52309ae321234cdaf0f4d", size = 142268, upload-time = "2026-03-31T16:14:40.972Z" }, - { url = "https://files.pythonhosted.org/packages/4a/5f/aa5dbaa6136d7ba55f5461ac2e885efc6e6349424a428927fd46d68f4396/orjson-3.11.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3223665349bbfb68da234acd9846955b1a0808cbe5520ff634bf253a4407009b", size = 424008, upload-time = "2026-03-31T16:14:42.637Z" }, - { url = "https://files.pythonhosted.org/packages/fa/aa/2c1962d108c7fe5e27aa03a354b378caf56d8eafdef15fd83dec081ce45a/orjson-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:61c9d357a59465736022d5d9ba06687afb7611dfb581a9d2129b77a6fcf78e59", size = 147942, upload-time = "2026-03-31T16:14:44.256Z" }, - { url = "https://files.pythonhosted.org/packages/47/d1/65f404f4c47eb1b0b4476f03ec838cac0c4aa933920ff81e5dda4dee14e7/orjson-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58fb9b17b4472c7b1dcf1a54583629e62e23779b2331052f09a9249edf81675b", size = 136640, upload-time = "2026-03-31T16:14:45.884Z" }, - { url = "https://files.pythonhosted.org/packages/90/5f/7b784aea98bdb125a2f2da7c27d6c2d2f6d943d96ef0278bae596d563f85/orjson-3.11.8-cp310-cp310-win32.whl", hash = "sha256:b43dc2a391981d36c42fa57747a49dae793ef1d2e43898b197925b5534abd10a", size = 132066, upload-time = "2026-03-31T16:14:47.397Z" }, - { url = "https://files.pythonhosted.org/packages/92/ec/2e284af8d6c9478df5ef938917743f61d68f4c70d17f1b6e82f7e3b8dba1/orjson-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:c98121237fea2f679480765abd566f7713185897f35c9e6c2add7e3a9900eb61", size = 127609, upload-time = "2026-03-31T16:14:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, - { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, - { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, - { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, - { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, - { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, - { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, - { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, - { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, - { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, - { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, - { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, - { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, - { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, - { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, - { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, - { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, - { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, - { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, - { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, -] - [[package]] name = "packaging" version = "26.0" @@ -3653,126 +3088,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/07/5fd183858dff4d24840f07fc845f213cd371a19958558607ba22035dadd7/pg8000-1.31.5-py3-none-any.whl", hash = "sha256:0af2c1926b153307639868d2ee5cef6cd3a7d07448e12736989b10e1d491e201", size = 57816, upload-time = "2025-09-14T09:16:47.798Z" }, ] -[[package]] -name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, -] - -[[package]] -name = "plotly" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "narwhals" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/7f/0f100df1172aadf88a929a9dbb902656b0880ba4b960fe5224867159d8f4/plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6", size = 6911286, upload-time = "2026-04-09T20:36:45.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0", size = 9898444, upload-time = "2026-04-09T20:36:39.812Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - [[package]] name = "propcache" version = "0.4.1" @@ -4348,36 +3663,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - -[[package]] -name = "pytest-timeout" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -4749,67 +4034,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] -[[package]] -name = "simplejson" -version = "3.20.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/09/2bf3761de89ea2d91bdce6cf107dcd858892d0adc22c995684878826cc6b/simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5", size = 94039, upload-time = "2025-09-26T16:27:29.283Z" }, - { url = "https://files.pythonhosted.org/packages/0f/33/c3277db8931f0ae9e54b9292668863365672d90fb0f632f4cf9829cb7d68/simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d", size = 75894, upload-time = "2025-09-26T16:27:30.378Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ea/ae47b04d03c7c8a7b7b1a8b39a6e27c3bd424e52f4988d70aca6293ff5e5/simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1", size = 76116, upload-time = "2025-09-26T16:27:31.42Z" }, - { url = "https://files.pythonhosted.org/packages/4b/42/6c9af551e5a8d0f171d6dce3d9d1260068927f7b80f1f09834e07887c8c4/simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6", size = 138827, upload-time = "2025-09-26T16:27:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/2b/22/5e268bbcbe9f75577491e406ec0a5536f5b2fa91a3b52031fea51cd83e1d/simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01", size = 146772, upload-time = "2025-09-26T16:27:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/b4/800f14728e2ad666f420dfdb57697ca128aeae7f991b35759c09356b829a/simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3", size = 134497, upload-time = "2025-09-26T16:27:35.211Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda", size = 138172, upload-time = "2025-09-26T16:27:36.44Z" }, - { url = "https://files.pythonhosted.org/packages/09/36/4e282f5211b34620f1b2e4b51d9ddaab5af82219b9b7b78360a33f7e5387/simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2", size = 140272, upload-time = "2025-09-26T16:27:37.605Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b0/94ad2cf32f477c449e1f63c863d8a513e2408d651c4e58fe4b6a7434e168/simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4", size = 140468, upload-time = "2025-09-26T16:27:39.015Z" }, - { url = "https://files.pythonhosted.org/packages/e5/46/827731e4163be3f987cb8ee90f5d444161db8f540b5e735355faa098d9bc/simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8", size = 148700, upload-time = "2025-09-26T16:27:40.171Z" }, - { url = "https://files.pythonhosted.org/packages/c7/28/c32121064b1ec2fb7b5d872d9a1abda62df064d35e0160eddfa907118343/simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7", size = 141323, upload-time = "2025-09-26T16:27:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/46/b6/c897c54326fe86dd12d101981171a49361949f4728294f418c3b86a1af77/simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53", size = 74377, upload-time = "2025-09-26T16:27:42.533Z" }, - { url = "https://files.pythonhosted.org/packages/ad/87/a6e03d4d80cca99c1fee4e960f3440e2f21be9470e537970f960ca5547f1/simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476", size = 76081, upload-time = "2025-09-26T16:27:43.945Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633, upload-time = "2025-09-26T16:27:45.028Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309, upload-time = "2025-09-26T16:27:46.142Z" }, - { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308, upload-time = "2025-09-26T16:27:47.542Z" }, - { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733, upload-time = "2025-09-26T16:27:48.673Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397, upload-time = "2025-09-26T16:27:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654, upload-time = "2025-09-26T16:27:51.168Z" }, - { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913, upload-time = "2025-09-26T16:27:52.331Z" }, - { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568, upload-time = "2025-09-26T16:27:53.41Z" }, - { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239, upload-time = "2025-09-26T16:27:54.502Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497, upload-time = "2025-09-26T16:27:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069, upload-time = "2025-09-26T16:27:57.039Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158, upload-time = "2025-09-26T16:27:58.265Z" }, - { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911, upload-time = "2025-09-26T16:27:59.292Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523, upload-time = "2025-09-26T16:28:00.356Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844, upload-time = "2025-09-26T16:28:01.756Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655, upload-time = "2025-09-26T16:28:03.037Z" }, - { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335, upload-time = "2025-09-26T16:28:05.027Z" }, - { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519, upload-time = "2025-09-26T16:28:06.5Z" }, - { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571, upload-time = "2025-09-26T16:28:07.715Z" }, - { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367, upload-time = "2025-09-26T16:28:08.921Z" }, - { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205, upload-time = "2025-09-26T16:28:10.086Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823, upload-time = "2025-09-26T16:28:11.329Z" }, - { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997, upload-time = "2025-09-26T16:28:12.917Z" }, - { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367, upload-time = "2025-09-26T16:28:14.393Z" }, - { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285, upload-time = "2025-09-26T16:28:15.931Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969, upload-time = "2025-09-26T16:28:17.017Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530, upload-time = "2025-09-26T16:28:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846, upload-time = "2025-09-26T16:28:19.12Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661, upload-time = "2025-09-26T16:28:20.219Z" }, - { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579, upload-time = "2025-09-26T16:28:21.337Z" }, - { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797, upload-time = "2025-09-26T16:28:22.53Z" }, - { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851, upload-time = "2025-09-26T16:28:23.733Z" }, - { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598, upload-time = "2025-09-26T16:28:24.962Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498, upload-time = "2025-09-26T16:28:26.114Z" }, - { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129, upload-time = "2025-09-26T16:28:27.552Z" }, - { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359, upload-time = "2025-09-26T16:28:28.667Z" }, - { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717, upload-time = "2025-09-26T16:28:30.288Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289, upload-time = "2025-09-26T16:28:31.809Z" }, - { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972, upload-time = "2025-09-26T16:28:32.883Z" }, - { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -5116,12 +4340,9 @@ source = { virtual = "viewer" } dependencies = [ { name = "gevent" }, { name = "gunicorn" }, - { name = "kaleido" }, - { name = "matplotlib" }, { name = "mesop" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "plotly" }, { name = "pyaml" }, ] @@ -5129,11 +4350,8 @@ dependencies = [ requires-dist = [ { name = "gevent", specifier = ">=25.9.1" }, { name = "gunicorn", specifier = ">=25.1.0" }, - { name = "kaleido", specifier = ">=0.2.1" }, - { name = "matplotlib", specifier = ">=3.10.8" }, { name = "mesop", specifier = ">=1.2.1" }, { name = "pandas", specifier = ">=2.3.3" }, - { name = "plotly", specifier = ">=5.18.0" }, { name = "pyaml", specifier = ">=26.2.1" }, ] diff --git a/viewer/analyzer.md b/viewer/analyzer.md new file mode 100644 index 00000000..ec9fef01 --- /dev/null +++ b/viewer/analyzer.md @@ -0,0 +1,22 @@ +Analyze and summarize the following evaluation scoring data. + +Compute a general score between 0 and 100 based on the following formula: +General Score = 0.4 * goal_completion + 0.2 * trajectory_matcher + 0.2 * behavioral_metrics + 0.2 * parameter_analysis + +If any of these metrics are missing, redistribute the weights proportionally among the available ones. + +Include the calculated General Score prominently at the beginning of the summary. + +Use the following template for your analysis: + +### **Summary of Performance** +[Provide a concise summary of the overall performance here] + +### **Key Successes** +[Detail specific successes here] + +### **Key Failures** +[Detail specific failures here] + +### Areas for Improvement +[Suggest areas for improvement here] diff --git a/viewer/config/gemini_summarizer_model.yaml b/viewer/config/gemini_summarizer_model.yaml new file mode 100644 index 00000000..4d4e2fb9 --- /dev/null +++ b/viewer/config/gemini_summarizer_model.yaml @@ -0,0 +1,5 @@ +generator: gcp_vertex_gemini +vertex_model: gemini-2.5-pro +gcp_project_id: evalbench-dev +gcp_region: us-central1 +execs_per_minute: 5 diff --git a/viewer/config/summarizer_config.yaml b/viewer/config/summarizer_config.yaml new file mode 100644 index 00000000..2f31975f --- /dev/null +++ b/viewer/config/summarizer_config.yaml @@ -0,0 +1 @@ +model_config_path: ./gemini_summarizer_model.yaml diff --git a/viewer/main.py b/viewer/main.py index 6ee768b8..f7b97a64 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -6,6 +6,7 @@ import json import subprocess import precompute_trends +from summarizer import summarize_eval_scoring @me.stateclass class State: @@ -23,6 +24,10 @@ class State: selected_main_tab: str = "Status" trends_product_filter: str = "" cache_cleared_message: str = "" + ai_summary: str = "" + is_summarizing: bool = False + ai_score: float = 0.0 + show_formula: bool = False try: # Try to read version from file (created during build) @@ -262,6 +267,7 @@ def status_component(): cache_df = pd.read_csv(cache_file) for _, row in cache_df.iterrows(): data.append({ + 'AI Score': row['ai_score'] if 'ai_score' in row else None, 'Product': row['product'], 'Dataset': row['dataset'] if 'dataset' in row and not pd.isna(row['dataset']) else "N/A", 'Trajectory Matcher': row['trajectory'], @@ -334,6 +340,7 @@ def status_component(): headers = [ "Product", "Dataset", + "AI Score", "Trajectory Matcher", "Goal Completion", "Turn Count", @@ -411,6 +418,12 @@ def handler(e: me.ClickEvent): render_cell(product_val, color="#2563eb", on_click=click_handler) render_cell(dataset_val, color="#2563eb", on_click=click_handler) + if pd.isna(row['AI Score']): + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + else: + score_str = f"{row['AI Score']:.0f}%" + render_cell(score_str, get_color_for_pct(score_str)) + if is_na: # Make cells gray for products with no data render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") @@ -419,6 +432,7 @@ def handler(e: me.ClickEvent): render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") + render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") else: traj_str = f"{row['Trajectory Matcher']:.0f}%" render_cell(traj_str, get_color_for_pct(traj_str)) @@ -496,12 +510,20 @@ def list_view_component(directories, results_dir): try: cache_df = pd.read_csv(cache_file) for _, row in cache_df.iterrows(): + score = row['ai_score'] if 'ai_score' in row else 0.0 + if (score == 0.0 or pd.isna(score)) and 'ai_summary' in row and not pd.isna(row['ai_summary']): + import re + match = re.search(r"General Score:.*?(\d+(\.\d+)?)", row['ai_summary']) + if match: + score = float(match.group(1)) + summaries.append({ "id": str(row['job_id']), "date": str(row['run_time']) if not pd.isna(row['run_time']) else "N/A", "product": str(row['product']) if not pd.isna(row['product']) else "N/A", "requester": str(row['requester']) if not pd.isna(row['requester']) else "N/A", "dataset": str(row['dataset']) if 'dataset' in row and not pd.isna(row['dataset']) else "N/A", + "ai_score": f"{score:.0f}%" if not pd.isna(score) and score != 0.0 else "N/A", "exact_match": f"{row['exact_match']:.0f}%" if not pd.isna(row['exact_match']) else "N/A", "llmrater": f"{row['llmrater']:.0f}%" if not pd.isna(row['llmrater']) else "N/A", "trajectory_matcher": f"{row['trajectory']:.0f}%" if not pd.isna(row['trajectory']) else "N/A", @@ -530,6 +552,7 @@ def get_sort_key(x): "llmrater", "trajectory_matcher", "executable", + "ai_score", ]: if val == "N/A": return -1.0 if reverse else 101.0 @@ -1117,6 +1140,9 @@ def click_latency(e): def click_goal_comp(e): on_sort_click("goal_completion") + + def click_ai_score(e): + on_sort_click("ai_score") sort_handlers = { "id": click_id, @@ -1124,6 +1150,7 @@ def click_goal_comp(e): "product": click_product, "requester": click_requester, "dataset": click_dataset, + "ai_score": click_ai_score, "trajectory_matcher": click_traj, "goal_completion": click_goal_comp, "turn_count": click_turns, @@ -1218,6 +1245,7 @@ def render_header_cell(h_label, h_col, h_width): ("Product", "product", None), ("Requester", "requester", None), ("Dataset", "dataset", None), + ("AI Score", "ai_score", "10ch"), ( "Trajectory Matcher", "trajectory_matcher", @@ -1251,6 +1279,7 @@ def render_header_cell(h_label, h_col, h_width): prod = item["product"] req_val = item.get("requester", "N/A") dataset_val = item.get("dataset", "N/A") + ai_score_val = item.get("ai_score", "N/A") traj = item.get("trajectory_matcher", "N/A") goal_comp = item.get("goal_completion", "N/A") turns = item.get("turn_count", "N/A") @@ -1392,6 +1421,31 @@ def render_header_cell(h_label, h_col, h_width): color="#334155" ), ) + + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="10ch", + white_space="nowrap", + ) + ): + me.text( + ai_score_val, + style=me.Style( + color=get_color_for_pct(ai_score_val) + ), + ) with me.box( style=me.Style( @@ -1701,6 +1755,20 @@ def on_clear_cache_click(e: me.ClickEvent): def on_tab_change(e: me.ButtonToggleChangeEvent): state.selected_tab = e.value + + def on_generate_summary_click(e: me.ClickEvent): + state = me.state(State) + results_dir_full = os.path.join(results_dir, state.selected_directory) + state.ai_summary = summarize_eval_scoring(results_dir_full) + # Parse score + import re + match = re.search(r"\*\*General Score:\s*(\d+(\.\d+)?)[^*]*\*\*", state.ai_summary) + if match: + state.ai_score = float(match.group(1)) + + def on_info_click(e: me.ClickEvent): + state = me.state(State) + state.show_formula = not state.show_formula me.button_toggle( value=state.selected_tab, @@ -1716,7 +1784,6 @@ def on_tab_change(e: me.ButtonToggleChangeEvent): me.ButtonToggleButton( label="Conversations", value="Conversations" ), - # me.ButtonToggleButton(label="Summary", value="Summary"), ], on_change=on_tab_change, ) @@ -1764,6 +1831,51 @@ def get_val(cfg_name): dashboard.dashboard_component( os.path.join(results_dir, state.selected_directory) ) + + me.divider() + me.text("AI Summary", type="headline-5") + + if not state.ai_summary and state.selected_directory: + trends_cache_file = os.path.join(results_dir, "trends_cache.csv") + if os.path.exists(trends_cache_file): + try: + cache_df = pd.read_csv(trends_cache_file) + run_data = cache_df[cache_df['job_id'] == state.selected_directory] + if not run_data.empty and 'ai_summary' in run_data.columns: + summary = run_data['ai_summary'].values[0] + if not pd.isna(summary) and summary != "N/A": + state.ai_summary = summary + if not run_data.empty and 'ai_score' in run_data.columns: + score = run_data['ai_score'].values[0] + if not pd.isna(score): + state.ai_score = float(score) + + # Fallback if score was not parsed correctly in cache + if state.ai_score == 0.0 and state.ai_summary: + import re + match = re.search(r"General Score:.*?(\d+(\.\d+)?)", state.ai_summary) + if match: + state.ai_score = float(match.group(1)) + except Exception as e: + logging.error(f"Error reading AI summary from cache: {e}") + + if not state.ai_summary: + me.button("Generate AI Summary", on_click=on_generate_summary_click) + + if state.ai_summary: + with me.box(style=me.Style(display="flex", align_items="flex-start", margin=me.Margin(bottom="8px"))): + me.text(f"General Score: {state.ai_score}", type="headline-6") + with me.box(style=me.Style(margin=me.Margin(left="2px"), cursor="pointer"), on_click=on_info_click): + me.text("ⓘ", style=me.Style(color="#2563eb", font_size="12px")) + + if state.show_formula: + me.text("Formula: 0.4 * goal_completion + 0.2 * trajectory_matcher + 0.2 * behavioral_metrics + 0.2 * parameter_analysis", style=me.Style(font_size="14px", color="#6b7280", margin=me.Margin(bottom="16px"))) + + # Strip score from summary if present to avoid duplication + import re + clean_summary = re.sub(r"^\s*\*\*General Score:\s*\d+(\.\d+)?[^*]*\*\*\s*", "", state.ai_summary) + me.markdown(clean_summary) + elif state.selected_tab == "Conversations": def on_prev_conversation(e: me.ClickEvent): @@ -1828,20 +1940,6 @@ def on_next_conversation(e: me.ClickEvent): me.text( f"scores.csv not found in {state.selected_directory}" ) - elif state.selected_tab == "Summary": - summary_path = os.path.join( - results_dir, state.selected_directory, "summary.csv" - ) - if os.path.exists(summary_path): - try: - df = pd.read_csv(summary_path) - me.table(data_frame=df) - except Exception as e: - me.text(f"Error reading summary.csv: {e}") - else: - me.text( - f"summary.csv not found in {state.selected_directory}" - ) else: diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index 14e961d4..5a5fd294 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -24,6 +24,126 @@ def get_results_dir(): return results_dir_candidates[1] # Fallback to default + +def process_directory(d, results_dir): + run_dir = os.path.join(results_dir, d) + configs_file = os.path.join(run_dir, "configs.csv") + summary_file = os.path.join(run_dir, "summary.csv") + + if not (os.path.exists(configs_file) and os.path.exists(summary_file)): + return None + + try: + # Read configs + configs_df = pd.read_csv(configs_file) + + # Extract requester, product and dataset + requester_row = configs_df[configs_df['config'].str.contains('guitar_requester', na=False)] + product_row = configs_df[configs_df['config'].isin(['experiment_config.product_name', 'experiment_config.poduct_name'])] + + requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" + product = product_row['value'].values[0] if not product_row.empty else "unknown" + dataset_path = configs_df[configs_df['config'] == 'experiment_config.dataset_config']['value'].values[0] if 'experiment_config.dataset_config' in configs_df['config'].values else "unknown" + dataset = os.path.basename(dataset_path) if dataset_path != "unknown" else "unknown" + + # Read summary + summary_df = pd.read_csv(summary_file) + + # Extract metrics + latency_row = summary_df[summary_df['metric_name'] == 'end_to_end_latency'] + token_row = summary_df[summary_df['metric_name'] == 'token_consumption'] + trajectory_row = summary_df[summary_df['metric_name'] == 'trajectory_matcher'] + executable_row = summary_df[summary_df['metric_name'] == 'executable'] + turn_count_row = summary_df[summary_df['metric_name'] == 'turn_count'] + exact_match_row = summary_df[summary_df['metric_name'] == 'exact_match'] + llmrater_row = summary_df[summary_df['metric_name'] == 'llmrater'] + goal_completion_row = summary_df[summary_df['metric_name'] == 'goal_completion'] + + latency = float(latency_row['metric_score'].values[0]) if not latency_row.empty else 0.0 + tokens = float(token_row['metric_score'].values[0]) if not token_row.empty else 0.0 + turn_count = float(turn_count_row['metric_score'].values[0]) if not turn_count_row.empty else 0.0 + + def get_metric_pct(row): + if not row.empty: + correct = float(row['correct_results_count'].values[0]) + total = float(row['total_results_count'].values[0]) + return (correct / total) * 100 if total > 0 else 0.0 + return 0.0 + + trajectory = get_metric_pct(trajectory_row) + executable = get_metric_pct(executable_row) + exact_match = get_metric_pct(exact_match_row) + llmrater = get_metric_pct(llmrater_row) + goal_completion = get_metric_pct(goal_completion_row) + + if goal_completion == 0.0 and goal_completion_row.empty: + # Fallback to results.csv or scores.csv if goal_completion is missing from summary.csv + results_file = os.path.join(results_dir, d, "results.csv") + scores_file = os.path.join(results_dir, d, "scores.csv") + + file_to_read = None + if os.path.exists(results_file): + file_to_read = results_file + elif os.path.exists(scores_file): + file_to_read = scores_file + + if file_to_read: + try: + df = pd.read_csv(file_to_read) + if 'comparator' in df.columns and 'score' in df.columns: + gc_scores = df[df['comparator'] == 'goal_completion'] + if not gc_scores.empty: + correct = len(gc_scores[gc_scores['score'] == 100.0]) + total = len(gc_scores) + goal_completion = (correct / total) * 100 if total > 0 else 0.0 + logging.info(f"Computed goal_completion from {os.path.basename(file_to_read)} for {d}: {goal_completion}") + except Exception as e: + logging.warning(f"Error reading {os.path.basename(file_to_read)} for {d}: {e}") + + run_time = summary_df['run_time'].values[0] if not summary_df.empty else "unknown" + if run_time != "unknown": + try: + run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') + except Exception as e: + logging.warning(f"Failed to parse run_time '{run_time}': {e}") + + # Call AI Summarizer + ai_summary = "N/A" + ai_score = 0.0 + try: + from summarizer import summarize_eval_scoring + ai_summary = summarize_eval_scoring(run_dir) + + # Parse score from summary + import re + match = re.search(r"General Score:.*?(\d+(\.\d+)?)", ai_summary) + if match: + ai_score = float(match.group(1)) + except Exception as e: + logging.error(f"Error generating AI summary for {d}: {e}") + + return { + 'run_time': run_time, + 'requester': requester, + 'product': product, + 'dataset': dataset, + 'latency': latency, + 'tokens': tokens, + 'trajectory': trajectory, + 'executable': executable, + 'turn_count': turn_count, + 'exact_match': exact_match, + 'llmrater': llmrater, + 'goal_completion': goal_completion, + 'job_id': d, + 'ai_score': ai_score, + 'ai_summary': ai_summary + } + except Exception as e: + logging.error(f"Error reading data from {d}: {e}") + return None + + def precompute(): results_dir = get_results_dir() logging.info(f"Reading results from {results_dir}") @@ -84,113 +204,23 @@ def precompute(): successfully_processed = [] - for i, d in enumerate(new_directories): - if (i + 1) % 10 == 0 or i == total_new - 1: - logging.info(f"Precompute progress: {(i + 1) / total_new * 100:.1f}% ({i + 1}/{total_new})") - run_dir = os.path.join(results_dir, d) - configs_file = os.path.join(run_dir, "configs.csv") - summary_file = os.path.join(run_dir, "summary.csv") + from concurrent.futures import ThreadPoolExecutor + + logging.info(f"Processing {len(new_directories)} new directories with 10 threads...") + with ThreadPoolExecutor(max_workers=10) as executor: + futures = [executor.submit(process_directory, d, results_dir) for d in new_directories] - if os.path.exists(configs_file) and os.path.exists(summary_file): - try: - # Read configs - configs_df = pd.read_csv(configs_file) - - # Extract requester, product and dataset - requester_row = configs_df[configs_df['config'].str.contains('guitar_requester', na=False)] - product_row = configs_df[configs_df['config'].isin(['experiment_config.product_name', 'experiment_config.poduct_name'])] - - requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" - product = product_row['value'].values[0] if not product_row.empty else "unknown" - dataset_path = configs_df[configs_df['config'] == 'experiment_config.dataset_config']['value'].values[0] if 'experiment_config.dataset_config' in configs_df['config'].values else "unknown" - dataset = os.path.basename(dataset_path) if dataset_path != "unknown" else "unknown" - - if dataset != "unknown": - datasets.add(dataset) - - if product != "unknown" and str(product).strip() != "": - products.add(product) - if requester != "unknown" and str(requester).strip() != "": - requesters.add(requester) - - # Read summary - summary_df = pd.read_csv(summary_file) - - # Extract metrics - latency_row = summary_df[summary_df['metric_name'] == 'end_to_end_latency'] - token_row = summary_df[summary_df['metric_name'] == 'token_consumption'] - trajectory_row = summary_df[summary_df['metric_name'] == 'trajectory_matcher'] - executable_row = summary_df[summary_df['metric_name'] == 'executable'] - turn_count_row = summary_df[summary_df['metric_name'] == 'turn_count'] - exact_match_row = summary_df[summary_df['metric_name'] == 'exact_match'] - llmrater_row = summary_df[summary_df['metric_name'] == 'llmrater'] - goal_completion_row = summary_df[summary_df['metric_name'] == 'goal_completion'] - - latency = float(latency_row['metric_score'].values[0]) if not latency_row.empty else 0.0 - tokens = float(token_row['metric_score'].values[0]) if not token_row.empty else 0.0 - turn_count = float(turn_count_row['metric_score'].values[0]) if not turn_count_row.empty else 0.0 - - def get_metric_pct(row): - if not row.empty: - correct = float(row['correct_results_count'].values[0]) - total = float(row['total_results_count'].values[0]) - return (correct / total) * 100 if total > 0 else 0.0 - return 0.0 - - trajectory = get_metric_pct(trajectory_row) - executable = get_metric_pct(executable_row) - exact_match = get_metric_pct(exact_match_row) - llmrater = get_metric_pct(llmrater_row) - goal_completion = get_metric_pct(goal_completion_row) - if goal_completion == 0.0 and goal_completion_row.empty: - # Fallback to results.csv or scores.csv if goal_completion is missing from summary.csv - results_file = os.path.join(results_dir, d, "results.csv") - scores_file = os.path.join(results_dir, d, "scores.csv") - - file_to_read = None - if os.path.exists(results_file): - file_to_read = results_file - elif os.path.exists(scores_file): - file_to_read = scores_file - - if file_to_read: - try: - df = pd.read_csv(file_to_read) - if 'comparator' in df.columns and 'score' in df.columns: - gc_scores = df[df['comparator'] == 'goal_completion'] - if not gc_scores.empty: - correct = len(gc_scores[gc_scores['score'] == 100.0]) - total = len(gc_scores) - goal_completion = (correct / total) * 100 if total > 0 else 0.0 - logging.info(f"Computed goal_completion from {os.path.basename(file_to_read)} for {d}: {goal_completion}") - except Exception as e: - logging.warning(f"Error reading {os.path.basename(file_to_read)} for {d}: {e}") - - run_time = summary_df['run_time'].values[0] if not summary_df.empty else "unknown" - if run_time != "unknown": - try: - run_time = pd.to_datetime(run_time).strftime('%Y-%m-%d') - except Exception as e: - logging.warning(f"Failed to parse run_time '{run_time}': {e}") - - data.append({ - 'run_time': run_time, - 'requester': requester, - 'product': product, - 'dataset': dataset, - 'latency': latency, - 'tokens': tokens, - 'trajectory': trajectory, - 'executable': executable, - 'turn_count': turn_count, - 'exact_match': exact_match, - 'llmrater': llmrater, - 'goal_completion': goal_completion, - 'job_id': d - }) - successfully_processed.append(d) - except Exception as e: - print(f"Error reading data from {d}: {e}") + for future in futures: + res = future.result() + if res: + data.append(res) + successfully_processed.append(res['job_id']) + if res['product'] != "unknown" and str(res['product']).strip() != "": + products.add(res['product']) + if res['requester'] != "unknown" and str(res['requester']).strip() != "": + requesters.add(res['requester']) + if res['dataset'] != "unknown": + datasets.add(res['dataset']) if not data and existing_df.empty: logging.warning("No data found in any run directory and no existing cache.") diff --git a/viewer/pyproject.toml b/viewer/pyproject.toml index 2267c954..67b9853f 100644 --- a/viewer/pyproject.toml +++ b/viewer/pyproject.toml @@ -7,10 +7,10 @@ requires-python = ">=3.10" dependencies = [ "gevent>=25.9.1", "gunicorn>=25.1.0", - "matplotlib>=3.10.8", "mesop>=1.2.1", "pandas>=2.3.3", "pyaml>=26.2.1", - "plotly>=5.18.0", - "kaleido>=0.2.1", ] + +[tool.setuptools] +packages = [] diff --git a/viewer/summarizer.py b/viewer/summarizer.py new file mode 100644 index 00000000..af5f6ad6 --- /dev/null +++ b/viewer/summarizer.py @@ -0,0 +1,121 @@ +import sys +import os +# Add parent directory and parent/evalbench to path to resolve imports +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../evalbench"))) + +import json +import logging +import threading +import pandas as pd +from evalbench.generators.models import get_generator +from evalbench.util.config import load_yaml_config + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Global models dict for get_generator +global_models = {"lock": threading.Lock(), "registered_models": {}} + +def get_summarizer(): + """Loads the generator based on the config in viewer/summarizer_config.yaml.""" + config_path = os.path.join(os.path.dirname(__file__), "config", "summarizer_config.yaml") + if not os.path.exists(config_path): + raise FileNotFoundError(f"Config file not found at {config_path}") + + config = load_yaml_config(config_path) + model_config_path = config.get("model_config_path") + if not model_config_path: + raise ValueError("model_config_path not specified in summarizer_config.yaml") + + # Resolve path relative to the config file if it's relative + if not os.path.isabs(model_config_path): + model_config_path = os.path.abspath(os.path.join(os.path.dirname(config_path), model_config_path)) + + logger.info(f"Loading generator using config: {model_config_path}") + generator = get_generator(global_models, model_config_path) + return generator + +def summarize_eval_scoring(results_dir): + """Reads evals.csv and scores.csv from results_dir and generates a summary using Gemini.""" + evals_path = os.path.join(results_dir, "evals.csv") + scores_path = os.path.join(results_dir, "scores.csv") + + if not os.path.exists(evals_path): + return f"Error: evals.csv not found in {results_dir}" + + try: + evals_df = pd.read_csv(evals_path) + scores_df = pd.read_csv(scores_path) if os.path.exists(scores_path) else None + + # Read prompt from file + prompt_file = os.path.join(os.path.dirname(__file__), "analyzer.md") + prompt_instructions = "Analyze and summarize the following evaluation scoring data.\n\nProvide a concise summary of the performance, highlighting key failures or successes." + if os.path.exists(prompt_file): + with open(prompt_file, "r") as f: + prompt_instructions = f.read() + else: + logger.warning(f"Prompt file not found at {prompt_file}, using default instructions.") + + prompt = prompt_instructions + "\n\n" + prompt += "### Evals Data (Sample or Summary):\n" + # Include first few rows or a summary of evals + prompt += evals_df.head(5).to_string() + "\n\n" + + if scores_df is not None: + prompt += "### Scores Data:\n" + prompt += scores_df.to_string() + "\n\n" + + # Get generator or use API key directly + from google import genai + + api_key = os.environ.get("GOOGLE_API_KEY") + if api_key: + logger.info("Using GOOGLE_API_KEY for summarization") + client = genai.Client(api_key=api_key) + model_name = "gemini-2.5-flash" + else: + logger.info("Using default generator from config") + generator = get_summarizer() + client = generator.client + model_name = generator.vertex_model + + # Call Gemini directly to bypass sanitize_sql in generate_internal + logger.info("Calling Gemini for summarization...") + + import time + from google.genai.errors import ClientError + + max_retries = 5 + base_delay = 2 + + for attempt in range(max_retries): + try: + response = client.models.generate_content( + model=model_name, + contents=prompt, + ) + return response.text + except ClientError as e: + if "429" in str(e) and attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning(f"Rate limit hit (429). Retrying in {delay} seconds... (Attempt {attempt + 1}/{max_retries})") + time.sleep(delay) + else: + raise e + except Exception as e: + raise e + except Exception as e: + logger.exception("Failed to summarize eval scoring") + return f"Error during summarization: {e}" + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python summarizer.py ") + sys.exit(1) + + results_dir = sys.argv[1] + summary = summarize_eval_scoring(results_dir) + print("\n=== Summary ===\n") + print(summary) From f1ba5da804d92606c1e28231f4d22aee46148773 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Mon, 13 Apr 2026 23:53:57 +0000 Subject: [PATCH 23/44] feat: update Gemini model to flash, enhance chart interactivity with job IDs, and add AI score visualization by dataset --- viewer/config/gemini_summarizer_model.yaml | 2 +- viewer/static/chart.js | 11 ++++++++--- viewer/trends.py | 19 +++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/viewer/config/gemini_summarizer_model.yaml b/viewer/config/gemini_summarizer_model.yaml index 4d4e2fb9..32dedb72 100644 --- a/viewer/config/gemini_summarizer_model.yaml +++ b/viewer/config/gemini_summarizer_model.yaml @@ -1,5 +1,5 @@ generator: gcp_vertex_gemini -vertex_model: gemini-2.5-pro +vertex_model: gemini-2.5-flash gcp_project_id: evalbench-dev gcp_region: us-central1 execs_per_minute: 5 diff --git a/viewer/static/chart.js b/viewer/static/chart.js index f852cdfe..e81a4c78 100644 --- a/viewer/static/chart.js +++ b/viewer/static/chart.js @@ -6,7 +6,7 @@ function drawChart() { const title = window.chartConfig.title; const ylabel = window.chartConfig.ylabel; - const margin = { top: 60, right: 180, bottom: 60, left: 60 }; + const margin = { top: 60, right: 350, bottom: 60, left: 60 }; const container = document.getElementById('chart-container'); if (!container) return; @@ -104,7 +104,7 @@ function drawChart() { .on("mouseover", function (event) { d3.select(this).attr("r", 8).style("stroke-width", "3px"); tooltip.style("opacity", 1) - .html(`Product: ${d[hueCol]}
Time: ${d[xCol]}
Value: ${d[yCol]}`); + .html(`Product: ${d[hueCol]}
Time: ${d[xCol]}
Value: ${d[yCol]}
Eval ID: ${d.job_id}`); }) .on("mousemove", function (event) { tooltip.style("left", (event.pageX + 15) + "px") @@ -113,6 +113,11 @@ function drawChart() { .on("mouseout", function () { d3.select(this).attr("r", 5).style("stroke-width", "2px"); tooltip.style("opacity", 0); + }) + .on("click", function(event, d) { + if (d && d.job_id) { + window.open("/?job_id=" + d.job_id, "_blank"); + } }); }); @@ -154,7 +159,7 @@ function drawChart() { .attr("y", 6) .attr("dy", ".35em") .style("text-anchor", "start") - .text(d => d); + .text(d => d.replace('.json', '')); } // Initial draw diff --git a/viewer/trends.py b/viewer/trends.py index 38d2378a..cbd43de8 100644 --- a/viewer/trends.py +++ b/viewer/trends.py @@ -135,6 +135,8 @@ def trends_component(): requester = requester_row['value'].values[0] if not requester_row.empty else "unknown" product = product_row['value'].values[0] if not product_row.empty else "unknown" + dataset_path = configs_df[configs_df['config'] == 'experiment_config.dataset_config']['value'].values[0] if 'experiment_config.dataset_config' in configs_df['config'].values else "unknown" + dataset = os.path.basename(dataset_path) if dataset_path != "unknown" else "unknown" summary_df = pd.read_csv(summary_file) @@ -157,10 +159,12 @@ def trends_component(): 'run_time': run_time, 'requester': requester, 'product': product, + 'dataset': dataset, 'latency': latency, 'tokens': tokens, 'trajectory': trajectory, - 'job_id': d + 'job_id': d, + 'ai_score': 0.0 }) except Exception as e: logging.error(f"Error reading data from {d}: {e}") @@ -174,6 +178,9 @@ def trends_component(): # Filter by requester df = df[df['requester'] == 'cloud-db-nl2sql-testing-jobs'] + # Create product_dataset column for combined line + df['product_dataset'] = df['product'] + " (" + df['dataset'] + ")" + # Filter by product (remove unknown or empty) df = df[df['product'].notna() & (df['product'] != 'unknown') & (df['product'].str.strip() != '')] @@ -191,9 +198,10 @@ def trends_component(): return # Generate charts - latency_chart = generate_d3_chart(df, 'run_time', 'latency', 'product', 'Latency Trend', 'Latency (ms)') - token_chart = generate_d3_chart(df, 'run_time', 'tokens', 'product', 'Token Consumption Trend', 'Tokens') - trajectory_chart = generate_d3_chart(df, 'run_time', 'trajectory', 'product', 'Trajectory Score Trend', 'Score (%)') + latency_chart = generate_d3_chart(df, 'run_time', 'latency', 'product_dataset', 'Latency Trend', 'Latency (ms)') + token_chart = generate_d3_chart(df, 'run_time', 'tokens', 'product_dataset', 'Token Consumption Trend', 'Tokens') + trajectory_chart = generate_d3_chart(df, 'run_time', 'trajectory', 'product_dataset', 'Trajectory Score Trend', 'Score (%)') + ai_score_chart = generate_d3_chart(df, 'run_time', 'ai_score', 'product_dataset', 'AI Score Trend', 'Score') # Render charts @@ -268,6 +276,9 @@ def handler(e: me.ClickEvent): me.text(p, style=me.Style(color="#1f2937")) with me.box(style=me.Style(display="flex", flex_direction="column", gap="16px", width="100%")): + me.text("AI Score", style=me.Style(font_size="16px", font_weight="600")) + me.html(ai_score_chart, mode="sandboxed", style=me.Style(width="100%", height="550px")) + me.text("Latency", style=me.Style(font_size="16px", font_weight="600")) me.html(latency_chart, mode="sandboxed", style=me.Style(width="100%", height="550px")) From 3cd1f2230721ed59e7ec6b7f8a7591d7a2e233eb Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Mon, 13 Apr 2026 23:59:44 +0000 Subject: [PATCH 24/44] fix: remove unused json import and add fallback error message to summarization function --- viewer/summarizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer/summarizer.py b/viewer/summarizer.py index af5f6ad6..f394e4a9 100644 --- a/viewer/summarizer.py +++ b/viewer/summarizer.py @@ -4,7 +4,6 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../evalbench"))) -import json import logging import threading import pandas as pd @@ -109,6 +108,7 @@ def summarize_eval_scoring(results_dir): except Exception as e: logger.exception("Failed to summarize eval scoring") return f"Error during summarization: {e}" + return "Error: Unable to generate summary." if __name__ == "__main__": if len(sys.argv) < 2: From 1aefae6bcb073e1d4009232c5af1ce1b542bfc12 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 17:17:07 +0000 Subject: [PATCH 25/44] feat: add rows-to-show filter, sanitize event handler names, and refine UI layout and header rendering. --- viewer/conversations.py | 3 + viewer/main.py | 244 +++++++++++++++++++++++++++++----------- 2 files changed, 181 insertions(+), 66 deletions(-) diff --git a/viewer/conversations.py b/viewer/conversations.py index ed01a3a6..f3cc96bb 100644 --- a/viewer/conversations.py +++ b/viewer/conversations.py @@ -76,12 +76,14 @@ def conversations_component( flex_direction="row", gap="20px", align_items="flex-start", + flex_wrap="wrap", ) ): # --- Chat (left) --- with me.box( style=me.Style( flex="1", + min_width="400px", display="flex", flex_direction="column", gap="16px", @@ -198,6 +200,7 @@ def conversations_component( with me.box( style=me.Style( width="40%", + min_width="300px", flex_shrink="0", display="flex", flex_direction="column", diff --git a/viewer/main.py b/viewer/main.py index f7b97a64..cd42a8fc 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -28,6 +28,7 @@ class State: is_summarizing: bool = False ai_score: float = 0.0 show_formula: bool = False + rows_to_show: int = 10 try: # Try to read version from file (created during build) @@ -411,7 +412,10 @@ def handler(e: me.ClickEvent): st.selected_main_tab = "List" st.product_filter = p_val st.dataset_filter = d_val - handler.__name__ = f"click_status_row_{p_val}_{d_val}" + + safe_p = str(p_val).replace(" ", "_").replace(".", "_").replace("-", "_") + safe_d = str(d_val).replace(" ", "_").replace(".", "_").replace("-", "_") + handler.__name__ = f"click_status_row_{safe_p}_{safe_d}" return handler click_handler = make_click_handler(product_val, dataset_val) @@ -432,7 +436,6 @@ def handler(e: me.ClickEvent): render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") - render_cell("N/A", color="#94a3b8", cell_bg="#e2e8f0") else: traj_str = f"{row['Trajectory Matcher']:.0f}%" render_cell(traj_str, get_color_for_pct(traj_str)) @@ -461,7 +464,7 @@ def list_view_component(directories, results_dir): with me.box( style=me.Style( background="#ffffff", - padding=me.Padding.all("24px"), + padding=me.Padding.all("12px"), border_radius="12px", border=me.Border.all( me.BorderSide( @@ -581,6 +584,9 @@ def get_sort_key(x): return str(val) summaries.sort(key=get_sort_key, reverse=reverse) + + # Limit number of rows to show after filter/sort + summaries = summaries[:state.rows_to_show] # Extract unique values for filters from ALL summaries all_summaries = [] @@ -811,7 +817,9 @@ def handler(e: me.ClickEvent): st.product_filter = val st.open_dropdown = "" - handler.__name__ = f"click_prod_dd_{val}" + # Sanitize name for Mesop event routing + safe_val = str(val).replace(" ", "_").replace(".", "_").replace("-", "_") + handler.__name__ = f"click_prod_dd_{safe_val}" return handler mk_prod_dd = make_prod_dropdown_handler @@ -912,7 +920,9 @@ def handler(e: me.ClickEvent): st.requester_filter = val st.open_dropdown = "" - handler.__name__ = f"click_req_dd_{val}" + # Sanitize name for Mesop event routing + safe_val = str(val).replace(" ", "_").replace(".", "_").replace("-", "_") + handler.__name__ = f"click_req_dd_{safe_val}" return handler mk_req_dd = make_req_dropdown_handler @@ -1013,7 +1023,9 @@ def handler(e: me.ClickEvent): st.dataset_filter = val st.open_dropdown = "" - handler.__name__ = f"click_dataset_dd_{val}" + # Sanitize name for Mesop event routing + safe_val = str(val).replace(" ", "_").replace(".", "_").replace("-", "_") + handler.__name__ = f"click_dataset_dd_{safe_val}" return handler mk_dataset_dd = make_dataset_dropdown_handler @@ -1099,6 +1111,109 @@ def handler(e: me.ClickEvent): color="#1f2937" ), ) + + # Rows to Show Filter + def toggle_rows_dropdown(e: me.ClickEvent): + st = me.state(State) + if st.open_dropdown == "rows_to_show": + st.open_dropdown = "" + else: + st.open_dropdown = "rows_to_show" + + def make_rows_handler(val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.rows_to_show = val + st.open_dropdown = "" + + handler.__name__ = f"click_rows_{val}" + return handler + + with me.box( + style=me.Style( + position="relative", + width="120px", + ) + ): + # The Box acting as Dropdown Trigger + with me.box( + style=me.Style( + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=toggle_rows_dropdown, + ): + me.text( + f"Show: {state.rows_to_show}", + style=me.Style( + color="#1f2937" + ), + ) + + # The Popup List + if state.open_dropdown == "rows_to_show": + with me.box( + style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, + background="#ffffff", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + ) + ), + border_radius="4px", + width="100%", + max_height="200px", + overflow_y="auto", + ) + ): + for opt in [5, 10, 20, 50, 100]: + with me.box( + style=me.Style( + padding=me.Padding.all("8px"), + cursor="pointer", + ), + on_click=make_rows_handler(opt), + ): + me.text( + str(opt), + style=me.Style( + color="#1f2937" + ), + ) + + def on_reset_click(e: me.ClickEvent): + st = me.state(State) + st.eval_id_filter = "" + st.product_filter = "" + st.requester_filter = "" + st.dataset_filter = "" + st.open_dropdown = "" + + me.button( + "Reset", + on_click=on_reset_click, + style=me.Style( + background="#ef4444", + color="#ffffff", + font_weight="600", + padding=me.Padding.symmetric(vertical="8px", horizontal="16px"), + border_radius="4px", + cursor="pointer", + ) + ) def on_sort_click(col_name): s = me.state(State) @@ -1176,38 +1291,41 @@ def render_header_cell(h_label, h_col, h_width): ), cursor="pointer", width=h_width, - white_space="nowrap" if h_width else None, + white_space="normal", background="#f8fafc", ), on_click=sort_handlers[h_col], ): + s = me.state(State) + arrow = " ↓" if s.sort_descending else " ↑" + arrow_str = arrow if s.sort_column == h_col else "" + + words = h_label.split(" ") with me.box( style=me.Style( display="flex", + flex_direction="column", align_items="center", justify_content="center", color="#475569", ) ): - me.text(h_label) - s = me.state(State) - arrow = ( - " ↓" if s.sort_descending else " ↑" - ) - arrow_str = ( - arrow - if s.sort_column == h_col - else "" - ) - me.text( - arrow_str, - style=me.Style( - font_weight="bold", - color="#0284c7", - font_size="14px", - margin=me.Margin(left="4px"), - ), - ) + for i, w in enumerate(words): + if i == len(words) - 1: + with me.box(style=me.Style(display="flex", flex_direction="row", align_items="center")): + me.text(w) + if arrow_str: + me.text( + arrow_str, + style=me.Style( + font_weight="bold", + color="#0284c7", + font_size="14px", + margin=me.Margin(left="4px"), + ), + ) + else: + me.text(w) with me.box( style=me.Style( @@ -1241,33 +1359,17 @@ def render_header_cell(h_label, h_col, h_width): ): headers = [ ("Eval ID", "id", "36ch"), - ("Date", "date", "24ch"), - ("Product", "product", None), - ("Requester", "requester", None), - ("Dataset", "dataset", None), - ("AI Score", "ai_score", "10ch"), - ( - "Trajectory Matcher", - "trajectory_matcher", - "18ch", - ), - ( - "Goal Completion", - "goal_completion", - "16ch", - ), - ("Turn Count", "turn_count", "12ch"), - ("Executable", "executable", "12ch"), - ( - "Token Consumption", - "token_consumption", - "16ch", - ), - ( - "End-to-End Latency", - "end_to_end_latency", - "20ch", - ), + ("Date", "date", "20ch"), + ("Product", "product", "12ch"), + ("Requester", "requester", "12ch"), + ("Dataset", "dataset", "15ch"), + ("AI Score", "ai_score", "8ch"), + ("Trajectory Matcher", "trajectory_matcher", "12ch"), + ("Goal Completion", "goal_completion", "12ch"), + ("Turn Count", "turn_count", "8ch"), + ("Executable", "executable", "10ch"), + ("Token Consumption", "token_consumption", "12ch"), + ("End-to-End Latency", "end_to_end_latency", "12ch"), ] for label, col, width in headers: render_header_cell(label, col, width) @@ -1661,7 +1763,7 @@ def on_clear_cache_click(e: me.ClickEvent): with me.box( style=me.Style( background="#1e293b", - padding=me.Padding.symmetric(vertical="16px", horizontal="5%"), + padding=me.Padding.symmetric(vertical="8px", horizontal="5%"), margin=me.Margin(bottom="24px"), display="flex", justify_content="space-between", @@ -1946,19 +2048,29 @@ def on_next_conversation(e: me.ClickEvent): from trends import trends_component state = me.state(State) - def on_main_tab_change(e: me.ButtonToggleChangeEvent): - me.state(State).selected_main_tab = e.value + def make_tab_click_handler(tab_val): + def handler(e: me.ClickEvent): + st = me.state(State) + st.selected_main_tab = tab_val + handler.__name__ = f"click_main_tab_{tab_val}" + return handler - with me.box(style=me.Style(margin=me.Margin(bottom="16px"))): - me.button_toggle( - value=state.selected_main_tab, - buttons=[ - me.ButtonToggleButton(label="Status", value="Status"), - me.ButtonToggleButton(label="List", value="List"), - me.ButtonToggleButton(label="Charts", value="Charts"), - ], - on_change=on_main_tab_change, - ) + with me.box(style=me.Style(display="flex", gap="8px", margin=me.Margin(bottom="12px"))): + for tab in ["Status", "List", "Charts"]: + is_active = state.selected_main_tab == tab + with me.box( + style=me.Style( + padding=me.Padding.symmetric(vertical="6px", horizontal="12px"), + background="#1e293b" if is_active else "#f1f5f9", + color="#ffffff" if is_active else "#475569", + border_radius="4px", + cursor="pointer", + font_weight="600" if is_active else "500", + font_size="14px", + ), + on_click=make_tab_click_handler(tab), + ): + me.text(tab) if state.selected_main_tab == "List": try: From e1438a7a04cc89526fdc5abca20c9a1cda6415c0 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 19:15:28 +0000 Subject: [PATCH 26/44] refactor: decouple frontend startup script, fix event handler routing, and correct row limiting logic in viewer --- evalbench/evaluator/oneshotorchestrator.py | 4 +-- evalbench_service/Dockerfile | 1 + evalbench_service/supervisord_cloudrun.conf | 2 +- viewer/main.py | 30 ++++++++++++++------- viewer/run_frontend.sh | 2 ++ 5 files changed, 27 insertions(+), 12 deletions(-) create mode 100755 viewer/run_frontend.sh diff --git a/evalbench/evaluator/oneshotorchestrator.py b/evalbench/evaluator/oneshotorchestrator.py index 4e719640..b8de648a 100644 --- a/evalbench/evaluator/oneshotorchestrator.py +++ b/evalbench/evaluator/oneshotorchestrator.py @@ -7,7 +7,7 @@ import tempfile import threading import uuid -from multiprocessing import Manager +from multiprocessing import get_context import databases import generators.models as models @@ -59,7 +59,7 @@ def evaluate(self, dataset: list[EvalInputRequest]): tmp_buffer = None colab_progress_report = None - with Manager() as manager: + with get_context('spawn').Manager() as manager: sub_datasets, total_dataset_len, total_db_len = breakdown_datasets( dataset) try: diff --git a/evalbench_service/Dockerfile b/evalbench_service/Dockerfile index da961810..87aa0f48 100644 --- a/evalbench_service/Dockerfile +++ b/evalbench_service/Dockerfile @@ -33,6 +33,7 @@ RUN cp /evalbench/evalbench_service/supervisord_evalbench.conf /evalbench/superv RUN cp /evalbench/evalbench_service/supervisord_cloudrun.conf /evalbench/supervisord_cloudrun.conf RUN cp /evalbench/evalbench_service/entrypoint.sh /evalbench/entrypoint.sh && chmod +x /evalbench/entrypoint.sh RUN chmod +x /evalbench/evalbench/run.sh +RUN chmod +x /evalbench/viewer/run_frontend.sh WORKDIR /evalbench CMD ["/evalbench/entrypoint.sh"] diff --git a/evalbench_service/supervisord_cloudrun.conf b/evalbench_service/supervisord_cloudrun.conf index a68669a4..83fae017 100644 --- a/evalbench_service/supervisord_cloudrun.conf +++ b/evalbench_service/supervisord_cloudrun.conf @@ -5,7 +5,7 @@ logfile_maxbytes = 50MB logfile_backups=5 [program:evalbench_frontend] -command=bash -c "gunicorn -w 12 -k gevent main:me --bind :$${PORT:-3000} --forwarded-allow-ips=\"*\" --timeout 120" +command=/evalbench/viewer/run_frontend.sh directory=/evalbench/viewer autostart=true autorestart=true diff --git a/viewer/main.py b/viewer/main.py index cd42a8fc..804d2da9 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -415,7 +415,9 @@ def handler(e: me.ClickEvent): safe_p = str(p_val).replace(" ", "_").replace(".", "_").replace("-", "_") safe_d = str(d_val).replace(" ", "_").replace(".", "_").replace("-", "_") - handler.__name__ = f"click_status_row_{safe_p}_{safe_d}" + handler_name = f"click_status_row_{safe_p}_{safe_d}" + handler.__name__ = handler_name + globals()[handler_name] = handler return handler click_handler = make_click_handler(product_val, dataset_val) @@ -585,9 +587,6 @@ def get_sort_key(x): summaries.sort(key=get_sort_key, reverse=reverse) - # Limit number of rows to show after filter/sort - summaries = summaries[:state.rows_to_show] - # Extract unique values for filters from ALL summaries all_summaries = [] if s.eval_summaries: @@ -681,6 +680,9 @@ def get_sort_key(x): == state.dataset_filter ] + # Limit number of rows to show after filter/sort + summaries = summaries[:state.rows_to_show] + # Render filters UI with me.box( style=me.Style( @@ -718,7 +720,9 @@ def handler(e: me.ClickEvent): st.eval_id_filter = val st.open_dropdown = "" - handler.__name__ = f"click_eval_id_{val}" + handler_name = f"click_eval_id_{val}" + handler.__name__ = handler_name + globals()[handler_name] = handler return handler with me.box( @@ -819,7 +823,9 @@ def handler(e: me.ClickEvent): # Sanitize name for Mesop event routing safe_val = str(val).replace(" ", "_").replace(".", "_").replace("-", "_") - handler.__name__ = f"click_prod_dd_{safe_val}" + handler_name = f"click_prod_dd_{safe_val}" + handler.__name__ = handler_name + globals()[handler_name] = handler return handler mk_prod_dd = make_prod_dropdown_handler @@ -922,7 +928,9 @@ def handler(e: me.ClickEvent): # Sanitize name for Mesop event routing safe_val = str(val).replace(" ", "_").replace(".", "_").replace("-", "_") - handler.__name__ = f"click_req_dd_{safe_val}" + handler_name = f"click_req_dd_{safe_val}" + handler.__name__ = handler_name + globals()[handler_name] = handler return handler mk_req_dd = make_req_dropdown_handler @@ -1025,7 +1033,9 @@ def handler(e: me.ClickEvent): # Sanitize name for Mesop event routing safe_val = str(val).replace(" ", "_").replace(".", "_").replace("-", "_") - handler.__name__ = f"click_dataset_dd_{safe_val}" + handler_name = f"click_dataset_dd_{safe_val}" + handler.__name__ = handler_name + globals()[handler_name] = handler return handler mk_dataset_dd = make_dataset_dropdown_handler @@ -1126,7 +1136,9 @@ def handler(e: me.ClickEvent): st.rows_to_show = val st.open_dropdown = "" - handler.__name__ = f"click_rows_{val}" + handler_name = f"click_rows_{val}" + handler.__name__ = handler_name + globals()[handler_name] = handler return handler with me.box( diff --git a/viewer/run_frontend.sh b/viewer/run_frontend.sh new file mode 100755 index 00000000..b7e8ff68 --- /dev/null +++ b/viewer/run_frontend.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec gunicorn -w 12 -k sync main:me --bind :${PORT:-3000} --forwarded-allow-ips="*" --timeout 120 From a77ea13cbea86bd40a2a0206f7a87f399d014dd8 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 19:56:43 +0000 Subject: [PATCH 27/44] feat: add RWLock to SessionManager for thread safety and return error when session configuration is missing --- evalbench/eval_service.py | 8 +++- evalbench/util/sessionmgr.py | 71 ++++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/evalbench/eval_service.py b/evalbench/eval_service.py index 4cb14283..d2828363 100644 --- a/evalbench/eval_service.py +++ b/evalbench/eval_service.py @@ -124,8 +124,12 @@ async def Eval( session_id = rpc_id_var.get() session = SESSIONMANAGER.get_session(session_id) config, db_configs, model_config, setup_config = load_session_configs(session) - if config is not None: - config["session_id"] = session_id + if config is None: + context.set_code(grpc.StatusCode.FAILED_PRECONDITION) + context.set_details("Session not configured") + return eval_response_pb2.EvalResponse() + + config["session_id"] = session_id streaming_eval = session.get("streaming_eval", False) if session else False loop = asyncio.get_event_loop() diff --git a/evalbench/util/sessionmgr.py b/evalbench/util/sessionmgr.py index 85f27b3b..a2525d5e 100644 --- a/evalbench/util/sessionmgr.py +++ b/evalbench/util/sessionmgr.py @@ -1,5 +1,5 @@ import os -from threading import Thread +from threading import Thread, Lock import logging import time from absl import app @@ -8,6 +8,31 @@ SESSION_RESOURCES_PATH = "/tmp_sessions/" +class RWLock: + def __init__(self): + self.lock = Lock() + self.write_lock = Lock() + self.readers = 0 + + def acquire_read(self): + with self.lock: + self.readers += 1 + if self.readers == 1: + self.write_lock.acquire() + + def release_read(self): + with self.lock: + self.readers -= 1 + if self.readers == 0: + self.write_lock.release() + + def acquire_write(self): + self.write_lock.acquire() + + def release_write(self): + self.write_lock.release() + + class SessionManager: def __init__( self, @@ -15,6 +40,7 @@ def __init__( self.running = True self.sessions = {} self.ttl = 10800 + self.lock = RWLock() logging.debug("Starting reaper...") reaper = Thread(target=self.reaper, args=[]) reaper.daemon = True @@ -27,7 +53,11 @@ def get_ttl(self): return self.ttl def get_session(self, session_id): - return self.sessions.get(session_id) + self.lock.acquire_read() + try: + return self.sessions.get(session_id) + finally: + self.lock.release_read() def write_resource_files(self, session_id, resources): for resource in resources: @@ -52,20 +82,32 @@ def prune_resource_files(self, session_id): os.rmdir(path) def create_session(self, session_id): - if session_id in self.sessions.keys(): - logging.info(f"Session {session_id} already exists.") + self.lock.acquire_write() + try: + if session_id in self.sessions: + logging.info(f"Session {session_id} already exists.") + return self.sessions[session_id] + logging.info(f"Create session {session_id}.") + self.sessions[session_id] = { + "create_ts": time.time(), "session_id": session_id} return self.sessions[session_id] - logging.info(f"Create session {session_id}.") - self.sessions[session_id] = { - "create_ts": time.time(), "session_id": session_id} - return self.sessions[session_id] + finally: + self.lock.release_write() def get_sessions(self): - return self.sessions + self.lock.acquire_read() + try: + return dict(self.sessions) + finally: + self.lock.release_read() def delete_session(self, session_id): - if session_id in self.sessions: - del self.sessions[session_id] + self.lock.acquire_write() + try: + if session_id in self.sessions: + del self.sessions[session_id] + finally: + self.lock.release_write() def shutdown(self): self.running = False @@ -73,7 +115,12 @@ def shutdown(self): def reaper(self): while self.running: now = time.time() - to_delete = [sid for sid, s in self.sessions.items() if now - s["create_ts"] > self.ttl] + self.lock.acquire_read() + try: + to_delete = [sid for sid, s in self.sessions.items() if now - s["create_ts"] > self.ttl] + finally: + self.lock.release_read() + for sid in to_delete: logging.info(f"Delete session {sid}.") self.delete_session(sid) From 60fea62ef2d73dda80294e9694b9c6d29e873aef Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 20:00:30 +0000 Subject: [PATCH 28/44] chore: cleanup whitespace in Python files and expand Makefile phony targets --- Makefile | 14 +++++++++----- evalbench/eval_service.py | 2 +- evalbench/util/sessionmgr.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 98571686..b08aafe0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,11 @@ #!/usr/bin/make -f -default:deploy -.PHONY: default +default: deploy + +.PHONY: default build build-test container shell push-test push push-corprun \ + deploy deploy-test deploy-corprun create-precompute-job run-precompute-job \ + undeploy undeploy-test redeploy redeploy-test pod-shell pod-shell-test \ + proto clean test style run binary CONTAINER_ENGINE ?= docker @@ -28,10 +32,10 @@ TYPE != awk -F '=' '/GOOGLE_ROLE/ { print $$2 }' /etc/lsb-release build: git rev-parse --short HEAD > viewer/version.txt || echo "unknown" > viewer/version.txt - $(CONTAINER_ENGINE) build -t evalbench -f evalbench_service/Dockerfile . + $(CONTAINER_ENGINE) build -t evalbench -f evalbench_service/Dockerfile . build-test: - $(CONTAINER_ENGINE) build -t evalbench-test -f evalbench_service/Dockerfile . + $(CONTAINER_ENGINE) build -t evalbench-test -f evalbench_service/Dockerfile . container: $(CONTAINER_ENGINE) stop evalbench_server || true @@ -42,7 +46,7 @@ container: -v ~/.config/gcloud:/root/.config/gcloud \ -e GOOGLE_CLOUD_PROJECT=cloud-db-nl2sql \ -e MESOP_XSRF_CHECK=false \ - --cap-add=SYS_PTRACE \ + --cap-add=SYS_PTRACE \ -p 3000:3000 \ -p 50051:50051 \ -e TYPE=$(TYPE) evalbench:latest diff --git a/evalbench/eval_service.py b/evalbench/eval_service.py index d2828363..ae7aa741 100644 --- a/evalbench/eval_service.py +++ b/evalbench/eval_service.py @@ -128,7 +128,7 @@ async def Eval( context.set_code(grpc.StatusCode.FAILED_PRECONDITION) context.set_details("Session not configured") return eval_response_pb2.EvalResponse() - + config["session_id"] = session_id streaming_eval = session.get("streaming_eval", False) if session else False diff --git a/evalbench/util/sessionmgr.py b/evalbench/util/sessionmgr.py index a2525d5e..96706d81 100644 --- a/evalbench/util/sessionmgr.py +++ b/evalbench/util/sessionmgr.py @@ -120,7 +120,7 @@ def reaper(self): to_delete = [sid for sid, s in self.sessions.items() if now - s["create_ts"] > self.ttl] finally: self.lock.release_read() - + for sid in to_delete: logging.info(f"Delete session {sid}.") self.delete_session(sid) From d98d90f71414818e25dfca53848321e43e1bc91e Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 23:08:17 +0000 Subject: [PATCH 29/44] feat: implement Gemini-based evaluation comparison tool and update latency displays to minutes --- viewer/ai_comparer.md | 8 ++ viewer/ai_comparer.py | 155 ++++++++++++++++++++++ viewer/main.py | 300 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 442 insertions(+), 21 deletions(-) create mode 100644 viewer/ai_comparer.md create mode 100644 viewer/ai_comparer.py diff --git a/viewer/ai_comparer.md b/viewer/ai_comparer.md new file mode 100644 index 00000000..cacf3f30 --- /dev/null +++ b/viewer/ai_comparer.md @@ -0,0 +1,8 @@ +Compare the following two evaluation runs. Highlight differences in performance, errors, and trajectories. + +Please provide a structured comparison covering: +1. **Executive Summary**: A brief overview of the differences. +2. **Performance**: Compare goal completion and other metrics. **Always present the metrics comparison in a markdown table with columns: Metric, Run 1 Value, Run 2 Value, and Difference.** +3. **Errors**: Detail any errors encountered in either run and how they differ. +4. **Trajectories**: Compare the tool call sequences. +5. **Conclusion**: Final assessment of which run performed better and why. diff --git a/viewer/ai_comparer.py b/viewer/ai_comparer.py new file mode 100644 index 00000000..13f50db4 --- /dev/null +++ b/viewer/ai_comparer.py @@ -0,0 +1,155 @@ +import sys +import os +import logging +import threading +import pandas as pd + +# Add parent directory and parent/evalbench to path to resolve imports +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../evalbench"))) + +from evalbench.generators.models import get_generator +from evalbench.util.config import load_yaml_config + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Global models dict for get_generator +global_models = {"lock": threading.Lock(), "registered_models": {}} + +def get_results_dir(): + # Try to read from environment variable + res_dir = os.environ.get("RESULTS_DIR") + if res_dir: + return res_dir + + # Check multiple locations for results directory + results_dir_candidates = [ + "/tmp_session_files/results", + os.path.join(os.path.dirname(os.path.dirname(__file__)), "results"), + os.path.join(os.getcwd(), "results"), + "/evalbench/results" + ] + + for candidate in results_dir_candidates: + if os.path.exists(candidate): + logger.info(f"Found results directory at: {candidate}") + return candidate + + logger.warning("Results directory not found in candidates, defaulting to current directory results") + return os.path.join(os.getcwd(), "results") + +def get_summarizer(): + """Loads the generator based on the config in viewer/summarizer_config.yaml.""" + config_path = os.path.join(os.path.dirname(__file__), "config", "summarizer_config.yaml") + if not os.path.exists(config_path): + raise FileNotFoundError(f"Config file not found at {config_path}") + + config = load_yaml_config(config_path) + model_config_path = config.get("model_config_path") + if not model_config_path: + raise ValueError("model_config_path not specified in summarizer_config.yaml") + + # Resolve path relative to the config file if it's relative + if not os.path.isabs(model_config_path): + model_config_path = os.path.abspath(os.path.join(os.path.dirname(config_path), model_config_path)) + + logger.info(f"Loading generator using config: {model_config_path}") + generator = get_generator(global_models, model_config_path) + return generator + +def compare_evals(id1, id2): + """Compares two evaluation runs using Gemini.""" + results_dir = get_results_dir() + + path1 = os.path.join(results_dir, id1) + path2 = os.path.join(results_dir, id2) + + if not os.path.exists(path1): + return f"Error: Directory for {id1} not found at {path1}" + if not os.path.exists(path2): + return f"Error: Directory for {id2} not found at {path2}" + + try: + evals1 = pd.read_csv(os.path.join(path1, "evals.csv")) + scores1 = pd.read_csv(os.path.join(path1, "scores.csv")) if os.path.exists(os.path.join(path1, "scores.csv")) else None + + evals2 = pd.read_csv(os.path.join(path2, "evals.csv")) + scores2 = pd.read_csv(os.path.join(path2, "scores.csv")) if os.path.exists(os.path.join(path2, "scores.csv")) else None + + prompt_file = os.path.join(os.path.dirname(__file__), "ai_comparer.md") + prompt_instructions = "Compare the following two evaluation runs. Highlight differences in performance, errors, and trajectories." + if os.path.exists(prompt_file): + with open(prompt_file, "r") as f: + prompt_instructions = f.read() + else: + logger.warning(f"Prompt file not found at {prompt_file}, using default instructions.") + + prompt = prompt_instructions + "\n\n" + + prompt += f"### Run 1: {id1}\n" + prompt += evals1.head(5).to_string() + "\n\n" + if scores1 is not None: + prompt += "Scores:\n" + scores1.to_string() + "\n\n" + + prompt += f"### Run 2: {id2}\n" + prompt += evals2.head(5).to_string() + "\n\n" + if scores2 is not None: + prompt += "Scores:\n" + scores2.to_string() + "\n\n" + + # Get generator or use API key directly + from google import genai + + api_key = os.environ.get("GOOGLE_API_KEY") + if api_key: + logger.info("Using GOOGLE_API_KEY for comparison") + client = genai.Client(api_key=api_key) + model_name = "gemini-2.5-flash" + else: + logger.info("Using default generator from config") + generator = get_summarizer() + client = generator.client + model_name = generator.vertex_model + + logger.info(f"Calling Gemini ({model_name}) for comparison...") + + import time + from google.genai.errors import ClientError + + max_retries = 5 + base_delay = 2 + + for attempt in range(max_retries): + try: + response = client.models.generate_content( + model=model_name, + contents=prompt, + ) + return response.text + except ClientError as e: + if "429" in str(e) and attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning(f"Rate limit hit (429). Retrying in {delay} seconds... (Attempt {attempt + 1}/{max_retries})") + time.sleep(delay) + else: + raise e + except Exception as e: + raise e + + except Exception as e: + logger.exception("Failed to compare evals") + return f"Error during comparison: {e}" + + return "Error: Unable to generate comparison." + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: python ai_comparer.py ") + sys.exit(1) + + id1 = sys.argv[1] + id2 = sys.argv[2] + result = compare_evals(id1, id2) + print("\n=== Comparison ===\n") + print(result) diff --git a/viewer/main.py b/viewer/main.py index 804d2da9..98c57dfc 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -7,6 +7,7 @@ import subprocess import precompute_trends from summarizer import summarize_eval_scoring +from ai_comparer import compare_evals @me.stateclass class State: @@ -29,6 +30,13 @@ class State: ai_score: float = 0.0 show_formula: bool = False rows_to_show: int = 10 + selected_evals: str = "[]" + base_product: str = "" + base_dataset: str = "" + compare_tab_visible: bool = False + compare_evals: str = "[]" + select_mode_active: bool = False + ai_comparison: str = "" try: # Try to read version from file (created during build) @@ -191,7 +199,7 @@ def get_eval_details(results_dir, dir_name): elif name == "token_consumption": details["token_consumption"] = f"{correct:.0f}" elif name == "end_to_end_latency": - details["end_to_end_latency"] = f"{correct:.0f}" + details["end_to_end_latency"] = f"{correct / 60000.0:.2f}m" except Exception as e: logging.warning(f"Error reading summary.csv for {dir_name}: {e}") @@ -454,7 +462,10 @@ def handler(e: me.ClickEvent): render_cell(exec_str, get_color_for_pct(exec_str)) render_cell(f"{row['Token Consumption']:.0f}") - render_cell(f"{row['End-to-End Latency']:.0f}") + if pd.isna(row['End-to-End Latency']): + render_cell("N/A") + else: + render_cell(f"{row['End-to-End Latency'] / 60000.0:.2f}m") else: me.text("No evaluation data found for known products.") else: @@ -463,6 +474,11 @@ def handler(e: me.ClickEvent): def list_view_component(directories, results_dir): state = me.state(State) + import json + try: + selected_evals_list = json.loads(state.selected_evals) + except Exception: + selected_evals_list = [] with me.box( style=me.Style( background="#ffffff", @@ -536,7 +552,7 @@ def list_view_component(directories, results_dir): "turn_count": f"{row['turn_count']:.1f}" if not pd.isna(row['turn_count']) else "N/A", "executable": f"{row['executable']:.0f}%" if not pd.isna(row['executable']) else "N/A", "token_consumption": f"{row['tokens']:.0f}" if not pd.isna(row['tokens']) else "N/A", - "end_to_end_latency": f"{row['latency']:.0f}" if not pd.isna(row['latency']) else "N/A" + "end_to_end_latency": f"{row['latency'] / 60000.0:.2f}m" if not pd.isna(row['latency']) else "N/A" }) s.eval_summaries = json.dumps(summaries) except Exception as e: @@ -586,7 +602,7 @@ def get_sort_key(x): return str(val) summaries.sort(key=get_sort_key, reverse=reverse) - + # Extract unique values for filters from ALL summaries all_summaries = [] if s.eval_summaries: @@ -679,7 +695,20 @@ def get_sort_key(x): if x.get("dataset", "N/A") == state.dataset_filter ] - + + if state.base_product: + summaries = [ + x + for x in summaries + if x["product"] == state.base_product + ] + if state.base_dataset: + summaries = [ + x + for x in summaries + if x.get("dataset", "N/A") == state.base_dataset + ] + # Limit number of rows to show after filter/sort summaries = summaries[:state.rows_to_show] @@ -1206,6 +1235,34 @@ def handler(e: me.ClickEvent): ), ) + def make_select_handler(job_id, item_product, item_dataset): + def handler(e: me.ClickEvent): + st = me.state(State) + import json + try: + sel = json.loads(st.selected_evals) + except Exception: + sel = [] + + if job_id in sel: + sel.remove(job_id) + if not sel: + st.base_product = "" + st.base_dataset = "" + else: + if len(sel) == 0: + sel.append(job_id) + st.base_product = item_product + st.base_dataset = item_dataset + elif len(sel) == 1: + sel.append(job_id) + + st.selected_evals = json.dumps(sel) + + safe_id = str(job_id).replace(" ", "_").replace(".", "_").replace("-", "_") + handler.__name__ = f"click_select_{safe_id}" + return handler + def on_reset_click(e: me.ClickEvent): st = me.state(State) st.eval_id_filter = "" @@ -1213,6 +1270,12 @@ def on_reset_click(e: me.ClickEvent): st.requester_filter = "" st.dataset_filter = "" st.open_dropdown = "" + st.selected_evals = "[]" + st.base_product = "" + st.base_dataset = "" + st.select_mode_active = False + st.compare_tab_visible = False + st.ai_comparison = "" me.button( "Reset", @@ -1226,6 +1289,24 @@ def on_reset_click(e: me.ClickEvent): cursor="pointer", ) ) + + def on_toggle_select_mode(e: me.ClickEvent): + st = me.state(State) + st.select_mode_active = not st.select_mode_active + + me.button( + "Compare", + on_click=on_toggle_select_mode, + style=me.Style( + background="#0284c7" if state.select_mode_active else "#e2e8f0", + color="#ffffff" if state.select_mode_active else "#475569", + font_weight="600", + padding=me.Padding.symmetric(vertical="8px", horizontal="16px"), + border_radius="4px", + cursor="pointer", + margin=me.Margin(left="8px"), + ) + ) def on_sort_click(col_name): s = me.state(State) @@ -1271,7 +1352,11 @@ def click_goal_comp(e): def click_ai_score(e): on_sort_click("ai_score") + def click_select(e): + pass + sort_handlers = { + "select": click_select, "id": click_id, "date": click_date, "product": click_product, @@ -1339,6 +1424,73 @@ def render_header_cell(h_label, h_col, h_width): else: me.text(w) + # Selection Toolbar + if selected_evals_list: + with me.box( + style=me.Style( + display="flex", + flex_direction="row", + justify_content="space-between", + align_items="center", + padding=me.Padding.all("8px"), + background="#e0f2fe", + border_radius="4px", + margin=me.Margin(top="16px"), + ) + ): + me.text(f"Selected: {len(selected_evals_list)} / 2", style=me.Style(color="#0369a1", font_weight="600")) + + def on_clear_selection(e: me.ClickEvent): + st = me.state(State) + st.selected_evals = "[]" + st.base_product = "" + st.base_dataset = "" + + def on_compare_click(e: me.ClickEvent): + st = me.state(State) + st.compare_tab_visible = True + st.compare_evals = st.selected_evals + st.selected_main_tab = "Compare" + + if not st.ai_comparison: + st.ai_comparison = "Comparing..." + logging.info("Set ai_comparison to Comparing... in on_compare_click") + yield + + import json + try: + comp_evals = json.loads(st.compare_evals) + except Exception: + comp_evals = [] + + logging.info(f"comp_evals in on_compare_click: {comp_evals}") + if len(comp_evals) == 2: + logging.info("Starting compare_evals in on_compare_click...") + st.ai_comparison = compare_evals(comp_evals[0], comp_evals[1]) + logging.info("Finished compare_evals in on_compare_click.") + yield + + with me.box(style=me.Style(display="flex", gap="8px")): + me.button( + "Clear", + on_click=on_clear_selection, + style=me.Style( + background="#ef4444", + color="#ffffff", + font_weight="600", + ) + ) + if len(selected_evals_list) == 2 and me.state(State).selected_main_tab != "Compare": + me.button( + "Compare", + on_click=on_compare_click, + style=me.Style( + background="#10b981", + color="#ffffff", + font_weight="600", + ), + ) + with me.box( style=me.Style( max_height="600px", @@ -1369,7 +1521,10 @@ def render_header_cell(h_label, h_col, h_width): letter_spacing="0.05em", ) ): - headers = [ + headers = [] + if state.select_mode_active: + headers.append(("Select", "select", "8ch")) + headers.extend([ ("Eval ID", "id", "36ch"), ("Date", "date", "20ch"), ("Product", "product", "12ch"), @@ -1382,7 +1537,7 @@ def render_header_cell(h_label, h_col, h_width): ("Executable", "executable", "10ch"), ("Token Consumption", "token_consumption", "12ch"), ("End-to-End Latency", "end_to_end_latency", "12ch"), - ] + ]) for label, col, width in headers: render_header_cell(label, col, width) @@ -1415,6 +1570,38 @@ def render_header_cell(h_label, h_col, h_width): background=bg_color, ) ): + # Select checkbox + if state.select_mode_active: + with me.box( + style=me.Style( + display="table-cell", + padding=me.Padding.symmetric( + vertical="10px", horizontal="16px" + ), + text_align="center", + border=me.Border.all( + me.BorderSide( + width="1px", + color="#e2e8f0", + style="solid", + ) + ), + width="8ch", + ) + ): + is_selected = d in selected_evals_list + label = "✅" if is_selected else "⬜" + me.button( + label, + on_click=make_select_handler(d, prod, dataset_val), + style=me.Style( + background="transparent", + color="#0284c7" if is_selected else "#64748b", + font_weight="bold", + border=me.Border.all(me.BorderSide(width="0px")), + cursor="pointer", + ), + ) # Eval ID as a link/button with me.box( style=me.Style( @@ -1727,11 +1914,49 @@ def app(): render_app_content() +def on_status_tab_click(e: me.ClickEvent): + st = me.state(State) + st.selected_main_tab = "Status" + logging.info("Tab clicked: Status") + +def on_list_tab_click(e: me.ClickEvent): + st = me.state(State) + st.selected_main_tab = "List" + logging.info("Tab clicked: List") + +def on_charts_tab_click(e: me.ClickEvent): + st = me.state(State) + st.selected_main_tab = "Charts" + logging.info("Tab clicked: Charts") + +def on_compare_tab_click(e: me.ClickEvent): + st = me.state(State) + st.selected_main_tab = "Compare" + logging.info("Tab clicked: Compare") + if not st.ai_comparison: + st.ai_comparison = "Comparing..." + logging.info("Set ai_comparison to Comparing...") + yield + + import json + try: + comp_evals = json.loads(st.compare_evals) + except Exception: + comp_evals = [] + + logging.info(f"comp_evals: {comp_evals}") + if len(comp_evals) == 2: + logging.info("Starting compare_evals...") + st.ai_comparison = compare_evals(comp_evals[0], comp_evals[1]) + logging.info("Finished compare_evals.") + yield + + def render_app_content(): try: state = me.state(State) results_dir = get_results_dir() - logging.info(f"render_app_content: selected_directory='{state.selected_directory}'") + logging.info(f"render_app_content: selected_directory='{state.selected_directory}', selected_evals='{state.selected_evals}', selected_main_tab='{state.selected_main_tab}'") directories = [] if os.path.exists(results_dir): @@ -2060,17 +2285,25 @@ def on_next_conversation(e: me.ClickEvent): from trends import trends_component state = me.state(State) - def make_tab_click_handler(tab_val): - def handler(e: me.ClickEvent): - st = me.state(State) - st.selected_main_tab = tab_val - handler.__name__ = f"click_main_tab_{tab_val}" - return handler - - with me.box(style=me.Style(display="flex", gap="8px", margin=me.Margin(bottom="12px"))): - for tab in ["Status", "List", "Charts"]: + with me.box(style=me.Style(margin=me.Margin(bottom="12px"))): + tabs = ["Status", "List", "Charts"] + if state.compare_tab_visible: + tabs.append("Compare") + for tab in tabs: is_active = state.selected_main_tab == tab - with me.box( + tab_text = tab + if tab == "Compare" and state.ai_comparison == "Comparing...": + tab_text += " (Loading...)" + + click_handler = None + if tab == "Status": click_handler = on_status_tab_click + elif tab == "List": click_handler = on_list_tab_click + elif tab == "Charts": click_handler = on_charts_tab_click + elif tab == "Compare": click_handler = on_compare_tab_click + + me.button( + tab_text, + on_click=click_handler, style=me.Style( padding=me.Padding.symmetric(vertical="6px", horizontal="12px"), background="#1e293b" if is_active else "#f1f5f9", @@ -2079,10 +2312,9 @@ def handler(e: me.ClickEvent): cursor="pointer", font_weight="600" if is_active else "500", font_size="14px", + margin=me.Margin(right="8px") ), - on_click=make_tab_click_handler(tab), - ): - me.text(tab) + ) if state.selected_main_tab == "List": try: @@ -2094,6 +2326,32 @@ def handler(e: me.ClickEvent): trends_component() elif state.selected_main_tab == "Status": status_component() + elif state.selected_main_tab == "Compare": + import json + try: + comp_evals = json.loads(state.compare_evals) + except Exception: + comp_evals = [] + + with me.box(style=me.Style(padding=me.Padding.all("16px"))): + me.text("Comparison", type="headline-5") + if len(comp_evals) == 2: + with me.box(style=me.Style(margin=me.Margin(bottom="16px"))): + me.markdown(f'Comparing: {comp_evals[0]} vs {comp_evals[1]}') + + if not state.ai_comparison or state.ai_comparison == "Comparing...": + me.text("Comparing...", style=me.Style(font_weight="bold", color="#0284c7", margin=me.Margin(bottom="16px"))) + else: + with me.box(style=me.Style( + background="#ffffff", + padding=me.Padding.all("16px"), + border_radius="8px", + border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), + margin=me.Margin(top="16px") + )): + me.markdown(state.ai_comparison) + else: + me.text("Invalid comparison state.") except Exception as e: From 3529693936f585f90a64f8688ec4f98a806b437f Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 23:46:40 +0000 Subject: [PATCH 30/44] style: increase dropdown trigger width to 300px --- viewer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer/main.py b/viewer/main.py index 98c57dfc..2d813f03 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -1072,7 +1072,7 @@ def handler(e: me.ClickEvent): with me.box( style=me.Style( position="relative", - width="200px", + width="300px", ) ): # The Box acting as Dropdown Trigger From 7f8e2f06c8d071dd2ac8c886e54365bd74ba252d Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Tue, 14 Apr 2026 23:56:13 +0000 Subject: [PATCH 31/44] refactor: relocate ai_comparer config and update general score parsing format --- viewer/ai_comparer.py | 2 +- viewer/analyzer.md | 2 +- viewer/{ => config}/ai_comparer.md | 0 viewer/precompute_trends.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename viewer/{ => config}/ai_comparer.md (100%) diff --git a/viewer/ai_comparer.py b/viewer/ai_comparer.py index 13f50db4..026ffd99 100644 --- a/viewer/ai_comparer.py +++ b/viewer/ai_comparer.py @@ -78,7 +78,7 @@ def compare_evals(id1, id2): evals2 = pd.read_csv(os.path.join(path2, "evals.csv")) scores2 = pd.read_csv(os.path.join(path2, "scores.csv")) if os.path.exists(os.path.join(path2, "scores.csv")) else None - prompt_file = os.path.join(os.path.dirname(__file__), "ai_comparer.md") + prompt_file = os.path.join(os.path.dirname(__file__), "config", "ai_comparer.md") prompt_instructions = "Compare the following two evaluation runs. Highlight differences in performance, errors, and trajectories." if os.path.exists(prompt_file): with open(prompt_file, "r") as f: diff --git a/viewer/analyzer.md b/viewer/analyzer.md index ec9fef01..f212f449 100644 --- a/viewer/analyzer.md +++ b/viewer/analyzer.md @@ -5,7 +5,7 @@ General Score = 0.4 * goal_completion + 0.2 * trajectory_matcher + 0.2 * behavio If any of these metrics are missing, redistribute the weights proportionally among the available ones. -Include the calculated General Score prominently at the beginning of the summary. +Include the calculated General Score as the very first line of your response in the exact format: `General Score: XX` (where XX is the calculated score). Do not put any text or markdown before it. Use the following template for your analysis: diff --git a/viewer/ai_comparer.md b/viewer/config/ai_comparer.md similarity index 100% rename from viewer/ai_comparer.md rename to viewer/config/ai_comparer.md diff --git a/viewer/precompute_trends.py b/viewer/precompute_trends.py index 5a5fd294..56225ad1 100644 --- a/viewer/precompute_trends.py +++ b/viewer/precompute_trends.py @@ -116,7 +116,7 @@ def get_metric_pct(row): # Parse score from summary import re - match = re.search(r"General Score:.*?(\d+(\.\d+)?)", ai_summary) + match = re.search(r"General Score.*?(\d+(\.\d+)?)", ai_summary, re.IGNORECASE) if match: ai_score = float(match.group(1)) except Exception as e: From dada69627265d846ce8a751ff87198ac7b20e909 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 00:04:04 +0000 Subject: [PATCH 32/44] refactor: remove redundant local json imports in main.py --- viewer/main.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 2d813f03..a9c65139 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -474,7 +474,6 @@ def handler(e: me.ClickEvent): def list_view_component(directories, results_dir): state = me.state(State) - import json try: selected_evals_list = json.loads(state.selected_evals) except Exception: @@ -1238,7 +1237,6 @@ def handler(e: me.ClickEvent): def make_select_handler(job_id, item_product, item_dataset): def handler(e: me.ClickEvent): st = me.state(State) - import json try: sel = json.loads(st.selected_evals) except Exception: @@ -1457,7 +1455,6 @@ def on_compare_click(e: me.ClickEvent): logging.info("Set ai_comparison to Comparing... in on_compare_click") yield - import json try: comp_evals = json.loads(st.compare_evals) except Exception: @@ -1938,7 +1935,6 @@ def on_compare_tab_click(e: me.ClickEvent): logging.info("Set ai_comparison to Comparing...") yield - import json try: comp_evals = json.loads(st.compare_evals) except Exception: @@ -2327,7 +2323,6 @@ def on_next_conversation(e: me.ClickEvent): elif state.selected_main_tab == "Status": status_component() elif state.selected_main_tab == "Compare": - import json try: comp_evals = json.loads(state.compare_evals) except Exception: From c438fb800e43a5733046e6d0356827141b378bb9 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 15:35:15 +0000 Subject: [PATCH 33/44] fix: include database identifier in error logs when connection acquisition fails --- evalbench/evaluator/evaluator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evalbench/evaluator/evaluator.py b/evalbench/evaluator/evaluator.py index 0d7bedea..4d1a7ea5 100644 --- a/evalbench/evaluator/evaluator.py +++ b/evalbench/evaluator/evaluator.py @@ -151,7 +151,7 @@ def evaluate( except Exception as e: logging.error( - f"Failed to acquire DB connection from queue: {e}") + f"Failed to acquire DB connection from queue for database '{eval_output.get('database')}': {e}") eval_output["generated_error"] = f"Failed to acquire DB connection: {e}" record_successful_sql_exec(progress_reporting) work = scorework.ScorerWork( From bbb7645d6fb2d48af0e4e44adae0be890811549d Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 15:41:30 +0000 Subject: [PATCH 34/44] fix: update error handling to log critical failures using logging.error instead of logging.info --- evalbench/databases/mysql.py | 6 +++--- evalbench/databases/postgres.py | 6 +++--- evalbench/databases/sqlserver.py | 6 +++--- evalbench/evaluator/dataagentorchestrator.py | 4 ++-- evalbench/evaluator/interactorchestrator.py | 4 ++-- evalbench/evaluator/oneshotorchestrator.py | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/evalbench/databases/mysql.py b/evalbench/databases/mysql.py index 2515dc44..30e78545 100644 --- a/evalbench/databases/mysql.py +++ b/evalbench/databases/mysql.py @@ -221,7 +221,7 @@ def _run_execute( self.max_attempts, ) except ResourceExhaustedError as e: - logging.info( + logging.error( "Resource Exhausted on MySQL DB. Giving up execution. Try reducing execs_per_minute." ) return None, None, None @@ -274,7 +274,7 @@ def drop_tmp_database(self, database_name: str): self.tmp_dbs.remove(database_name) _, _, error = self.execute(f"DROP DATABASE {database_name};") if error: - logging.info(f"Could not delete database: {error}") + logging.error(f"Could not delete database: {error}") def ensure_database_exists(self, database_name: str) -> None: if getattr(self, "use_cloud_sql", False): @@ -357,4 +357,4 @@ def delete_tmp_user(self, username: str): self.tmp_users.remove(username) _, _, error = self.execute(DELETE_USER_QUERY.format(USERNAME=username)) if error: - logging.info(f"Could not delete tmp user due to {error}") + logging.error(f"Could not delete tmp user due to {error}") diff --git a/evalbench/databases/postgres.py b/evalbench/databases/postgres.py index 2e3609c9..b4fa6207 100644 --- a/evalbench/databases/postgres.py +++ b/evalbench/databases/postgres.py @@ -186,7 +186,7 @@ def _run_execute(query: str, eval_query: Optional[str] = None, rollback=False): self.max_attempts, ) except ResourceExhaustedError as e: - logging.info( + logging.error( "Resource Exhausted on Postgres DB. Giving up execution. Try reducing execs_per_minute." ) return None, None, None @@ -240,7 +240,7 @@ def drop_tmp_database(self, database_name: str): self.tmp_dbs.remove(database_name) _, error = self._execute_auto_commit(f"DROP DATABASE {database_name};") if error: - logging.info(f"Could not delete database: {error}") + logging.error(f"Could not delete database: {error}") def ensure_database_exists(self, database_name: str) -> None: from google.cloud.sql.connector import Connector @@ -307,7 +307,7 @@ def delete_tmp_user(self, username: str): self.tmp_users.remove(username) _, _, error = self.execute(DELETE_USER_QUERY.format(USERNAME=username)) if error: - logging.info(f"Could not delete tmp user due to {error}") + logging.error(f"Could not delete tmp user due to {error}") ##################################################### ##################################################### diff --git a/evalbench/databases/sqlserver.py b/evalbench/databases/sqlserver.py index 36471b76..3aa70a13 100644 --- a/evalbench/databases/sqlserver.py +++ b/evalbench/databases/sqlserver.py @@ -171,7 +171,7 @@ def _run_execute(query: str, eval_query: Optional[str] = None, rollback=False): self.max_attempts, ) except ResourceExhaustedError as e: - logging.info( + logging.error( "Resource Exhausted on SQLServer DB. Giving up execution. Try reducing execs_per_minute." ) return None, None, None @@ -225,7 +225,7 @@ def drop_tmp_database(self, database_name: str): self.tmp_dbs.remove(database_name) _, error = self._execute_autocommit(f"DROP DATABASE {database_name};") if error: - logging.info(f"Could not delete database: {error}") + logging.error(f"Could not delete database: {error}") def ensure_database_exists(self, database_name: str) -> None: query = f"IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'{database_name}') CREATE DATABASE {database_name};" @@ -290,7 +290,7 @@ def delete_tmp_user(self, username: str): self.tmp_users.remove(username) _, _, error = self.execute(DELETE_USER_QUERY.format(USERNAME=username)) if error: - logging.info(f"Could not delete tmp user due to {error}") + logging.error(f"Could not delete tmp user due to {error}") ##################################################### ##################################################### diff --git a/evalbench/evaluator/dataagentorchestrator.py b/evalbench/evaluator/dataagentorchestrator.py index dd5a7ad6..58b607f1 100644 --- a/evalbench/evaluator/dataagentorchestrator.py +++ b/evalbench/evaluator/dataagentorchestrator.py @@ -172,7 +172,7 @@ def evaluate_sub_dataset( ) record_successful_setup(progress_reporting) except Exception as e: - logging.info( + logging.error( f"Skipping {query_type} queries as DB {database} " + f"could not be setup properly in {dialect} due to {e}." ) @@ -196,7 +196,7 @@ def evaluate_sub_dataset( total_eval_outputs.extend(eval_outputs) total_scoring_results.extend(scoring_results) except Exception as e: - logging.info( + logging.error( f"Failed to evaluate {sub_dataset_len} {query_type} queries " + f"on DB {database} on {dialect}. Due to {e}" ) diff --git a/evalbench/evaluator/interactorchestrator.py b/evalbench/evaluator/interactorchestrator.py index 12284e7f..6bef89a8 100644 --- a/evalbench/evaluator/interactorchestrator.py +++ b/evalbench/evaluator/interactorchestrator.py @@ -172,7 +172,7 @@ def evaluate_sub_dataset( ) record_successful_setup(progress_reporting) except Exception as e: - logging.info( + logging.error( f"Skipping {query_type} queries as DB {database} " + f"could not be setup properly in {dialect} due to {e}." ) @@ -197,7 +197,7 @@ def evaluate_sub_dataset( total_scoring_results.extend(scoring_results) break except Exception as e: - logging.info( + logging.error( f"Failed to evaluate {sub_dataset_len} {query_type} queries " + f"on DB {database} on {dialect}. Due to {e}" ) diff --git a/evalbench/evaluator/oneshotorchestrator.py b/evalbench/evaluator/oneshotorchestrator.py index b8de648a..9ca99505 100644 --- a/evalbench/evaluator/oneshotorchestrator.py +++ b/evalbench/evaluator/oneshotorchestrator.py @@ -190,7 +190,7 @@ def evaluate_sub_dataset( ) record_successful_setup(progress_reporting) except Exception as e: - logging.info( + logging.error( f"Skipping {query_type} queries as DB {database} " + f"could not be setup properly in {dialect} due to {e}." ) @@ -215,7 +215,7 @@ def evaluate_sub_dataset( total_eval_outputs.extend(eval_outputs) total_scoring_results.extend(scoring_results) except Exception as e: - logging.info( + logging.error( f"Failed to evaluate {sub_dataset_len} {query_type} queries " + f"on DB {database} on {dialect}. Due to {e}") From b07489a34ef1351315115cc7f012384788ea3e21 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 15:58:16 +0000 Subject: [PATCH 35/44] feat: update VPA mode to Initial to prevent disruptive evictions --- evalbench_service/k8s/vertical-autoscale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evalbench_service/k8s/vertical-autoscale.yaml b/evalbench_service/k8s/vertical-autoscale.yaml index 43b700c8..f5882f75 100644 --- a/evalbench_service/k8s/vertical-autoscale.yaml +++ b/evalbench_service/k8s/vertical-autoscale.yaml @@ -9,7 +9,7 @@ spec: name: evalbench-eval-server-deploy apiVersion: apps/v1 updatePolicy: - updateMode: Auto + updateMode: Initial resourcePolicy: containerPolicies: - containerName: "*" From b7e10d25d4bf7a653c924cdf39ca7d01a4a156ad Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 16:16:46 +0000 Subject: [PATCH 36/44] feat: set pod resources to 2/3 of node capacity and disable VPA overrides --- evalbench_service/k8s/evalbench.yaml | 8 ++++---- evalbench_service/k8s/vertical-autoscale.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/evalbench_service/k8s/evalbench.yaml b/evalbench_service/k8s/evalbench.yaml index 7fb89f78..9f4f233d 100644 --- a/evalbench_service/k8s/evalbench.yaml +++ b/evalbench_service/k8s/evalbench.yaml @@ -37,11 +37,11 @@ spec: name: evalbench-eval resources: requests: - cpu: "100m" - memory: "256Mi" + cpu: "42" + memory: "168Gi" limits: - cpu: "1000m" - memory: "1Gi" + cpu: "42" + memory: "168Gi" securityContext: allowPrivilegeEscalation: true capabilities: diff --git a/evalbench_service/k8s/vertical-autoscale.yaml b/evalbench_service/k8s/vertical-autoscale.yaml index f5882f75..14902959 100644 --- a/evalbench_service/k8s/vertical-autoscale.yaml +++ b/evalbench_service/k8s/vertical-autoscale.yaml @@ -13,7 +13,7 @@ spec: resourcePolicy: containerPolicies: - containerName: "*" - mode: Auto + mode: "Off" controlledResources: - cpu - memory From 676ea5cc322708567f6d8bb43f669a93b94be2df Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 17:32:57 +0000 Subject: [PATCH 37/44] refactor: remove redundant whitespace after filtering summaries --- viewer/main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 6e84c903..75891a92 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -707,10 +707,7 @@ def get_sort_key(x): for x in summaries if x.get("dataset", "N/A") == state.base_dataset ] - - # Limit number of rows to show after filter/sort - summaries = summaries[:state.rows_to_show] - + # Limit number of rows to show after filter/sort summaries = summaries[:state.rows_to_show] From 64905e55de40f5fbbee6f15565b65616209735bc Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 18:49:50 +0000 Subject: [PATCH 38/44] feat: include session_id in EvalResponse for Ping, Connect, EvalConfig, and ListEvalInputs methods --- evalbench/eval_service.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/evalbench/eval_service.py b/evalbench/eval_service.py index ae7aa741..9e96b197 100644 --- a/evalbench/eval_service.py +++ b/evalbench/eval_service.py @@ -70,7 +70,8 @@ async def Ping( request: eval_request_pb2.PingRequest, context: grpc.ServicerContext, ) -> eval_response_pb2.EvalResponse: - return eval_response_pb2.EvalResponse(response="ack") + session_id = rpc_id_var.get() + return eval_response_pb2.EvalResponse(response="ack", session_id=session_id) async def Connect( self, @@ -81,7 +82,7 @@ async def Connect( session = SESSIONMANAGER.get_session(session_id) if session is not None: session["streaming_eval"] = request.streaming_eval - return eval_response_pb2.EvalResponse(response="ack") + return eval_response_pb2.EvalResponse(response="ack", session_id=session_id) async def EvalConfig( self, @@ -99,7 +100,8 @@ async def EvalConfig( session = SESSIONMANAGER.get_session(rpc_id_var.get()) SESSIONMANAGER.write_resource_files(rpc_id_var.get(), request.resources) set_session_configs(session, experiment_config) - return eval_response_pb2.EvalResponse(response="ack") + session_id = rpc_id_var.get() + return eval_response_pb2.EvalResponse(response="ack", session_id=session_id) async def ListEvalInputs( self, @@ -195,7 +197,7 @@ async def Eval( response = json.dumps({"job_id": job_id, "summary": summary}) else: response = f"{job_id}" - return eval_response_pb2.EvalResponse(response=response) + return eval_response_pb2.EvalResponse(response=response, session_id=session_id) def _process_results( From c4f232630fa9a5a051d9a8a65720d30514b8de9f Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Wed, 15 Apr 2026 19:41:33 +0000 Subject: [PATCH 39/44] chore: reduce k8s resource requests and implement session persistence from disk on initialization --- evalbench/util/sessionmgr.py | 17 +++++++++++++++++ evalbench_service/k8s/evalbench.yaml | 8 ++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/evalbench/util/sessionmgr.py b/evalbench/util/sessionmgr.py index 96706d81..61172614 100644 --- a/evalbench/util/sessionmgr.py +++ b/evalbench/util/sessionmgr.py @@ -41,11 +41,28 @@ def __init__( self.sessions = {} self.ttl = 10800 self.lock = RWLock() + self.load_sessions_from_disk() logging.debug("Starting reaper...") reaper = Thread(target=self.reaper, args=[]) reaper.daemon = True reaper.start() + def load_sessions_from_disk(self): + try: + if not os.path.exists(SESSION_RESOURCES_PATH): + return + for sid in os.listdir(SESSION_RESOURCES_PATH): + dir_path = os.path.join(SESSION_RESOURCES_PATH, sid) + if os.path.isdir(dir_path): + mtime = os.path.getmtime(dir_path) + logging.info(f"Loading session {sid} from disk with mtime {mtime}.") + self.sessions[sid] = { + "create_ts": mtime, + "session_id": sid, + } + except Exception as e: + logging.error(f"Error loading sessions from disk: {e}") + def set_ttl(self, ttl): self.ttl = ttl diff --git a/evalbench_service/k8s/evalbench.yaml b/evalbench_service/k8s/evalbench.yaml index 9f4f233d..083fdd14 100644 --- a/evalbench_service/k8s/evalbench.yaml +++ b/evalbench_service/k8s/evalbench.yaml @@ -37,11 +37,11 @@ spec: name: evalbench-eval resources: requests: - cpu: "42" - memory: "168Gi" + cpu: "20" + memory: "80Gi" limits: - cpu: "42" - memory: "168Gi" + cpu: "20" + memory: "80Gi" securityContext: allowPrivilegeEscalation: true capabilities: From 23aebed02f0989a2f99ca505f7cfb99bac05e31b Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 16 Apr 2026 00:10:23 +0000 Subject: [PATCH 40/44] feat: add interactive zooming and legend-based filtering to trend charts --- viewer/main.py | 2 + viewer/static/chart.js | 221 ++++++++++++++++++++++++++++++++--------- viewer/trends.py | 115 ++++++++++----------- 3 files changed, 225 insertions(+), 113 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index 75891a92..a3113e11 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -239,6 +239,8 @@ def on_load(e: me.LoadEvent): + + def status_component(): results_dir = get_results_dir() directories = [] diff --git a/viewer/static/chart.js b/viewer/static/chart.js index e81a4c78..5db0921f 100644 --- a/viewer/static/chart.js +++ b/viewer/static/chart.js @@ -1,10 +1,12 @@ function drawChart() { const data = window.chartData; - const xCol = window.chartConfig.xCol; - const yCol = window.chartConfig.yCol; - const hueCol = window.chartConfig.hueCol; - const title = window.chartConfig.title; - const ylabel = window.chartConfig.ylabel; + const config = window.chartConfig; + + const xCol = config.xCol; + const yCol = config.yCol; + const hueCol = config.hueCol; + const title = config.title; + const ylabel = config.ylabel; const margin = { top: 60, right: 350, bottom: 60, left: 60 }; const container = document.getElementById('chart-container'); @@ -14,18 +16,22 @@ function drawChart() { const height = 500 - margin.top - margin.bottom; // Clear previous SVG - d3.select("#chart").selectAll("*").remove(); + d3.select('#chart').selectAll("*").remove(); - const svg = d3.select("#chart") + // Sort data by time + data.sort((a, b) => new Date(a[xCol]) - new Date(b[xCol])); + data.forEach((d, i) => d.index = i); + + const svg = d3.select('#chart') .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); - // X axis - const x = d3.scalePoint() - .domain(data.map(d => d[xCol])) + // X axis - Use linear scale with indices to support zooming while maintaining even spacing + const x = d3.scaleLinear() + .domain([0, data.length - 1]) .range([0, width]); // Y axis @@ -43,11 +49,21 @@ function drawChart() { .attr("class", "grid") .call(d3.axisLeft(y).tickSize(-width).tickFormat("")); - // Axes - svg.append("g") + // X Axis Label formatter + const xAxis = d3.axisBottom(x) + .tickFormat(i => { + const idx = Math.round(i); + if (Math.abs(i - idx) < 0.1 && data[idx]) { + return data[idx][xCol]; + } + return ""; + }); + + const gX = svg.append("g") .attr("transform", `translate(0,${height})`) - .call(d3.axisBottom(x)) - .selectAll("text") + .call(xAxis); + + gX.selectAll("text") .attr("transform", "rotate(-45)") .style("text-anchor", "end") .attr("class", "axis-label"); @@ -67,25 +83,54 @@ function drawChart() { // Group data by product const dataByProduct = d3.group(data, d => d[hueCol]); + // Clip path + svg.append("defs").append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + + // Chart body + const chartBody = svg.append("g") + .attr("clip-path", "url(#clip)"); + + // Zoom + const zoom = d3.zoom() + .scaleExtent([1, 20]) + .translateExtent([[0, 0], [width, height]]) + .extent([[0, 0], [width, height]]) + .on("zoom", zoomed); + + // Zoom rect + chartBody.append("rect") + .attr("width", width) + .attr("height", height) + .style("fill", "none") + .style("pointer-events", "all") + .call(zoom); + // Draw smooth lines and areas dataByProduct.forEach((productData, product) => { + const productIndex = products.indexOf(product); // Area - svg.append("path") + chartBody.append("path") .datum(productData) - .attr("class", "area") + .attr("class", `area product-${productIndex}`) .attr("d", d3.area() - .x(d => x(d[xCol])) + .x(d => x(d.index)) .y0(height) .y1(d => y(d[yCol])) ) - .style("fill", color(product)); + .style("fill", color(product)) + .style("opacity", 0.1) + .style("pointer-events", "none"); // Line - svg.append("path") + chartBody.append("path") .datum(productData) - .attr("class", "line") + .attr("class", `line product-${productIndex}`) .attr("d", d3.line() - .x(d => x(d[xCol])) + .x(d => x(d.index)) .y(d => y(d[yCol])) ) .style("stroke", color(product)); @@ -94,32 +139,29 @@ function drawChart() { // Add dots and tooltips const tooltip = d3.select("#tooltip"); - data.forEach(d => { - svg.append("circle") - .attr("cx", x(d[xCol])) - .attr("cy", y(d[yCol])) - .attr("r", 5) - .attr("fill", color(d[hueCol])) - .attr("class", "dot") - .on("mouseover", function (event) { - d3.select(this).attr("r", 8).style("stroke-width", "3px"); - tooltip.style("opacity", 1) - .html(`Product: ${d[hueCol]}
Time: ${d[xCol]}
Value: ${d[yCol]}
Eval ID: ${d.job_id}`); - }) - .on("mousemove", function (event) { - tooltip.style("left", (event.pageX + 15) + "px") - .style("top", (event.pageY - 28) + "px"); - }) - .on("mouseout", function () { - d3.select(this).attr("r", 5).style("stroke-width", "2px"); - tooltip.style("opacity", 0); - }) - .on("click", function(event, d) { - if (d && d.job_id) { - window.open("/?job_id=" + d.job_id, "_blank"); - } - }); - }); + chartBody.selectAll(".dot") + .data(data) + .enter() + .append("circle") + .attr("cx", d => x(d.index)) + .attr("cy", d => y(d[yCol])) + .attr("r", 5) + .attr("fill", d => color(d[hueCol])) + .attr("class", d => `dot product-${products.indexOf(d[hueCol])}`) + .on("mouseover", function (event, d) { + d3.select(this).attr("r", 8).style("stroke-width", "3px"); + tooltip.style("opacity", 1) + .html(`Product: ${d[hueCol]}
Time: ${d[xCol]}
Value: ${d[yCol]}
Eval ID: ${d.job_id}`); + }) + .on("mousemove", function (event) { + tooltip.style("left", (event.pageX + 15) + "px") + .style("top", (event.pageY - 28) + "px"); + }) + .on("mouseout", function () { + d3.select(this).attr("r", 5).style("stroke-width", "2px"); + tooltip.style("opacity", 0); + }) + // Add Title svg.append("text") @@ -145,7 +187,57 @@ function drawChart() { .data(products) .enter().append("g") .attr("class", "legend") - .attr("transform", (d, i) => `translate(${width + 20}, ${i * 25})`); + .attr("transform", (d, i) => `translate(${width + 20}, ${i * 25})`) + .style("cursor", "pointer") + .on("click", function(event, product) { + const productIndex = products.indexOf(product); + + // Check if ANY OTHER line is visible + let anyOtherVisible = false; + products.forEach((p, i) => { + if (i !== productIndex) { + const el = d3.selectAll(`.line.product-${i}`); + if (el.style("opacity") !== "0") { + anyOtherVisible = true; + } + } + }); + + if (anyOtherVisible) { + // ISOLATE + products.forEach((p, i) => { + const newOpacity = (i === productIndex) ? 1 : 0; + const areaOpacity = (i === productIndex) ? 0.1 : 0; + + d3.selectAll(`.line.product-${i}, .dot.product-${i}`) + .transition().duration(200).style("opacity", newOpacity) + .style("pointer-events", newOpacity === 0 ? "none" : "all"); + + d3.selectAll(`.area.product-${i}`) + .transition().duration(200).style("opacity", areaOpacity); + + // Update legend + const leg = d3.selectAll(".legend").filter(d => d === p); + leg.select("rect").style("opacity", newOpacity === 0 ? 0.3 : 1); + leg.select("text").style("opacity", newOpacity === 0 ? 0.5 : 1); + }); + } else { + // RESTORE + products.forEach((p, i) => { + d3.selectAll(`.line.product-${i}, .dot.product-${i}`) + .transition().duration(200).style("opacity", 1) + .style("pointer-events", "all"); + + d3.selectAll(`.area.product-${i}`) + .transition().duration(200).style("opacity", 0.1); + + // Update legend + const leg = d3.selectAll(".legend").filter(d => d === p); + leg.select("rect").style("opacity", 1); + leg.select("text").style("opacity", 1); + }); + } + }); legend.append("rect") .attr("x", 0) @@ -160,6 +252,39 @@ function drawChart() { .attr("dy", ".35em") .style("text-anchor", "start") .text(d => d.replace('.json', '')); + + function zoomed(event) { + const newX = event.transform.rescaleX(x); + + // Update axis + gX.call(xAxis.scale(newX)); + gX.selectAll("text") + .attr("transform", "rotate(-45)") + .style("text-anchor", "end"); + + // Update lines + chartBody.selectAll(".line") + .attr("d", function(d) { + return d3.line() + .x(p => newX(p.index)) + .y(p => y(p[yCol])) + (d); + }); + + // Update areas + chartBody.selectAll(".area") + .attr("d", function(d) { + return d3.area() + .x(p => newX(p.index)) + .y0(height) + .y1(p => y(p[yCol])) + (d); + }); + + // Update dots + chartBody.selectAll(".dot") + .attr("cx", d => newX(d.index)); + } } // Initial draw diff --git a/viewer/trends.py b/viewer/trends.py index cbd43de8..30642717 100644 --- a/viewer/trends.py +++ b/viewer/trends.py @@ -37,34 +37,27 @@ def generate_d3_chart(df, x_col, y_col, hue_col, title, ylabel): @@ -89,9 +82,6 @@ def generate_d3_chart(df, x_col, y_col, hue_col, title, ylabel): """ return html - - - def trends_component(): results_dir = get_results_dir() @@ -221,59 +211,54 @@ def handler(e: me.ClickEvent): st = me.state(State) st.trends_product_filter = val st.open_dropdown = "" - handler.__name__ = f"click_trends_product_{val}" return handler - with me.box(style=me.Style(display="flex", align_items="center", gap="8px", margin=me.Margin(bottom="16px"))): - me.text("Filter by Product:", style=me.Style(font_weight="600")) - - with me.box(style=me.Style(position="relative", width="200px")): - # Trigger + with me.box(style=me.Style(position="relative", width="300px")): + with me.box( + style=me.Style( + padding=me.Padding.all("12px"), + background="#f8fafc", + border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), + border_radius="6px", + cursor="pointer", + display="flex", + justify_content="space-between", + align_items="center", + ), + on_click=toggle_trends_product_dropdown, + ): + me.text(state.trends_product_filter if state.trends_product_filter else "All Products", style=me.Style(font_weight="500")) + me.text("▼", style=me.Style(font_size="10px", color="#64748b")) + + if state.open_dropdown == "trends_product": with me.box( style=me.Style( + position="absolute", + top="100%", + left="0", + z_index=10, background="#ffffff", border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), border_radius="4px", - padding=me.Padding.all("8px"), - cursor="pointer", - ), - on_click=toggle_trends_product_dropdown, - ): - me.text( - state.trends_product_filter if state.trends_product_filter else "All Products", - style=me.Style(color="#1f2937"), + width="100%", + max_height="200px", + overflow_y="auto", ) - - # Popup - if state.open_dropdown == "trends_product": + ): + # All option with me.box( - style=me.Style( - position="absolute", - top="100%", - left="0", - z_index=10, - background="#ffffff", - border=me.Border.all(me.BorderSide(width="1px", color="#e2e8f0")), - border_radius="4px", - width="100%", - max_height="200px", - overflow_y="auto", - ) + style=me.Style(padding=me.Padding.all("8px"), cursor="pointer"), + on_click=make_product_handler(""), ): - # All option + me.text("All Products", style=me.Style(color="#1f2937")) + + # Product options + for p in all_products: with me.box( style=me.Style(padding=me.Padding.all("8px"), cursor="pointer"), - on_click=make_product_handler(""), + on_click=make_product_handler(p), ): - me.text("All Products", style=me.Style(color="#1f2937")) - - # Product options - for p in all_products: - with me.box( - style=me.Style(padding=me.Padding.all("8px"), cursor="pointer"), - on_click=make_product_handler(p), - ): - me.text(p, style=me.Style(color="#1f2937")) + me.text(p, style=me.Style(color="#1f2937")) with me.box(style=me.Style(display="flex", flex_direction="column", gap="16px", width="100%")): me.text("AI Score", style=me.Style(font_size="16px", font_weight="600")) From 787a38a321e8638f8cba1861c02ab8351a50b313 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 16 Apr 2026 00:12:32 +0000 Subject: [PATCH 41/44] refactor: replace custom tab button implementation with me.button_toggle component --- viewer/main.py | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/viewer/main.py b/viewer/main.py index a3113e11..b27d21d6 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -2283,36 +2283,25 @@ def on_next_conversation(e: me.ClickEvent): from trends import trends_component state = me.state(State) + def on_main_tab_change(e: me.ButtonToggleChangeEvent): + st = me.state(State) + st.selected_main_tab = e.value + logging.info(f"Main tab changed to: {e.value}") + with me.box(style=me.Style(margin=me.Margin(bottom="12px"))): - tabs = ["Status", "List", "Charts"] + buttons = [ + me.ButtonToggleButton(label="Status", value="Status"), + me.ButtonToggleButton(label="List", value="List"), + me.ButtonToggleButton(label="Charts", value="Charts"), + ] if state.compare_tab_visible: - tabs.append("Compare") - for tab in tabs: - is_active = state.selected_main_tab == tab - tab_text = tab - if tab == "Compare" and state.ai_comparison == "Comparing...": - tab_text += " (Loading...)" - - click_handler = None - if tab == "Status": click_handler = on_status_tab_click - elif tab == "List": click_handler = on_list_tab_click - elif tab == "Charts": click_handler = on_charts_tab_click - elif tab == "Compare": click_handler = on_compare_tab_click + buttons.append(me.ButtonToggleButton(label="Compare", value="Compare")) - me.button( - tab_text, - on_click=click_handler, - style=me.Style( - padding=me.Padding.symmetric(vertical="6px", horizontal="12px"), - background="#1e293b" if is_active else "#f1f5f9", - color="#ffffff" if is_active else "#475569", - border_radius="4px", - cursor="pointer", - font_weight="600" if is_active else "500", - font_size="14px", - margin=me.Margin(right="8px") - ), - ) + me.button_toggle( + value=state.selected_main_tab, + buttons=buttons, + on_change=on_main_tab_change, + ) if state.selected_main_tab == "List": try: From 85e09898a6415697bde186b77e1a02a56f0f0452 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Thu, 16 Apr 2026 17:59:23 +0000 Subject: [PATCH 42/44] fix: correct syntax error by replacing trailing parenthesis with semicolon in chart.js --- viewer/static/chart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer/static/chart.js b/viewer/static/chart.js index 5db0921f..1ebf0ee2 100644 --- a/viewer/static/chart.js +++ b/viewer/static/chart.js @@ -160,7 +160,7 @@ function drawChart() { .on("mouseout", function () { d3.select(this).attr("r", 5).style("stroke-width", "2px"); tooltip.style("opacity", 0); - }) + }); // Add Title From f15c1963fab7ab0d98916725fcdfa4ba0cac6d28 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Fri, 17 Apr 2026 20:09:48 +0000 Subject: [PATCH 43/44] feat: add support for cross-evaluation comparison via query parameters and handle symlinked directories during session cleanup --- evalbench/util/sessionmgr.py | 5 ++++- viewer/main.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/evalbench/util/sessionmgr.py b/evalbench/util/sessionmgr.py index 61172614..62810d7d 100644 --- a/evalbench/util/sessionmgr.py +++ b/evalbench/util/sessionmgr.py @@ -95,7 +95,10 @@ def prune_resource_files(self, session_id): os.remove(file_path) for dir in dirs: dir_path = os.path.join(root, dir) - os.rmdir(dir_path) + if os.path.islink(dir_path): + os.unlink(dir_path) + else: + os.rmdir(dir_path) os.rmdir(path) def create_session(self, session_id): diff --git a/viewer/main.py b/viewer/main.py index b27d21d6..a893da77 100644 --- a/viewer/main.py +++ b/viewer/main.py @@ -237,6 +237,17 @@ def on_load(e: me.LoadEvent): if job_id and job_id in directories: state.selected_directory = job_id + tab = me.query_params.get("tab") + eval1 = me.query_params.get("eval1") + eval2 = me.query_params.get("eval2") + + if tab == "compare" and eval1 and eval2: + state.selected_main_tab = "Compare" + state.compare_tab_visible = True + state.compare_evals = json.dumps([eval1, eval2]) + # Trigger the AI comparison + state.ai_comparison = compare_evals(eval1, eval2) + From da5cb766f083d82ccf7b1d62eba4a83843397b59 Mon Sep 17 00:00:00 2001 From: Ismail Mehdi Date: Mon, 20 Apr 2026 20:37:19 +0000 Subject: [PATCH 44/44] feat: allow quoted newlines in BigQuery load job configuration --- evalbench/reporting/bqstore.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evalbench/reporting/bqstore.py b/evalbench/reporting/bqstore.py index 997472ef..8b94f28f 100644 --- a/evalbench/reporting/bqstore.py +++ b/evalbench/reporting/bqstore.py @@ -109,6 +109,7 @@ def store(self, results, type: STORETYPE): ) job_config = bigquery.LoadJobConfig() job_config.autodetect = True + job_config.allow_quoted_newlines = True job_config.schema_update_options = [ bigquery.SchemaUpdateOption.ALLOW_FIELD_ADDITION, bigquery.SchemaUpdateOption.ALLOW_FIELD_RELAXATION,