bitsofchris Claude Opus 4.7 (1M context) commited on
Commit
7e7a097
·
1 Parent(s): bb2faab

Switch to 15-min autorefresh, drop manual button, sync all cycle archives

Browse files

- app.py: AUTO_REFRESH_SECONDS = 15 min, CACHE_TTL = 14 min so the next
autorefresh always misses the cache.
- app.py: remove the 'Refresh forecast' button and replace with a status
line. Dropdowns still trigger re-render (cheap — they hit the cache).
- app.py: each autorefresh tick now also calls sync.sync_cycle for every
cycle_type (5min / 30min / 4hour) so the all-channel Ecowitt archive
in data/ecowitt.db stays current. Rate-limit aware.
- src/persist.py: push_all / pull_all / push_all_async — multi-file
commit that ships forecasts.db + ecowitt.db together to the HF
Dataset. push_db_async on user-triggered refresh stays as before
for snappier round-trips; the autorefresh thread does the full push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +37 -8
  2. src/persist.py +86 -0
app.py CHANGED
@@ -17,7 +17,7 @@ from datetime import datetime, timedelta, timezone
17
  import gradio as gr
18
  import pandas as pd
19
 
20
- from src import ecowitt, forecast_log, nws, persist
21
  from src.forecast import forecast_series
22
  from src.weather_ui import (
23
  aligned_comparison_markdown,
@@ -26,8 +26,8 @@ from src.weather_ui import (
26
  hero_markdown,
27
  )
28
 
29
- CACHE_TTL_SECONDS = 60 * 60
30
- AUTO_REFRESH_SECONDS = 60 * 60
31
  DISPLAY_TZ = os.environ.get("DISPLAY_TZ", "America/New_York")
32
  PLACE_NAME = os.environ.get("PLACE_NAME", "Yaphank, NY")
33
 
@@ -158,8 +158,9 @@ def refresh(cycle_label: str = "Hourly", horizon_label: str = "24 h"):
158
  strip = emoji_strip_markdown(nws_df_raw, DISPLAY_TZ, n=12)
159
  scoreboard = render_scoreboard(log_conn)
160
 
161
- # Backup the SQLite log to the HF dataset (non-blocking).
162
  persist.push_db_async()
 
163
 
164
  return hero, comparison_md, strip, fig, scoreboard
165
 
@@ -198,10 +199,37 @@ def render_scoreboard(conn) -> str:
198
 
199
 
200
  # --- auto-refresh background thread --------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  def _autorefresh_loop():
202
  while True:
203
  try:
204
- refresh()
 
 
205
  except Exception: # noqa: BLE001
206
  print("[autorefresh] error during refresh:")
207
  traceback.print_exc()
@@ -243,7 +271,9 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
243
  choices=list(HORIZON_CONFIG.keys()), value="24 h",
244
  label="Forecast horizon", scale=1,
245
  )
246
- refresh_btn = gr.Button("Refresh forecast", variant="primary", scale=1)
 
 
247
 
248
  scoreboard_md = gr.Markdown()
249
  plot = gr.Plot(label="Forecast")
@@ -251,12 +281,11 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
251
  outputs = [hero_md, comparison_md, strip_md, plot, scoreboard_md]
252
  inputs = [cycle_dd, horizon_dd]
253
  demo.load(refresh, inputs=inputs, outputs=outputs)
254
- refresh_btn.click(refresh, inputs=inputs, outputs=outputs)
255
  cycle_dd.change(refresh, inputs=inputs, outputs=outputs)
256
  horizon_dd.change(refresh, inputs=inputs, outputs=outputs)
257
 
258
 
259
  if __name__ == "__main__":
260
- persist.pull_db() # bootstrap the forecast log from the HF Dataset
261
  _start_autorefresh()
262
  demo.launch()
 
17
  import gradio as gr
18
  import pandas as pd
19
 
