Spaces:
Running
Running
| """ | |
| USIS.PY Orchestrateur du module USIS | |
| βββββββββββββββββββββββββββββββββββββ | |
| Responsabilites : | |
| - Chargement gspread + enrichissement silencieux DX_* puis EX_* | |
| - Navbar secondaire USIS via st.radio | |
| - CSS scope [data-testid="stMain"] => n'affecte JAMAIS la | |
| navbar principale Vortex-Flux dans la sidebar Streamlit | |
| - Dispatch vers Data_description.py et Data_exploration.py | |
| Integration streamlit_app.py : | |
| from modules import Usis | |
| elif menu == "USIS": | |
| Usis.show_usis_module(client, SHEET_NAME) | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| try: | |
| from Analytics import Data_description as dd | |
| from Analytics import Data_exploration as de | |
| from Analytics import ML_Feature_Store_Analytics as mla | |
| except ImportError: | |
| import sys, os | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| from Analytics import Data_description as dd | |
| from Analytics import Data_exploration as de | |
| from Analytics import ML_Feature_Store_Analytics as mla | |
| _FONT = "JetBrains Mono, Courier New, monospace" | |
| _C = { | |
| "bg": "#0a0e12", | |
| "card": "#0f141a", | |
| "border": "rgba(80,100,120,0.25)", | |
| "accent": "#58a6ff", | |
| "ex": "#f39c12", | |
| "red": "#c0392b", | |
| "text": "#a8b8c8", | |
| "subtext": "#5a6a7a", | |
| } | |
| _MODES = ["U0 INTEGRITE", "U1 DESCRIPTION", "U2 EXPLORATION", "U3 ML FEATURE STORE"] | |
| _SECTIONS = [ | |
| ("CLIENTS", "Clients_KYC"), | |
| ("GARANTS", "Garants_KYC"), | |
| ("PRETS", "Prets_Master"), | |
| ("PRETS UPDATE", "Prets_Update"), | |
| ("REMBOURSEMENTS", "Remboursements"), | |
| ("AJUSTEMENTS", "Ajustements_Echeances"), | |
| ] | |
| _SHEETS = [s for _, s in _SECTIONS] | |
| # ββ CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Scope critique : [data-testid="stMain"] garantit que ces regles | |
| # ne touchent JAMAIS les radio/elements de la navbar principale Vortex-Flux. | |
| # La sidebar Streamlit est dans section[data-testid="stSidebar"] ; | |
| # les colonnes USIS sont dans le contenu principal, hors de cette section. | |
| _NAV_CSS = f""" | |
| <style> | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS USIS - scope [data-testid="stMain"] uniquement. | |
| La sidebar Streamlit vit dans [data-testid="stSidebar"], | |
| completement en dehors de stMain : zero risque de collision. | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* Label du widget radio = header de groupe (MODE / SECTION) */ | |
| [data-testid="stMain"] [data-testid="stRadio"] > label {{ | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 0.48rem !important; | |
| letter-spacing: 2.5px !important; | |
| text-transform: uppercase !important; | |
| color: {_C['subtext']} !important; | |
| margin-bottom: 2px !important; | |
| padding: 0 !important; | |
| font-weight: normal !important; | |
| opacity: 0.6 !important; | |
| }} | |
| /* Supprime le gap entre options */ | |
| [data-testid="stMain"] [data-testid="stRadio"] > div {{ | |
| gap: 0 !important; | |
| flex-direction: column !important; | |
| }} | |
| /* Style de base d'une option */ | |
| [data-testid="stMain"] [data-testid="stRadio"] > div > label {{ | |
| display: flex !important; | |
| align-items: center !important; | |
| padding: 7px 12px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 0.60rem !important; | |
| letter-spacing: 1.5px !important; | |
| text-transform: uppercase !important; | |
| color: {_C['subtext']} !important; | |
| border-left: 3px solid transparent !important; | |
| background: transparent !important; | |
| cursor: pointer !important; | |
| margin: 0 !important; | |
| border-radius: 0 !important; | |
| white-space: nowrap !important; | |
| transition: color 0.1s, background 0.1s !important; | |
| }} | |
| /* Masque les cercles radio */ | |
| [data-testid="stMain"] [data-testid="stRadio"] > div > label > div:first-child {{ | |
| display: none !important; | |
| }} | |
| /* Hover */ | |
| [data-testid="stMain"] [data-testid="stRadio"] > div > label:hover {{ | |
| color: {_C['text']} !important; | |
| background: rgba(88,166,255,0.04) !important; | |
| }} | |
| /* Option active : bleu accent */ | |
| [data-testid="stMain"] [data-testid="stRadio"] > div > label[aria-checked="true"] {{ | |
| color: {_C['accent']} !important; | |
| border-left-color: {_C['accent']} !important; | |
| background: rgba(88,166,255,0.12) !important; | |
| font-weight: 700 !important; | |
| }} | |
| /* Stats */ | |
| .usis-stats {{ | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.50rem; | |
| color: {_C['subtext']}; | |
| padding: 8px 12px; | |
| line-height: 1.9; | |
| opacity: 0.5; | |
| border-top: 1px solid {_C['border']}; | |
| margin-top: 4px; | |
| }} | |
| </style> | |
| """ | |
| def _load_sheets(_client, sheet_name: str) -> dict: | |
| try: | |
| sh = _client.open(sheet_name) | |
| except Exception as e: | |
| st.error(f"[ USIS ] Impossible d'ouvrir '{sheet_name}' : {e}") | |
| return {s: pd.DataFrame() for s in _SHEETS} | |
| out = {} | |
| for tab in _SHEETS: | |
| try: | |
| ws = sh.worksheet(tab) | |
| data = ws.get_all_records() | |
| df = pd.DataFrame(data) | |
| if not df.empty: | |
| df.columns = [c.strip() for c in df.columns] | |
| out[tab] = df | |
| except Exception: | |
| out[tab] = pd.DataFrame() | |
| return out | |
| def _render_nav(sheets: dict) -> tuple: | |
| """ | |
| Navbar USIS via st.radio. | |
| Le CSS est injecte AVANT les colonnes dans show_usis_module. | |
| Retourne (mode: str, section_key: str). | |
| """ | |
| # ββ Stats silencieuses ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| loaded = sum(1 for df in sheets.values() if isinstance(df, __import__('pandas').DataFrame) and not df.empty) | |
| dx_n = sum(pd.Index(df.columns.astype(str)).str.startswith("DX_").sum() | |
| for df in sheets.values() if isinstance(df, pd.DataFrame)) | |
| ex_n = sum(pd.Index(df.columns.astype(str)).str.startswith("EX_").sum() | |
| for df in sheets.values() if isinstance(df, pd.DataFrame)) | |
| # ββ Radio MODE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Pas de st.markdown wrapper : chaque markdown cree un bloc Streamlit | |
| # avec de la hauteur, ce qui pousserait le contenu vers le bas. | |
| mode_sel = st.radio( | |
| "MODE", | |
| _MODES, | |
| key="usis_mode", | |
| label_visibility="visible", | |
| ) | |
| # Derive la cle mode courte | |
| mode = "U0" if "U0" in mode_sel else "U1" if "U1" in mode_sel else "U2" | |
| # ββ Radio SECTION (U1 et U2 seulement) βββββββββββββββββββββββββββββββββββ | |
| section_key = "CLIENTS" | |
| if mode in ("U1", "U2"): | |
| section_labels = [lbl for lbl, _ in _SECTIONS] | |
| labels_display = [ | |
| f"{lbl} [{len(sheets.get(sh, pd.DataFrame()))}]" | |
| if len(sheets.get(sh, pd.DataFrame())) > 0 | |
| else f"{lbl} [--]" | |
| for lbl, sh in _SECTIONS | |
| ] | |
| sel_display = st.radio( | |
| "SECTION", | |
| labels_display, | |
| key="usis_section", | |
| label_visibility="visible", | |
| ) | |
| idx = labels_display.index(sel_display) | |
| section_key = section_labels[idx] | |
| # ββ Stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown( | |
| f'<div class="usis-stats">' | |
| f'FEUILLES : {loaded}/{len(_SHEETS)}<br>' | |
| f'DX_* : {dx_n}<br>' | |
| f'EX_* : {ex_n}' | |
| f'</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| return mode, section_key | |
| def show_usis_module(client, sheet_name: str): | |
| """ | |
| Appele depuis streamlit_app.py : | |
| from modules import Usis | |
| elif menu == "USIS": | |
| Usis.show_usis_module(client, SHEET_NAME) | |
| """ | |
| # Chargement en pleine largeur AVANT les colonnes (spinner visible) | |
| with st.spinner("[ USIS ] CHARGEMENT ET ENRICHISSEMENT EN COURS..."): | |
| raw = _load_sheets(client, sheet_name) | |
| sheets = dd.enrich_dx(raw) # 28 scores DX_* | |
| sheets = de.enrich_ex(sheets) # 27 scores EX_* | |
| sheets = mla.enrich_fl(sheets) # 23 features FL_* | |
| loaded = sum(1 for df in sheets.values() if isinstance(df, __import__('pandas').DataFrame) and not df.empty) | |
| if loaded == 0: | |
| st.error("[ USIS ] Aucune feuille chargee. Verifiez la connexion Google Sheets.") | |
| return | |
| # CSS injecte UNE FOIS ici, avant les colonnes, pour ne pas creer | |
| # de bloc Streamlit supplementaire dans nav_col qui decalerait le contenu. | |
| st.markdown(_NAV_CSS, unsafe_allow_html=True) | |
| # Layout : navbar (1.1) | gap (0.05) | contenu (4.5) | |
| nav_col, _, main_col = st.columns([1.1, 0.05, 4.5]) | |
| with nav_col: | |
| mode, section = _render_nav(sheets) | |
| with main_col: | |
| # En-tete a l'interieur de la colonne contenu uniquement | |
| st.markdown( | |
| f'<div style="margin-bottom:4px;">' | |
| f'<span style="font-family:{_FONT};font-size:0.58rem;font-weight:700;' | |
| f'letter-spacing:3px;text-transform:uppercase;color:{_C["subtext"]};' | |
| f'border:1px solid {_C["border"]};padding:3px 8px;">MODULE USIS</span>' | |
| f'</div>' | |
| f'<h1 style="margin-top:10px;font-family:{_FONT};">' | |
| f'SURVEILLANCE INTEGRITE SANTE</h1>', | |
| unsafe_allow_html=True, | |
| ) | |
| fl_count = sum(c.startswith("FL_") for c in sheets.get("Clients_KYC", __import__("pandas").DataFrame()).columns) | |
| st.caption( | |
| "U0 Integrite | " | |
| "U1 Description (28 DX_*) | " | |
| "U2 Exploration (27 EX_*) | " | |
| f"U3 ML Feature Store ({fl_count} FL_*)" | |
| ) | |
| st.divider() | |
| if mode == "U0": | |
| dd.render_unit0(sheets) | |
| elif mode == "U1": | |
| dd.render_description(sheets, section) | |
| elif mode == "U2": | |
| filtered = de.render_filter_bar(sheets) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| de.render_exploration(filtered, section) | |
| elif mode == "U3": | |
| mla.render_feature_store(sheets) |