*** visual.py 2025-01-22 00:00:00.000000000 +0000 --- visual.py 2025-10-07 00:00:00.000000000 +0000 *************** *** 1,10 **** import os import json import socket import platform import numpy as np import pandas as pd import streamlit as st import sys from streamlit_autorefresh import st_autorefresh - st.set_page_config( page_title="Баскетбол", page_icon="🏀", layout="wide", --- 1,10 ---- import os import json import socket import platform import numpy as np import pandas as pd import streamlit as st import sys from streamlit_autorefresh import st_autorefresh + st.set_page_config( page_title="Баскетбол", page_icon="🏀", layout="wide", *************** *** 164,169 **** --- 164,216 ---- def ensure_state(key: str, default=None): # Инициализирует ключ один раз и возвращает значение return st.session_state.setdefault(key, default) + + # ======== UNIVERSAL SAFE RENDER WRAPPER ======== + def _is_empty_like(x) -> bool: + if x is None: + return True + # DataFrame + if isinstance(x, pd.DataFrame): + return x.empty + # Pandas Styler + try: + # импортируем лениво, чтобы не ломаться, если нет pandas.io.formats.style в рантайме + from pandas.io.formats.style import Styler # type: ignore + if isinstance(x, Styler): + # у Styler нет __len__, но есть .data + return getattr(x, "data", pd.DataFrame()).empty + except Exception: + pass + # пустые коллекции + if isinstance(x, (list, tuple, dict, set)): + return len(x) == 0 + return False + + def safe_show(func, *args, **kwargs): + """ + Безопасно вызывает функции отображения Streamlit (dataframe, table, metric, image, markdown, ...). + - Ничего не рендерит, если основной аргумент данных пуст/None. + - Нормализует height (убирает None, <0; приводит к int). + - Возвращает результат вызова func (нужно для dataframe-selection). + - Перехватывает исключения и показывает предупреждение. + """ + # Если среди позиционных/именованных аргументов есть пустые/None-данные — не показываем + for a in args: + if _is_empty_like(a): + return None + for k, v in kwargs.items(): + # пропускаем не-данные параметры (типа width/unsafe_allow_html) + if k.lower() in ("height", "width", "use_container_width", "unsafe_allow_html", "on_select", "selection_mode", "column_config", "hide_index", "border", "delta_color", "key"): + continue + if _is_empty_like(v): + return None + + # height -> валидный int, иначе уберём + if "height" in kwargs: + h = kwargs.get("height") + if h is None: + kwargs.pop("height") + else: + try: + h = int(h) + if h < 0: + kwargs.pop("height") + else: + kwargs["height"] = h + except Exception: + kwargs.pop("height") + + try: + return func(*args, **kwargs) + except Exception as e: + st.warning(f"⚠️ Ошибка при отображении: {e}") + return None + # ======== /SAFE WRAPPER ======== + *************** *** 221,231 **** col1, col4, col2, col5, col3 = st.columns([1, 5, 3, 5, 1]) t1 = (result.get("team1") or {}) t2 = (result.get("team2") or {}) - if t1.get("logo"): - col1.image(t1["logo"], width=100) team1_name = t1.get("name") or "" team2_name = t2.get("name") or "" - if team1_name or team2_name: - col2.markdown( - f"

{team1_name} — {team2_name}

", - unsafe_allow_html=True, - ) - if t2.get("logo"): - col3.image(t2["logo"], width=100) + safe_show(col1.image, t1.get("logo"), width=100) + if team1_name or team2_name: + safe_show( + col2.markdown, + f"

{team1_name} — {team2_name}

", + unsafe_allow_html=True, + ) + safe_show(col3.image, t2.get("logo"), width=100) col4_1, col4_2, col4_3 = col4.columns((1, 1, 1)) col5_1, col5_2, col5_3 = col5.columns((1, 1, 1)) *************** *** 237,253 **** if isinstance(cached_team_stats, list) and len(cached_team_stats) > 0: v1 = cached_team_stats[0].get("val1") v2 = cached_team_stats[0].get("val2") if v1 is not None and v2 is not None: val1, val2 = int(v1), int(v2) delta_color_1 = "off" if val1 == val2 else "normal" - col4_1.metric("Points", v1, val1 - val2, delta_color_1) - col5_3.metric("Points", v2, val2 - val1, delta_color_1) + safe_show(col4_1.metric, "Points", v1, val1 - val2, delta_color_1) + safe_show(col5_3.metric, "Points", v2, val2 - val1, delta_color_1) - col4_3.metric("TimeOuts", len(timeout1)) - col5_1.metric("TimeOuts", len(timeout2)) + safe_show(col4_3.metric, "TimeOuts", len(timeout1)) + safe_show(col5_1.metric, "TimeOuts", len(timeout2)) if isinstance(cached_live_status, list) and cached_live_status: foulsA = (cached_live_status[0] or {}).get("foulsA") foulsB = (cached_live_status[0] or {}).get("foulsB") if foulsA is not None: - col4_2.metric("Fouls", foulsA) + safe_show(col4_2.metric, "Fouls", foulsA) if foulsB is not None: - col5_2.metric("Fouls", foulsB) + safe_show(col5_2.metric, "Fouls", foulsB) *************** *** 270,280 **** for q1, q2, col1_i, col2_i in zip(score_by_quarter_1, score_by_quarter_2, col_1_col, col_2_col): count_q += 1 name_q = f"OT{count_q-4}" if count_q > 4 else f"Q{count_q}" try: delta_color = "off" if int(q1) == int(q2) else "normal" - col1_i.metric(name_q, q1, int(q1) - int(q2), delta_color, border=True) - col2_i.metric(name_q, q2, int(q2) - int(q1), delta_color, border=True) + safe_show(col1_i.metric, name_q, q1, int(q1) - int(q2), delta_color, border=True) + safe_show(col2_i.metric, name_q, q2, int(q2) - int(q1), delta_color, border=True) except (ValueError, TypeError): # если кривые данные в JSON, просто пропустим pass *************** *** 403,424 **** team1_styled = ( team1_data.style.apply(highlight_grey, axis=1) .apply(highlight_foul, subset="foul") .apply(highlight_max, subset="pts") ) team2_styled = ( team2_data.style.apply(highlight_grey, axis=1) .apply(highlight_foul, subset="foul") .apply(highlight_max, subset="pts") ) # Вывод данных col_player1, col_player2 = tab_temp_1.columns((5, 5)) - event1 = col_player1.dataframe( - team1_styled, - column_config=config, - hide_index=True, - height=460, - on_select="rerun", - selection_mode=[ - "single-row", - ], - ) - event2 = col_player2.dataframe( - team2_styled, - column_config=config, - hide_index=True, - height=460, - on_select="rerun", - selection_mode=[ - "single-row", - ], - ) + event1 = safe_show( + col_player1.dataframe, + team1_styled, + column_config=config, + hide_index=True, + height=460, + on_select="rerun", + selection_mode=["single-row"], + ) + event2 = safe_show( + col_player2.dataframe, + team2_styled, + column_config=config, + hide_index=True, + height=460, + on_select="rerun", + selection_mode=["single-row"], + ) if event1 and getattr(event1, "selection", None) and event1.selection.get("rows"): selected_index1 = event1.selection["rows"][0] st.session_state["player1"] = ( selected_index1 # Сохранение состояния в session_state *************** *** 433,441 **** if player_data_1["num"]: z, a, b, c, d, e = col_player1.columns((1, 6, 1, 1, 1, 1)) - z.metric("Номер", player_data_1["num"], border=False) - a.metric("Игрок", player_data_1["NameGFX"], border=False) - b.metric("Амплуа", player_data_1["roleShort"], border=False) - c.metric("Возраст", player_data_1["age"], border=False) - d.metric("Рост", player_data_1["height"].split()[0], border=False) - e.metric("Вес", player_data_1["weight"].split()[0], border=False) - - col_player1.dataframe( - selected_player_1, - column_config=config_season, - hide_index=True, - ) + safe_show(z.metric, "Номер", player_data_1["num"], border=False) + safe_show(a.metric, "Игрок", player_data_1["NameGFX"], border=False) + safe_show(b.metric, "Амплуа", player_data_1["roleShort"], border=False) + safe_show(c.metric, "Возраст", player_data_1["age"], border=False) + safe_show(d.metric, "Рост", player_data_1["height"].split()[0], border=False) + safe_show(e.metric, "Вес", player_data_1["weight"].split()[0], border=False) + + safe_show( + col_player1.dataframe, + selected_player_1, + column_config=config_season, + hide_index=True, + ) *************** *** 446,454 **** if player_data_2["num"]: z, a, b, c, d, e = col_player2.columns((1, 6, 1, 1, 1, 1)) - z.metric("Номер", player_data_2["num"], border=False) - a.metric("Игрок", player_data_2["NameGFX"], border=False) - b.metric("Амплуа", player_data_2["roleShort"], border=False) - c.metric("Возраст", player_data_2["age"], border=False) - d.metric("Рост", player_data_2["height"].split()[0], border=False) - e.metric("Вес", player_data_2["weight"].split()[0], border=False) - - col_player2.dataframe( - selected_player_2, - column_config=config_season, - hide_index=True, - ) + safe_show(z.metric, "Номер", player_data_2["num"], border=False) + safe_show(a.metric, "Игрок", player_data_2["NameGFX"], border=False) + safe_show(b.metric, "Амплуа", player_data_2["roleShort"], border=False) + safe_show(c.metric, "Возраст", player_data_2["age"], border=False) + safe_show(d.metric, "Рост", player_data_2["height"].split()[0], border=False) + safe_show(e.metric, "Вес", player_data_2["weight"].split()[0], border=False) + + safe_show( + col_player2.dataframe, + selected_player_2, + column_config=config_season, + hide_index=True, + ) *************** *** 459,468 **** if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34: cached_team_stats_new = [ cached_team_stats[0], *cached_team_stats[25:29], cached_team_stats[7], cached_team_stats[33], *cached_team_stats[9:11], *cached_team_stats[15:17], ] - tab_temp_2.table(cached_team_stats_new) + safe_show(tab_temp_2.table, cached_team_stats_new) *************** *** 470,484 **** - if isinstance(cached_referee, (list, pd.DataFrame)): - tab_temp_3.dataframe(cached_referee, height=600, column_config={"flag": st.column_config.ImageColumn("flag")}) - - column_config_ref = { "flag": st.column_config.ImageColumn( "flag", ), } if cached_referee: - tab_temp_3.dataframe(cached_referee, height=600, column_config=column_config_ref) + safe_show(tab_temp_3.dataframe, cached_referee, height=600, column_config=column_config_ref) *************** *** 503,511 **** styled = df_st.style.apply(highlight_teams, axis=1) - tab_temp_4.dataframe( - styled, - column_config={"logo": st.column_config.ImageColumn("logo")}, - hide_index=True, - height=610, - ) + safe_show( + tab_temp_4.dataframe, + styled, + column_config={"logo": st.column_config.ImageColumn("logo")}, + hide_index=True, + height=610, + ) *************** *** 552,558 **** ] col.write(q) - col.dataframe(df_col) + safe_show(col.dataframe, df_col) # Овертаймы for index, col in enumerate(columns_quarters): q = columns_quarters_name_ot[index] df_col = [ --- 564,570 ---- *************** *** 572,578 **** ] col.write(q) - col.dataframe(df_col) + safe_show(col.dataframe, df_col) *************** *** 582,586 **** if isinstance(cached_play_by_play, list) and isinstance(cached_game_online, dict): plays = (cached_game_online.get("result") or {}).get("plays") or [] if plays: - tab_temp_6.table(cached_play_by_play) + safe_show(tab_temp_6.table, cached_play_by_play) *************** *** 703,724 **** height1 = 38 * max(count_game_1, 10) height2 = 38 * max(count_game_2, 10) - col1_schedule.dataframe( - team1_data, - hide_index=True, - height=int(min(height1, 1200)), - column_config=column_config, - ) - col2_schedule.dataframe( - team2_data, - hide_index=True, - height=int(min(height2, 1200)), - column_config=column_config, - ) + safe_show( + col1_schedule.dataframe, + team1_data, + hide_index=True, + height=int(min(height1, 1200)), + column_config=column_config, + ) + safe_show( + col2_schedule.dataframe, + team2_data, + hide_index=True, + height=int(min(height2, 1200)), + column_config=column_config, + ) *************** *** 836,845 **** filtered_data_pbp = temp_data_pbp[mask1] count_pbp = len(filtered_data_pbp) column_pbp = ["num", "info", "who", "period", "time"] column_config_pbp = { "info": st.column_config.TextColumn(width="medium"), "who": st.column_config.TextColumn(width="large"), } - col2_pbp.dataframe( - filtered_data_pbp[column_pbp], - column_config=column_config_pbp, - hide_index=True, - height=(38 * count_pbp if count_pbp > 10 else None), - ) + safe_show( + col2_pbp.dataframe, + filtered_data_pbp[column_pbp], + column_config=column_config_pbp, + hide_index=True, + height=(38 * count_pbp if count_pbp > 10 else None), + ) else: st.info("Данных play-by-play нет.")