-
Notifications
You must be signed in to change notification settings - Fork 4
Implement AsyncAPI documentation #244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
0bc282c
Add AsyncAPI router and update dependencies
febus982 04d3d91
Create stub HTML rendered AsyncAPI docs
febus982 bc1d4d4
Add example websocket endpoint
febus982 d56ea39
Example rendering AsyncAPI from pydantic models and mapping
febus982 3ba9523
Stub router class to attempt auto-asyncapi-registration
febus982 53b53fc
Add TODO for overlapping models
febus982 aebbfb5
Batch-create json schemas
febus982 b7d99e7
Restructure registry
febus982 271ac7e
Create global registry with a decorator to register channels and func…
febus982 8109c87
Update dependencies
febus982 6a0dd9b
Change decorator implementation to functions
febus982 b87baad
Update line-length to 120 and update dependencies
febus982 1db967c
Fix edge case for messages without $defs
febus982 3679c94
Code style
febus982 7f7211e
Get application name from config, rename config dependency
febus982 d49d2cc
Add tests
febus982 fdcc28f
Include docs in schema
febus982 72ed197
Reduce cognitive complexity
febus982 6199961
Add details on documentation for AsyncAPI docs
febus982 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
authors = [ | ||
{name = "Federico Busetti", email = "[email protected]"}, | ||
] | ||
requires-python = "<3.14,>=3.9" | ||
requires-python = "<3.14,>=3.10" | ||
name = "bootstrap-fastapi-service" | ||
version = "0.1.0" | ||
description = "" | ||
|
@@ -14,7 +14,7 @@ dependencies = [ | |
"cloudevents-pydantic<1.0.0,>=0.0.3", | ||
"dependency-injector[pydantic]<5.0.0,>=4.41.0", | ||
"dramatiq[redis,watch]<2.0.0,>=1.17.1", | ||
"hiredis<4.0.0,>=3.1.0", # Recommended by dramatiq | ||
"hiredis<4.0.0,>=3.1.0", # Recommended by dramatiq | ||
"httpx>=0.23.0", | ||
"opentelemetry-distro[otlp]", | ||
"opentelemetry-instrumentation", | ||
|
@@ -28,6 +28,7 @@ dependencies = [ | |
"SQLAlchemy[asyncio,mypy]<3.0.0,>=2.0.0", | ||
"sqlalchemy-bind-manager", | ||
"structlog<25.1.1,>=25.1.0", | ||
"pydantic-asyncapi>=0.2.1", | ||
] | ||
|
||
[dependency-groups] | ||
|
@@ -115,6 +116,7 @@ testpaths = [ | |
|
||
[tool.ruff] | ||
target-version = "py39" | ||
line-length = 120 | ||
extend-exclude = [ | ||
"docs", | ||
] | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
from typing import List, Literal, Optional, Type | ||
|
||
import pydantic_asyncapi.v3 as pa | ||
from pydantic import BaseModel | ||
|
||
_info: pa.Info = pa.Info( | ||
title="AsyncAPI", | ||
version="1.0.0", | ||
) | ||
|
||
|
||
_servers = {} # type: ignore | ||
_channels = {} # type: ignore | ||
_operations = {} # type: ignore | ||
_components_schemas = {} # type: ignore | ||
|
||
|
||
def get_schema() -> pa.AsyncAPI: | ||
""" | ||
Function `get_schema` provides the complete AsyncAPI schema for the application, complying with | ||
version 3.0.0 of the AsyncAPI specification. It includes detailed information about info metadata, | ||
components, servers, channels, and operations required to set up and describe the asynchronous | ||
communication layer. | ||
|
||
Returns: | ||
pa.AsyncAPI: A fully constructed AsyncAPI schema object based on predefined configurations. | ||
""" | ||
return pa.AsyncAPI( | ||
asyncapi="3.0.0", | ||
info=_info, | ||
components=pa.Components( | ||
schemas=_components_schemas, | ||
), | ||
servers=_servers, | ||
channels=_channels, | ||
operations=_operations, | ||
) | ||
|
||
|
||
def init_asyncapi_info( | ||
title: str, | ||
version: str = "1.0.0", | ||
) -> None: | ||
""" | ||
Initializes the AsyncAPI information object with the specified title and version. | ||
|
||
This function creates and initializes an AsyncAPI Info object, which includes | ||
mandatory fields such as title and version. The title represents the name of the | ||
AsyncAPI document, and the version represents the version of the API. | ||
|
||
Parameters: | ||
title (str): The title of the AsyncAPI document. | ||
version (str): The version of the AsyncAPI document. Defaults to "1.0.0". | ||
|
||
Returns: | ||
None | ||
""" | ||
# We can potentially add the other info supported by pa.Info | ||
global _info | ||
_info = pa.Info( | ||
title=title, | ||
version=version, | ||
) | ||
|
||
|
||
def register_server( | ||
id: str, | ||
host: str, | ||
protocol: str, | ||
pathname: Optional[str] = None, | ||
) -> None: | ||
""" | ||
Registers a server with a unique identifier and its associated properties. | ||
This function accepts information about the server such as its host, | ||
protocol, and optionally its pathname, and stores it in the internal | ||
server registry identified by the unique ID. The parameters must be | ||
provided appropriately for proper registration. The server registry | ||
ensures that server configurations can be retrieved and managed based | ||
on the assigned identifier. | ||
|
||
Args: | ||
id: str | ||
A unique identifier for the server being registered. It is used | ||
as the key in the internal server registry. | ||
host: str | ||
The host address of the server. This may be an IP address or | ||
a domain name. | ||
protocol: str | ||
Communication protocol used by the server, such as "http" or "https". | ||
pathname: Optional[str] | ||
The optional pathname of the server. If provided, it will be | ||
associated with the registered server. | ||
|
||
Returns: | ||
None | ||
This function does not return a value. It modifies the internal | ||
server registry to include the provided server details. | ||
""" | ||
# TODO: Implement other server parameters | ||
_servers[id] = pa.Server( | ||
host=host, | ||
protocol=protocol, | ||
) | ||
if pathname is not None: | ||
_servers[id].pathname = pathname | ||
|
||
|
||
def _create_base_channel(address: str, channel_id: str) -> pa.Channel: | ||
"""Create a basic channel with minimum required parameters.""" | ||
return pa.Channel( | ||
address=address, | ||
servers=[], | ||
messages={}, | ||
) | ||
|
||
|
||
def _add_channel_metadata(channel: pa.Channel, description: Optional[str], title: Optional[str]) -> None: | ||
"""Add optional metadata to the channel.""" | ||
if description is not None: | ||
channel.description = description | ||
if title is not None: | ||
channel.title = title | ||
|
||
|
||
def _add_server_reference(channel: pa.Channel, server_id: Optional[str]) -> None: | ||
"""Add server reference to the channel if server exists.""" | ||
if server_id is not None and server_id in _servers: | ||
channel.servers.append(pa.Reference(ref=f"#/servers/{server_id}")) # type: ignore | ||
|
||
|
||
def register_channel( | ||
febus982 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
address: str, | ||
id: Optional[str] = None, | ||
description: Optional[str] = None, | ||
title: Optional[str] = None, | ||
server_id: Optional[str] = None, | ||
) -> None: | ||
""" | ||
Registers a communication channel with the specified parameters. | ||
|
||
Args: | ||
address (str): The address of the channel. | ||
id (Optional[str]): Unique identifier for the channel. Defaults to None. | ||
description (Optional[str]): Description of the channel. Defaults to None. | ||
title (Optional[str]): Title to be associated with the channel. Defaults to None. | ||
server_id (Optional[str]): Server identifier to link this channel to. Defaults to None. | ||
|
||
Returns: | ||
None | ||
""" | ||
channel_id = id or address | ||
channel = _create_base_channel(address, channel_id) | ||
_add_channel_metadata(channel, description, title) | ||
_add_server_reference(channel, server_id) | ||
_channels[channel_id] = channel | ||
|
||
|
||
def _register_message_schema(message: Type[BaseModel], operation_type: Literal["receive", "send"]) -> None: | ||
"""Register message schema in components schemas.""" | ||
message_json_schema = message.model_json_schema( | ||
mode="validation" if operation_type == "receive" else "serialization", | ||
ref_template="#/components/schemas/{model}", | ||
) | ||
|
||
_components_schemas[message.__name__] = message_json_schema | ||
|
||
if message_json_schema.get("$defs"): | ||
_components_schemas.update(message_json_schema["$defs"]) | ||
|
||
|
||
def _create_channel_message(channel_id: str, message: Type[BaseModel]) -> pa.Reference: | ||
"""Create channel message and return reference to it.""" | ||
_channels[channel_id].messages[message.__name__] = pa.Message( # type: ignore | ||
payload=pa.Reference(ref=f"#/components/schemas/{message.__name__}") | ||
) | ||
return pa.Reference(ref=f"#/channels/{channel_id}/messages/{message.__name__}") | ||
|
||
|
||
def register_channel_operation( | ||
febus982 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
channel_id: str, | ||
operation_type: Literal["receive", "send"], | ||
messages: List[Type[BaseModel]], | ||
operation_name: Optional[str] = None, | ||
) -> None: | ||
""" | ||
Registerm a channel operation with associated messages. | ||
|
||
Args: | ||
channel_id: Channel identifier | ||
operation_type: Type of operation ("receive" or "send") | ||
messages: List of message models | ||
operation_name: Optional operation name | ||
|
||
Raises: | ||
ValueError: If channel_id doesn't exist | ||
""" | ||
if not _channels.get(channel_id): | ||
raise ValueError(f"Channel {channel_id} does not exist.") | ||
|
||
operation_message_refs = [] | ||
|
||
for message in messages: | ||
_register_message_schema(message, operation_type) | ||
message_ref = _create_channel_message(channel_id, message) | ||
operation_message_refs.append(message_ref) | ||
|
||
operation_id = operation_name or f"{channel_id}-{operation_type}" | ||
_operations[operation_id] = pa.Operation( | ||
action=operation_type, | ||
channel=pa.Reference(ref=f"#/channels/{channel_id}"), | ||
messages=operation_message_refs, | ||
traits=[], | ||
) | ||
|
||
# TODO: Define operation traits | ||
# if operation_name is not None: | ||
# _operations[operation_name or f"{channel_id}-{operation_type}"].traits.append( | ||
# pa.OperationTrait( | ||
# title=operation_name, | ||
# summary=f"{operation_name} operation summary", | ||
# description=f"{operation_name} operation description", | ||
# ) | ||
# ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.