Phase 8 — Polish¶
Status: Implemented (P8.1–P8.5)
ROADMAP tasks: P8.1, P8.2, P8.3, P8.4, P8.5
ADRs: docs/adr/0005-ytdlp-metadata-source.md, docs/adr/0002-typer-rich-cli.md, docs/adr/0014-secrets-env-keyring.md, docs/adr/0001-python-and-uv.md
Scope¶
Round out the tool for daily use: optional YouTube Data API metadata source, installation and shell completion docs, cost reporting, and the README.
- P8.1
sources/youtube_api.py(YouTubeDataApiSource) behind theapiextra (decision D7). - P8.2
uv tool install thumbforgedocumentation and first-run walkthrough (docs/user-guide/getting-started.md). - P8.3 shell completion (
thumbforge --install-completion, Typer builtin) documented and smoke-tested on PowerShell, bash, zsh. - P8.4
runs cost <run>report and acostcolumn inruns show. - P8.5
README.mdfinal content, CHANGELOG viagit-cliff, first tagv0.1.0(release workflow fromphase-0-infra.md).
Non-goals¶
- New providers; the plugin group exists for that.
- GUI/web UI.
- Metrics upload or telemetry of any kind.
Interfaces¶
YouTube Data API source¶
# pyproject.toml
[project.optional-dependencies]
api = ["google-api-python-client", "google-auth"]
class YouTubeDataApiSource: # implements MetadataSource from phase-2-youtube-fetch.md
key: ClassVar[str] = "api"
def __init__(self, api_key: str) -> None: ...
- Selected with
fetch --source api; the key is resolved by the Phase 3 secrets order (THUMBFORGE_PROVIDERS__API__API_KEY, then keyring servicethumbforgeusernameapi, written bythumbforge provider set-key api— the same keyring path is reused for sources). - Missing extra →
SourceErrorwith hintuv tool install "thumbforge[api]", exit1; missing key →SourceErrorwith hintthumbforge provider set-key api, exit1. - Field mapping to
VideoMeta/PlaylistMeta/ChannelMetais identical toYtDlpSource;source = "api"on stored rows. Pagination viapageTokenuntil exhausted; quota errors (403 quotaExceeded) map toSourceErrorand are not retried.
Cost report¶
RunRepository.cost_report(run_id: str) -> CostReport
# CostReport(run_id, iterations: int, with_cost_data: int, tokens_in: int, tokens_out: int,
# credits: Decimal | None, currency: str | None, duration_ms: int); .no_cost_data
thumbforge runs cost <run> prints the report (JSON {"cost": {...}} with --json; credits is a decimal string). Values aggregate iteration.cost_json; an iteration with cost_json NULL (failed, unfinished, or a provider with no usage figures) adds nothing to the sums and is counted in no_cost_data. A run with no cost data at all prints a sentence saying so instead of zeros; its JSON keeps the zero totals beside with_cost_data: 0. A run's cost is its own iterations only: child runs (a batch built on a hero, a refinement) are not folded in. credits stays null unless a provider reports it: S8 measured Antigravity's usage as tokens only.
runs show gains a Cost column (1200 in / 80 out, plus credits when reported, — when nothing was reported), and each iteration in its JSON gains cost (tokens_in, tokens_out, credits, currency, or null).
Completion¶
thumbforge --install-completion and --show-completion are Typer's builtin flags. They take no shell argument: the shell is detected from the parent process, so the command is run from inside the shell to set up. The user-guide page docs/user-guide/how-to/shell-completion.md documents, per shell, where the script lands, how to reload and how to undo (PowerShell: the profile named by $PROFILE). tests/unit/test_cli_completion.py drives the _THUMBFORGE_COMPLETE protocol the installed scripts use against the real app.
Behaviour¶
fetch --source apiproduces rows indistinguishable from--source ytdlpexceptsource; re-fetching with a different source updatessourceandfetched_at.runs coston a FakeProvider run says it has no cost data (JSON: zero tokens,with_cost_data = 0,no_cost_data = <n>); on an Antigravity run reports the summedusagefields from the envelopes.README.mdcovers: install,fetch→thumb generate→thumb pick→batchin five commands, provider setup (Antigravity login,skip_permissionsnote from decision D4), config path, docs link.git-cliffgeneratesCHANGELOG.mdfrom Conventional Commits at tag time; thev0.1.0tag triggersrelease.yml. PyPI publishing happens only if decision D5 says yes; otherwise thepublishjob is removed before tagging.
Acceptance criteria¶
uv sync --locked --extra apithenthumbforge fetch <playlist> --source apiwith a valid key (integration marker) stores the same 12 videos as the ytdlp fixture withsource = api.- Note (P8.1): the ytdlp fixture keeps only the first 12 items of a playlist that is much longer live (183 in spike S11) and changes over time, so
tests/integration/test_youtube_api_live.pyasserts at least 12 items with contiguous positions rather than the fixture's exact ids; the fixture-level "same 12 videos" check istests/contract/test_source_contract.py. Only thechanneltable has asourcecolumn, sosource = apiis persisted on the channel row;VideoMeta/PlaylistMetacarry it in memory. - Without the extra,
thumbforge fetch <url> --source apiexits1and prints the install hint; with the extra but no key, exits1with theset-keyhint. thumbforge runs cost <fake run>says the run has no cost data for a 4-iteration hero run; with--jsonit reportstokens_in0 andno_cost_data4.thumbforge --install-completion, run from PowerShell, appends to the PowerShell profile andthumbforge th<TAB>completes tothumbin a fresh shell; the same holds for bash and zsh with their own rc files (manual check, recorded in the commit message).uv tool install .from a checkout putsthumbforgeon PATH andthumbforge --versionprintsthumbforge 0.1.0.- Tagging
v0.1.0runsrelease.ymlto a greenbuildjob withdist/thumbforge-0.1.0-py3-none-any.whlattached to the GitHub release.
Test plan¶
- Unit:
YouTubeDataApiSourceagainst recorded API JSON intests/fixtures/youtube_api/*.jsonwith a stubbedbuild(); quota error mapping; pagination; extra-missing and key-missing hints viaCliRunner;runs costaggregation (tests/unit/test_cli_runs.py). - Integration (
-m integration): one live Data API playlist fetch whenTHUMBFORGE_PROVIDERS__API__API_KEYis set. - Contract:
MetadataSourcecontract test parametrised over{ytdlp, api}(added here;tests/contract/test_source_contract.py). - Golden: none.
Open spikes¶
- S8 cost/usage fields from Antigravity — decides whether
credits/currencyare ever non-null. - S12 Python 3.15 GA — add to CI matrix and
requires-pythonnote in README beforev0.1.0if GA has happened. - Decision D7 — if the Data API source is dropped, P8.1 and the
apiextra are removed from this spec andfetch --source apikeeps the Phase 2 behaviour (exit2).