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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ Click **Try it out** to execute the request directly from the browser and see th

<img src="docs/photo/swagger_ui_check_execute.png">

Swagger UI also ships with a dark theme (toggle in the top-right corner):

<img src="docs/photo/swagger_dark.png">

Prefer a clean, read-only view? FastHTTP also serves [ReDoc](https://github.com/Redocly/redoc) at `/redoc`:

<img src="docs/photo/redoc.png">

### Upgrade the example

Now modify `main.py` to get more out of FastHTTP. Each upgrade below builds on the previous one.
Expand Down
13 changes: 13 additions & 0 deletions docs/en/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ app.web_run(base_url="/api")

This serves the documentation endpoints at:
- `http://127.0.0.1:8000/api/docs`
- `http://127.0.0.1:8000/api/redoc`
- `http://127.0.0.1:8000/api/openapi.json`
- `http://127.0.0.1:8000/api/request`

Expand All @@ -134,6 +135,18 @@ async def get_data(resp: Response) -> dict:

![Swagger UI Response](../photo/swagger_ui_check_web.png)

### Dark Mode

Swagger UI includes a dark theme toggle in the top-right corner:

![Swagger UI Dark Mode](../photo/swagger_dark.png)

### ReDoc

A clean, read-only alternative view is served at `/redoc`:

![ReDoc](../photo/redoc.png)

### 404 Page

![404 Not Found](../photo/404_not_found.png)
Expand Down
4 changes: 3 additions & 1 deletion docs/en/tutorial/openapi/swagger-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ app.web_run()
After running, open in browser:

- **Swagger UI**: `http://127.0.0.1:8000/docs`
- **ReDoc**: `http://127.0.0.1:8000/redoc`
- **OpenAPI Schema**: `http://127.0.0.1:8000/openapi.json`

## Available Endpoints

| Endpoint | Description |
|----------|-------------|
| `/docs` | Swagger UI interface |
| `/docs` | Swagger UI interface (dark mode toggle in the top-right corner) |
| `/redoc` | ReDoc — clean, read-only alternative view |
| `/openapi.json` | OpenAPI schema in JSON |
| `/request` | Proxy for executing requests |

Expand Down
Binary file added docs/photo/redoc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/photo/swagger_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/ru/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ app.web_run(base_url="/api")

В этом случае endpoints документации будут доступны по адресам:
- `http://127.0.0.1:8000/api/docs`
- `http://127.0.0.1:8000/api/redoc`
- `http://127.0.0.1:8000/api/openapi.json`
- `http://127.0.0.1:8000/api/request`

Expand All @@ -134,6 +135,18 @@ async def get_data(resp: Response) -> dict:

![Swagger UI Response](../photo/swagger_ui_check_web.png)

### Тёмная тема

В Swagger UI есть переключатель тёмной темы в правом верхнем углу:

![Swagger UI Dark Mode](../photo/swagger_dark.png)

### ReDoc

Альтернативный read-only просмотр схемы доступен по адресу `/redoc`:

![ReDoc](../photo/redoc.png)

### Страница 404

![404 Not Found](../photo/404_not_found.png)
Expand Down
12 changes: 7 additions & 5 deletions docs/ru/tutorial/openapi/swagger-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ app.web_run()
После запуска откройте в браузере:

- **Swagger UI**: `http://127.0.0.1:8000/docs`
- **ReDoc**: `http://127.0.0.1:8000/redoc`
- **OpenAPI схема**: `http://127.0.0.1:8000/openapi.json`

## Доступные endpoints

| Endpoint | Описание |
| --------------- | ------------------------------ |
| `/docs` | Интерфейс Swagger UI |
| `/openapi.json` | OpenAPI схема в JSON |
| `/request` | Прокси для выполнения запросов |
| Endpoint | Описание |
| --------------- | ---------------------------------------------- |
| `/docs` | Интерфейс Swagger UI (тёмная тема — переключатель справа сверху) |
| `/redoc` | ReDoc — альтернативный read-only просмотр |
| `/openapi.json` | OpenAPI схема в JSON |
| `/request` | Прокси для выполнения запросов |

## Использование Swagger UI

Expand Down
84 changes: 84 additions & 0 deletions examples/openapi/pydantic_schema_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Demonstrates OpenAPI schema generation built entirely on Pydantic:
enums, nested models, Field descriptions/examples, and error models
are all picked up automatically via `model_json_schema()` — nothing
is hand-mapped.

Run it, then open:
- http://127.0.0.1:8010/docs (Swagger UI — filter box, dark mode toggle,
persisted auth, request snippets)
- http://127.0.0.1:8010/redoc (ReDoc — cleaner read-only view)
- http://127.0.0.1:8010/openapi.json
"""

from enum import Enum

from pydantic import BaseModel, Field

from fasthttp import FastHTTP
from fasthttp.response import Response


class Role(str, Enum):
"""Enum fields resolve to a shared `$ref` + `enum` list, not a plain string."""

admin = "admin"
editor = "editor"
viewer = "viewer"


class Address(BaseModel):
"""Nested models are discovered automatically and added to components/schemas."""

city: str = Field(description="City name", examples=["Berlin"])
zip_code: str = Field(description="Postal code", examples=["10115"])


class CreateUserRequest(BaseModel):
name: str = Field(description="Full name", examples=["Ada Lovelace"])
role: Role = Role.viewer
address: Address


class UserResponse(BaseModel):
id: int
name: str
role: Role
address: Address


class ErrorResponse(BaseModel):
detail: str = Field(description="Human-readable error message")


app = FastHTTP(
title="Pydantic-native OpenAPI demo",
version="1.0.0",
)


@app.post(
url="https://jsonplaceholder.typicode.com/users",
request_model=CreateUserRequest,
response_model=UserResponse,
responses={404: {"model": ErrorResponse}},
tags=["users"],
)
async def create_user(resp: Response) -> UserResponse:
"""Create a user (nested model + enum in both request and response)."""
return resp.json()


@app.get(
url="https://jsonplaceholder.typicode.com/users",
response_model=list[UserResponse],
params={"_limit": 5},
tags=["users"],
)
async def list_users(resp: Response) -> list[UserResponse]:
"""List users — `_limit` query param is pre-filled as an example in Swagger UI."""
return resp.json()


if __name__ == "__main__":
app.web_run(port=8010)
16 changes: 14 additions & 2 deletions fasthttp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
SessionMiddleware,
)
from .openapi.generator import generate_openapi_schema
from .openapi.swagger import get_not_found_html, get_swagger_html
from .openapi.swagger import get_not_found_html, get_redoc_html, get_swagger_html
from .openapi.urls import build_docs_urls
from .routing import Route, Router
from .security import Security
Expand Down Expand Up @@ -1887,8 +1887,14 @@ async def handle_request(
get_swagger_html(
openapi_url=self.docs_urls["openapi_url"],
request_url=self.docs_urls["request_url"],
redoc_url=self.docs_urls["redoc_url"],
),
)
elif path == self.docs_urls["redoc_url"]:
await self._send_html(
send,
get_redoc_html(openapi_url=self.docs_urls["openapi_url"]),
)
elif path == self.docs_urls["openapi_url"]:
if self._openapi_cache is None:
schema = generate_openapi_schema(
Expand Down Expand Up @@ -1942,6 +1948,7 @@ async def _send_404(self, send: Callable[..., Any], _path: str = "/") -> None:
html = get_not_found_html(
docs_url=self.docs_urls["docs_url"],
openapi_url=self.docs_urls["openapi_url"],
redoc_url=self.docs_urls["redoc_url"],
)
await send(
{
Expand Down Expand Up @@ -2015,15 +2022,20 @@ async def _handle_proxy( # noqa: C901
else:
kwargs["content"] = str(req_body)

started_at = time.perf_counter()
if self._shared_client is not None:
response = await self._shared_client.request(**kwargs)
else:
async with httpx.AsyncClient(proxy=self.fasthttp.proxy) as tmp:
response = await tmp.request(**kwargs)
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)

result: dict[str, Any] = {
"status": response.status_code,
"headers": dict(response.headers),
"headers": {
**dict(response.headers),
"x-fasthttp-duration-ms": str(duration_ms),
},
"body": response.text,
}

Expand Down
Loading
Loading