""" MODULE SHEETS_CACHE — cache partage pour les lectures Google Sheets ===================================================================== Chaque interaction Streamlit (clic sur un bouton, coche une case, ouverture d'un expander...) relance l'integralite du script de la page. Sans cache, cela relit a chaque fois toutes les feuilles Google Sheets utilisees par cette page — plusieurs lectures par clic — ce qui declenche regulierement des erreurs 429 (quota Google Sheets depasse) sur les pages a fort trafic de clics (validation des prets, remboursements...). On met donc ces lectures en cache le temps d'un court TTL, et on vide explicitement le cache juste apres toute ecriture (append_row/update_cell/ update) pour ne jamais laisser un agent decider sur des donnees perimees. Usage : sh = client.open(sheet_name) df = get_cached_df(sh, sheet_name, "Prets_Requests") ... ws.append_row([...]) clear_sheets_cache() # apres CHAQUE ecriture Ne pas utiliser ce cache pour une lecture dont le resultat sert immediatement a decider d'ecrire (ex: garde anti-doublon avant un append_row) : lire directement la feuille dans ce cas precis, sinon le cache peut masquer une ecriture toute recente faite par un autre onglet/ tentative precedente. ATTENTION — numerisation automatique : get_cached_df()/get_cached_dfs_batch() reproduisent le comportement PAR DEFAUT de get_all_records() (numericise_all de gspread), qui devine si une valeur texte est un nombre. Un module qui appelle deliberement get_all_records(numericise_ignore=['all']) (voir Check_Up_Loans.py : la numerisation automatique corrompt les decimales au format francais, "1,5" devient 15) doit continuer a lire directement la feuille sans passer par ce cache, sous peine de reintroduire cette corruption silencieuse. """ import pandas as pd import streamlit as st from core.gspread_fr import numericise_fr as numericise DEFAULT_TTL = 60 # secondes — assez court pour rester a jour, assez long pour absorber une rafale de clics @st.cache_data(ttl=DEFAULT_TTL, show_spinner=False) def cached_get_all_records(_sh, sheet_key, worksheet_name): """Lecture mise en cache de get_all_records(). `_sh` (objet Spreadsheet deja ouvert) est ignore pour le hachage du cache (convention Streamlit du prefixe underscore) ; `sheet_key` (le nom du classeur, une chaine) sert de cle de cache a sa place.""" return _sh.worksheet(worksheet_name).get_all_records() @st.cache_data(ttl=DEFAULT_TTL, show_spinner=False) def cached_get_all_values(_sh, sheet_key, worksheet_name): """Equivalent de cached_get_all_records() pour get_all_values().""" return _sh.worksheet(worksheet_name).get_all_values() def get_cached_df(sh, sheet_key, worksheet_name): """Retourne un DataFrame pandas a partir d'une lecture mise en cache.""" return pd.DataFrame(cached_get_all_records(sh, sheet_key, worksheet_name)) @st.cache_data(ttl=DEFAULT_TTL, show_spinner=False) def _cached_batch_values(_sh, sheet_key, worksheet_names: tuple): """Lit plusieurs feuilles EN UN SEUL appel API (values_batch_get) au lieu d'un appel par feuille : une page qui a besoin de N feuilles ne consomme plus que 1 lecture au lieu de N sur le quota partage (60 lectures/minute, tous utilisateurs de l'app confondus — voir la note sur le code 429).""" resp = _sh.values_batch_get(list(worksheet_names)) return [vr.get("values", []) for vr in resp.get("valueRanges", [])] def get_cached_dfs_batch(sh, sheet_key, worksheet_names, numericise_ignore_columns=None): """Variante groupee de get_cached_df() pour plusieurs feuilles a la fois (1 appel API au lieu de N). Reproduit le comportement de get_all_records() (numericisation des valeurs via l'utilitaire interne de gspread, lignes completees a la longueur de l'en-tete), sauf pour les colonnes indiquees dans `numericise_ignore_columns`, qui restent brutes pour preserver leur format decimal localise. Retourne {nom_feuille: DataFrame}.""" raw = _cached_batch_values(sh, sheet_key, tuple(worksheet_names)) numericise_ignore_columns = numericise_ignore_columns or {} # `numericise_ignore_columns` conserve brutes les colonnes localisées choisies. out = {} for name, values in zip(worksheet_names, raw): if not values: out[name] = pd.DataFrame() continue header, *rows = values width = len(header) # Lignes plus longues que l'en-tete (cellules ecrites hors des colonnes # nommees, ex. geoloc KYC ajoutee en fin de ligne) : tronquees comme # le fait get_all_records(), sinon pd.DataFrame leve une erreur. padded = [row[:width] + [""] * (width - len(row)) for row in rows] ignored_indexes = { index for index, column in enumerate(header) if column in numericise_ignore_columns.get(name, ()) } numericised = [ [value if index in ignored_indexes else numericise(value) for index, value in enumerate(row)] for row in padded ] out[name] = pd.DataFrame(numericised, columns=header) return out def clear_sheets_cache(): """A appeler juste apres toute ecriture (append_row/update_cell/update) sur n'importe quelle feuille, pour que le prochain rerun relise des donnees fraiches au lieu de servir le cache perime. Vide tout le cache (plutot qu'une entree precise) : les ecritures sont rares comparees aux lectures, le cout d'un cache un peu trop large est negligeable face au risque d'afficher des donnees obsoletes apres une ecriture.""" from core import sheets_quota sheets_quota.vider() cached_get_all_records.clear() cached_get_all_values.clear() _cached_batch_values.clear()