Skip to content

Commit

Permalink
refactor: openApi made lazy. (sparckles#1066)
Browse files Browse the repository at this point in the history
* refactor: openApi made lazy. This is a staging post so there are extra tests (in all the decorators). Fixes a potential issue in __init__ where the default argument creates a shared object at parse time rather than a separate one for each instance. As only one instance, not normally an issue but not good practice.
One FIXME is an edge case in init_openapi where neither if clause evaluates to True for openapi_file_path (should we log an error or what?)

* refactor: openApi made lazy.
All add_route methods have auth_required, openapi_name and openapi_tags which get stored in the route.
Instead of incrementally adding them to openapi routes they are added all at once in app.start
include_routes now additionally tracks the list of routers whose routes have been added to the main app. This will be used more later to avoid merging routes until app.start for more flexibility

* refactor: try to solve the speed issue by improving handling moving from allowing None for openApi_tags to not

* refactor: slightly optimised lower_http_method I've tested with 100,000 loops. This is marginally faster.
This PR lowers slows full test suite by 0.004s on my system. However, those benchmarks are mostly setup. Once we are making calls to a running server none of this code is being executed.

* refactor: remove extra Robyn instance variable to avoid extra side effects in the future. No consistent peformance impact on 100,000 Robyn(__file__) calls
  • Loading branch information
dave42w authored Dec 31, 2024
1 parent 8aa97ea commit 2aad774
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 35 deletions.
77 changes: 45 additions & 32 deletions robyn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(
file_object: str,
config: Config = Config(),
openapi_file_path: Optional[str] = None,
openapi: OpenAPI = OpenAPI(),
openapi: Optional[OpenAPI] = None,
dependencies: DependencyMap = DependencyMap(),
) -> None:
directory_path = os.path.dirname(os.path.abspath(file_object))
Expand All @@ -52,10 +52,7 @@ def __init__(
self.dependencies = dependencies
self.openapi = openapi

if openapi_file_path:
openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
elif Path(self.directory_path).joinpath("openapi.json").exists():
openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
self.init_openapi(openapi_file_path)

if not bool(os.environ.get("ROBYN_CLI", False)):
# the env variables are already set when are running through the cli
Expand All @@ -81,6 +78,20 @@ def __init__(
self.event_handlers: dict = {}
self.exception_handler: Optional[Callable] = None
self.authentication_handler: Optional[AuthenticationHandler] = None
self.included_routers: List[Router] = []

def init_openapi(self, openapi_file_path: Optional[str]) -> None:
if self.config.disable_openapi:
return

if self.openapi is None:
self.openapi = OpenAPI()

if openapi_file_path:
self.openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
elif Path(self.directory_path).joinpath("openapi.json").exists():
self.openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
# TODO! what about when the elif fails?

def _handle_dev_mode(self):
cli_dev_mode = self.config.dev # --dev
Expand All @@ -101,6 +112,8 @@ def add_route(
handler: Callable,
is_const: bool = False,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: Union[List[str], None] = None,
):
"""
Connect a URI to a handler
Expand All @@ -116,6 +129,8 @@ def add_route(
"""
injected_dependencies = self.dependencies.get_dependency_map(self)

list_openapi_tags: List[str] = openapi_tags if openapi_tags else []

if auth_required:
self.middleware_router.add_auth_middleware(endpoint)(handler)

Expand All @@ -136,6 +151,9 @@ def add_route(
endpoint=endpoint,
handler=handler,
is_const=is_const,
auth_required=auth_required,
openapi_name=openapi_name,
openapi_tags=list_openapi_tags,
exception_handler=self.exception_handler,
injected_dependencies=injected_dependencies,
)
Expand Down Expand Up @@ -239,6 +257,15 @@ def is_port_in_use(self, port: int) -> bool:
raise Exception(f"Invalid port number: {port}")

def _add_openapi_routes(self, auth_required: bool = False):
if self.config.disable_openapi:
return

if self.openapi is None:
logger.error("No openAPI")
return

self.router.prepare_routes_openapi(self.openapi, self.included_routers)

self.add_route(
route_type=HttpMethod.GET,
endpoint="/openapi.json",
Expand Down Expand Up @@ -325,9 +352,7 @@ def get(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("get", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required)
return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required, openapi_name, openapi_tags)

return inner

Expand All @@ -348,9 +373,7 @@ def post(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("post", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -371,9 +394,7 @@ def put(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("put", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -394,9 +415,7 @@ def delete(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("delete", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -417,9 +436,7 @@ def patch(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("patch", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -440,9 +457,7 @@ def head(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("head", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -463,9 +478,7 @@ def options(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("options", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -486,8 +499,7 @@ def connect(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("connect", endpoint, openapi_name, openapi_tags, handler)
return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -508,9 +520,7 @@ def trace(
"""

def inner(handler):
self.openapi.add_openapi_path_obj("trace", endpoint, openapi_name, openapi_tags, handler)

return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required)
return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)

return inner

Expand All @@ -520,11 +530,14 @@ def include_router(self, router):
:param router Robyn: the router object to include the routes from
"""
self.included_routers.append(router)

self.router.routes.extend(router.router.routes)
self.middleware_router.global_middlewares.extend(router.middleware_router.global_middlewares)
self.middleware_router.route_middlewares.extend(router.middleware_router.route_middlewares)

self.openapi.add_subrouter_paths(router.openapi)
if not self.config.disable_openapi and self.openapi is not None:
self.openapi.add_subrouter_paths(self.openapi)

# extend the websocket routes
prefix = router.prefix
Expand Down
2 changes: 1 addition & 1 deletion robyn/processpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def spawn_process(
server.set_response_headers_exclude_paths(excluded_response_headers_paths)

for route in routes:
route_type, endpoint, function, is_const = route
route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags = route
server.add_route(route_type, endpoint, function, is_const)

for middleware_type, middleware_function in global_middlewares:
Expand Down
24 changes: 22 additions & 2 deletions robyn/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from robyn.authentication import AuthenticationHandler, AuthenticationNotConfiguredError
from robyn.dependency_injection import DependencyMap
from robyn.jsonify import jsonify
from robyn.openapi import OpenAPI
from robyn.responses import FileResponse
from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url
from robyn.types import Body, Files, FormData, IPAddress, Method, PathParams
Expand All @@ -19,11 +20,18 @@
_logger = logging.getLogger(__name__)


def lower_http_method(method: HttpMethod):
return (str(method))[11:].lower()


class Route(NamedTuple):
route_type: HttpMethod
route: str
function: FunctionInfo
is_const: bool
auth_required: bool
openapi_name: str
openapi_tags: List[str]


class RouteMiddleware(NamedTuple):
Expand Down Expand Up @@ -108,6 +116,9 @@ def add_route( # type: ignore
endpoint: str,
handler: Callable,
is_const: bool,
auth_required: bool,
openapi_name: str,
openapi_tags: List[str],
exception_handler: Optional[Callable],
injected_dependencies: dict,
) -> Union[Callable, CoroutineType]:
Expand Down Expand Up @@ -226,7 +237,7 @@ def inner_handler(*args, **kwargs):
params,
new_injected_dependencies,
)
self.routes.append(Route(route_type, endpoint, function, is_const))
self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags))
return async_inner_handler
else:
function = FunctionInfo(
Expand All @@ -236,9 +247,18 @@ def inner_handler(*args, **kwargs):
params,
new_injected_dependencies,
)
self.routes.append(Route(route_type, endpoint, function, is_const))
self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags))
return inner_handler

def prepare_routes_openapi(self, openapi: OpenAPI, included_routers: List) -> None:
for route in self.routes:
openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler)

# TODO! after include_routes does not immediatelly merge all the routes
# for router in included_routers:
# for route in router:
# openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler)

def get_routes(self) -> List[Route]:
return self.routes

Expand Down

0 comments on commit 2aad774

Please sign in to comment.