20
+ from src import ecowitt, forecast_log, nws, persist, storage, sync
21
  from src.forecast import forecast_series
22
  from src.weather_ui import (
23
  aligned_comparison_markdown,
 
26
  hero_markdown,
27
  )
28
 
29
+ AUTO_REFRESH_SECONDS = 15 * 60 # background tick + archive sync
30
+ CACHE_TTL_SECONDS = AUTO_REFRESH_SECONDS - 60 # so autorefresh always refetches
31
  DISPLAY_TZ = os.environ.get("DISPLAY_TZ", "America/New_York")
32
  PLACE_NAME = os.environ.get("PLACE_NAME", "Yaphank, NY")
33
 
 
158
  strip = emoji_strip_markdown(nws_df_raw, DISPLAY_TZ, n=12)
159
  scoreboard = render_scoreboard(log_conn)
160
 
161
+ # Backup forecast log to HF Dataset (non-blocking).
162
  persist.push_db_async()
163
+ # The full archive sync + push happens in the autorefresh thread.
164
 
165
  return hero, comparison_md, strip, fig, scoreboard
166
 
 
199
 
200
 
201
  # --- auto-refresh background thread --------------------------------------
202
+ ECOWITT_ARCHIVE_DB = "data/ecowitt.db"
203
+
204
+
205
+ def _sync_archive_all_cycles() -> None:
206
+ """Refresh the SQLite archive (data/ecowitt.db) for every cycle_type
207
+ so the local mirror of Ecowitt's storage stays current."""
208
+ try:
209
+ cfg = ecowitt.EcowittConfig.from_env()
210
+ except RuntimeError:
211
+ return
212
+ conn = storage.connect(ECOWITT_ARCHIVE_DB)
213
+ try:
214
+ for cycle in sync.CYCLES:
215
+ try:
216
+ sync.sync_cycle(cfg, conn, cycle, verbose=False)
217
+ except ecowitt.EcowittRateLimitError as err:
218
+ print(f"[autorefresh] rate-limited on {cycle.name}: {err} — skipping rest")
219
+ break
220
+ except Exception: # noqa: BLE001
221
+ print(f"[autorefresh] sync error on {cycle.name}:")
222
+ traceback.print_exc()
223
+ finally:
224
+ conn.close()
225
+
226
+
227
  def _autorefresh_loop():
228
  while True:
229
  try:
230
+ refresh() # live forecast + forecasts.db log
231
+ _sync_archive_all_cycles() # 5min/30min/4hour raw archive
232
+ persist.push_all_async() # back up both DBs to HF Dataset
233
  except Exception: # noqa: BLE001
234
  print("[autorefresh] error during refresh:")
235
  traceback.print_exc()
 
271
  choices=list(HORIZON_CONFIG.keys()), value="24 h",
272
  label="Forecast horizon", scale=1,
273
  )
274
+ gr.Markdown(
275
+ "<span style='opacity:0.55'>🔄 Live data + forecast auto-refresh every 15 minutes.</span>"
276
+ )
277
 
278
  scoreboard_md = gr.Markdown()
279
  plot = gr.Plot(label="Forecast")
 
281
  outputs = [hero_md, comparison_md, strip_md, plot, scoreboard_md]
282
  inputs = [cycle_dd, horizon_dd]
283
  demo.load(refresh, inputs=inputs, outputs=outputs)
 
284
  cycle_dd.change(refresh, inputs=inputs, outputs=outputs)
285
  horizon_dd.change(refresh, inputs=inputs, outputs=outputs)
286
 
287
 
288
  if __name__ == "__main__":
289
+ persist.pull_all() # bootstrap forecast log + archive from the HF Dataset
290
  _start_autorefresh()
291
  demo.launch()
