File size: 5,783 Bytes
397b23d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c20487a
 
 
 
 
 
 
 
 
397b23d
c20487a
397b23d
c73b85c
397b23d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c20487a
 
 
 
 
 
 
 
 
 
e3cb468
c20487a
 
 
e3cb468
 
 
c20487a
e3cb468
 
c20487a
 
 
 
 
 
 
e6b6e8c
 
 
 
e3cb468
 
 
 
 
 
 
 
c20487a
 
 
 
397b23d
 
 
 
 
 
 
dd08ad5
 
397b23d
 
c20487a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"""
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()