Skip to content

Commit 46f9c28

Browse files
authored
Merge pull request #118 from touale/opt-api-router-tags
Opt api router tags
2 parents 51b8094 + d7f98dc commit 46f9c28

11 files changed

Lines changed: 83 additions & 17 deletions

File tree

src/framex/consts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919
"/health",
2020
"/ping",
2121
]
22+
23+
AUTH_COOKIE_NAME = "framex_token"

src/framex/driver/application.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ async def _on_start(deployment: Any) -> None:
9595
redirect_slashes=False,
9696
)
9797

98+
application.state.tags_metadata_map = []
99+
98100
if settings.auth.oauth:
99101
application.add_api_route(
100102
settings.auth.oauth.redirect_uri,
@@ -118,6 +120,7 @@ async def get_open_api_endpoint(_: Annotated[str, Depends(authenticate)]) -> dic
118120
version=VERSION,
119121
description=build_openapi_description(),
120122
routes=application.routes,
123+
tags=application.state.tags_metadata_map,
121124
)
122125

123126
@application.exception_handler(HTTPException)

src/framex/driver/auth.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from starlette.requests import Request
99

1010
from framex.config import settings
11-
from framex.consts import DOCS_URL
11+
from framex.consts import AUTH_COOKIE_NAME, DOCS_URL
1212

1313
api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
1414

@@ -32,7 +32,7 @@ def auth_jwt(request: Request) -> bool:
3232
if not settings.auth.oauth:
3333
return False
3434

35-
token = request.cookies.get("token")
35+
token = request.cookies.get(AUTH_COOKIE_NAME)
3636
if not token:
3737
return False
3838

@@ -49,7 +49,7 @@ def auth_jwt(request: Request) -> bool:
4949

