Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,15 +304,15 @@ small components with constant args can be inlined. Use

```python
# Flask
from kida.contrib.flask import KidaFlask
kida = KidaFlask(app)
from kida.contrib.flask import init_kida, render_template
kida_env = init_kida(app)
Comment on lines 305 to +308

# Starlette / FastAPI
from kida.contrib.starlette import KidaStarlette
templates = KidaStarlette(directory="templates")
from kida.contrib.starlette import KidaTemplates
templates = KidaTemplates(directory="templates")

# Django
TEMPLATES = [{"BACKEND": "kida.contrib.django.KidaDjango", ...}]
TEMPLATES = [{"BACKEND": "kida.contrib.django.KidaTemplates", ...}]
```

</details>
Expand Down
2 changes: 2 additions & 0 deletions changelog.d/156.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added current-version Flask, Django, and FastAPI integration guides and
smoke-tested component examples covering full-page and fragment rendering.
35 changes: 32 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,43 @@ buttons inside cards, cards inside pages.
cd examples/design_system && python app.py
```

### `flask_components/` -- Typed Components in Flask

A real Flask 3.1 app using `kida.contrib.flask`: a typed form component on the
full-page route and `render_block()` for a POST fragment response. Includes a
non-network smoke path used by CI.

```bash
uv run python examples/flask_components/app.py --smoke
```

### `django_components/` -- Typed Components in Django

A minimal Django 6.0 app registering `kida.contrib.django.KidaTemplates` through
the standard `TEMPLATES` setting. The GET route uses `django.shortcuts.render`;
the POST route returns a Kida block fragment.

```bash
uv run python examples/django_components/app.py --smoke
```

### `fastapi_components/` -- Typed Components in FastAPI

A FastAPI 0.139 / Starlette 1.3 app using `KidaTemplates.TemplateResponse()` and
an ASGI-tested POST fragment route, without requiring multipart parsing.

```bash
uv run python examples/fastapi_components/app.py --smoke
```

### `fastapi_async/` -- FastAPI Integration

`render_stream_async()` with FastAPI's `StreamingResponse` for true streaming HTML
delivery. Templates with `{% async for %}` consume async data sources while the
response streams to the client.
delivery on Python 3.14+. Templates with `{% async for %}` consume async data
sources while the response streams to the client.

```bash
pip install fastapi uvicorn
uv add fastapi uvicorn kida-templates
cd examples/fastapi_async && uvicorn app:app --reload
```

Expand Down
18 changes: 18 additions & 0 deletions examples/django_components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Django Components

Add typed Kida components to an existing Django app on Python 3.14+:

```bash
uv add django kida-templates
uv run python app.py
```

Open <http://127.0.0.1:8000>. The `TEMPLATES` setting registers Kida as a
normal Django backend; the GET route uses `django.shortcuts.render`, and the
POST route renders only the `preview` block.

Run the non-network smoke path with:

```bash
uv run python app.py --smoke
```
88 changes: 88 additions & 0 deletions examples/django_components/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Typed Kida components inside an existing Django app."""

from __future__ import annotations

import sys
from pathlib import Path

import django
from django.conf import settings
from django.core.management import execute_from_command_line
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.template import engines
from django.test import Client
from django.urls import path

TEMPLATES_DIR = Path(__file__).parent / "templates"

if not settings.configured:
settings.configure(
ALLOWED_HOSTS=["localhost", "testserver"],
DEBUG=True,
MIDDLEWARE=[],
ROOT_URLCONF=__name__,
SECRET_KEY="kida-example",
TEMPLATES=[
{
"BACKEND": "kida.contrib.django.KidaTemplates",
"DIRS": [TEMPLATES_DIR],
"NAME": "kida",
"OPTIONS": {"autoescape": True},
}
],
)

django.setup()


def home(request: HttpRequest) -> HttpResponse:
"""Render the full page through Django's configured template backend."""
return render(
request,
"components.html",
{"title": "First component", "summary": "Edit me"},
)


def preview(request: HttpRequest) -> HttpResponse:
"""Return only the preview block through the configured Kida backend."""
template = engines["kida"].env.get_template("components.html")
html = template.render_block(
"preview",
title=request.POST.get("title", ""),
summary=request.POST.get("summary", ""),
)
return HttpResponse(html)


urlpatterns = [
path("", home),
path("preview", preview),
]


def smoke() -> None:
"""Exercise both routes with Django's real test client."""
client = Client()

page = client.get("/")
assert page.status_code == 200
assert "Component form" in page.content.decode()

fragment = client.post(
"/preview",
{"title": "<Admin>", "summary": "Static validation"},
)
fragment_html = fragment.content.decode()
assert fragment.status_code == 200
assert "&lt;Admin&gt;" in fragment_html
assert "<form" not in fragment_html
print("django_components OK")


if __name__ == "__main__":
if "--smoke" in sys.argv:
smoke()
else:
execute_from_command_line([sys.argv[0], "runserver", "127.0.0.1:8000"])
33 changes: 33 additions & 0 deletions examples/django_components/templates/components.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% def panel(title: str) %}
<section class="panel">
<h1>{{ title }}</h1>
{% slot %}
</section>
{% enddef %}

{% def text_field(name: str, label: str, value: str = "") %}
<label>
{{ label }}
<input name="{{ name }}" value="{{ value }}">
</label>
{% enddef %}

<!doctype html>
<title>Kida Components</title>

{% block form %}
{% call panel("Component form") %}
<form method="post" action="/preview">
{{ text_field("title", "Title", title) }}
{{ text_field("summary", "Summary", summary) }}
<button type="submit">Preview</button>
</form>
{% endcall %}
{% endblock %}

{% block preview %}
<article id="preview">
<h2>{{ title }}</h2>
<p>{{ summary }}</p>
</article>
{% endblock %}
7 changes: 4 additions & 3 deletions examples/fastapi_async/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# FastAPI Integration

`render_stream_async()` with FastAPI's `StreamingResponse` for true streaming HTML delivery.
`render_stream_async()` with FastAPI's `StreamingResponse` for true streaming
HTML delivery on Python 3.14+.

## Run

```bash
pip install fastapi uvicorn
uv add fastapi uvicorn kida-templates
cd examples/fastapi_async && uvicorn app:app --reload
```

## Test

```bash
pytest examples/fastapi_async/ -v
uv run pytest examples/fastapi_async/ -v
```

## What It Shows
Expand Down
2 changes: 1 addition & 1 deletion examples/fastapi_async/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async def full() -> StreamingResponse:

def main() -> None:
if fastapi is None:
print("FastAPI not installed. Install with: pip install fastapi uvicorn")
print("FastAPI not installed. Install with: uv add fastapi uvicorn kida-templates")
return
print("Run with: uvicorn app:app --reload")
print("Endpoints:")
Expand Down
46 changes: 28 additions & 18 deletions examples/fastapi_async/test_fastapi_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,56 @@
Skips gracefully if fastapi or httpx are not installed.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

fastapi = pytest.importorskip("fastapi")
httpx = pytest.importorskip("httpx")
if TYPE_CHECKING:
from collections.abc import AsyncIterator

from starlette.testclient import TestClient # noqa: E402
pytest.importorskip("fastapi")
httpx = pytest.importorskip("httpx")


class TestFastApiAsyncApp:
"""Verify FastAPI streaming integration with kida."""

@pytest.fixture
def client(self, example_app) -> TestClient:
"""Create a test client from the example FastAPI app."""
return TestClient(example_app.app)

def test_streaming_endpoint_returns_html(self, client: TestClient) -> None:
response = client.get("/")
async def client(self, example_app) -> AsyncIterator[httpx.AsyncClient]:
"""Create an HTTPX client over the example's ASGI transport."""
transport = httpx.ASGITransport(app=example_app.app)
async with httpx.AsyncClient(
transport=transport,
base_url="http://testserver",
) as client:
yield client

async def test_streaming_endpoint_returns_html(self, client: httpx.AsyncClient) -> None:
response = await client.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]

def test_streaming_endpoint_has_content(self, client: TestClient) -> None:
response = client.get("/")
async def test_streaming_endpoint_has_content(self, client: httpx.AsyncClient) -> None:
response = await client.get("/")
assert "Dashboard" in response.text
assert "Revenue" in response.text
assert "$1.2M" in response.text

def test_streaming_endpoint_has_all_items(self, client: TestClient) -> None:
response = client.get("/")
async def test_streaming_endpoint_has_all_items(self, client: httpx.AsyncClient) -> None:
response = await client.get("/")
assert "Users" in response.text
assert "Orders" in response.text

def test_full_endpoint_returns_html(self, client: TestClient) -> None:
response = client.get("/full")
async def test_full_endpoint_returns_html(self, client: httpx.AsyncClient) -> None:
response = await client.get("/full")
assert response.status_code == 200
assert "Dashboard" in response.text

def test_both_endpoints_produce_same_content(self, client: TestClient) -> None:
streaming = client.get("/")
full = client.get("/full")
async def test_both_endpoints_produce_same_content(self, client: httpx.AsyncClient) -> None:
streaming = await client.get("/")
full = await client.get("/full")
# Both should contain the same data items
for item in ["Revenue", "Users", "Orders"]:
assert item in streaming.text
Expand Down
18 changes: 18 additions & 0 deletions examples/fastapi_components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# FastAPI Components

Add typed Kida components to an existing FastAPI app on Python 3.14+:

```bash
uv add fastapi uvicorn kida-templates
uv run python app.py
```

Open <http://127.0.0.1:8000>. The GET route uses Kida's
`TemplateResponse()` adapter; the POST route renders only the `preview` block
and returns an `HTMLResponse`.

Run the non-network smoke path with:

```bash
uv run python app.py --smoke
```
Loading
Loading