From 9408fd21cc435fbdbafa6052c96a5cb31c51a6fe Mon Sep 17 00:00:00 2001 From: Ade Oshineye Date: Sat, 11 Jul 2026 20:43:42 +0100 Subject: [PATCH] Add privacy notice and harden deployment tooling --- CONTRIBUTING.md | 3 +- Makefile | 15 +++++-- README.md | 7 ++- docs/lessons-learned.md | 3 +- docs/pr-evidence/README.md | 2 +- docs/turnstile-runner-protection-spec.md | 4 +- package-lock.json | 51 ++-------------------- package.json | 3 ++ public/og/manifest.json | 1 + public/og/privacy.jpg | Bin 0 -> 68224 bytes scripts/build_social_cards.py | 11 +++++ scripts/lint_seo_cache.py | 7 ++- scripts/smoke_deployment.py | 1 + src/app.py | 22 +++++++++- src/asset_manifest.py | 2 +- src/main.py | 8 ++++ src/templates/layout.html | 2 +- src/templates/privacy.html | 35 +++++++++++++++ tests/test_app.py | 53 +++++++++++++++++++++-- tests/test_main_routes.py | 12 +++++ 20 files changed, 175 insertions(+), 67 deletions(-) create mode 100644 public/og/privacy.jpg create mode 100644 src/templates/privacy.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index baea918..76ed308 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,10 @@ make build ## Before opening a pull request -Install the committed dependency locks and start the local Worker with the repository-pinned Wrangler: +Use Node 22, install the committed dependency locks, and start the local Worker with the repository-pinned Wrangler: ```bash +node --version # v22.x uv sync --locked --all-groups npm ci --ignore-scripts make dev diff --git a/Makefile b/Makefile index d808413..8c64815 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,19 @@ PY := uv run --python 3.13 NODE_DEPS_STAMP := node_modules/.package-lock.json -.PHONY: node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint +.PHONY: check-node-version node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint -$(NODE_DEPS_STAMP): package.json package-lock.json +check-node-version: + @major="$$(node -p 'process.versions.node.split(".")[0]')"; \ + if test "$$major" != "22"; then \ + echo "Node 22 is required for Pywrangler/Pyodide (found $$(node --version))." >&2; \ + exit 1; \ + fi + +$(NODE_DEPS_STAMP): package.json package-lock.json | check-node-version npm ci --ignore-scripts -node-deps: $(NODE_DEPS_STAMP) +node-deps: check-node-version $(NODE_DEPS_STAMP) test: $(PY) python -m unittest discover -s tests -v @@ -117,5 +124,5 @@ smoke-deployment: $(PY) scripts/smoke_deployment.py $(URL) deploy: node-deps check-generated - uv run --group workers pywrangler sync + uv run --group workers pywrangler sync --force uv run --group workers pywrangler deploy diff --git a/README.md b/README.md index cd1cc9d..12dc590 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Production: (`workers.dev` is disabled so the - Learner-behavior reporting from Worker wide events (`docs/learner-analytics.md`) - A quality-gate suite for registries, rubric scores, coverage, and teaching structure that fails builds when content regresses - `/about` page describing the verification pipeline, runner sandbox, figure grammar, and live design tokens +- `/privacy` notice covering runner submissions, operational logs, Invisible Turnstile, cookies, and optional browser CDNs - Copy buttons on read-only source cells - Keyboard prev/next navigation (`←`/`→`) on example pages - "Copy link" sharing of the runner's current code via `#code=` URLs @@ -67,9 +68,10 @@ This project is developed with red-green-refactor TDD: 2. Implement the smallest change that makes it pass. 3. Refactor while keeping tests green. -Install the exact committed Python and Node dependency sets, then run: +Use Node 22 (Pywrangler's Pyodide runtime is not compatible with Node 26), then install the exact committed Python and Node dependency sets: ```bash +node --version # v22.x uv sync --locked --all-groups npm ci --ignore-scripts make test @@ -231,5 +233,6 @@ Use the active Cloudflare-supported Python version. - Dynamic Worker IDs include Python version, example slug, and submitted code hash. - Dynamic Workers run with `globalOutbound: null` and tight CPU/subrequest limits. - POST example runs carry `Cache-Control: no-store` and are never cached. -- Wrangler is an exact dev dependency in `package-lock.json`; `make dev` and `make deploy` install/use that local version through Pywrangler. +- Wrangler is an exact dev dependency in `package-lock.json`; `make dev` and `make deploy` require Node 22 and install/use that local version through Pywrangler. +- `make deploy` forces a fresh Pywrangler vendor sync so an empty or stale `python_modules/` directory cannot produce a dependency-free upload. - Production still requires the account-level WAF/rate-limit rule documented in `docs/turnstile-runner-protection-spec.md`; repository settings alone do not prove that external control is active. diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index 144ce06..0fca073 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -125,6 +125,7 @@ git diff --check - **When every score is green, resist score inflation and look for unsupported claims.** The current steady state is intentionally boring: one documented `hello-world` waiver, no below-target example diagrams, no below-target journey figures, no orphaned examples, and no stale weak-section backlog. Future audits should preserve that baseline by finding semantic drift, note claims without cells, weak `See also` edges, or journey sections that no longer match their support examples — not by raising 9.0 scores to 9.5 without new evidence. - **Journey audit is graph audit plus outcome audit.** A journey section is healthy only when its examples form a prerequisite-respecting mental map and its declared outcomes are backed by cells on those examples. A section can have a beautiful figure and still fail if the support list is a catalog slice, if the `See also` graph isolates one example, or if the section caption describes a conceptual shift the examples do not actually make. - **A green total can still hide a weak criterion.** The journey-figure audit found every section figure above the project gate while 15 sections still reused a lesson paint function, which weakened the independence-from-lesson-figures dimension. Bespoke runtime, control-flow, iteration, types, and reliability section figures cleared that watchlist; keep tracking criterion-level weakness even when the aggregate score remains shippable. +- **Deployment tooling must fail before an empty Python vendor upload.** Pywrangler 1.9.3's Pyodide interpreter requires Node 22; under Node 26 its Node flag fails and a stale sync token can leave `python_modules/` containing only metadata while the sync command still reports success. Pin Node 22, force `pywrangler sync --force` before deploy, and confirm the Wrangler module table includes FastAPI's vendored modules rather than `Vendored Modules 0.00 KiB`. - **Deployment smoke belongs beside CI, and POST smoke must assert rendered output.** `scripts/smoke_deployment.py` checks rendered Worker pages, runtime-boundary pages, journey pages, prototype review pages, and representative Dynamic Worker POST runs for HTTP failures, exception markers, and stale edited-code output. Build success is not enough; the deployed Worker must render and execute edited examples. With Turnstile enabled, submitted code can appear in the editor textarea even when it did not run, so POST smoke must inspect the output panel rather than searching the whole HTML document. - **Observability smoke should assert the custom event, not the whole tail envelope.** Use unique `x-request-id` values, exercise cache miss/hit/bypass and client-error paths, and assert on the structured payload inside `logs[].message[]`. If Turnstile is enabled, Dynamic Worker error-path probes need the smoke bypass secret; otherwise they only verify the Turnstile-fail path. - **Turnstile should be secret-gated, session-scoped, and invisible until needed.** Protect edited-code POST runs only when `TURNSTILE_SECRET_KEY` and an explicit challenge mode are configured, lazy-load/render the Invisible-mode widget only after the server returns a challenge-required marker, and issue a signed clearance cookie so normal session runs skip Siteverify. The Cloudflare widget mode is `Invisible`; the client-side render option is `execution: "execute"`, not `size: "invisible"`. If production smoke must POST through a protected endpoint, use a separate `PBE_SMOKE_BYPASS_SECRET` header so smoke remains a deployment check rather than a CAPTCHA solver. See `docs/turnstile-runner-protection-spec.md` for the full runner-protection design. @@ -135,7 +136,7 @@ git diff --check - **Dark mode for locked-palette SVGs: change the mat, not the art.** The marginalia grammar hardcodes the light palette in every figure, and recolouring 109 figures would have meant touching the locked grammar and re-auditing geometry. Instead, dark mode renders each figure on a light "paper" chip (`--figure-paper` background, small padding, rounded corners). The figures stay byte-identical, read as intentional artifacts, and the grammar's palette constraint survives. - **Theme changes need live browser state, not just first-paint CSS.** Read-only Shiki blocks are rendered with the current light/dark theme and rerender when the OS preference changes. CodeMirror rebuilds its theme compartment from the same `matchMedia` change signal. Both also retain readable plain-text fallbacks when the optional CDN graph is unavailable. - **Rasterized artifacts need structural gates, not byte-parity gates.** Social-card bytes vary across Chrome versions and platforms, so CI validates deterministic input provenance, the exact expected JPEG filename set, JPEG decodability, and 1200×630 dimensions rather than comparing raster bytes. The SEO linter separately verifies that every `og:image` URL resolves to a committed card. -- **Social cards should reuse the curated figure set.** Each example's card composes its marginalia figure beside the title and summary, so a shared link carries the same diagram the page teaches with. `render_first_figure(slug)` is the only new marginalia surface the card builder needed; one Chrome session rasterizes the current 119-card set via CDP navigation and `Page.captureScreenshot` clips. +- **Social cards should reuse the curated figure set.** Each example's card composes its marginalia figure beside the title and summary, so a shared link carries the same diagram the page teaches with. `render_first_figure(slug)` is the only new marginalia surface the card builder needed; one Chrome session rasterizes the current 120-card set via CDP navigation and `Page.captureScreenshot` clips. - **Copy that describes data state goes stale silently.** Journey meta descriptions still claimed "explicit placeholders for missing examples" long after the last gap placeholder was filled, and the gap-rendering UI suggested holes that no longer existed. Prose that asserts a property of the data ("all examples run," "placeholders mark gaps") should either be derived from the data or covered by a check; otherwise fixing the data falsifies the copy. - **Make targets that import the example loader must go through uv.** The loader executes example code that uses 3.12+ syntax (`type UserId = int`), so any script importing `src.app` breaks under an older system `python3` even though its shebang says `python3`. CI masks this by installing 3.13 as the default interpreter. Build steps that import the catalog (`build_search_index`, `build_social_cards`) run as `uv run --python 3.13 scripts/...` in the Makefile so local machines with an older `python3` behave like CI. - **Index notes text, not just titles, and normalize at build time.** The search index concatenates the slug words and every note line, lowercased at build time, so concept queries ("walrus", "GIL"-style vocabulary that titles avoid) hit the right example and the client never re-normalizes entry content. Exporting the ranking function from `search.js` lets a plain Node script (`make search-ranking-test`) assert ranking behaviour against the real generated index without a JS test framework. diff --git a/docs/pr-evidence/README.md b/docs/pr-evidence/README.md index 38aa6db..1d504f1 100644 --- a/docs/pr-evidence/README.md +++ b/docs/pr-evidence/README.md @@ -30,7 +30,7 @@ These artifacts provide reviewable evidence for the runner, journey-page, and so | [Runtime journey page](pr-6-journey-runtime.png) | `/journeys/runtime` renders its overview, first learning outcome, figure, and linked examples at a 1200×1260 desktop viewport. | `a85bbf7` | `eb1db35d5ff7fc7974ad3a3891b694aaa5f8f277ba291b94a24c62767340e04d` | | [`/journeys` social card](../../public/og/journeys.jpg) | The exact 1200×630 JPEG referenced by the `/journeys` `og:image` tag. | `a85bbf7` | `3042997bb2b3acd9e2ab74106f5b4d3d4c58dafcfe23f1ddc548ed39e4548a3c` | -`public/og/journeys.jpg` is a tracked PR artifact and its deterministic HTML input is recorded in `public/og/manifest.json`. `make check-social-cards` verifies its provenance, the exact 119-card JPEG set, and each card's JPEG decodability and 1200×630 dimensions. +`public/og/journeys.jpg` is a tracked PR artifact and its deterministic HTML input is recorded in `public/og/manifest.json`. `make check-social-cards` verifies its provenance, the exact 120-card JPEG set, and each card's JPEG decodability and 1200×630 dimensions. Regenerate each page with its commit checked out, serve the rendered `/examples/values` HTML plus `public/` on a local HTTP server, then run: diff --git a/docs/turnstile-runner-protection-spec.md b/docs/turnstile-runner-protection-spec.md index 7c869f4..7b40feb 100644 --- a/docs/turnstile-runner-protection-spec.md +++ b/docs/turnstile-runner-protection-spec.md @@ -382,7 +382,7 @@ The following non-secret Worker vars are checked into `wrangler.jsonc`: Do **not** put secret keys in `wrangler.jsonc`. -Manual browser verification and production smoke with `PBE_SMOKE_BYPASS_SECRET` both passed after deploying session-scoped Invisible-mode Turnstile. +Manual browser verification and production smoke with `PBE_SMOKE_BYPASS_SECRET` both passed after deploying session-scoped Invisible-mode Turnstile. A privacy notice is published at `/privacy` and linked from every page footer. Docs: @@ -393,7 +393,7 @@ Docs: Invisible mode note: Cloudflare documents that invisible widgets have no visual footprint, and that widget `size` values are `normal`, `flexible`, or `compact`; invisibility is selected by widget mode, not by `size="invisible"`. -Privacy note: Cloudflare documents that using Invisible mode requires referencing the Cloudflare Turnstile Privacy Addendum in your own privacy policy. +Privacy note: Cloudflare documents that using Invisible mode requires referencing the Cloudflare Turnstile Privacy Addendum in your own privacy policy. The globally linked `/privacy` notice now includes that addendum, Cloudflare links, cookie details, runner processing, operational-log fields, and optional CDN disclosures. ## Remaining TODO diff --git a/package-lock.json b/package-lock.json index 9d69098..ba47f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "name": "pythonbyexample-tooling", "devDependencies": { "wrangler": "4.110.0" + }, + "engines": { + "node": "22.x" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -684,9 +687,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -704,9 +704,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -724,9 +721,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -744,9 +738,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -764,9 +755,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -784,9 +772,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -804,9 +789,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -824,9 +806,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -844,9 +823,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -870,9 +846,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -896,9 +869,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -922,9 +892,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -948,9 +915,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -974,9 +938,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1000,9 +961,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1026,9 +984,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index fb81650..02efd30 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "scripts": { "wrangler": "wrangler" }, + "engines": { + "node": "22.x" + }, "devDependencies": { "wrangler": "4.110.0" } diff --git a/public/og/manifest.json b/public/og/manifest.json index 78ebe08..938564e 100644 --- a/public/og/manifest.json +++ b/public/og/manifest.json @@ -88,6 +88,7 @@ "paramspec.jpg": "bd96b51b333f30496c90b4bed08edf867e583258c208786dab8690acbd7ac727", "partial-functions.jpg": "fd8ae2052a7d6b1cb17418111fb5083887b0903c19ef37394777b5ab304ad0f0", "positional-only-parameters.jpg": "93d22d48edb838e725b7a1f7bf285e72dad09bb56d151f21eb91ecc3f36ddff8", + "privacy.jpg": "3eb2a28f8ace3441f6b95ad43facefaa78dabd11c0061bbfc70ef7a5b15c4fa7", "properties.jpg": "f42d17549e38b8b4ee8b18797deee917495ed75807da28bdcfca05db27c66a9b", "protocols.jpg": "fee2838223a57c3352a848f4988ada030e48bd9b4bb36e496682f68e6556fa2c", "recursion.jpg": "d3287842bdefa7ad12e8634f98e394e47d4b1990f7efcd8aa60a9bc418997830", diff --git a/public/og/privacy.jpg b/public/og/privacy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc369cd8b20f60c2080e0954aaf41f10adc2fe11 GIT binary patch literal 68224 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 literal 0 HcmV?d00001 diff --git a/scripts/build_social_cards.py b/scripts/build_social_cards.py index d2001c2..669b8a9 100644 --- a/scripts/build_social_cards.py +++ b/scripts/build_social_cards.py @@ -97,10 +97,21 @@ def render_about_social_card_html() -> str: ) +def render_privacy_social_card_html() -> str: + return _card_shell( + _card_body( + eyebrow="Python By Example · Privacy", + title="Privacy and data use", + summary="How the runner, operational logs, Turnstile, cookies, and optional browser dependencies handle data.", + ) + ) + + def card_html() -> dict[str, str]: cards = { "about": render_about_social_card_html(), "home": render_home_card_html(), + "privacy": render_privacy_social_card_html(), "journeys": render_journeys_index_social_card_html(), } cards.update({example["slug"]: render_social_card_html(example) for example in list_examples()}) diff --git a/scripts/lint_seo_cache.py b/scripts/lint_seo_cache.py index e941524..f5c3160 100755 --- a/scripts/lint_seo_cache.py +++ b/scripts/lint_seo_cache.py @@ -11,7 +11,7 @@ sys.path.insert(0, str(ROOT)) from scripts.fingerprint_assets import ASSETS, PUBLIC, html_version # noqa: E402 -from src.app import JOURNEYS, SITE_URL, list_examples, render_about, render_example_page, render_home, render_journeys_index, render_journey_page, render_sitemap # noqa: E402 +from src.app import JOURNEYS, SITE_URL, list_examples, render_about, render_example_page, render_home, render_journeys_index, render_journey_page, render_privacy, render_sitemap # noqa: E402 from src.asset_manifest import ASSET_PATHS, HTML_CACHE_VERSION # noqa: E402 META_DESCRIPTION_RE = re.compile(r'') @@ -79,6 +79,8 @@ def assert_sitemap(failures: list[str]) -> None: fail(f"sitemap: missing {url}", failures) if f"{SITE_URL}/about" not in sitemap: fail("sitemap: missing about URL", failures) + if f"{SITE_URL}/privacy" not in sitemap: + fail("sitemap: missing privacy URL", failures) if f"{SITE_URL}/journeys" not in sitemap: fail("sitemap: missing journeys index URL", failures) for journey in JOURNEYS: @@ -130,6 +132,7 @@ def main() -> int: assert_sitemap(failures) assert_page_metadata("home", render_home(), "/", failures) assert_page_metadata("about", render_about(), "/about", failures) + assert_page_metadata("privacy", render_privacy(), "/privacy", failures) assert_page_metadata("journeys-index", render_journeys_index(), "/journeys", failures) for example in list_examples(): assert_page_metadata( @@ -149,7 +152,7 @@ def main() -> int: for failure in failures: print(f"FAIL: {failure}", file=sys.stderr) return 1 - print(f"SEO/cache lint passed for 1 home page, 1 about page, 1 journey index, {len(JOURNEYS)} journeys, and {len(list_examples())} example pages.") + print(f"SEO/cache lint passed for 1 home page, 1 about page, 1 privacy page, 1 journey index, {len(JOURNEYS)} journeys, and {len(list_examples())} example pages.") return 0 diff --git a/scripts/smoke_deployment.py b/scripts/smoke_deployment.py index 7dca4ba..b97d537 100755 --- a/scripts/smoke_deployment.py +++ b/scripts/smoke_deployment.py @@ -19,6 +19,7 @@ SMOKE_PATHS = [ "/", "/about", + "/privacy", "/examples/values", "/examples/async-await", "/examples/networking", diff --git a/src/app.py b/src/app.py index cd3d3a7..23edd77 100644 --- a/src/app.py +++ b/src/app.py @@ -423,8 +423,26 @@ def render_about() -> str: ) +def render_privacy() -> str: + return _layout( + "Privacy", + _template("privacy.html"), + description="How Python By Example handles runner submissions, operational logs, Cloudflare Turnstile verification, cookies, and optional code-editor CDN requests.", + path="/privacy", + og_image=f"{SITE_URL}/og/privacy.jpg", + structured_data={ + "@context": "https://schema.org", + "@type": "WebPage", + "name": "Python By Example privacy notice", + "url": f"{SITE_URL}/privacy", + "description": "Privacy information for Python By Example, including its editable runner, Cloudflare Turnstile, operational logs, cookies, and optional browser dependencies.", + "inLanguage": "en", + }, + ) + + def render_sitemap() -> str: - paths = ["/", "/about", "/journeys"] + paths = ["/", "/about", "/privacy", "/journeys"] paths.extend(f'/journeys/{journey["slug"]}' for journey in JOURNEYS) paths.extend(f'/examples/{example["slug"]}' for example in list_examples()) entries = "".join(f"{html.escape(SITE_URL + path)}" for path in paths) @@ -739,6 +757,8 @@ def route(url: str, method: str = "GET", turnstile_site_key: str | None = None) ) if method == "GET" and path == "/about": return AppResponse(render_about(), headers={"Content-Type": "text/html; charset=utf-8"}) + if method == "GET" and path == "/privacy": + return AppResponse(render_privacy(), headers={"Content-Type": "text/html; charset=utf-8"}) if method == "GET" and path == "/journeys": return AppResponse(render_journeys_index(), headers={"Content-Type": "text/html; charset=utf-8"}) if method == "GET" and path.startswith("/journeys/"): diff --git a/src/asset_manifest.py b/src/asset_manifest.py index f269ba4..27f2a51 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. ASSET_PATHS = {'SITE_CSS': '/site.07c59ae07f87.css', 'SYNTAX_JS': '/syntax-highlight.c405e604fc10.js', 'EDITOR_JS': '/editor.3827f6497b54.js', 'RUNNER_JS': '/runner.96b105eef218.js', 'SEARCH_JS': '/search.e5a5822d9917.js', 'SEARCH_INDEX': '/search-index.332712b2e502.json'} -HTML_CACHE_VERSION = '3f7b7d26c98f' +HTML_CACHE_VERSION = '7405fb4b3979' diff --git a/src/main.py b/src/main.py index 7a59b8c..2ecb8ef 100644 --- a/src/main.py +++ b/src/main.py @@ -28,6 +28,7 @@ render_journeys_index, render_mobile_run_first_option, render_not_found, + render_privacy, render_sitemap, ) from asset_manifest import HTML_CACHE_VERSION @@ -247,6 +248,11 @@ async def about_page(): return _html(render_about()) +@app.get("/privacy", response_class=HTMLResponse) +async def privacy_page(): + return _html(render_privacy()) + + @app.get("/journeys", response_class=HTMLResponse) async def journeys_index(): return _html(render_journeys_index()) @@ -556,6 +562,8 @@ async def not_found(path: str, request: Request): ) if path == "about": return _html(render_about()) + if path == "privacy": + return _html(render_privacy()) if path == "journeys": return _html(render_journeys_index()) if path.startswith("journeys/"): diff --git a/src/templates/layout.html b/src/templates/layout.html index 4512ed2..4a1751f 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -26,7 +26,7 @@
__CONTENT__
- + __RUNNER_SCRIPT__ diff --git a/src/templates/privacy.html b/src/templates/privacy.html new file mode 100644 index 0000000..fe0b754 --- /dev/null +++ b/src/templates/privacy.html @@ -0,0 +1,35 @@ +
+ +
+

