Skip to content

Commit

Permalink
Merge branch 'master' into feature/tenants-ready-status
Browse files Browse the repository at this point in the history
  • Loading branch information
byewokko committed Dec 11, 2024
2 parents fed1ce8 + 91efe34 commit a414fae
Show file tree
Hide file tree
Showing 22 changed files with 1,332 additions and 162 deletions.
4 changes: 1 addition & 3 deletions asab/api/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,7 @@ def get_data(item):

def _on_change_threadsafe(self, watched_event):
# Runs on a thread, returns the process back to the main thread
def _update_cache():
self.App.TaskService.schedule(self._rescan_advertised_instances())
self.App.Loop.call_soon_threadsafe(_update_cache)
self.App.TaskService.schedule_threadsafe(self._rescan_advertised_instances())


def session(
Expand Down
88 changes: 42 additions & 46 deletions asab/api/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .doc_templates import SWAGGER_OAUTH_PAGE, SWAGGER_DOC_PAGE
from ..web.auth import noauth
from ..web.tenant import allow_no_tenant


##
Expand Down Expand Up @@ -38,7 +39,7 @@ def __init__(self, api_service, app, web_container, config_section_name="asab:do

self.Manifest = api_service.Manifest

self.DefaultRouteTag: str = asab.Config["asab:doc"].get("default_route_tag") # default: 'module_name'
self.DefaultRouteTag: str = asab.Config.get("asab:doc", "default_route_tag")
if self.DefaultRouteTag not in ["module_name", "class_name"]:
raise ValueError(
"Unknown default_route_tag: {}. Choose between options "
Expand All @@ -51,13 +52,11 @@ def build_swagger_documentation(self, host) -> dict:
"""
Take a docstring of the class and a docstring of methods and merge them into Swagger data.
"""
app_doc_string: str = self.App.__doc__
app_description: str = get_docstring_description(app_doc_string)
specification: dict = {
"openapi": "3.0.1",
"info": {
"title": "{}".format(self.App.__class__.__name__),
"description": app_description,
"description": get_docstring_description(self.App.__doc__),
"contact": {
"name": "ASAB-based microservice",
"url": "https://www.github.com/teskalabs/asab",
Expand All @@ -77,12 +76,11 @@ def build_swagger_documentation(self, host) -> dict:
}

# Application specification
app_info: dict = get_docstring_yaml_dict(self.App)
specification.update(app_info)
specification.update(get_docstring_yaml(self.App))

# Find asab and microservice routes, sort them alphabetically by the first tag
asab_routes = []
microservice_routes = []
routes = []

for route in self.WebContainer.WebApp.router.routes():
if route.method == "HEAD":
Expand All @@ -91,15 +89,14 @@ def build_swagger_documentation(self, host) -> dict:
continue

# Determine which routes are asab-based
path: str = self.get_route_path(route)
if re.search("asab", path) or re.search("/doc", path) or re.search("/oauth2-redirect.html", path):
if re.search("(asab|doc|oauth2-redirect.html|bspump)", self.get_route_path(route)):
asab_routes.append(self.parse_route_data(route))
else:
microservice_routes.append(self.parse_route_data(route))
routes.append(self.parse_route_data(route))

microservice_routes.sort(key=get_first_tag)
routes.sort(key=get_first_tag)

for endpoint in microservice_routes:
for endpoint in routes:
endpoint_name = list(endpoint.keys())[0]
# if endpoint already exists, then update, else create a new one
spec_endpoint = specification["paths"].get(endpoint_name)
Expand All @@ -122,51 +119,49 @@ def parse_route_data(self, route) -> dict:
"""
Take a route (a single method of an endpoint) and return its description data.
"""
path_parameters: list = extract_path_parameters(route)
handler_name: str = get_handler_name(route)

# Parse docstring description and yaml data
docstring: str = route.handler.__doc__
docstring_description: str = get_docstring_description(docstring)
docstring_description += "\n\n**Handler:** `{}`".format(handler_name)
docstring_yaml_dict: dict = get_docstring_yaml_dict(route.handler)
docstring_description = "{}\n\n**Handler:** `{}`".format(
get_docstring_description(route.handler.__doc__),
get_handler_name(route)
)

# Create route info dictionary
route_info_data: dict = {
"summary": docstring_description.split("\n")[0],
"description": docstring_description,
"responses": {"200": {"description": "Success"}},
route_data: dict = {
"summary": docstring_description.split("\n", 1)[0],
"description": docstring_description.split("\n", 1)[1],
"responses": {"200": {"description": "Success."}},
"parameters": [],
"tags": []
}

# Update it with parsed YAML and add query parameters
if docstring_yaml_dict is not None:
route_info_data.update(docstring_yaml_dict)
for query_parameter in docstring_yaml_dict.get("parameters", []):
if query_parameter.get("parameters"):
route_info_data["parameters"].append(query_parameter["parameters"])
docstring_yaml: dict = get_docstring_yaml(route.handler)
if docstring_yaml is not None:
route_data.update(docstring_yaml)

for path_parameter in path_parameters:
route_info_data["parameters"].append(path_parameter)
# Find all path parameters, add them if not specified in docstring.
for path_parameter in get_path_parameters(route):
if path_parameter.get("name") is not None:
if path_parameter["name"] not in [r["name"] for r in route_data["parameters"]]:
route_data["parameters"].append(path_parameter)

# Add default tag if not specified in docstring yaml
if len(route_info_data["tags"]) == 0:
if len(route_data["tags"]) == 0:
# Try to get the tags from class docstring
class_tags = get_class_tags(route)
if class_tags:
route_info_data["tags"] = class_tags[:1]
route_data["tags"] = class_tags[:1]
# Or generate tag from component name
elif self.DefaultRouteTag == "class_name":
route_info_data["tags"] = [get_class_name(route)]
route_data["tags"] = [get_class_name(route)]
elif self.DefaultRouteTag == "module_name":
route_info_data["tags"] = [get_module_name(route)]
route_data["tags"] = [get_module_name(route)]

# Create the route dictionary
route_path: str = self.get_route_path(route)
method_name: str = route.method.lower()
method_dict: dict = get_json_schema(route)
method_dict.update(route_info_data)
method_dict.update(route_data)

return {route_path: {method_name: method_dict}}

Expand Down Expand Up @@ -201,7 +196,7 @@ def create_security_schemes(self) -> dict:
)
return security_schemes_dict

def get_version_from_manifest(self) -> dict:
def get_version_from_manifest(self) -> str:
"""
Get version from MANIFEST.json if exists.
"""
Expand All @@ -226,6 +221,7 @@ def get_route_path(self, route) -> str:


@noauth
@allow_no_tenant
# This is the web request handler
async def doc(self, request):
"""
Expand All @@ -250,6 +246,7 @@ async def doc(self, request):


@noauth
@allow_no_tenant
async def oauth2_redirect(self, request):
"""
Required for the authorization to work.
Expand All @@ -261,6 +258,7 @@ async def oauth2_redirect(self, request):


@noauth
@allow_no_tenant
async def openapi(self, request):
"""
Download OpenAPI (version 3) API documentation (aka Swagger) in YAML.
Expand Down Expand Up @@ -300,20 +298,20 @@ def get_docstring_description(docstring: typing.Optional[str]) -> str:
return description


def extract_path_parameters(route) -> list:
def get_path_parameters(route) -> list:
"""
Take a single route and return its parameters.
Return path parameters of a route.
"""
parameters: list = []
route_info = route.get_info()
if "formatter" in route_info:
path = route_info["formatter"]
for params in re.findall(r'\{[^\}]+\}', path):
parameters.append({
'in': 'path',
'name': params[1:-1],
'required': True,
'schema': {'type': 'string'},
"in": "path",
"name": params[1:-1],
"required": True,
"schema": {"type": "string"},
})
return parameters

Expand All @@ -337,9 +335,7 @@ def get_class_name(route) -> str:
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")
return get_docstring_yaml(route.handler.__self__.__class__).get("tags")


def get_module_name(route) -> str:
Expand Down Expand Up @@ -368,7 +364,7 @@ def get_first_tag(route_data: dict) -> str:
return method.get("tags")[0].lower()


def get_docstring_yaml_dict(component) -> dict:
def get_docstring_yaml(component) -> dict:
"""
Inspect the docstring of a component for YAML data and parse it if there is any.
"""
Expand Down
46 changes: 44 additions & 2 deletions asab/api/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..web.rest.json import json_response
from ..log import LOG_NOTICE
from ..web.auth import noauth
from ..web.tenant import allow_no_tenant

##

Expand Down Expand Up @@ -86,17 +87,58 @@ def emit(self, record):


@noauth
@allow_no_tenant
async def get_logs(self, request):
'''
"""
Get logs.
---
tags: ['asab.log']
'''
responses:
"200":
description: Logs.
content:
application/json:
schema:
type: array
items:
type: object
properties:
t:
type: string
description: Time when the log was emitted.
example: 2024-12-10T14:28:53.421079Z
C:
type: string
description: Class that produced the log.
example: myapp.myservice.service
M:
type: string
description: Log message.
example: Periodic check finished.
l:
type: int
example: 6
oneOf:
- title: Alert
const: 1
- title: Critical
const: 2
- title: Error
const: 3
- title: Warning
const: 4
- title: Notice
const: 5
- title: Info
const: 6
"""

return json_response(request, self.Buffer)


@noauth
@allow_no_tenant
async def ws(self, request):
'''
# Live feed of logs over websocket
Expand Down
Loading

0 comments on commit a414fae

Please sign in to comment.