5050
def authenticate(request: Request, api_key: str | None = Depends(api_key_header)) -> None:
5151
if settings.auth.oauth:
52-
if token := request.cookies.get("token"):
52+
if token := request.cookies.get(AUTH_COOKIE_NAME):
5353
try:
5454
jwt.decode(
5555
token,
@@ -120,7 +120,7 @@ async def oauth_callback(code: str) -> Response:
120120

121121
res = RedirectResponse(url=DOCS_URL, status_code=status.HTTP_302_FOUND)
122122
res.set_cookie(
123-
"token",
123+
AUTH_COOKIE_NAME,
124124
create_jwt(user_info),
125125
httponly=True,
126126
samesite="lax",

src/framex/driver/ingress.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(self, deployments: list[Any], plugin_apis: list["PluginApi"]) -> No
6060
stream=plugin_api.stream,
6161
direct_output=plugin_api.raw_response,
6262
tags=plugin_api.tags,
63+
description=plugin_api.description,
6364
**plugin_api.extend_kwargs,
6465
)
6566

@@ -73,6 +74,7 @@ def register_route(
7374
stream: bool = False,
7475
direct_output: bool = False,
7576
tags: list[str] | None = None,
77+
description: str | None = None,
7678
auth_keys: list[str] | None = None,
7779
include_in_schema: bool = True,
7880
**kwargs: Any,
@@ -161,6 +163,7 @@ def _verify_api_key(request: Request, api_key: str | None = Depends(api_key_head
161163
response_class=StreamingResponse if stream else JSONResponse,
162164
dependencies=dependencies,
163165
include_in_schema=include_in_schema,
166+
description=description,
164167
**kwargs,
165168
)
166169
methods_str = ",".join(m.upper() for m in methods)
@@ -188,6 +191,9 @@ def add_api_route(
188191
endpoint: Callable[..., Any],
189192
*,
190193
methods: list[str] | None = None,
194+
tags: list[str] | None = None,
195+
description: str | None = None,
196+
include_in_schema: bool = True,
191197
**kwargs: Any,
192198
) -> None:
193199
method_set: set[str] = {m.upper() for m in methods} if methods else {"GET"}
@@ -201,4 +207,22 @@ def add_api_route(
201207
):
202208
raise RuntimeError(f"Duplicate API route: {sorted(method_set)} {norm_path}")
203209

204-
app.add_api_route(path, endpoint, methods=list(method_set), **kwargs)
210+
app.add_api_route(
211+
path,
212+
endpoint,
213+
methods=list(method_set), # type: ignore
214+
tags=tags, # type: ignore
215+
include_in_schema=include_in_schema,
216+
**kwargs,
217+
)
218+
219+
if include_in_schema and tags:
220+
names = list({tag["name"] for tag in app.state.tags_metadata_map})
221+
for tag in tags:
222+
if tag not in names:
223+
app.state.tags_metadata_map.append(
224+
{
225+
"name": tag,
226+
"description": description,
227+
},
228+
)

src/framex/plugin/base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,17 @@ async def _call_remote_api(self, api_name: str, **kwargs: Any) -> Any:
7373

7474
def _post_call_remote_api_hook(self, data: Any) -> Any:
7575
return data
76+
77+
78+
def build_plugin_description(
79+
author: str,
80+
version: str,
81+
description: str,
82+
repo: str,
83+
) -> str:
84+
return (
85+
f"**Author**: {author}\n\n"
86+
f"**Version**: {version}\n\n"
87+
f"**Description**: {description}\n\n"
88+
f"**Repo**: [{repo}]({repo})"
89+
)

src/framex/plugin/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class PluginApi(BaseModel):
3434
params: list[tuple[str, type[Any] | Callable[..., Any]]] = Field(default_factory=list)
3535
call_type: ApiType = ApiType.HTTP
3636
tags: list[str] | None = None
37+
description: str | None = None
3738
stream: bool = False
3839
raw_response: bool = False
3940
extend_kwargs: dict[str, Any] = Field(default_factory=dict)

src/framex/plugin/on.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from framex.adapter import get_adapter
1111
from framex.consts import API_STR, PROXY_PLUGIN_NAME
12+
from framex.plugin.base import build_plugin_description
1213
from framex.plugin.model import ApiType, PluginApi, PluginDeployment
1314
from framex.utils import cache_decode, cache_encode, extract_method_params, plugin_to_deployment_name
1415

@@ -42,11 +43,16 @@ def decorator(cls: type) -> type:
4243
if func.__tags:
4344
tags: list[str] = func.__tags
4445
else:
45-
author: str = plugin.module.__plugin_meta__.author
46-
description = plugin.module.__plugin_meta__.description
47-
version: str = plugin.module.__plugin_meta__.version
48-
version = f"v{version}" if not version.startswith("v") else version
49-
tags = [f"{plugin.name} ({version}) by {author}: {description}"]
46+
tags = [plugin.name]
47+
48+
version: str = plugin.module.__plugin_meta__.version
49+
version = f"v{version}" if not version.startswith("v") else version
50+
description = build_plugin_description(
51+
plugin.module.__plugin_meta__.author,
52+
version,
53+
plugin.module.__plugin_meta__.description,
54+
plugin.module.__plugin_meta__.url,
55+
)
5056

5157
plugin_apis.append(
5258
PluginApi(
@@ -57,6 +63,7 @@ def decorator(cls: type) -> type:
5763
params=params,
5864
call_type=call_type,
5965
tags=tags,
66+
description=description,
6067
stream=func.__expose_stream,
6168
raw_response=raw_response,
6269
extend_kwargs=func.__kwargs,

src/framex/plugins/proxy/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010

1111
from framex.adapter import get_adapter
1212
from framex.adapter.base import BaseAdapter
13-
from framex.consts import BACKEND_NAME, PROXY_FUNC_HTTP_PATH, PROXY_PLUGIN_NAME, VERSION
13+
from framex.consts import BACKEND_NAME, PROXY_FUNC_HTTP_PATH, PROXY_PLUGIN_NAME
1414
from framex.log import logger
1515
from framex.plugin import BasePlugin, PluginApi, PluginMetadata, on_register
16+
from framex.plugin.base import build_plugin_description
1617
from framex.plugin.model import ApiType
1718
from framex.plugin.on import on_request
1819
from framex.plugins.proxy.builder import (
@@ -23,15 +24,14 @@
2324
to_multipart_annotation,
2425
type_map,
2526
)
26-
from framex.plugins.proxy.config import ProxyPluginConfig, settings
27+
from framex.plugins.proxy.config import VERSION, ProxyPluginConfig, settings
2728
from framex.plugins.proxy.model import ProxyFunc, ProxyFuncHttpBody
2829
from framex.utils import cache_decode, cache_encode, shorten_str
2930

3031
__plugin_meta__ = PluginMetadata(
3132
name="proxy",
3233
version=VERSION,
33-
description="一个特殊的 framx proxy 插件, 充当透明代理。"
34-
"它接收 API 请求并将其转发到已配置的外部 HTTP 端点,并将响应返回给调用者。",
34+
description="proxy 是 FrameX 的核心系统插件, 作为透明代理转发 API 请求至外部 HTTP 端点并返回响应。",
3535
author="touale",
3636
url="https://github.com/touale/FrameX-kit",
3737
required_remote_apis=[],
@@ -170,6 +170,12 @@ async def _parse_openai_docs(self, url: str) -> None:
170170
func_name="register_route",
171171
)
172172
handle = adapter.get_handle(PROXY_PLUGIN_NAME)
173+
description = build_plugin_description(
174+
__plugin_meta__.author,
175+
f"v{__plugin_meta__.version}",
176+
__plugin_meta__.description,
177+
__plugin_meta__.url,
178+
)
173179
await adapter.call_func(
174180
plugin_api,
175181
path=path,
@@ -180,6 +186,7 @@ async def _parse_openai_docs(self, url: str) -> None:
180186
stream=is_stream,
181187
direct_output=True,
182188
tags=[f"{__plugin_meta__.name}({url})"],
189+
description=description,
183190
)
184191

185192
# Proxy api to map

src/framex/plugins/proxy/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from framex.config import AuthConfig
66
from framex.plugin import get_plugin_config
77

8+
VERSION = "0.3.0"
9+
810

911
class ProxyUrlRuleConfig(BaseModel):
1012
enable: list[str] = Field(default_factory=list)

tests/driver/test_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from starlette.requests import Request
1212

1313
from framex.config import AuthConfig
14-
from framex.consts import DOCS_URL
14+
from framex.consts import AUTH_COOKIE_NAME, DOCS_URL
1515
from framex.driver.application import create_fastapi_application
1616
from framex.driver.auth import auth_jwt, authenticate, create_jwt, oauth_callback
1717

@@ -219,6 +219,6 @@ def test_docs_accessible_with_valid_jwt(self):
219219
JWT_SECRET,
220220
algorithm="HS256",
221221
)
222-
client.cookies.set("token", token)
222+
client.cookies.set(AUTH_COOKIE_NAME, token)
223223
resp = client.get("/docs", follow_redirects=False)
224224
assert resp.status_code == status.HTTP_200_OK

0 commit comments

Comments
 (0)