pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
105 lines
3.3 KiB
105 lines
3.3 KiB
from collections.abc import Sequence
|
|
from typing import Any
|
|
|
|
from fastapi import routing
|
|
from fastapi.encoders import jsonable_encoder
|
|
from starlette.routing import BaseRoute
|
|
|
|
|
|
def get_asyncapi_channel(
|
|
*,
|
|
route: routing.APIWebSocketRoute,
|
|
) -> dict[str, Any]:
|
|
"""Generate AsyncAPI channel definition for a WebSocket route."""
|
|
channel: dict[str, Any] = {}
|
|
|
|
# WebSocket channels typically have subscribe operation
|
|
# (client subscribes to receive messages from server)
|
|
operation: dict[str, Any] = {
|
|
"operationId": route.name or f"websocket_{route.path_format}",
|
|
}
|
|
|
|
# Basic message schema - can be enhanced later with actual message types
|
|
# For WebSockets, messages can be sent in both directions
|
|
message: dict[str, Any] = {
|
|
"contentType": "application/json",
|
|
}
|
|
|
|
operation["message"] = message
|
|
channel["subscribe"] = operation
|
|
|
|
# WebSockets are bidirectional, so we also include publish
|
|
# (client can publish messages to server)
|
|
publish_operation: dict[str, Any] = {
|
|
"operationId": f"{route.name or f'websocket_{route.path_format}'}_publish",
|
|
"message": message,
|
|
}
|
|
channel["publish"] = publish_operation
|
|
|
|
return channel
|
|
|
|
|
|
def get_asyncapi(
|
|
*,
|
|
title: str,
|
|
version: str,
|
|
asyncapi_version: str = "2.6.0",
|
|
summary: str | None = None,
|
|
description: str | None = None,
|
|
routes: Sequence[BaseRoute],
|
|
servers: list[dict[str, str | Any]] | None = None,
|
|
terms_of_service: str | None = None,
|
|
contact: dict[str, str | Any] | None = None,
|
|
license_info: dict[str, str | Any] | None = None,
|
|
external_docs: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Generate AsyncAPI schema from FastAPI application routes.
|
|
|
|
Filters for WebSocket routes and generates AsyncAPI 2.6.0 compliant schema.
|
|
"""
|
|
info: dict[str, Any] = {"title": title, "version": version}
|
|
if summary:
|
|
info["summary"] = summary
|
|
if description:
|
|
info["description"] = description
|
|
if terms_of_service:
|
|
info["termsOfService"] = terms_of_service
|
|
if contact:
|
|
info["contact"] = contact
|
|
if license_info:
|
|
info["license"] = license_info
|
|
|
|
output: dict[str, Any] = {"asyncapi": asyncapi_version, "info": info}
|
|
|
|
# Add default WebSocket server if no servers provided and we have WebSocket routes
|
|
websocket_routes = [
|
|
route for route in routes or [] if isinstance(route, routing.APIWebSocketRoute)
|
|
]
|
|
if websocket_routes and not servers:
|
|
# Default WebSocket server - can be overridden by providing servers parameter
|
|
output["servers"] = [
|
|
{
|
|
"url": "ws://localhost:8000",
|
|
"protocol": "ws",
|
|
"description": "WebSocket server",
|
|
}
|
|
]
|
|
elif servers:
|
|
output["servers"] = servers
|
|
|
|
channels: dict[str, dict[str, Any]] = {}
|
|
|
|
# Filter routes to only include WebSocket routes
|
|
for route in routes or []:
|
|
if isinstance(route, routing.APIWebSocketRoute):
|
|
channel = get_asyncapi_channel(route=route)
|
|
if channel:
|
|
channels[route.path_format] = channel
|
|
|
|
output["channels"] = channels
|
|
|
|
if external_docs:
|
|
output["externalDocs"] = external_docs
|
|
|
|
return jsonable_encoder(output, by_alias=True, exclude_none=True) # type: ignore
|
|
|