Vortex-Flux / src /modules /Usis.py
klydekushy's picture
Update src/modules/Usis.py
c33927a verified
"""
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>
"""
@st.cache_data(ttl=300, show_spinner=False)
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)