From b9f836250aeeb441e43258bc567f55ec832cf547 Mon Sep 17 00:00:00 2001 From: Jamie Holding Date: Mon, 28 Sep 2026 19:17:06 +0100 Subject: [PATCH] fix(backfill): an anonymous run says so at the END, where it is read Cube spotted that every documented example omits the API key. The docs are the smaller half of the problem: without a key the command SUCCEEDS. It reads the 7 days anonymous access allows, writes 433 rows of Magic Kingdom instead of about 94,000, and exits 0. The existing notice is printed before a run that takes minutes, so it scrolls away, and the last thing on screen is "done: 433 rows". For a customer who has just paid for 400 days that is indistinguishable from success -- there is a file, there is no error, and the number means nothing unless you already know what it should have been. A keyless example is worse than a broken one, because a broken one gets reported. So the notice is repeated at the end of the run, naming what the key would have bought. Both READMEs and the command's own epilog now export THEMEPARKS_API_KEY before the example that needs it, and keep saying that --list does not. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 6 ++++- tests/unit/test_backfill.py | 45 +++++++++++++++++++++++++++++++++++++ themeparks/backfill.py | 20 ++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77050dd..9703e9a 100644 --- a/README.md +++ b/README.md @@ -317,9 +317,13 @@ Installing the library installs `themeparks-backfill`, which does all of the above and stops before the walls: ```bash +# How far back it reaches is your plan, so set the key first: without one you get +# the 7 days anonymous access allows, and the run still succeeds, quietly. +export THEMEPARKS_API_KEY=tpw_your_key + themeparks-backfill "Disneyland Park" # a park, by name or id themeparks-backfill "Walt Disney World Resort" # a destination: every park in it -themeparks-backfill --list disney # find an id. Needs no key. +themeparks-backfill --list disney # find an id. This part needs no key. ``` It reads how far back your own key may ask and starts there, writes NDJSON or diff --git a/tests/unit/test_backfill.py b/tests/unit/test_backfill.py index 8dcc998..733c7af 100644 --- a/tests/unit/test_backfill.py +++ b/tests/unit/test_backfill.py @@ -1345,3 +1345,48 @@ class FakeEntity: entityType = "ATTRACTION" # noqa: N815 - the API's own spelling assert _ref(FakeEntity()).entity_type == "ATTRACTION" + + +class TestAnAnonymousRunSaysSoWhenItFinishes: + """The warning at the START scrolls away. The last line must carry it too. + + Running the documented example without a key succeeds: 433 rows of Magic + Kingdom instead of ~94,000, exit 0, and a file. A customer who has just paid + for 400 days has no reason to think anything went wrong -- the notice was + four lines printed before a run that takes minutes, and the last thing on + screen is "done: 433 rows". + """ + + def _run(self, tmp_path: Path, argv: list[str], monkeypatch) -> None: + monkeypatch.setattr(backfill, "_catalogue", lambda tp: [("p", "Park", "d", "Dest")]) + monkeypatch.setattr(backfill, "_run_all", lambda tp, targets, args: 0) + monkeypatch.setattr(backfill, "ThemeParks", lambda **kw: _NullClient()) + backfill.main([*argv, "--out", str(tmp_path)]) + + def test_an_anonymous_run_says_so_at_the_end(self, tmp_path: Path, capsys, monkeypatch) -> None: + monkeypatch.delenv("THEMEPARKS_API_KEY", raising=False) + self._run(tmp_path, ["p"], monkeypatch) + err = capsys.readouterr().err + # Both ends: before, so it can be acted on, and after, so it is the last + # thing read. + assert err.count("7 days") >= 2 + assert "ANONYMOUS ACCESS" in err + assert err.rstrip().endswith("keys: https://www.themeparks.wiki/profile") + + def test_a_run_with_a_key_says_nothing_of_the_sort( + self, tmp_path: Path, capsys, monkeypatch + ) -> None: + self._run(tmp_path, ["p", "--api-key", "tpw_test"], monkeypatch) + err = capsys.readouterr().err + assert "ANONYMOUS" not in err + assert "no API key" not in err + + +class _NullClient: + """A client that is never actually used: _run_all is stubbed out.""" + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False diff --git a/themeparks/backfill.py b/themeparks/backfill.py index c22187d..27921c1 100644 --- a/themeparks/backfill.py +++ b/themeparks/backfill.py @@ -1090,6 +1090,10 @@ def _print_list(catalogue: list[tuple[str, str, str, str]], needle: str | None) EPILOG = """examples: + export THEMEPARKS_API_KEY=tpw_your_key + how far back this reaches is your plan, so without a key you get the 7 + days anonymous access allows -- and the run still succeeds, quietly. + themeparks-backfill "Disneyland Park" the whole daily history your plan reaches, as NDJSON, into the current directory @@ -1248,7 +1252,21 @@ def main(argv: list[str] | None = None) -> int: file=sys.stderr, ) - return _run_all(tp, targets, args) + status = _run_all(tp, targets, args) + # SAID AGAIN AT THE END, and this is the point of it. The warning above + # is printed before a run that takes minutes, so it scrolls away, and the + # last thing on screen is "done: 433 rows" -- which for a customer who + # thought they were downloading five years is indistinguishable from + # success. They paid for 400 days and got seven, exit 0, no complaint. + if not args.api_key: + print( + "\nthat was ANONYMOUS ACCESS: the last 7 days only.\n" + " a free key reads 30 days, Pro 400, Business the whole archive\n" + " set THEMEPARKS_API_KEY and run the same command again\n" + " keys: https://www.themeparks.wiki/profile", + file=sys.stderr, + ) + return status return 0