Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API 的插件框架 #413

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
3 changes: 1 addition & 2 deletions web/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
# http://binux.me
# Created on 2012-12-15 16:15:50

import os
import sys
import importlib
import os
import pkgutil

from . import base
Expand Down
4 changes: 2 additions & 2 deletions web/handlers/about.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
# http://binux.me
# Created on 2014-08-08 21:06:02

from .base import *
from . import api
from .api import MultiArgument, BodyArgument
from .api import BodyArgument, MultiArgument
from .base import *


class AboutHandler(BaseHandler):
Expand Down
29 changes: 17 additions & 12 deletions web/handlers/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import os
import importlib
import json
import os
import pkgutil
import importlib
import typing
from typing import Iterable, Callable
import dataclasses
from dataclasses import dataclass
from functools import partial
from collections import defaultdict
from dataclasses import dataclass
from html import escape
from typing import Callable, Iterable

from tornado.web import HTTPError

from ..base import BaseHandler
from libs.safe_eval import safe_eval

from ..base import BaseHandler, logger_Web_Handler

URL_PREFIX = "/v1/"

BaseType = typing.Union[str, int, float, bool, None]


class ApiError(HTTPError):
def __init__(self, status_code: int, reason: str, *args, **kwargs):
def __init__(self, status_code: int, reason: str, log_message :str | None = None, *args, **kwargs):
# 对于 HTTPError,log_message 是打印到控制台的内容,reason 是返回给用户的内容
# 我们希望 API 的用户可以直接从 Web 界面看到错误信息,所以将 log_message 和 reason 设置为相同的内容
super().__init__(status_code, reason, reason=reason, *args, **kwargs)
if log_message is None:
log_message = reason
super().__init__(status_code, log_message=log_message, reason=reason, *args, **kwargs)


@dataclass()
Expand Down Expand Up @@ -213,7 +214,7 @@ def api_write(self, data):
if len(data) == 1:
# 如果只有一个键值对,直接返回值
# 不递归处理,默认 API 不会返回过于复杂的类型
self.write(tuple(data.values())[0])
self.write(str(tuple(data.values())[0]))
else:
# 如果有多个键值对,返回 JSON
self.api_write_json(data)
Expand All @@ -230,10 +231,14 @@ def api_write(self, data):
# 其他类型转换为 JSON
self.api_write_json(data)

def api_write_json(self, data: dict[str, typing.Any], ensure_ascii=False, indent=4):
def api_write_json(self, data: dict[str, typing.Any], ensure_ascii=False, indent=4, escape_html=False, escape_quote=True):
"""将 json 数据写入响应"""
self.set_header("Content-Type", "application/json; charset=UTF-8")
self.write(json.dumps(data, ensure_ascii=ensure_ascii, indent=indent))
if escape_html:
data = escape(json.dumps(data, ensure_ascii=ensure_ascii, indent=indent), quote=escape_quote)
else:
data = json.dumps(data, ensure_ascii=ensure_ascii, indent=indent).replace('</', '<\\/')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里应该没必要进行转义,因为 Content-Type 已经设置为 application/json

更进一步,API 的返回内容默认为 text/plain 吧,避免 API 作者忘记转义(比如我);bytes 类型则 application/octet-stream;JSON application/json
应该没有 API 需要使用其他类型的需求,所以就不用允许 API 自己设置 Content-Type 了

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我的建议是默认返回JSON格式的code,message,data信息

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

既然是api就一步到位,规范一下

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样设计的话,我觉得对应的系统对 JSON respond 的处理能力需要增强,目前系统对返回内容好像只支持正则匹配。

思考了一下好像不难实现,提供一个 respond content 的魔法变量和一个 JSON 函数应该就可以了,比如 {{ json_parse(content)['code'] }}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样设计的话,我觉得对应的系统对 JSON respond 的处理能力需要增强,目前系统对返回内容好像只支持正则匹配。

思考了一下好像不难实现,提供一个 respond content 的魔法变量和一个 JSON 函数应该就可以了,比如 {{ json_parse(content)['code'] }}

现在的模式:
image