src/persist.py CHANGED
@@ -101,3 +101,89 @@ def push_db_async(local_path: str = DEFAULT_LOCAL) -> None:
101
  threading.Thread(
102
  target=push_db, args=(local_path,), daemon=True, name="persist-push"
103
  ).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  threading.Thread(
102
  target=push_db, args=(local_path,), daemon=True, name="persist-push"
103
  ).start()
104
+
105
+
106
+ # --- multi-file push (forecast log + Ecowitt archive in one commit) ------
107
+ ARCHIVE_LOCAL = "data/ecowitt.db"
108
+ ARCHIVE_PATH_IN_REPO = "ecowitt.db"
109
+ _multi_lock = threading.Lock()
110
+ _multi_last = 0.0
111
+
112
+
113
+ def push_all(
114
+ forecast_local: str = DEFAULT_LOCAL,
115
+ archive_local: str = ARCHIVE_LOCAL,
116
+ ) -> bool:
117
+ """Upload both DBs in a single dataset commit."""
118
+ global _multi_last
119
+ tok = _token()
120
+ if not tok:
121
+ return False
122
+ if time.time() - _multi_last < PUSH_MIN_INTERVAL:
123
+ return False
124
+ if not _multi_lock.acquire(blocking=False):
125
+ return False
126
+ try:
127
+ from huggingface_hub import CommitOperationAdd, HfApi # noqa: PLC0415
128
+ api = HfApi(token=tok)
129
+ ops = []
130
+ for local, in_repo in (
131
+ (forecast_local, PATH_IN_REPO),
132
+ (archive_local, ARCHIVE_PATH_IN_REPO),
133
+ ):
134
+ if os.path.exists(local):
135
+ ops.append(CommitOperationAdd(path_in_repo=in_repo, path_or_fileobj=local))
136
+ if not ops:
137
+ return False
138
+ api.create_commit(
139
+ repo_id=_repo_id(),
140
+ repo_type="dataset",
141
+ operations=ops,
142
+ commit_message="forecast log + archive update",
143
+ )
144
+ _multi_last = time.time()
145
+ sizes = ", ".join(f"{op.path_in_repo}={os.path.getsize(forecast_local if op.path_in_repo==PATH_IN_REPO else archive_local)}B" for op in ops)
146
+ print(f"[persist] pushed multi to {_repo_id()} ({sizes})")
147
+ return True
148
+ except Exception: # noqa: BLE001
149
+ print("[persist] push_all failed:")
150
+ traceback.print_exc()
151
+ return False
152
+ finally:
153
+ _multi_lock.release()
154
+
155
+
156
+ def push_all_async(
157
+ forecast_local: str = DEFAULT_LOCAL,
158
+ archive_local: str = ARCHIVE_LOCAL,
159
+ ) -> None:
160
+ threading.Thread(
161
+ target=push_all, args=(forecast_local, archive_local),
162
+ daemon=True, name="persist-push-all",
163
+ ).start()
164
+
165
+
166
+ def pull_all(
167
+ forecast_local: str = DEFAULT_LOCAL,
168
+ archive_local: str = ARCHIVE_LOCAL,
169
+ ) -> None:
170
+ """Pull both DBs from the dataset on startup. Each missing file is silently skipped."""
171
+ pull_db(forecast_local)
172
+ # Pull the archive too if it exists.
173
+ tok = _token()
174
+ if not tok:
175
+ return
176
+ try:
177
+ from huggingface_hub import hf_hub_download # noqa: PLC0415
178
+ downloaded = hf_hub_download(
179
+ repo_id=_repo_id(),
180
+ repo_type="dataset",
181
+ filename=ARCHIVE_PATH_IN_REPO,
182
+ token=tok,
183
+ )
184
+ os.makedirs(os.path.dirname(archive_local) or ".", exist_ok=True)
185
+ shutil.copyfile(downloaded, archive_local)
186
+ print(f"[persist] pulled archive ({os.path.getsize(archive_local)} bytes)")
187
+ except Exception:
188
+ # 404 on first run is expected.
189
+ pass