Skip to content

Commit 2012627

Browse files
authored
feat: add rpc methods to openapi (#18)
* feat: add rpc methods to openapi * style: fix linters
1 parent b1fc599 commit 2012627

File tree

6 files changed

+356
-234
lines changed

6 files changed

+356
-234
lines changed

app/api/routers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from app.api.endpoints.botx import router as bot_router
55
from app.api.endpoints.healthcheck import router as healthcheck_router
66

7-
router = APIRouter()
7+
router = APIRouter(include_in_schema=False)
88

99
router.include_router(healthcheck_router)
1010
router.include_router(bot_router)

app/main.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Application with configuration for events, routers and middleware."""
22

33
from functools import partial
4-
from typing import Optional
4+
from typing import Any, Dict, Optional
55

66
from fastapi import FastAPI
77
from pybotx import Bot, CallbackRepoProto
@@ -12,8 +12,10 @@
1212
from app.caching.redis_repo import RedisRepo
1313
from app.constants import BOT_PROJECT_NAME
1414
from app.db.sqlalchemy import build_db_session_factory, close_db_connections
15+
from app.services.openapi import custom_openapi
1516
from app.services.static_files import StaticFilesCustomHeaders
1617
from app.settings import settings
18+
from app.smartapp.smartapp import smartapp
1719

1820

1921
async def startup(bot: Bot) -> None:
@@ -47,7 +49,7 @@ def get_application(
4749

4850
bot = get_bot(callback_repo)
4951

50-
application = FastAPI(title=BOT_PROJECT_NAME, openapi_url=None)
52+
application = FastAPI()
5153
application.state.bot = bot
5254

5355
application.add_event_handler("startup", partial(startup, bot))
@@ -68,4 +70,14 @@ def get_application(
6870
name="smartapp_files",
6971
)
7072

73+
def get_custom_openapi() -> Dict[str, Any]: # noqa: WPS430
74+
return custom_openapi(
75+
title=BOT_PROJECT_NAME,
76+
version="0.1.0",
77+
fastapi_routes=application.routes,
78+
rpc_router=smartapp.router,
79+
)
80+
81+
application.openapi = get_custom_openapi # type: ignore
82+
7183
return application

app/services/openapi.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""OpenAPI utils."""
2+
from typing import Any, Dict, List, Optional, Sequence, Union
3+
4+
from fastapi import routing
5+
from fastapi.encoders import jsonable_encoder
6+
from fastapi.openapi.models import OpenAPI
7+
from fastapi.openapi.utils import get_flat_models_from_routes, get_openapi_path
8+
from fastapi.utils import get_model_definitions
9+
from pybotx_smartapp_rpc import RPCRouter
10+
from pybotx_smartapp_rpc.openapi_utils import (
11+
get_rpc_flat_models_from_routes,
12+
get_rpc_openapi_path,
13+
)
14+
from pydantic.schema import get_model_name_map
15+
from starlette.routing import BaseRoute
16+
17+
18+
def custom_openapi(
19+
*,
20+
title: str,
21+
version: str,
22+
openapi_version: str = "3.0.2",
23+
description: Optional[str] = None,
24+
fastapi_routes: Sequence[BaseRoute],
25+
rpc_router: RPCRouter,
26+
tags: Optional[List[Dict[str, Any]]] = None,
27+
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
28+
terms_of_service: Optional[str] = None,
29+
contact: Optional[Dict[str, Union[str, Any]]] = None,
30+
license_info: Optional[Dict[str, Union[str, Any]]] = None,
31+
) -> Dict[str, Any]:
32+
info: Dict[str, Any] = {"title": title, "version": version}
33+
if description:
34+
info["description"] = description
35+
if terms_of_service:
36+
info["termsOfService"] = terms_of_service
37+
if contact:
38+
info["contact"] = contact
39+
if license_info:
40+
info["license"] = license_info
41+
output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
42+
if servers:
43+
output["servers"] = servers
44+
components: Dict[str, Dict[str, Any]] = {}
45+
paths: Dict[str, Dict[str, Any]] = {}
46+
# FastAPI
47+
flat_fastapi_models = get_flat_models_from_routes(fastapi_routes)
48+
fastapi_model_name_map = get_model_name_map(flat_fastapi_models)
49+
fast_api_definitions = get_model_definitions(
50+
flat_models=flat_fastapi_models, model_name_map=fastapi_model_name_map
51+
)
52+
53+
# pybotx RPC
54+
flat_rpc_models = get_rpc_flat_models_from_routes(rpc_router)
55+
rpc_model_name_map = get_model_name_map(flat_rpc_models)
56+
rpc_definitions = get_model_definitions(
57+
flat_models=flat_rpc_models, model_name_map=rpc_model_name_map
58+
)
59+
60+
for route in fastapi_routes:
61+
if isinstance(route, routing.APIRoute):
62+
result = get_openapi_path(
63+
route=route, model_name_map=fastapi_model_name_map
64+
)
65+
if result:
66+
path, security_schemes, path_definitions = result
67+
if path:
68+
paths.setdefault(route.path_format, {}).update(path)
69+
if security_schemes:
70+
components.setdefault("securitySchemes", {}).update(
71+
security_schemes
72+
)
73+
if path_definitions:
74+
fast_api_definitions.update(path_definitions)
75+
76+
for method_name in rpc_router.rpc_methods.keys():
77+
if not rpc_router.rpc_methods[method_name].include_in_schema:
78+
continue
79+
80+
result = get_rpc_openapi_path( # type: ignore
81+
method_name=method_name,
82+
route=rpc_router.rpc_methods[method_name],
83+
model_name_map=rpc_model_name_map,
84+
)
85+
if result:
86+
path, path_definitions = result # type: ignore
87+
if path:
88+
paths.setdefault(method_name, {}).update(path)
89+
90+
if path_definitions:
91+
rpc_definitions.update(path_definitions)
92+
93+
if fast_api_definitions:
94+
components["schemas"] = {
95+
k: fast_api_definitions[k] for k in sorted(fast_api_definitions)
96+
}
97+
if rpc_definitions:
98+
components.setdefault("schemas", {}).update(
99+
{k: rpc_definitions[k] for k in sorted(rpc_definitions)}
100+
)
101+
if components:
102+
output["components"] = components
103+
output["paths"] = paths
104+
if tags:
105+
output["tags"] = tags
106+
107+
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True)

0 commit comments

Comments
 (0)