新UI: 类型(下拉: 简单, 正则(和旧版兼容), 代码), 来源(下拉: respond.[content, header, status]), 变量名, 规则, 结果
类型为代码时则不显示来源,可以使用 {{ respond.content }} 之类的

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没有看到新UI的界面?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在写了.jpg,先把方案写下来讨论一下

self.write(data)
Fixed Show fixed Hide fixed


def api_wrap(
Expand Down
20 changes: 10 additions & 10 deletions web/handlers/api/example.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json
import traceback
import typing
from html import escape

import config

from . import ApiBase, Argument, MultiArgument, BodyArgument, api_wrap, ApiError

from ..base import logger_Web_Handler
from . import (ApiBase, ApiError, Argument, BodyArgument, MultiArgument,
api_wrap)

# API 插件设计准则:
# - 路径为 /api/name(旧版保持原先的 /util/name)
Expand Down Expand Up @@ -42,15 +44,15 @@
# 最简单的 API 示例
class Echo1(ApiBase):
api_name = "回声"
api_description = "输出 = 输入" # 支持 HTML 标签,比如 <br/>
api_description = "输出 = 输入" # 不支持 HTML 标签,比如 <br/>
a76yyyy marked this conversation as resolved.
Show resolved Hide resolved
api_url = "echo1" # 框架会自动添加前缀 /api/

@api_wrap(
arguments=(Argument(name="text", description="输入的文字", required=True),),
example={"text": "hello world"},
)
async def get(self, text: str):
return text
return escape(text)

post = get

Expand All @@ -66,7 +68,7 @@ class Echo2(ApiBase):
example={"text": "hello world"},
)
async def get(self, text: str):
return text
return escape(text)

# 不提供 example 的示例
@api_wrap(
Expand All @@ -75,7 +77,7 @@ async def get(self, text: str):
),
)
async def post(self, text: bytes):
return text
return escape(text)


# __filter__ 和 直接设置 example_display 的示例
Expand Down Expand Up @@ -174,8 +176,6 @@ async def get(self, code: int, reason: str):
try:
eee = 9 / 0
except ZeroDivisionError as e:
if config.traceback_print:
traceback.print_exc() # 根据全局设置决定是否在控制台打印完整报错信息
raise ApiError(500, str(e)) # 还是返回 500,但是前端有报错原因,控制台打印简短信息
else:
raise ApiError(code, reason)
Expand Down Expand Up @@ -235,7 +235,7 @@ class Echo0(ApiBase):
async def get(self):
# 使用原生 tornado API,详细参考 tornado 文档
text = self.get_argument("text", "")
self.write(text)
self.write(escape(text))
Fixed Show fixed Hide fixed

# 此处的声明只用于生成前端文档
get.api = {
Expand All @@ -246,4 +246,4 @@ async def get(self):
post = get


handlers = (Echo1, Echo2, Echon, Concat, Sum, Example, Json, Echo0)
handlers = (Echon, Concat, Sum, Example, Json)
4 changes: 2 additions & 2 deletions web/tpl/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ <h2>
<tr>
<td rowspan="{{ rowspan }}">{{ api.api_name }}</td>
<td rowspan="{{ rowspan }}" class="showbut" id="api_url{{i}}">
<a href="{{ api.api_url }}" target="_blank" rel="nofollow">api:/{{ api.api_url }}</a>
<a href="{{ api.api_url }}" target="_blank" rel="nofollow noopener noreferrer">api:/{{ api.api_url }}</a>
<button class="btn hljs-button" data-clipboard-target="#api_url{{i}}">复制</button>
</td>
<td rowspan="{{ rowspan }}">{{ api.api_description | safe }}</td>
Expand Down Expand Up @@ -79,7 +79,7 @@ <h2>
{% endif %}

<td rowspan="{{ 1 if args == 0 else args }}" class="showbut autowrap" id="api_exam{{i}}">
<a href="{{ method['example'] }}" target="_blank" rel="nofollow">api:/{{ method['example'] }}</a>
<a href="{{ method['example'] }}" target="_blank" rel="nofollow noopener noreferrer">api:/{{ method['example'] }}</a>
<button class="btn hljs-button" data-clipboard-target="#api_exam{{i}}">复制</button>
<td>
</tr>
Expand Down