Privacy

+

Privacy and data use

+

Effective 11 July 2026. Python By Example has no accounts or advertising, and we do not sell personal information.

+
+
+

Requests and operational logs

+

The site runs on Cloudflare Workers. Cloudflare processes ordinary request information needed to deliver and protect the service, which can include your IP address, request headers, browser information, requested path, and timing data. Cloudflare's platform log envelope may retain this request metadata according to the account's Workers Logs settings.

+

Our structured application events record operational fields such as the requested path, example slug, whether submitted code differed from the example, a short code hash, byte count, execution outcome, and duration. They do not include raw submitted code, request bodies, Turnstile tokens, cookies, authorization headers, or the full IP address in application-defined fields.

+
+
+

Editable Python runner

+

When you press Run, the submitted Python source is sent to a Cloudflare Dynamic Worker for isolated execution. Outbound network access is disabled. The source is used to execute that request and to derive a hash used for safe Worker reuse; it is not included in our structured analytics event.

+
+
+

Cloudflare Turnstile Privacy Addendum

+

To protect the runner from abuse, an Invisible-mode Cloudflare Turnstile check may run when a browser session first submits edited code. Turnstile may process browser, device, network, and IP signals to distinguish people from automated abuse. See Cloudflare's Privacy Policy and Turnstile documentation.

