Skip to content

Commit 2aad774

Browse files
authored
refactor: openApi made lazy. (#1066)
* 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
1 parent 8aa97ea commit 2aad774

File tree

3 files changed

+68
-35
lines changed

3 files changed

+68
-35
lines changed

robyn/__init__.py

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __init__(
4242
file_object: str,
4343
config: Config = Config(),
4444
openapi_file_path: Optional[str] = None,
45-
openapi: OpenAPI = OpenAPI(),
45+
openapi: Optional[OpenAPI] = None,
4646
dependencies: DependencyMap = DependencyMap(),
4747
) -> None:
4848
directory_path = os.path.dirname(os.path.abspath(file_object))
@@ -52,10 +52,7 @@ def __init__(
5252
self.dependencies = dependencies
5353
self.openapi = openapi
5454

55-
if openapi_file_path:
56-
openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
57-
elif Path(self.directory_path).joinpath("openapi.json").exists():
58-
openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
55+
self.init_openapi(openapi_file_path)
5956

6057
if not bool(os.environ.get("ROBYN_CLI", False)):
6158
# the env variables are already set when are running through the cli
@@ -81,6 +78,20 @@ def __init__(
8178
self.event_handlers: dict = {}
8279
self.exception_handler: Optional[Callable] = None
8380
self.authentication_handler: Optional[AuthenticationHandler] = None
81+
self.included_routers: List[Router] = []
82+
83+
def init_openapi(self, openapi_file_path: Optional[str]) -> None:
84+
if self.config.disable_openapi:
85+
return
86+
87+
if self.openapi is None:
88+
self.openapi = OpenAPI()
89+
90+
if openapi_file_path:
91+
self.openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
92+
elif Path(self.directory_path).joinpath("openapi.json").exists():
93+
self.openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
94+
# TODO! what about when the elif fails?
8495

8596
def _handle_dev_mode(self):
8697
cli_dev_mode = self.config.dev # --dev
@@ -101,6 +112,8 @@ def add_route(
101112
handler: Callable,
102113
is_const: bool = False,
103114
auth_required: bool = False,
115+
openapi_name: str = "",
116+
openapi_tags: Union[List[str], None] = None,
104117
):
105118
"""
106119
Connect a URI to a handler
@@ -116,6 +129,8 @@ def add_route(
116129
"""
117130
injected_dependencies = self.dependencies.get_dependency_map(self)
118131

132+
list_openapi_tags: List[str] = openapi_tags if openapi_tags else []
133+
119134
if auth_required:
120135
self.middleware_router.add_auth_middleware(endpoint)(handler)
121136

@@ -136,6 +151,9 @@ def add_route(
136151
endpoint=endpoint,
137152
handler=handler,
138153
is_const=is_const,
154+
auth_required=auth_required,
155+
openapi_name=openapi_name,
156+
openapi_tags=list_openapi_tags,
139157
exception_handler=self.exception_handler,
140158
injected_dependencies=injected_dependencies,
141159
)
@@ -239,6 +257,15 @@ def is_port_in_use(self, port: int) -> bool:
239257
raise Exception(f"Invalid port number: {port}")
240258

241259
def _add_openapi_routes(self, auth_required: bool = False):
260+
if self.config.disable_openapi:
261+
return
262+
263+
if self.openapi is None:
264+
logger.error("No openAPI")
265+
return
266+
267+
self.router.prepare_routes_openapi(self.openapi, self.included_routers)
268+
242269
self.add_route(
243270
route_type=HttpMethod.GET,
244271
endpoint="/openapi.json",
@@ -325,9 +352,7 @@ def get(
325352
"""
326353

327354
def inner(handler):
328-
self.openapi.add_openapi_path_obj("get", endpoint, openapi_name, openapi_tags, handler)
329-
330-
return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required)
355+
return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required, openapi_name, openapi_tags)
331356

332357
return inner
333358

@@ -348,9 +373,7 @@ def post(
348373
"""
349374

350375
def inner(handler):
351-
self.openapi.add_openapi_path_obj("post", endpoint, openapi_name, openapi_tags, handler)
352-
353-
return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required)
376+
return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
354377

355378
return inner
356379

@@ -371,9 +394,7 @@ def put(
371394
"""
372395

373396
def inner(handler):
374-
self.openapi.add_openapi_path_obj("put", endpoint, openapi_name, openapi_tags, handler)
375-
376-
return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required)
397+
return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
377398

378399
return inner
379400

@@ -394,9 +415,7 @@ def delete(
394415
"""
395416

396417
def inner(handler):
397-
self.openapi.add_openapi_path_obj("delete", endpoint, openapi_name, openapi_tags, handler)
398-
399-
return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required)
418+
return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
400419

401420
return inner
402421

@@ -417,9 +436,7 @@ def patch(
417436
"""
418437

419438
def inner(handler):
420-
self.openapi.add_openapi_path_obj("patch", endpoint, openapi_name, openapi_tags, handler)
421-
422-
return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required)
439+
return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
423440

424441
return inner
425442

@@ -440,9 +457,7 @@ def head(
440457
"""
441458

442459
def inner(handler):
443-
self.openapi.add_openapi_path_obj("head", endpoint, openapi_name, openapi_tags, handler)
444-
445-
return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required)
460+
return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
446461

447462
return inner
448463

@@ -463,9 +478,7 @@ def options(
463478
"""
464479

465480
def inner(handler):
466-
self.openapi.add_openapi_path_obj("options", endpoint, openapi_name, openapi_tags, handler)
467-
468-
return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required)
481+
return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
469482

470483
return inner
471484

@@ -486,8 +499,7 @@ def connect(
486499
"""
487500

488501
def inner(handler):
489-
self.openapi.add_openapi_path_obj("connect", endpoint, openapi_name, openapi_tags, handler)
490-
return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required)
502+
return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
491503

492504
return inner
493505

@@ -508,9 +520,7 @@ def trace(
508520
"""
509521

510522
def inner(handler):
511-
self.openapi.add_openapi_path_obj("trace", endpoint, openapi_name, openapi_tags, handler)
512-
513-
return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required)
523+
return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
514524

515525
return inner
516526

@@ -520,11 +530,14 @@ def include_router(self, router):
520530
521531
:param router Robyn: the router object to include the routes from
522532
"""
533+
self.included_routers.append(router)
534+
523535
self.router.routes.extend(router.router.routes)
524536
self.middleware_router.global_middlewares.extend(router.middleware_router.global_middlewares)
525537
self.middleware_router.route_middlewares.extend(router.middleware_router.route_middlewares)
526538

527-
self.openapi.add_subrouter_paths(router.openapi)
539+
if not self.config.disable_openapi and self.openapi is not None:
540+
self.openapi.add_subrouter_paths(self.openapi)
528541

529542
# extend the websocket routes
530543
prefix = router.prefix

robyn/processpool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def spawn_process(
182182
server.set_response_headers_exclude_paths(excluded_response_headers_paths)
183183

184184
for route in routes:
185-
route_type, endpoint, function, is_const = route
185+
route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags = route
186186
server.add_route(route_type, endpoint, function, is_const)
187187

188188
for middleware_type, middleware_function in global_middlewares:

robyn/router.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from robyn.authentication import AuthenticationHandler, AuthenticationNotConfiguredError
1212
from robyn.dependency_injection import DependencyMap
1313
from robyn.jsonify import jsonify
14+
from robyn.openapi import OpenAPI
1415
from robyn.responses import FileResponse
1516
from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url
1617
from robyn.types import Body, Files, FormData, IPAddress, Method, PathParams
@@ -19,11 +20,18 @@
1920
_logger = logging.getLogger(__name__)
2021

2122

23+
def lower_http_method(method: HttpMethod):
24+
return (str(method))[11:].lower()
25+
26+
2227
class Route(NamedTuple):
2328
route_type: HttpMethod
2429
route: str
2530
function: FunctionInfo
2631
is_const: bool
32+
auth_required: bool
33+
openapi_name: str
34+
openapi_tags: List[str]
2735

2836

2937
class RouteMiddleware(NamedTuple):
@@ -108,6 +116,9 @@ def add_route( # type: ignore
108116
endpoint: str,
109117
handler: Callable,
110118
is_const: bool,
119+
auth_required: bool,
120+
openapi_name: str,
121+
openapi_tags: List[str],
111122
exception_handler: Optional[Callable],
112123
injected_dependencies: dict,
113124
) -> Union[Callable, CoroutineType]:
@@ -226,7 +237,7 @@ def inner_handler(*args, **kwargs):
226237
params,
227238
new_injected_dependencies,
228239
)
229-
self.routes.append(Route(route_type, endpoint, function, is_const))
240+
self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags))
230241
return async_inner_handler
231242
else:
232243
function = FunctionInfo(
@@ -236,9 +247,18 @@ def inner_handler(*args, **kwargs):
236247
params,
237248
new_injected_dependencies,
238249
)
239-
self.routes.append(Route(route_type, endpoint, function, is_const))
250+
self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags))
240251
return inner_handler
241252

253+
def prepare_routes_openapi(self, openapi: OpenAPI, included_routers: List) -> None:
254+
for route in self.routes:
255+
openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler)
256+
257+
# TODO! after include_routes does not immediatelly merge all the routes
258+
# for router in included_routers:
259+
# for route in router:
260+
# openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler)
261+
242262
def get_routes(self) -> List[Route]:
243263
return self.routes
244264

0 commit comments

Comments
 (0)