diff --git a/parrant/lineage/display/html/explore.py b/parrant/lineage/display/html/explore.py index 628b485..5e0df9d 100644 --- a/parrant/lineage/display/html/explore.py +++ b/parrant/lineage/display/html/explore.py @@ -258,8 +258,9 @@ def _setup_templates_and_routes(self) -> None: @self.app.get("/", response_class=HTMLResponse) async def home(request: Request) -> Any: return self.templates.TemplateResponse( - "graph.html", - {"request": request, "data": GraphData().model_dump(), "explore_mode": True}, + request=request, + name="graph.html", + context={"data": GraphData().model_dump(), "explore_mode": True}, ) @self.app.get("/api/graph") diff --git a/poetry.lock b/poetry.lock index 6303b22..512fd27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -54,7 +54,7 @@ version = "4.12.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, @@ -739,12 +739,59 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "idna" version = "3.19" @@ -2567,4 +2614,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "e4461a62f7eae1e2f5113600bf13d160740ab7b74bb89760f60fe97972cb4f5b" +content-hash = "741e87981e54553c2c74546cdadb50c91eb422cc3c0c8f10d38d468b8e8558cc" diff --git a/pyproject.toml b/pyproject.toml index 2b3b74d..9693acf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,9 +49,18 @@ mkdocs-click = ">=0.8.1,<0.9.0" requests = ">=2.32.0,<3.0.0" types-requests = ">=2.32.0,<3.0.0" types-pyyaml = "^6.0.12.20260815" +httpx = ">=0.27.0,<1.0.0" [tool.pytest.ini_options] pythonpath = ["."] +# Fail tests on any deprecation (e.g. starlette's old TemplateResponse convention, removed +# in 1.6 — issue #139), instead of warning silently until the upstream removal ships to end +# users. If a third-party-internal deprecation we can't fix starts breaking CI, add a scoped +# "ignore:::" entry below the error rules rather than loosening them. +filterwarnings = [ + "error::DeprecationWarning", + "error::PendingDeprecationWarning", +] [tool.black] line-length = 100 diff --git a/tests/e2e/test_lineage_api.py b/tests/e2e/test_lineage_api.py index 558720d..bb91b74 100644 --- a/tests/e2e/test_lineage_api.py +++ b/tests/e2e/test_lineage_api.py @@ -328,6 +328,21 @@ def collect_resource_types(nodes: list) -> None: pytest.skip(f"Could not test snapshot lineage endpoint: {e}") +def test_home_page_renders(dbt_artifacts: Dict[str, Any], server_port: int) -> None: + """The root page must render — it is the only template-rendering route, so API-only + coverage misses it entirely (issue #139 shipped a server that answered every /api/* + call but 500'd on the page users actually open).""" + catalog_path = Path(dbt_artifacts["catalog_path"]) + manifest_path = Path(dbt_artifacts["manifest_path"]) + + with lineage_server(catalog_path, manifest_path, server_port) as port: + response = requests.get(f"http://127.0.0.1:{port}/", timeout=10) + + assert response.status_code == 200, f"GET / returned {response.status_code}" + assert "text/html" in response.headers.get("content-type", "") + assert "Parrant" in response.text + + def test_coverage_endpoint(dbt_artifacts: Dict[str, Any], server_port: int) -> None: """Verify /api/coverage exposes the artifact coverage block to the explorer UI.""" catalog_path = Path(dbt_artifacts["catalog_path"]) diff --git a/tests/integration/test_lineage_explorer_integration.py b/tests/integration/test_lineage_explorer_integration.py index a72b8c6..aad1895 100644 --- a/tests/integration/test_lineage_explorer_integration.py +++ b/tests/integration/test_lineage_explorer_integration.py @@ -1,4 +1,5 @@ import pytest +from fastapi.testclient import TestClient from parrant.lineage.display.html.explore import LineageExplorer from parrant.artifacts.registry import ModelRegistry from parrant.lineage.service import LineageService @@ -64,6 +65,27 @@ def test_html_display_nodes(lineage_service, registry): ), f"Starting model '{start_model_name}' not found in graph nodes" +def test_home_route_renders_explorer_page(lineage_service): + """GET / — the only route that renders a Jinja2 template — returns the explorer page. + + Guards the ``TemplateResponse`` calling convention: starlette 1.6 removed the legacy + ``(name, context-with-request)`` shim, turning every page load into + ``TypeError: unhashable type: 'dict'`` (issue #139). Runs in-process so the + DeprecationWarning-as-error filter in pyproject.toml catches a deprecated calling + style while it is still just a warning on older starlette.""" + explorer = LineageExplorer(host="127.0.0.1", port=8000) + explorer.set_lineage_service(lineage_service) + + with TestClient(explorer.app) as client: + response = client.get("/") + + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/html") + assert "Parrant" in response.text + # explore_mode context reached the template + assert "explore-panel" in response.text + + def test_lineage_includes_impact_summary(lineage_service): """Test that the lineage endpoint includes impact_summary in the response.""" lineage_explorer = LineageExplorer(host="127.0.0.1", port=8000)