+

After successful verification, the site sets pbe_turnstile_clearance, an HttpOnly, Secure, SameSite=Lax cookie scoped to /examples. It normally expires after eight hours and prevents repeated verification during the same learning session.

+
+
+

Code editor and syntax highlighting

+

Example pages can load the CodeMirror editor and Shiki syntax highlighter from esm.sh. When those optional modules load, the CDN receives ordinary network request information such as your IP address and browser headers. If the CDN is unavailable, the page retains a plain-text editor and readable server-rendered code.

+
+
+

Retention and choices

+

Operational logs are retained according to the configured Cloudflare Workers Logs retention settings and are used for reliability, abuse prevention, and aggregate interaction reporting. You can read all examples without an account. Disabling cookies may cause Turnstile verification to repeat when you run edited code.

+

Questions or requests about this notice can be opened in the project's public issue tracker. Do not include private or sensitive information in a public issue.

+
+
+

Changes to this notice

+

Material changes will update the effective date above and ship through the public source repository.

+
+
diff --git a/tests/test_app.py b/tests/test_app.py index fa0db64..b046487 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -9,7 +9,7 @@ import unittest from pathlib import Path -from src.app import build_dynamic_worker_code, get_example, list_examples, render_example_page, render_home, route +from src.app import build_dynamic_worker_code, get_example, list_examples, render_example_page, render_home, render_privacy, route from src.example_loader import EXAMPLES_DIR, load_manifest, verify_example_output from src.asset_manifest import ASSET_PATHS from src.example_sources_data import EXAMPLE_SOURCE_FILES @@ -438,7 +438,11 @@ def test_generated_drift_is_blocked_before_commit_and_merge(self): package = json.loads((ROOT / "package.json").read_text()) lock = json.loads((ROOT / "package-lock.json").read_text()) self.assertEqual(package["devDependencies"]["wrangler"], "4.110.0") + self.assertEqual(package["engines"]["node"], "22.x") self.assertEqual(lock["packages"][""]["devDependencies"]["wrangler"], "4.110.0") + makefile = (ROOT / "Makefile").read_text() + self.assertIn("check-node-version", makefile) + self.assertIn("pywrangler sync --force", makefile) self.assertFalse((ROOT / ".github" / "workflows" / "regenerate-generated-files.yml").exists()) def test_conditionals_are_substantive_not_bare_minimum(self): @@ -798,13 +802,16 @@ def test_card_html_includes_journeys_index(self): self.assertIn("Python learning journeys", cards["journeys"]) self.assertIn("Python By Example · Journeys", cards["journeys"]) - def test_card_html_includes_about_page(self): + def test_card_html_includes_about_and_privacy_pages(self): from scripts.build_social_cards import card_html cards = card_html() self.assertIn("about", cards) self.assertIn("How this site is made", cards["about"]) self.assertIn("Python By Example · About", cards["about"]) + self.assertIn("privacy", cards) + self.assertIn("Privacy and data use", cards["privacy"]) + self.assertIn("Python By Example · Privacy", cards["privacy"]) def test_runtime_journeys_and_edge_labels_load_from_editorial_registry(self): from src import app as app_module @@ -822,6 +829,7 @@ def test_sitemap_lists_every_page_with_canonical_urls(self): self.assertIn('', response.body) self.assertIn("https://www.pythonbyexample.dev/", response.body) self.assertIn("https://www.pythonbyexample.dev/about", response.body) + self.assertIn("https://www.pythonbyexample.dev/privacy", response.body) self.assertIn("https://www.pythonbyexample.dev/journeys", response.body) from src.app import JOURNEYS @@ -831,7 +839,7 @@ def test_sitemap_lists_every_page_with_canonical_urls(self): self.assertIn(f"https://www.pythonbyexample.dev/examples/{example['slug']}", response.body) self.assertEqual( response.body.count(""), - 3 + len(JOURNEYS) + len(list_examples()), + 4 + len(JOURNEYS) + len(list_examples()), ) def test_example_pages_carry_learning_resource_json_ld(self): @@ -942,6 +950,45 @@ def test_about_page_meets_the_seo_linter_bar(self): self.assertRegex(page, r'src="/syntax-highlight\.[0-9a-f]{12}\.js"') +class PrivacyPageTests(unittest.TestCase): + def test_privacy_page_is_routable_and_globally_linked(self): + response = route("https://example.test/privacy") + self.assertEqual(response.status, 200) + page = response.body + self.assertIn("Privacy · Python By Example", page) + self.assertIn('', page) + self.assertIn('', page) + self.assertTrue((ROOT / "public" / "og" / "privacy.jpg").exists()) + for rendered in (render_home(), render_example_page(get_example("hello-world")), page): + self.assertIn('Privacy', rendered) + + def test_privacy_notice_discloses_runner_turnstile_logs_cdn_and_cookie(self): + page = render_privacy() + for required in ( + "Cloudflare Turnstile Privacy Addendum", + "https://www.cloudflare.com/privacypolicy/", + "pbe_turnstile_clearance", + "eight hours", + "submitted Python source", + "esm.sh", + "IP address", + "raw submitted code", + ): + with self.subTest(required=required): + self.assertIn(required, page) + self.assertIn("do not sell", page) + + def test_privacy_page_has_webpage_structured_data_without_editor_assets(self): + page = render_privacy() + match = re.search(r'', page, re.S) + self.assertIsNotNone(match) + data = json.loads(match.group(1)) + self.assertEqual(data["@type"], "WebPage") + self.assertEqual(data["url"], "https://www.pythonbyexample.dev/privacy") + self.assertNotIn("/editor.", page) + self.assertNotIn("search-index", page) + + class CopyButtonTests(unittest.TestCase): def test_syntax_script_injects_copy_buttons_on_source_cells(self): js = (ROOT / "public" / "syntax-highlight.js").read_text() diff --git a/tests/test_main_routes.py b/tests/test_main_routes.py index f83ab61..6d97468 100644 --- a/tests/test_main_routes.py +++ b/tests/test_main_routes.py @@ -40,6 +40,18 @@ def test_about_page_stays_html_through_explicit_and_catch_all_routes(self): self.assertEqual(response.status_code, 200) self.assertIn('rel="canonical" href="https://www.pythonbyexample.dev/about"', response.content) + def test_privacy_page_stays_html_through_explicit_and_catch_all_routes(self): + main = self.import_main() + explicit = asyncio.run(main.privacy_page()) + fallback = asyncio.run(main.not_found("privacy", self.request("https://example.test/privacy"))) + for response in (explicit, fallback): + self.assertTrue(response.headers["Content-Type"].startswith("text/html")) + self.assertEqual(response.status_code, 200) + self.assertIn( + 'rel="canonical" href="https://www.pythonbyexample.dev/privacy"', + response.content, + ) + def test_unknown_paths_fall_back_to_html(self): main = self.import_main() response = asyncio.run(