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)