adwitiyashukla commited on
Commit
02ff16e
·
verified ·
1 Parent(s): 1a0bcb9

Sync from GitHub b6f1daf

Browse files
Files changed (1) hide show
  1. app.py +59 -0
app.py CHANGED
@@ -69,6 +69,65 @@ def database_path() -> Path:
69
  return slim if slim.exists() else ROOT / "data" / "gold" / "gridpulse.duckdb"
70
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  @st.cache_data(ttl=900, show_spinner=False)
73
  def run_query(sql: str, params: tuple = ()) -> pd.DataFrame:
74
  path = database_path()
 
69
  return slim if slim.exists() else ROOT / "data" / "gold" / "gridpulse.duckdb"
70
 
71
 
72
+ def data_version() -> str:
73
+ """Identify the currently deployed artifacts by size and modification time.
74
+
75
+ Every cached function below carries its own 900 second expiry, and each of
76
+ those clocks starts when that function is first called rather than when the
77
+ data changed. The caches therefore expire at different moments, and in the
78
+ window between them the page can report an hourly row count from one
79
+ training run beside an accuracy figure from the previous one. Both numbers
80
+ are individually correct and the pair is wrong, which is the least useful
81
+ kind of error to put in front of someone.
82
+
83
+ The weekly refresh rewrites the warehouse and the headline together, so
84
+ comparing their size and mtime detects a new deployment directly instead of
85
+ waiting for a timer to guess that one happened.
86
+ """
87
+ parts = []
88
+ for path in (database_path(), ROOT / "artifacts" / "headline.json"):
89
+ try:
90
+ stat = path.stat()
91
+ parts.append(f"{path.name}:{stat.st_size}:{stat.st_mtime_ns}")
92
+ except OSError:
93
+ parts.append(f"{path.name}:absent")
94
+ return "|".join(parts)
95
+
96
+
97
+ @st.cache_resource
98
+ def _deployed_version() -> dict[str, str | None]:
99
+ """Hold the last seen data version, shared across every user session.
100
+
101
+ Deliberately `cache_resource` rather than `session_state`. Session state is
102
+ per visitor, so the check below would fire once for each new arrival and
103
+ every one of them would clear a cache that was already correct. A resource
104
+ is shared process-wide, so the artifacts are detected as changed exactly
105
+ once no matter how many people are on the page. `st.cache_data.clear()`
106
+ does not touch `cache_resource`, so this record survives the clearing it
107
+ triggers.
108
+ """
109
+ return {"version": None}
110
+
111
+
112
+ def invalidate_caches_if_data_changed() -> None:
113
+ """Expire every cache at once when the artifacts change underneath us.
114
+
115
+ Streamlit Cloud reruns the script when a new commit lands but does not
116
+ always restart the process, so `st.cache_data` entries can outlive the files
117
+ they were derived from. Clearing on a version change makes a refresh visible
118
+ immediately and, more importantly, keeps the numbers on the page mutually
119
+ consistent.
120
+ """
121
+ record = _deployed_version()
122
+ current = data_version()
123
+ if record["version"] != current:
124
+ st.cache_data.clear()
125
+ record["version"] = current
126
+
127
+
128
+ invalidate_caches_if_data_changed()
129
+
130
+
131
  @st.cache_data(ttl=900, show_spinner=False)
132
  def run_query(sql: str, params: tuple = ()) -> pd.DataFrame:
133
  path = database_path()