-
Notifications
You must be signed in to change notification settings - Fork 0
feat: per-plugin download trigger in the sidebar UI #32
Description
Overview
Each source plugin's sidebar expander should contain a button that lets the user trigger the data collection phase without leaving the app or opening a terminal. This bridges the gap between the documented two-phase model (download → display) and the actual user experience, where right now the download step is invisible inside the UI.
Motivation
The architecture doc (#31) clearly separates collection from display, but the current sidebar only surfaces the display side (a file path selector). A first-time user who has never run a sync script has no obvious path forward from inside the app. A download button inside each plugin's expander makes the collection phase a first-class UI action while preserving the architectural boundary — the button triggers the CLI script; load() still reads only from the resulting local file.
Proposed Design
SourcePlugin ABC changes
Add two new optional methods to SourcePlugin:
def can_download(self) -> bool:
"""Return True if this plugin supports in-app triggered downloads.
Plugins that only accept manual exports (e.g. a ZIP from a website)
should return False. Plugins with a CLI sync script should return True.
"""
return False
def download(self, config: dict[str, Any]) -> None:
"""Run the collection phase for this plugin.
Called by the sidebar when the user clicks the download button.
This is the ONLY place in the plugin that may make network calls
or use credentials. It must write its output to the path(s) declared
in get_config_fields() so that a subsequent load() call succeeds.
Args:
config: Same config dict passed to load(), giving access to the
output path where data should be written.
Raises:
RuntimeError: With a user-readable message if the download fails
(missing credentials, network error, etc.).
"""
raise NotImplementedErrorPlugins that support automated download override both methods. Plugins backed by a manual website export (e.g. Goodreads, Netflix) leave can_download() returning False and show instructional text in place of the button.
Sidebar UI changes
Inside each plugin's st.sidebar.expander, after the file path selector(s):
- If
plugin.can_download()isTrue: show a "Download latest data" button. On click, callplugin.download(config)in ast.spinner, catchRuntimeErrorand surface it asst.error, refresh on success. - If
plugin.can_download()isFalse: show ast.captionwith a short instruction linking to the source's export page (sourced from a newEXPORT_INSTRUCTIONSclass attribute).
┌─ 🎧 Last.fm Music History ──────────────────────────────────┐
│ Last.fm CSV file │
│ [G:/My Drive/Projects/Last.fm/tracks.csv ] [...] │
│ │
│ [↓ Download latest data] │
└──────────────────────────────────────────────────────────────┘
┌─ 📺 Netflix ─────────────────────────────────────────────────┐
│ Viewing history CSV │
│ [ ] [...] │
│ │
│ ℹ Export at netflix.com → Account → Viewing Activity │
└──────────────────────────────────────────────────────────────┘
SourcePlugin attribute additions
EXPORT_INSTRUCTIONS: str = ""
# Human-readable sentence shown when can_download() is False.
# Example: "Export at letterboxd.com → Settings → Data Export → Download ZIP"Acceptance Criteria
-
can_download()anddownload()added toSourcePluginABC as optional overrides (defaultFalse/NotImplementedError) -
EXPORT_INSTRUCTIONSclass attribute added with empty-string default - Sidebar renders a download button for plugins where
can_download()isTrue - Sidebar renders
st.caption(EXPORT_INSTRUCTIONS)for plugins wherecan_download()isFalse(and instructions are non-empty) -
download()is called insidest.spinner;RuntimeErrorsurfaces asst.error; success triggersst.rerun()so the file path selector reflects the new file -
download()implementations write credentials from env vars (AUTOBIO_*), never from hardcoded values -
LastFmPlugin:can_download()returnsTrue;download()delegates to the existingautobiographer.pyfetch pipeline;AUTOBIO_LASTFM_API_KEY/AUTOBIO_LASTFM_USERNAMErequired -
SwarmPlugin:can_download()returnsFalse;EXPORT_INSTRUCTIONSlinks to the Foursquare data export page - Unit tests:
can_download()contract for both built-in plugins;download()called correctly when button is clicked (mock the underlying fetch); instructions rendered whencan_download()isFalse - Full quality gate passes (
ruff,mypy,pytest --cov ≥ 80%) - Architecture contract maintained:
load()still contains zero network calls after this change
Out of Scope
- Progress reporting within a long-running download (follow-up)
- Scheduling / auto-refresh (follow-up)
- Download for plugins not yet implemented (each plugin issue handles its own)
References
- Plugin architecture principles: docs: plugin architecture — data sovereignty and download-then-display contract #31
- Plugin model: feat: plugin model with SourcePlugin ABC, registry, and DataBroker #8
- Sidebar file path selectors: feat: multi-page navigation with st.navigation and Material icons #29