Skip to content

Commit

Permalink
Merge pull request #570 from TeskaLabs/feature/openapi-class-tags
Browse files Browse the repository at this point in the history
Get OpenAPI route tag form class docstring
  • Loading branch information
byewokko committed Apr 12, 2024
2 parents 3f5cc1c + f1f6293 commit 17ec761
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 47 deletions.
117 changes: 71 additions & 46 deletions asab/api/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,8 @@ def build_swagger_documentation(self, host) -> dict:
}

# Application specification
app_info: dict = self.get_docstring_yaml_dict(app_doc_string)
if app_info is not None:
specification.update(app_info)
app_info: dict = get_docstring_yaml_dict(self.App)
specification.update(app_info)

# Find asab and microservice routes, sort them alphabetically by the first tag
asab_routes = []
Expand Down Expand Up @@ -130,7 +129,7 @@ def parse_route_data(self, route) -> dict:
docstring: str = route.handler.__doc__
docstring_description: str = get_docstring_description(docstring)
docstring_description += "\n\n**Handler:** `{}`".format(handler_name)
docstring_yaml_dict: dict = self.get_docstring_yaml_dict(docstring)
docstring_yaml_dict: dict = get_docstring_yaml_dict(route.handler)

# Create route info dictionary
route_info_data: dict = {
Expand All @@ -153,8 +152,12 @@ def parse_route_data(self, route) -> dict:

# Add default tag if not specified in docstring yaml
if len(route_info_data["tags"]) == 0:
# Use the default one
if self.DefaultRouteTag == "class_name":
# Try to get the tags from class docstring
class_tags = get_class_tags(route)
if class_tags:
route_info_data["tags"] = class_tags[:1]
# Or generate tag from component name
elif self.DefaultRouteTag == "class_name":
route_info_data["tags"] = [get_class_name(route)]
elif self.DefaultRouteTag == "module_name":
route_info_data["tags"] = [get_module_name(route)]
Expand All @@ -168,28 +171,10 @@ def parse_route_data(self, route) -> dict:
return {route_path: {method_name: method_dict}}


def get_docstring_yaml_dict(self, docstring: typing.Optional[str]) -> typing.Optional[dict]:
"""Take the docstring of a function and return additional data if they exist."""

parsed_yaml_docstring_dict: typing.Optional[dict] = None

if docstring is not None:
docstring = inspect.cleandoc(docstring)
dashes_index = docstring.find("\n---\n")
if dashes_index >= 0:
try:
parsed_yaml_docstring_dict = yaml.load(
docstring[dashes_index:], Loader=yaml.SafeLoader
) # everything after --- goes to add_dict
except yaml.YAMLError as e:
L.error(
"Failed to parse '{}' doc string {}".format(
self.App.__class__.__name__, e
))
return parsed_yaml_docstring_dict

def create_security_schemes(self) -> dict:
"""Create security schemes."""
"""
Create security schemes.
"""
security_schemes_dict = {}
if self.AuthorizationUrl and self.TokenUrl:
security_schemes_dict = {
Expand Down Expand Up @@ -217,15 +202,19 @@ def create_security_schemes(self) -> dict:
return security_schemes_dict

def get_version_from_manifest(self) -> dict:
"""Get version from MANIFEST.json if exists."""
"""
Get version from MANIFEST.json if exists.
"""
if self.Manifest:
version = self.Manifest["version"]
else:
version = "unknown"
return version

def get_route_path(self, route) -> str:
"""Take a route and return its path."""
"""
Take a route and return its path.
"""
route_info = route.get_info()
if "path" in route_info:
path = route_info["path"]
Expand Down Expand Up @@ -254,7 +243,7 @@ async def doc(self, request):
title=self.App.__class__.__name__,
swagger_css_url=swagger_css_url,
swagger_js_url=swagger_js_url,
openapi_url="http://{}/asab/v1/openapi".format(base_url),
openapi_url="https://{}/asab/v1/openapi".format(base_url),
)

return aiohttp.web.Response(text=doc_page, content_type="text/html")
Expand Down Expand Up @@ -292,25 +281,28 @@ async def openapi(self, request):


def get_docstring_description(docstring: typing.Optional[str]) -> str:
"""Take the docstring of a function and parse it into description. Omit everything that comes after '---'."""
if docstring is not None:
docstring = inspect.cleandoc(docstring)
dashes_index = docstring.find(
"\n---\n"
) # find the index of the first three dashes

# everything before --- goes to description
if dashes_index >= 0:
description = docstring[:dashes_index]
else:
description = docstring
"""
Take the docstring of a function and parse it into description. Omit everything that comes after '---'.
"""
if docstring is not None:
docstring = inspect.cleandoc(docstring)
dashes_index = docstring.find(
"\n---\n"
) # find the index of the first three dashes

# everything before --- goes to description
if dashes_index >= 0:
description = docstring[:dashes_index]
else:
description = ""
return description
description = docstring
else:
description = ""
return description


def extract_path_parameters(route) -> list:
"""Take a single route and return its parameters.
"""
Take a single route and return its parameters.
"""
parameters: list = []
route_info = route.get_info()
Expand Down Expand Up @@ -342,6 +334,14 @@ def get_class_name(route) -> str:
return class_name


def get_class_tags(route) -> typing.Optional[list]:
if not inspect.ismethod(route.handler):
return None
handler_class = route.handler.__self__.__class__
yaml_dict = get_docstring_yaml_dict(handler_class)
return yaml_dict.get("tags")


def get_module_name(route) -> str:
return str(route.handler.__module__)

Expand All @@ -359,8 +359,33 @@ def get_json_schema(route) -> dict:


def get_first_tag(route_data: dict) -> str:
"""Get tag from route data. Used for sorting tags alphabetically."""
"""
Get tag from route data. Used for sorting tags alphabetically.
"""
for endpoint in route_data.values():
for method in endpoint.values():
if method.get("tags"):
return method.get("tags")[0].lower()


def get_docstring_yaml_dict(component) -> dict:
"""
Inspect the docstring of a component for YAML data and parse it if there is any.
"""
docstring = component.__doc__
parsed_yaml_docstring_dict = {}

if docstring is not None:
docstring = inspect.cleandoc(docstring)
dashes_index = docstring.find("\n---\n")
if dashes_index >= 0:
try:
parsed_yaml_docstring_dict = yaml.load(
docstring[dashes_index:], Loader=yaml.SafeLoader
) # everything after --- goes to add_dict
except yaml.YAMLError as e:
L.error(
"Failed to parse '{}' doc string {}".format(
component.__qualname__, e
))
return parsed_yaml_docstring_dict
2 changes: 1 addition & 1 deletion asab/api/doc_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
<script
id="api-reference"
data-url="{openapi_url}"></script>
<script src="https://www.unpkg.com/@scalar/api-reference"></script>
<script src="https://unpkg.com/@scalar/api-reference"></script>
</body>
</html>
"""

0 comments on commit 17ec761

Please sign in to comment.