Vortex-Flux / src /core /sheets_cache.py
jaimalklyde's picture
Claude Sonnet 5.5
perf(sheets): bouclier anti-429 global (cache GET gspread, invalidation sur ecriture, secours perime, reessais)
dd08ad5
Raw History Blame Contribute Delete
5.78 kB
"""
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()