From 560661b8272beb57c9d8ead957a1225cd5ee4d94 Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Tue, 18 Apr 2023 20:44:15 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E7=9A=84=20API=20plugin=20=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/handlers/__init__.py | 46 +++--- web/handlers/about.py | 3 +- web/handlers/api/__init__.py | 276 +++++++++++++++++++++++++++++++++++ web/handlers/api/example.py | 40 +++++ web/tpl/about.html | 97 +++++++++++- 5 files changed, 437 insertions(+), 25 deletions(-) create mode 100644 web/handlers/api/__init__.py create mode 100644 web/handlers/api/example.py diff --git a/web/handlers/__init__.py b/web/handlers/__init__.py index c69bbc25d3c..b2a7e1f8812 100644 --- a/web/handlers/__init__.py +++ b/web/handlers/__init__.py @@ -7,26 +7,32 @@ import os import sys +import importlib +import pkgutil -sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) from . import base -handlers = [] -ui_modules = {} -ui_methods = {} -modules = [] -for file in os.listdir(os.path.dirname(__file__)): - if not file.endswith(".py"): - continue - if file == "__init__.py": - continue - modules.append(file[:-3]) - -for module in modules: - module = __import__('%s.%s' % (__package__, module), fromlist = ["handlers"]) - if hasattr(module, "handlers"): - handlers.extend(module.handlers) - if hasattr(module, "ui_modules"): - ui_modules.update(module.ui_modules) - if hasattr(module, "ui_methods"): - ui_methods.update(module.ui_methods) + +def load_modules(): + handlers: list[tuple[str, base.BaseHandler]] = [] + ui_modules: dict[str, base.BaseUIModule] = {} + ui_methods: dict[str, base.BaseWebSocket] = {} + + path = os.path.join(os.path.dirname(__file__), "") + for finder, name, ispkg in pkgutil.iter_modules([path]): + module = importlib.import_module("." + name, __name__) + if hasattr(module, "handlers"): + handlers.extend(module.handlers) + if hasattr(module, "ui_modules"): + ui_modules.update(module.ui_modules) + if hasattr(module, "ui_methods"): + ui_methods.update(module.ui_methods) + + return handlers, ui_modules, ui_methods + + +handlers: list[tuple[str, base.BaseHandler]] +ui_modules: dict[str, base.BaseUIModule] +ui_methods: dict[str, base.BaseWebSocket] + +handlers, ui_modules, ui_methods = load_modules() diff --git a/web/handlers/about.py b/web/handlers/about.py index 454695312f6..18c9a553e06 100644 --- a/web/handlers/about.py +++ b/web/handlers/about.py @@ -6,12 +6,13 @@ # Created on 2014-08-08 21:06:02 from .base import * +from . import api class AboutHandler(BaseHandler): @tornado.web.addslash async def get(self): - await self.render('about.html') + await self.render('about.html', apis=api.apis) return handlers = [ diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py new file mode 100644 index 00000000000..d6fbfa9c36e --- /dev/null +++ b/web/handlers/api/__init__.py @@ -0,0 +1,276 @@ +import os +import json +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 tornado.web import HTTPError + +from ..base import BaseHandler +from libs.safe_eval import safe_eval + + +URL_PREFIX = "/api/" + +BaseType = typing.Union[str, int, float, bool, None] + + +class ApiError(HTTPError): + def __init__(self, status_code: int, reason: str, *args, **kwargs): + # 对于 HTTPError,log_message 是打印到控制台的内容,reason 是返回给用户的内容 + # 我们希望 API 的用户可以直接从 Web 界面看到错误信息,所以将 log_message 和 reason 设置为相同的内容 + super().__init__(status_code, reason, reason=reason, *args, **kwargs) + + +@dataclass() +class Argument(object): + name: str + '''参数名称 + 例如:"regex"''' + required: bool + """参数是否必须 + 调用 API 时,缺少必须参数会返回 HTTP 400 错误""" + description: str + '''参数描述 + 例如:"正则表达式"''' + type: "type" = str + """参数类型,默认为 str + 设置了 multi 时,type 描述的是列表内元素的类型""" + type_display: str | None = None + """参数类型在前端的显示值,默认为 self.type.__name__""" + init: Callable[[str], typing.Any] = None # type: ignore + """参数初始化函数,初始化规则如下: + 如果用户未提供且 self.type Callable,则使用 self.type; + 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 + 该初始化函数原型为 init(str) -> self.type,例如 int("123"), float("123.456")等""" + multi: bool = False + """是否为多值参数,比如 ?a=1&a=2&a=3 -> a=[1,2,3]""" + # multi 为 True 时,参数类型为 list[self.type] + # multi 为 True 时,default 必须为 list 或 tuple(如果 default 为 None,会被自动转换为空 tuple)""" + default: BaseType | Iterable[BaseType] = None + """参数默认值 + 如果设置了 multi,则 default 类型须为 Iterable[str|int|float|bool](默认为空 tuple); + 如果设置了 required,则 default 强制为 None; + 其他情况下 default 类型应为 Optionl[str|int|float|bool]。 + + API 被调用时,用户若为提供该参数,则使用 init(default) 作为参数值。""" + default_display: str | None = None + """默认值在前端的显示值,默认为 repr(self.default)""" + from_body: bool = False + """从 request.body 初始化,比如 POST JSON 情形 + 与 multi 互斥""" + + def __post_init__(self): + if self.init is None: + self.init = self.type + if not isinstance(self.init, Callable): + self.init = lambda x: x + + if self.required: + self.default = None + self.default_display = "❎" + + if self.multi and self.default is None: + self.default = tuple() + + if self.default_display is None: + self.default_display = repr(self.default) + + if self.type_display is None: + self.type_display = self.type.__name__ + + if self.multi and self.from_body: + # multi 和 from_body 互斥 + raise ValueError(f"Argument {self.name} is multi but from_body") + + +class ApiMetaclass(type): + # 用于实现属性值修改和 method 装饰的元类 + def __new__(cls, name, bases, attrs): + if name == "ApiBase": + return super().__new__(cls, name, bases, attrs) + + attrs["api_url"] = URL_PREFIX + attrs["api_url"] + + t = defaultdict(list) + for method_name in ("get", "post", "put", "delete"): + if method_name not in attrs: + continue + func = attrs[method_name] + t[func].append(method_name) + + api_frontend = {} + rowspan = 0 + methods = {} + for k, v in t.items(): + methods[", ".join(x.upper() for x in v)] = k.api + if k.api["example"]: + k.api["example"] = f'{attrs["api_url"]}?{k.api["example"]}' + else: + k.api["example"] = attrs["api_url"] + l = len(k.api["arguments"]) + rowspan += l if l else 1 + api_frontend["rowspan"] = rowspan + api_frontend["methods"] = methods + + attrs["api_frontend"] = api_frontend + + return super().__new__(cls, name, bases, attrs) + + +class ApiBase(BaseHandler, metaclass=ApiMetaclass): + api_name: str + '''API 名称 + 例如:"正则表达式替换"''' + api_url: str + """API 的 URL,会自动加上前缀 "/api/"。可以使用正则表达式,但是不建议。 + 例如:"delay" (加上前缀后为"/api/delay")""" + api_description: str + '''API 功能说明,支持 HTML 标签 + 例如:"使用正则表达式匹配字符串"''' + api_frontend: dict + """API 前端显示的信息,自动生成""" + + def api_get_arguments(self, args_def: Iterable[Argument]) -> dict[str, typing.Any]: + """获取 API 的所有参数""" + args: dict[str, typing.Any] = {} + + for arg in args_def: + init = arg.init + + if arg.multi: + vs = self.get_arguments(arg.name) + if not vs: + if arg.required: + raise ApiError(400, f"参数 {arg.name} 不能为空") + vs: Iterable[str] = arg.default # type: ignore + value = [] + for v in vs: + if not isinstance(v, arg.type): + v = init(v) + value.append(v) + elif arg.from_body: + value = init(self.request.body.decode()) + else: + value = self.get_argument(arg.name, arg.default) # type: ignore + if value is None and arg.required: + log = f"参数 {arg.name} 不能为空" + raise ApiError(400, log) + if value is not None and not isinstance(value, arg.type): + value = init(value) + args[arg.name] = value + + return args + + def api_write(self, data): + if data is None: + # 空 + return + elif isinstance(data, typing.Dict): + if len(data) == 1: + # 如果只有一个键值对,直接返回值 + # 不递归处理,默认 API 不会返回过于复杂的类型 + self.write(tuple(data.values())[0]) + else: + # 如果有多个键值对,返回 JSON + self.api_write_json(data) + elif isinstance(data, str): + # str 类型直接返回 + self.write(data) + elif isinstance(data, (int, float)): + # 简单类型转为 str 返回 + self.write(str(data)) + elif isinstance(data, bytes): + self.set_header("Content-Type", "application/octet-stream") + self.write(data) + else: + # 其他类型转换为 JSON + self.api_write_json(data) + + def api_write_json(self, data: dict[str, typing.Any], ensure_ascii=False, indent=4): + """将 json 数据写入响应""" + self.set_header("Content-Type", "application/json; charset=UTF-8") + self.write(json.dumps(data, ensure_ascii=ensure_ascii, indent=indent)) + + +def api_wrap( + # func: Callable | None = None, + # *, + arguments: Iterable[Argument] = [], + example: dict[str, BaseType | Iterable[BaseType]] = {}, + example_display: str = "", + display: bool = True, +): + """设置 API 参数、示例、说明等""" + + def decorate(func: Callable): + async def wrapper(self: "ApiBase") -> None: + args: Iterable[Argument] = wrapper.api["arguments"] + kwargs = self.api_get_arguments(args) + + ret = await func(self, **kwargs) + filters = self.get_arguments("__filter__") + if filters and isinstance(ret, dict): + filtered = {k: ret[k] for k in filters if k in ret} + else: + filtered = ret + self.api_write(filtered) + + # 生成 example url + nonlocal example_display + if not example_display and example: + kv = [] + for arg in arguments: + arg: Argument + k = arg.name + if k not in example: + if arg.required: + raise ValueError(f'api example: "{k}" is required') + continue + if arg.multi: + e = example[k] + if not isinstance(e, (list, tuple)): + raise ValueError(f'api example: "{k}" should be list or tuple') + for v in e: + kv.append(f"{k}={v}") + else: + kv.append(f"{k}={example[k]}") + example_display = "&".join(kv) + + # 保存 api 信息 + wrapper.api = { + "arguments": arguments, + "example": example_display, + "display": display, + } + + return wrapper + + return decorate + + +def load_all_api() -> tuple[list[ApiBase], list[tuple[str, ApiBase]]]: + handlers: list[tuple[str, ApiBase]] = [] + apis: list[ApiBase] = [] + + path = os.path.dirname(__file__) + for finder, name, ispkg in pkgutil.iter_modules([path]): + module = importlib.import_module("." + name, __name__) + if not hasattr(module, "handlers"): + continue + apis.extend(module.handlers) + for handler in module.handlers: + handlers.append((handler.api_url, handler)) + + return apis, handlers + + +# apis 是给 about.py 看的,用于生成前端页面 +# handlers 是给 handlers 看的,用于注册路由 +# 其实可以合并 +apis, handlers = load_all_api() diff --git a/web/handlers/api/example.py b/web/handlers/api/example.py new file mode 100644 index 00000000000..b59cfcd7bf7 --- /dev/null +++ b/web/handlers/api/example.py @@ -0,0 +1,40 @@ +from . import ApiBase, Argument, api_wrap + + +class Echo1(ApiBase): + api_name = "回声" + api_description = "输出 = 输入" + api_url = "echo1" + + @api_wrap( + arguments=(Argument(name="text", description="输入的文字", required=True),), + example={"text": "hello world"}, + ) + async def get(self, text: str): + return text + + post = get + + +class Echo2(ApiBase): + api_name = "回声" + api_description = "输出 = 输入" + api_url = "echo2" + + @api_wrap( + arguments=(Argument(name="text", description="输入的文字", required=True),), + example={"text": "hello world"}, + ) + async def get(self, text: str): + return text + + @api_wrap( + arguments=( + Argument(name="text", description="输入的文字", required=True, from_body=True), + ), + ) + async def post(self, text: str): + return text + + +handlers = (Echo1, Echo2) diff --git a/web/tpl/about.html b/web/tpl/about.html index aaf85a85162..dbccf4b91cb 100644 --- a/web/tpl/about.html +++ b/web/tpl/about.html @@ -9,18 +9,107 @@ {% block body %} {{ utils.header(current_user) }} -{% raw %} +
-

- 常用API -

+ +

+ API +

+ + + + + + + + + + + + + + + + + + + + + + {% for i in range(apis | length) %} + {% set api = apis[i] %} + {% set rowspan = api.api_frontend['rowspan'] %} + {% set methods = api.api_frontend['methods'] %} + + + + + {% for name, method in methods.items() %} + {% set argv = method['arguments'] %} + {% set args = argv|length %} + + + + {% if args > 0 %} + + + + + + + + {% else %} + + + + + + + + {% endif %} + + + + {% if args > 1 %} + {% for j in range(1, args) %} + + + + + + + + + + {% endfor %} + {% endif %} + {% endfor %} + + {% endfor %} + + +
名称地址说明方法参数用例
名称描述类型默认必须多重From body
{{ api.api_name }} + api:/{{ api.api_url }} + + {{ api.api_description | safe }}{{ name }}{{ argv[0].name }}{{ argv[0].description | safe }}{{ argv[0].type_display }}{{ argv[0].default_display }}{{ "✅" if argv[0].required else " " }}{{ "✅" if argv[0].multi else " " }}{{ "✅" if argv[0].from_body else " " }}        + api:/{{ method['example'] }} + + +
{{ api.api_arguments[j].name }}{{ api.api_arguments[j].description | safe }}{{ api.api_arguments[j].type_display }}{{ api.api_arguments[j].default_display }}{{ "✅" if api.api_arguments[j].required else " " }}{{ "✅" if api.api_arguments[j].multi else " " }}{{ "✅" if api.api_arguments[j].from_body else " " }}
+ +{% raw %} + +

+ 旧.常用API +

+ From 0e6de9506449f3dfefd3c11fc7a9e5c9b74e5f39 Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Wed, 19 Apr 2023 16:22:28 +0800 Subject: [PATCH 02/11] API examples --- web/handlers/api/__init__.py | 4 +- web/handlers/api/example.py | 220 ++++++++++++++++++++++++++++++++++- web/tpl/about.html | 18 +-- 3 files changed, 225 insertions(+), 17 deletions(-) diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index d6fbfa9c36e..2df1b3021d4 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -38,7 +38,7 @@ class Argument(object): description: str '''参数描述 例如:"正则表达式"''' - type: "type" = str + type: typing.Type = str """参数类型,默认为 str 设置了 multi 时,type 描述的是列表内元素的类型""" type_display: str | None = None @@ -204,7 +204,6 @@ def api_wrap( arguments: Iterable[Argument] = [], example: dict[str, BaseType | Iterable[BaseType]] = {}, example_display: str = "", - display: bool = True, ): """设置 API 参数、示例、说明等""" @@ -246,7 +245,6 @@ async def wrapper(self: "ApiBase") -> None: wrapper.api = { "arguments": arguments, "example": example_display, - "display": display, } return wrapper diff --git a/web/handlers/api/example.py b/web/handlers/api/example.py index b59cfcd7bf7..e7e612e1f71 100644 --- a/web/handlers/api/example.py +++ b/web/handlers/api/example.py @@ -1,10 +1,49 @@ -from . import ApiBase, Argument, api_wrap +import json +import traceback +import typing +import config +from . import ApiBase, Argument, api_wrap, ApiError + + +# API 插件设计准则: +# - 路径为 /api/name(旧版保持原先的 /util/name) +# - 使用 HTTP 状态码标示状态,而不是 Rtv["状态"] = "200" +# - 调用 API 时,缺少必须参数会自动返回 HTTP 400 错误 +# - 处理请求时,未被处理的异常会自动返回 HTTP 500 错误 +# - 访问未实现的 API method 会自动返回 HTTP 405 错误。如只实现了 GET 时访问 POST。 +# 如果 POST 和 GET 实现完全相同,可以在 get 函数后写上 post = get +# - 建议使用 `raise ApiError(status_code, reason)` 设置异常代码和原因(见 ExampleErrorHandler) +# - 允许 URL 传参(url?key=value)和 POST form 传参,不允许 /delay/value 形式传参(即不允许在 URL 中使用正则), +# 不建议使用 POST JSON 传参(见 JSONHandler) +# - 参数尽量使用简单类型,参数的初始化函数尽量使用内置函数,使用 safe_eval 代替 eval,避免使用 safe_eval +# - 所有的 key 都使用 ASCII 字符,而不是中英文混用 +# - 返回值:简单类型直接返回(str、int、float); +# dict 只有一对键值对的直接返回值,多条返回值的转为 JSON 格式; +# bytes 类型会设置 Content-Type: application/octet-stream 头,然后直接返回; +# 其他情形都转为 JSON 格式。 +# 如果希望避免不可控,可以将返回值处理为 str 类型。 +# 支持传参 `__filter__` 对返回 dict 进行过滤,过滤后只剩一对的,直接返回值。 +# 例: +# ``` +# > api://api/timestamp +# < {"timestamp": 1625068800, "weak": "4/26", day: "182/165", ...} +# +# > api://api/timestamp?__fileter__=timestamp +# < 1625068800 +# +# > api://api/timestamp?__fileter__=timestamp&__fileter__=weak +# < {"timestamp": 1625068800, "weak": "4/26"} +# ``` +# - 其他规范见 ApiBase 和 Argument 源码 + + +# 最简单的 API 示例 class Echo1(ApiBase): api_name = "回声" - api_description = "输出 = 输入" - api_url = "echo1" + api_description = "输出 = 输入" # 支持 HTML 标签,比如
+ api_url = "echo1" # 框架会自动添加前缀 /api/ @api_wrap( arguments=(Argument(name="text", description="输入的文字", required=True),), @@ -16,8 +55,9 @@ async def get(self, text: str): post = get +# get 和 post 使用不同参数的示例 class Echo2(ApiBase): - api_name = "回声" + api_name = "回声 2" api_description = "输出 = 输入" api_url = "echo2" @@ -28,6 +68,7 @@ class Echo2(ApiBase): async def get(self, text: str): return text + # 不提供 example 的示例 @api_wrap( arguments=( Argument(name="text", description="输入的文字", required=True, from_body=True), @@ -37,4 +78,173 @@ async def post(self, text: str): return text -handlers = (Echo1, Echo2) +# __filter__ 和 直接设置 example_display 的示例 +class Echon(ApiBase): + api_name = "回声 n" + api_description = "输出 = 输入*n" + api_url = "echon" + + @api_wrap( + arguments=( + Argument(name="text", required=True, description="输入", type=str), + Argument(name="n", required=True, description="n", type=int), + ), + example_display="text=测试输入&n=3&__filter__=text_0", + ) + async def get(self, text: str, n: int): + d = {f"text_{i}": text for i in range(n)} + return d + + +# 用于演示 multi 的示例 +class Concat(ApiBase): + api_name = "连接" + api_description = "输出 = sep.join(text)" + api_url = "concat" + + @api_wrap( + arguments=( + Argument( + name="texts", required=True, description="输入", type=str, multi=True + ), + Argument(name="sep", required=True, description="n", type=str), + ), + example={"texts": ["1", "2", "9"], "sep": ","}, + ) + async def get(self, texts: list[str], sep: str): + return sep.join(texts) + + +# 用于演示 multi 的示例 API:Sum +class Sum(ApiBase): + api_name = "累加" + api_description = "输出 = sum(输入)" + api_url = "sum" + + @api_wrap( + arguments=( + Argument( + name="input", required=True, description="输入", type=int, multi=True + ), + ), + example={"input": [1, 2, 9]}, + ) + async def get(self, input: list[int]): + return sum(input) + + +# 复杂类型的示例 +class Example(ApiBase): + class ArgType(object): + def __init__(self, s: str): # 构造函数参数必须是一个 str + ss = s.split(",")[-1] + self.v = int(ss) + + api_name = "复杂参数类型" + api_description = "输出 = class(obj)" + api_url = "exam2" + + @api_wrap( + arguments=( + Argument(name="obj", required=True, description="输入", type=ArgType), + ), + example={"obj": "1,2,3,4,5,6,7,8,9"}, + ) + async def get(self, obj: ArgType): + return obj.v + + +# 异常示例 +class Error(ApiBase): + api_name = "异常" + api_description = "引发异常的示例" + api_url = "error" + + @api_wrap( + arguments=( + Argument("code", False, "错误代码", int, default=400), + Argument("reason", False, "错误原因", str, default="测试错误"), + ), + example={"code": 400, "reason": "测试错误"}, + ) + async def get(self, code: int, reason: str): + if code == 999: + eee = 9 / 0 # 会直接返回 500,前端不显示详细报错原因,控制台打印完整报错信息 + elif code == 998: + 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) + + +# from_body 和 api_write_json 的示例 +class Json(ApiBase): + api_name = "json echo" + api_description = "POST JSON 传参示例" + api_url = "json" + + @api_wrap( + arguments=( + Argument( + name="data", + required=True, + description="输入 JSON", + type=typing.Union[dict, list, int, float, bool, None], + type_display="json", + init=json.loads, + ), + ), + # example={"data": {"a": 1, "b": 2}}, # 直接传 dict 的话,Python 将其转化为 str 时,会优先使用单引号,和 JSON 不兼容 + example={"data": '{"a": 1, "b": 2}'}, + ) + async def get(self, data: dict): + return data + + @api_wrap( + arguments=( + Argument( + name="data", + required=True, + description="输入 JSON", + type=typing.Union[dict, list, int, float, bool, None], + type_display="json", + init=json.loads, + from_body=True, + ), + Argument( + name="indent", required=False, description="缩进", type=int, default=4 + ), + ), + example={"data": '{"a": 1, "b": 2}', "indent": 2}, + ) + async def post(self, data: dict, indent: int): + self.api_write_json( + data, indent=indent + ) # 使用 self.api_write_json() 不会受到 __filter__ 影响 + + +# 如何使用传统方法实现 API(不建议使用 +class Echo0(ApiBase): + api_name = "回声" + api_description = "输出 = 输入" # 支持 HTML 标签,比如
+ api_url = "echo0" # 框架会自动添加前缀 /api/ + + async def get(self): + # 使用原生 tornado API,详细参考 tornado 文档 + text = self.get_argument("text", '') + self.write(text) + + # 此处的声明只用于生成前端文档 + get.api = { + 'arguments': (Argument(name="text", description="输入的文字", required=True),), + 'example': 'text=hello world', + } + + post = get + + +handlers = (Echo1, Echo2, Echon, Concat, Sum, Example, Json, Echo0) diff --git a/web/tpl/about.html b/web/tpl/about.html index dbccf4b91cb..5cc5c21bccd 100644 --- a/web/tpl/about.html +++ b/web/tpl/about.html @@ -35,8 +35,8 @@

- + @@ -64,8 +64,8 @@

- + {% else %} @@ -87,13 +87,13 @@

{% if args > 1 %} {% for j in range(1, args) %}

- - - - - - - + + + + + + + {% endfor %} {% endif %} From bcebbaf9a8a7035d31f858ff49d8721a1da6eee4 Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Fri, 21 Apr 2023 20:27:17 +0800 Subject: [PATCH 03/11] =?UTF-8?q?API=20plugin=20=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/handlers/about.py | 13 +++- web/handlers/api/__init__.py | 126 +++++++++++++++++++++++------------ web/handlers/api/example.py | 33 +++++---- web/tpl/about.html | 8 +-- 4 files changed, 112 insertions(+), 68 deletions(-) diff --git a/web/handlers/about.py b/web/handlers/about.py index 18c9a553e06..ccbd517f9af 100644 --- a/web/handlers/about.py +++ b/web/handlers/about.py @@ -7,14 +7,21 @@ from .base import * from . import api +from .api import MultiArgument, BodyArgument class AboutHandler(BaseHandler): @tornado.web.addslash async def get(self): - await self.render('about.html', apis=api.apis) + await self.render( + "about.html", + apis=api.apis, + ismulti=lambda x: isinstance(x, MultiArgument), + isbody=lambda x: isinstance(x, BodyArgument), + ) return + handlers = [ - ('/about/?', AboutHandler), - ] + ("/about/?", AboutHandler), +] diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index 2df1b3021d4..8a5f816e300 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -28,7 +28,7 @@ def __init__(self, status_code: int, reason: str, *args, **kwargs): @dataclass() -class Argument(object): +class ArgumentBase(object): name: str '''参数名称 例如:"regex"''' @@ -43,15 +43,11 @@ class Argument(object): 设置了 multi 时,type 描述的是列表内元素的类型""" type_display: str | None = None """参数类型在前端的显示值,默认为 self.type.__name__""" - init: Callable[[str], typing.Any] = None # type: ignore + init: Callable[[str | bytes], typing.Any] = None # type: ignore """参数初始化函数,初始化规则如下: 如果用户未提供且 self.type Callable,则使用 self.type; 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 该初始化函数原型为 init(str) -> self.type,例如 int("123"), float("123.456")等""" - multi: bool = False - """是否为多值参数,比如 ?a=1&a=2&a=3 -> a=[1,2,3]""" - # multi 为 True 时,参数类型为 list[self.type] - # multi 为 True 时,default 必须为 list 或 tuple(如果 default 为 None,会被自动转换为空 tuple)""" default: BaseType | Iterable[BaseType] = None """参数默认值 如果设置了 multi,则 default 类型须为 Iterable[str|int|float|bool](默认为空 tuple); @@ -61,9 +57,6 @@ class Argument(object): API 被调用时,用户若为提供该参数,则使用 init(default) 作为参数值。""" default_display: str | None = None """默认值在前端的显示值,默认为 repr(self.default)""" - from_body: bool = False - """从 request.body 初始化,比如 POST JSON 情形 - 与 multi 互斥""" def __post_init__(self): if self.init is None: @@ -75,18 +68,83 @@ def __post_init__(self): self.default = None self.default_display = "❎" - if self.multi and self.default is None: - self.default = tuple() - if self.default_display is None: self.default_display = repr(self.default) - + if self.type_display is None: self.type_display = self.type.__name__ - if self.multi and self.from_body: - # multi 和 from_body 互斥 - raise ValueError(f"Argument {self.name} is multi but from_body") + def get_value(self, api: "ApiBase") -> typing.Any: + ... + + +@dataclass() +class Argument(ArgumentBase): + """URL Query 和 POST form 参数""" + + init: Callable[[str], typing.Any] = None # type: ignore + """参数初始化函数,初始化规则如下: + 如果用户未提供且 self.type Callable,则使用 self.type; + 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 + 该初始化函数原型为 init(str) -> self.type,例如 int("123"), float("123.456")等""" + + def get_value(self, api: "ApiBase") -> typing.Any: + + value = api.get_argument(self.name, self.default) # type: ignore + if value is None and self.required: + raise ApiError( + 400, f"API {api.api_name}({api.api_url}) 参数 {self.name} 不能为空" + ) + if value is not None and not isinstance(value, self.type): + value = self.init(value) + return value + + +@dataclass() +class MultiArgument(ArgumentBase): + """多值参数,比如 ?a=1&a=2&a=3 -> a=[1,2,3]""" + + init: Callable[[str], typing.Any] = None # type: ignore + """参数初始化函数,初始化规则如下: + 如果用户未提供且 self.type Callable,则使用 self.type; + 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 + 该初始化函数原型为 init(str) -> self.type,例如 int("123"), float("123.456")等""" + + def get_value(self, api: "ApiBase") -> tuple: + vs = api.get_arguments(self.name) + if not vs: + if self.required: + raise ApiError( + 400, f"API {api.api_name}({api.api_url}) 参数 {self.name} 不能为空" + ) + vs: Iterable[str] = self.default # type: ignore + r = [] + for v in vs: + if not isinstance(v, self.type): + v = self.init(v) + r.append(v) + return tuple(r) + + +@dataclass() +class BodyArgument(ArgumentBase): + """从 request.body 初始化,比如 POST JSON 情形 + 初始化函数原型为 init(bytes) -> self.type""" + + init: Callable[[bytes], typing.Any] = None # type: ignore + """参数初始化函数,初始化规则如下: + 如果用户未提供且 self.type Callable,则使用 self.type; + 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 + 该初始化函数原型为 init(bytes) -> self.type""" + + def __post_init__(self): + self.default = "" + self.default_display = "" + + return super().__post_init__() + + def get_value(self, api: "ApiBase"): + return self.init(api.request.body) class ApiMetaclass(type): @@ -136,34 +194,14 @@ class ApiBase(BaseHandler, metaclass=ApiMetaclass): api_frontend: dict """API 前端显示的信息,自动生成""" - def api_get_arguments(self, args_def: Iterable[Argument]) -> dict[str, typing.Any]: + def api_get_arguments( + self, args_def: Iterable[ArgumentBase] + ) -> dict[str, typing.Any]: """获取 API 的所有参数""" args: dict[str, typing.Any] = {} for arg in args_def: - init = arg.init - - if arg.multi: - vs = self.get_arguments(arg.name) - if not vs: - if arg.required: - raise ApiError(400, f"参数 {arg.name} 不能为空") - vs: Iterable[str] = arg.default # type: ignore - value = [] - for v in vs: - if not isinstance(v, arg.type): - v = init(v) - value.append(v) - elif arg.from_body: - value = init(self.request.body.decode()) - else: - value = self.get_argument(arg.name, arg.default) # type: ignore - if value is None and arg.required: - log = f"参数 {arg.name} 不能为空" - raise ApiError(400, log) - if value is not None and not isinstance(value, arg.type): - value = init(value) - args[arg.name] = value + args[arg.name] = arg.get_value(self) return args @@ -201,7 +239,7 @@ def api_write_json(self, data: dict[str, typing.Any], ensure_ascii=False, indent def api_wrap( # func: Callable | None = None, # *, - arguments: Iterable[Argument] = [], + arguments: Iterable[ArgumentBase] = [], example: dict[str, BaseType | Iterable[BaseType]] = {}, example_display: str = "", ): @@ -209,7 +247,7 @@ def api_wrap( def decorate(func: Callable): async def wrapper(self: "ApiBase") -> None: - args: Iterable[Argument] = wrapper.api["arguments"] + args: Iterable[ArgumentBase] = wrapper.api["arguments"] kwargs = self.api_get_arguments(args) ret = await func(self, **kwargs) @@ -225,13 +263,13 @@ async def wrapper(self: "ApiBase") -> None: if not example_display and example: kv = [] for arg in arguments: - arg: Argument + arg: ArgumentBase k = arg.name if k not in example: if arg.required: raise ValueError(f'api example: "{k}" is required') continue - if arg.multi: + if isinstance(arg, MultiArgument): e = example[k] if not isinstance(e, (list, tuple)): raise ValueError(f'api example: "{k}" should be list or tuple') diff --git a/web/handlers/api/example.py b/web/handlers/api/example.py index e7e612e1f71..a1e573065bd 100644 --- a/web/handlers/api/example.py +++ b/web/handlers/api/example.py @@ -4,7 +4,7 @@ import config -from . import ApiBase, Argument, api_wrap, ApiError +from . import ApiBase, Argument, MultiArgument, BodyArgument, api_wrap, ApiError # API 插件设计准则: @@ -71,10 +71,10 @@ async def get(self, text: str): # 不提供 example 的示例 @api_wrap( arguments=( - Argument(name="text", description="输入的文字", required=True, from_body=True), + BodyArgument(name="text", description="输入的文字", required=True, init= lambda x: x), ), ) - async def post(self, text: str): + async def post(self, text: bytes): return text @@ -96,7 +96,7 @@ async def get(self, text: str, n: int): return d -# 用于演示 multi 的示例 +# 用于演示 MultiArgument 的示例 class Concat(ApiBase): api_name = "连接" api_description = "输出 = sep.join(text)" @@ -104,8 +104,8 @@ class Concat(ApiBase): @api_wrap( arguments=( - Argument( - name="texts", required=True, description="输入", type=str, multi=True + MultiArgument( + name="texts", required=True, description="输入", type=str ), Argument(name="sep", required=True, description="n", type=str), ), @@ -115,7 +115,7 @@ async def get(self, texts: list[str], sep: str): return sep.join(texts) -# 用于演示 multi 的示例 API:Sum +# 用于演示 MultiArgument 的示例 API:Sum class Sum(ApiBase): api_name = "累加" api_description = "输出 = sum(输入)" @@ -123,8 +123,8 @@ class Sum(ApiBase): @api_wrap( arguments=( - Argument( - name="input", required=True, description="输入", type=int, multi=True + MultiArgument( + name="input", required=True, description="输入", type=int ), ), example={"input": [1, 2, 9]}, @@ -181,7 +181,7 @@ async def get(self, code: int, reason: str): raise ApiError(code, reason) -# from_body 和 api_write_json 的示例 +# BodyArgument 和 api_write_json 的示例 class Json(ApiBase): api_name = "json echo" api_description = "POST JSON 传参示例" @@ -206,14 +206,13 @@ async def get(self, data: dict): @api_wrap( arguments=( - Argument( + BodyArgument( name="data", required=True, description="输入 JSON", type=typing.Union[dict, list, int, float, bool, None], type_display="json", - init=json.loads, - from_body=True, + init=lambda x: json.loads(x.decode()), ), Argument( name="indent", required=False, description="缩进", type=int, default=4 @@ -235,13 +234,13 @@ class Echo0(ApiBase): async def get(self): # 使用原生 tornado API,详细参考 tornado 文档 - text = self.get_argument("text", '') + text = self.get_argument("text", "") self.write(text) - + # 此处的声明只用于生成前端文档 get.api = { - 'arguments': (Argument(name="text", description="输入的文字", required=True),), - 'example': 'text=hello world', + "arguments": (Argument(name="text", description="输入的文字", required=True),), + "example": "text=hello world", } post = get diff --git a/web/tpl/about.html b/web/tpl/about.html index 5cc5c21bccd..1d7bfcd18af 100644 --- a/web/tpl/about.html +++ b/web/tpl/about.html @@ -66,8 +66,8 @@

- - + + {% else %} @@ -92,8 +92,8 @@

- - + + {% endfor %} {% endif %} From 7de14c14cd432a72d26e8cb571dcd0f98b7f4dd7 Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Sat, 6 May 2023 14:38:20 +0800 Subject: [PATCH 04/11] api://api/xx -> api://v1/xx --- web/handlers/api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index 8a5f816e300..ee1238c3979 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -15,7 +15,7 @@ from libs.safe_eval import safe_eval -URL_PREFIX = "/api/" +URL_PREFIX = "/v1/" BaseType = typing.Union[str, int, float, bool, None] From 4522323b01f623f87c9cecd6ae17b58d5ef6daad Mon Sep 17 00:00:00 2001 From: a76yyyy Date: Sun, 7 May 2023 00:07:41 +0800 Subject: [PATCH 05/11] Update Error message display --- web/handlers/base.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/web/handlers/base.py b/web/handlers/base.py index 268683809f7..7c2c137fef4 100644 --- a/web/handlers/base.py +++ b/web/handlers/base.py @@ -5,6 +5,10 @@ # http://binux.me # Created on 2012-12-15 16:16:38 +import json +import traceback +import typing + import jinja2 import tornado.web import tornado.websocket @@ -109,6 +113,37 @@ def check_permission(self, obj, mode='r'): self.evil(+5) raise HTTPError(401) return obj + + def write_error(self, status_code: int, **kwargs: typing.Any) -> None: + """Override to implement custom error pages. + + ``write_error`` may call `write`, `render`, `set_header`, etc + to produce output as usual. + + If this error was caused by an uncaught exception (including + HTTPError), an ``exc_info`` triple will be available as + ``kwargs["exc_info"]``. Note that this exception may not be + the "current" exception for purposes of methods like + ``sys.exc_info()`` or ``traceback.format_exc``. + """ + self.set_header("Content-Type", "application/json; charset=UTF-8") + trace = traceback.format_exception(*kwargs["exc_info"]) + if self.settings.get("serve_traceback") and "exc_info" in kwargs: + # in debug mode, try to send a traceback + # self.set_header("Content-Type", "text/plain; charset=UTF-8") + # for line in traceback.format_exception(*kwargs["exc_info"]): + # self.write(line) + + data = json.dumps({"code": status_code, "message": self._reason, "data": trace}, ensure_ascii=False, indent=4) + else: + self.set_header("Content-Type", "application/json; charset=UTF-8") + data = json.dumps({"code": status_code, "message": self._reason, "data": ""}, ensure_ascii=False, indent=4) + if config.traceback_print: + traceback.print_exception(*kwargs["exc_info"]) + if len(kwargs["exc_info"]) > 1: + logger_Web_Handler.debug(str(kwargs["exc_info"][1])) + self.write(data) + self.finish() class BaseWebSocket(tornado.websocket.WebSocketHandler): pass From 90d549d85b3d07ae20c4ff48c0a9be1102de84af Mon Sep 17 00:00:00 2001 From: a76yyyy Date: Sun, 7 May 2023 00:07:52 +0800 Subject: [PATCH 06/11] Fix bug from codeQL --- web/handlers/__init__.py | 3 +-- web/handlers/about.py | 4 ++-- web/handlers/api/__init__.py | 29 +++++++++++++++++------------ web/handlers/api/example.py | 20 ++++++++++---------- web/tpl/about.html | 4 ++-- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/web/handlers/__init__.py b/web/handlers/__init__.py index b2a7e1f8812..863e2e3b236 100644 --- a/web/handlers/__init__.py +++ b/web/handlers/__init__.py @@ -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 diff --git a/web/handlers/about.py b/web/handlers/about.py index ccbd517f9af..d414b263a61 100644 --- a/web/handlers/about.py +++ b/web/handlers/about.py @@ -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): diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index ee1238c3979..9add5d59d05 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -1,19 +1,18 @@ -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/" @@ -21,10 +20,12 @@ 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() @@ -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) @@ -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(' + api_description = "输出 = 输入" # 不支持 HTML 标签,比如
api_url = "echo1" # 框架会自动添加前缀 /api/ @api_wrap( @@ -50,7 +52,7 @@ class Echo1(ApiBase): example={"text": "hello world"}, ) async def get(self, text: str): - return text + return escape(text) post = get @@ -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( @@ -75,7 +77,7 @@ async def get(self, text: str): ), ) async def post(self, text: bytes): - return text + return escape(text) # __filter__ 和 直接设置 example_display 的示例 @@ -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) @@ -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)) # 此处的声明只用于生成前端文档 get.api = { @@ -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) diff --git a/web/tpl/about.html b/web/tpl/about.html index 1d7bfcd18af..0f495242e87 100644 --- a/web/tpl/about.html +++ b/web/tpl/about.html @@ -50,7 +50,7 @@

@@ -79,7 +79,7 @@

{% endif %}

From 8002a666d70c60ab05bf214d36c487b0e034bfad Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Wed, 17 May 2023 16:21:55 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E5=BC=BA=E5=88=B6=20JSON=20=E7=9A=84=20A?= =?UTF-8?q?PI=20=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/handlers/api/__init__.py | 108 ++++----- web/handlers/api/example.py | 448 +++++++++++++++++------------------ 2 files changed, 271 insertions(+), 285 deletions(-) diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index 9add5d59d05..149cef210bf 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -20,12 +20,21 @@ class ApiError(HTTPError): - def __init__(self, status_code: int, reason: str, log_message :str | None = None, *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 设置为相同的内容 if log_message is None: log_message = reason - super().__init__(status_code, log_message=log_message, reason=reason, *args, **kwargs) + super().__init__( + status_code, log_message=log_message, reason=reason, *args, **kwargs + ) @dataclass() @@ -154,6 +163,7 @@ def __new__(cls, name, bases, attrs): if name == "ApiBase": return super().__new__(cls, name, bases, attrs) + # 设置各种属性 attrs["api_url"] = URL_PREFIX + attrs["api_url"] t = defaultdict(list) @@ -178,8 +188,15 @@ def __new__(cls, name, bases, attrs): api_frontend["methods"] = methods attrs["api_frontend"] = api_frontend + _cls: ApiBase = super().__new__(cls, name, bases, attrs) # type: ignore - return super().__new__(cls, name, bases, attrs) + # 注册 API,定义即注册 + global apis + global handlers + apis.append(_cls) + handlers.append((attrs["api_url"], _cls)) + + return _cls class ApiBase(BaseHandler, metaclass=ApiMetaclass): @@ -194,6 +211,20 @@ class ApiBase(BaseHandler, metaclass=ApiMetaclass): 例如:"使用正则表达式匹配字符串"''' api_frontend: dict """API 前端显示的信息,自动生成""" + api_json_ascii = Argument( + name="__ascii__", + type=bool, + default=False, + required=False, + description="返回 JSON 是否强制 ASCII 编码(非 ASCII 字符会被转义为\\uxxxx形式)", + ) + api_json_indent = Argument( + name="__indent__", + type=int, + default=4, + required=False, + description="返回 JSON 缩进空格数,0 表示不缩进但是会换行,-1 表示不缩进也不换行", + ) def api_get_arguments( self, args_def: Iterable[ArgumentBase] @@ -207,48 +238,26 @@ def api_get_arguments( return args def api_write(self, data): - if data is None: - # 空 - return - elif isinstance(data, typing.Dict): - if len(data) == 1: - # 如果只有一个键值对,直接返回值 - # 不递归处理,默认 API 不会返回过于复杂的类型 - self.write(str(tuple(data.values())[0])) - else: - # 如果有多个键值对,返回 JSON - self.api_write_json(data) - elif isinstance(data, str): - # str 类型直接返回 - self.write(data) - elif isinstance(data, (int, float)): - # 简单类型转为 str 返回 - self.write(str(data)) - elif isinstance(data, bytes): - self.set_header("Content-Type", "application/octet-stream") - self.write(data) - else: - # 其他类型转换为 JSON - self.api_write_json(data) - - 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") - 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(' None: @@ -256,12 +265,7 @@ async def wrapper(self: "ApiBase") -> None: kwargs = self.api_get_arguments(args) ret = await func(self, **kwargs) - filters = self.get_arguments("__filter__") - if filters and isinstance(ret, dict): - filtered = {k: ret[k] for k in filters if k in ret} - else: - filtered = ret - self.api_write(filtered) + self.api_write(ret) # 生成 example url nonlocal example_display @@ -295,23 +299,15 @@ async def wrapper(self: "ApiBase") -> None: return decorate -def load_all_api() -> tuple[list[ApiBase], list[tuple[str, ApiBase]]]: - handlers: list[tuple[str, ApiBase]] = [] - apis: list[ApiBase] = [] - +def load_all_api(): path = os.path.dirname(__file__) for finder, name, ispkg in pkgutil.iter_modules([path]): module = importlib.import_module("." + name, __name__) - if not hasattr(module, "handlers"): - continue - apis.extend(module.handlers) - for handler in module.handlers: - handlers.append((handler.api_url, handler)) - - return apis, handlers + # 注册 API 的工作交给元类做了,定义即注册 # apis 是给 about.py 看的,用于生成前端页面 # handlers 是给 handlers 看的,用于注册路由 -# 其实可以合并 -apis, handlers = load_all_api() +apis: list[ApiBase] = [] +handlers: list[tuple[str, ApiBase]] = [] +load_all_api() diff --git a/web/handlers/api/example.py b/web/handlers/api/example.py index f7bfc78fdb1..f091c605f30 100644 --- a/web/handlers/api/example.py +++ b/web/handlers/api/example.py @@ -6,244 +6,234 @@ import config from ..base import logger_Web_Handler -from . import (ApiBase, ApiError, Argument, BodyArgument, MultiArgument, - api_wrap) +from . import ApiBase, ApiError, Argument, BodyArgument, MultiArgument, api_wrap # API 插件设计准则: -# - 路径为 /api/name(旧版保持原先的 /util/name) -# - 使用 HTTP 状态码标示状态,而不是 Rtv["状态"] = "200" -# - 调用 API 时,缺少必须参数会自动返回 HTTP 400 错误 -# - 处理请求时,未被处理的异常会自动返回 HTTP 500 错误 -# - 访问未实现的 API method 会自动返回 HTTP 405 错误。如只实现了 GET 时访问 POST。 +# - 路径为 /v1/name(旧版保持原先的 /util/name) +# - 返回内容强制为 JSON 格式,格式为:{"code": 200, "message": "ok", "data": "xxx"} +# 一些常见错误对应的 code: +# - 调用 API 时,缺少必须参数会自动返回 400 错误 +# - 未被处理的异常会自动返回 500 错误 +# - 访问未实现的 API method 会自动返回 405 错误。如只实现了 GET 时访问 POST。 # 如果 POST 和 GET 实现完全相同,可以在 get 函数后写上 post = get -# - 建议使用 `raise ApiError(status_code, reason)` 设置异常代码和原因(见 ExampleErrorHandler) +# - 建议使用 `raise ApiError(status_code, reason)` 设置异常代码和原因 # - 允许 URL 传参(url?key=value)和 POST form 传参,不允许 /delay/value 形式传参(即不允许在 URL 中使用正则), -# 不建议使用 POST JSON 传参(见 JSONHandler) # - 参数尽量使用简单类型,参数的初始化函数尽量使用内置函数,使用 safe_eval 代替 eval,避免使用 safe_eval # - 所有的 key 都使用 ASCII 字符,而不是中英文混用 -# - 返回值:简单类型直接返回(str、int、float); -# dict 只有一对键值对的直接返回值,多条返回值的转为 JSON 格式; -# bytes 类型会设置 Content-Type: application/octet-stream 头,然后直接返回; -# 其他情形都转为 JSON 格式。 -# 如果希望避免不可控,可以将返回值处理为 str 类型。 -# 支持传参 `__filter__` 对返回 dict 进行过滤,过滤后只剩一对的,直接返回值。 -# 例: -# ``` -# > api://api/timestamp -# < {"timestamp": 1625068800, "weak": "4/26", day: "182/165", ...} -# -# > api://api/timestamp?__fileter__=timestamp -# < 1625068800 -# -# > api://api/timestamp?__fileter__=timestamp&__fileter__=weak -# < {"timestamp": 1625068800, "weak": "4/26"} -# ``` -# - 其他规范见 ApiBase 和 Argument 源码 - - -# 最简单的 API 示例 -class Echo1(ApiBase): - api_name = "回声" - api_description = "输出 = 输入" # 不支持 HTML 标签,比如
- api_url = "echo1" # 框架会自动添加前缀 /api/ - - @api_wrap( - arguments=(Argument(name="text", description="输入的文字", required=True),), - example={"text": "hello world"}, - ) - async def get(self, text: str): - return escape(text) - - post = get - - -# get 和 post 使用不同参数的示例 -class Echo2(ApiBase): - api_name = "回声 2" - api_description = "输出 = 输入" - api_url = "echo2" - - @api_wrap( - arguments=(Argument(name="text", description="输入的文字", required=True),), - example={"text": "hello world"}, - ) - async def get(self, text: str): - return escape(text) - - # 不提供 example 的示例 - @api_wrap( - arguments=( - BodyArgument(name="text", description="输入的文字", required=True, init= lambda x: x), - ), - ) - async def post(self, text: bytes): - return escape(text) - - -# __filter__ 和 直接设置 example_display 的示例 -class Echon(ApiBase): - api_name = "回声 n" - api_description = "输出 = 输入*n" - api_url = "echon" - - @api_wrap( - arguments=( - Argument(name="text", required=True, description="输入", type=str), - Argument(name="n", required=True, description="n", type=int), - ), - example_display="text=测试输入&n=3&__filter__=text_0", - ) - async def get(self, text: str, n: int): - d = {f"text_{i}": text for i in range(n)} - return d - - -# 用于演示 MultiArgument 的示例 -class Concat(ApiBase): - api_name = "连接" - api_description = "输出 = sep.join(text)" - api_url = "concat" - - @api_wrap( - arguments=( - MultiArgument( - name="texts", required=True, description="输入", type=str - ), - Argument(name="sep", required=True, description="n", type=str), - ), - example={"texts": ["1", "2", "9"], "sep": ","}, - ) - async def get(self, texts: list[str], sep: str): - return sep.join(texts) - - -# 用于演示 MultiArgument 的示例 API:Sum -class Sum(ApiBase): - api_name = "累加" - api_description = "输出 = sum(输入)" - api_url = "sum" - - @api_wrap( - arguments=( - MultiArgument( - name="input", required=True, description="输入", type=int - ), - ), - example={"input": [1, 2, 9]}, - ) - async def get(self, input: list[int]): - return sum(input) - - -# 复杂类型的示例 -class Example(ApiBase): - class ArgType(object): - def __init__(self, s: str): # 构造函数参数必须是一个 str - ss = s.split(",")[-1] - self.v = int(ss) - - api_name = "复杂参数类型" - api_description = "输出 = class(obj)" - api_url = "exam2" - - @api_wrap( - arguments=( - Argument(name="obj", required=True, description="输入", type=ArgType), - ), - example={"obj": "1,2,3,4,5,6,7,8,9"}, - ) - async def get(self, obj: ArgType): - return obj.v - - -# 异常示例 -class Error(ApiBase): - api_name = "异常" - api_description = "引发异常的示例" - api_url = "error" - - @api_wrap( - arguments=( - Argument("code", False, "错误代码", int, default=400), - Argument("reason", False, "错误原因", str, default="测试错误"), - ), - example={"code": 400, "reason": "测试错误"}, - ) - async def get(self, code: int, reason: str): - if code == 999: - eee = 9 / 0 # 会直接返回 500,前端不显示详细报错原因,控制台打印完整报错信息 - elif code == 998: - try: - eee = 9 / 0 - except ZeroDivisionError as e: - raise ApiError(500, str(e)) # 还是返回 500,但是前端有报错原因,控制台打印简短信息 - else: - raise ApiError(code, reason) -# BodyArgument 和 api_write_json 的示例 -class Json(ApiBase): - api_name = "json echo" - api_description = "POST JSON 传参示例" - api_url = "json" - - @api_wrap( - arguments=( - Argument( - name="data", - required=True, - description="输入 JSON", - type=typing.Union[dict, list, int, float, bool, None], - type_display="json", - init=json.loads, +if config.debug: + # 示例 API,仅在 debug 模式下可见 + + # 最简单的 API 示例 + class Echo1(ApiBase): + api_name = "回声" + api_description = "输出 = 输入" # 不支持 HTML 标签,比如
+ 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 + + post = get + + # get 和 post 使用不同参数的示例 + class Echo2(ApiBase): + api_name = "回声 2" + api_description = "输出 = 输入" + api_url = "echo2" + + @api_wrap( + arguments=(Argument(name="text", description="输入的文字", required=True),), + example={"text": "hello world"}, + ) + async def get(self, text: str): + return text + + # 不提供 example 的示例 + @api_wrap( + arguments=( + BodyArgument( + name="text", description="输入的文字", required=True, init=lambda x: x + ), ), - ), - # example={"data": {"a": 1, "b": 2}}, # 直接传 dict 的话,Python 将其转化为 str 时,会优先使用单引号,和 JSON 不兼容 - example={"data": '{"a": 1, "b": 2}'}, - ) - async def get(self, data: dict): - return data - - @api_wrap( - arguments=( - BodyArgument( - name="data", - required=True, - description="输入 JSON", - type=typing.Union[dict, list, int, float, bool, None], - type_display="json", - init=lambda x: json.loads(x.decode()), + ) + async def post(self, text: bytes): + return text + + # 直接设置 example_display 的示例 + class Echon(ApiBase): + api_name = "回声 n" + api_description = "输出 = 输入*n" + api_url = "echon" + + @api_wrap( + arguments=( + Argument(name="text", required=True, description="输入", type=str), + Argument(name="n", required=True, description="n", type=int), ), - Argument( - name="indent", required=False, description="缩进", type=int, default=4 + example_display="text=测试输入&n=3", + ) + async def get(self, text: str, n: int): + d = {f"text_{i}": text for i in range(n)} + return d + + # 用于演示 MultiArgument 的示例 + class Concat(ApiBase): + api_name = "连接" + api_description = "输出 = sep.join(text)" + api_url = "concat" + + @api_wrap( + arguments=( + MultiArgument(name="texts", required=True, description="输入", type=str), + Argument(name="sep", required=True, description="n", type=str), ), - ), - example={"data": '{"a": 1, "b": 2}', "indent": 2}, - ) - async def post(self, data: dict, indent: int): - self.api_write_json( - data, indent=indent - ) # 使用 self.api_write_json() 不会受到 __filter__ 影响 - - -# 如何使用传统方法实现 API(不建议使用 -class Echo0(ApiBase): - api_name = "回声" - api_description = "输出 = 输入" # 支持 HTML 标签,比如
- api_url = "echo0" # 框架会自动添加前缀 /api/ - - async def get(self): - # 使用原生 tornado API,详细参考 tornado 文档 - text = self.get_argument("text", "") - self.write(escape(text)) - - # 此处的声明只用于生成前端文档 - get.api = { - "arguments": (Argument(name="text", description="输入的文字", required=True),), - "example": "text=hello world", - } - - post = get - + example={"texts": ["1", "2", "9"], "sep": ","}, + ) + async def get(self, texts: list[str], sep: str): + return sep.join(texts) + + # 用于演示 MultiArgument 的示例 API:Sum + class Sum(ApiBase): + api_name = "累加" + api_description = "输出 = sum(输入)" + api_url = "sum" + + @api_wrap( + arguments=( + MultiArgument(name="input", required=True, description="输入", type=int), + ), + example={"input": [1, 2, 9]}, + ) + async def get(self, input: list[int]): + return sum(input) + + # 复杂类型的示例 + class Example(ApiBase): + class ArgType(object): + def __init__(self, s: str): # 构造函数参数必须是一个 str + ss = s.split(",")[-1] + self.v = int(ss) + + api_name = "复杂参数类型" + api_description = "输出 = class(obj)" + api_url = "exam2" + + @api_wrap( + arguments=( + Argument(name="obj", required=True, description="输入", type=ArgType), + ), + example={"obj": "1,2,3,4,5,6,7,8,9"}, + ) + async def get(self, obj: ArgType): + return obj.v + + # 异常示例 + class ErrorCode(ApiBase): + api_name = "异常" + api_description = "引发异常的示例" + api_url = "error" + + @api_wrap( + arguments=( + Argument("code", False, "错误代码", int, default=400), + Argument("reason", False, "错误原因", str, default="测试错误"), + ), + example={"code": 400, "reason": "测试错误"}, + ) + async def get(self, code: int, reason: str): + raise ApiError(code, reason) -handlers = (Echon, Concat, Sum, Example, Json) + # 自动异常示例 + class Error2(ApiBase): + api_name = "异常" + api_description = "引发异常的示例" + api_url = "error2" + + @api_wrap( + arguments=(Argument("index", False, "索引,", int, default=0),), + example={"index": 0}, + ) + async def get(self, index: int): + match index: + case 0: + x = 0 / 0 + case 1: + try: + x = 0 / 0 + except ZeroDivisionError as e: + raise ApiError(500, str(e)) + case 2: + x = [0, 1, 2][9] + case 3: + x = {0: 9}[9] + case 4: + return bytes("JSON 无法处理 bytes", "utf-8") + case _: + raise ApiError(418, "I'm a teapot") + + # BodyArgument 和 api_write_json 的示例 + class Json(ApiBase): + api_name = "json echo" + api_description = "POST JSON 传参示例" + api_url = "json" + + @api_wrap( + arguments=( + Argument( + name="data", + required=True, + description="输入 JSON", + type=typing.Union[dict, list, int, float, bool, None], + type_display="json", + init=json.loads, + ), + ), + example={ + "data": '{"a": 1, "b": 2}',# {"a": 1, "b": 2}}, # 直接传 dict 的话,Python 将其转化为 str 时,会优先使用单引号,和 JSON 不兼容 + '__ascii__': True, # 每个 API 都会自动添加的参数,用于指示返回的 JSON 是否强制 ASCII 编码(非ASCII字符会被转为 \uXXXX 形式) + '__indent__': 2}, # 每个 API 都会自动添加的参数,用于指示返回的 JSON 的缩进,0 表示不缩进但是会换行,-1 表示不缩进也不换行 + ) + async def get(self, data: dict): + return data + + @api_wrap( + arguments=( + BodyArgument( + name="data", + required=True, + description="输入 JSON", + type=typing.Union[dict, list, int, float, bool, None], + type_display="json", + init=lambda x: json.loads(x.decode()), + ), + ), + example={"data": '{"a": 1, "b": 2}'}, + ) + async def post(self, data: dict): + return data + + # 如何使用传统方法实现 API(不建议使用 + class Echo0(ApiBase): + api_name = "回声" + api_description = "输出 = 输入" # 支持 HTML 标签,比如
+ api_url = "echo0" # 框架会自动添加前缀 /api/ + + async def get(self): + # 使用原生 tornado API,详细参考 tornado 文档 + text = self.get_argument("text", "") + self.write( + escape(text) + ) # 传统方法,框架没有帮忙设置 Content-Type,需要手动 escape,以避免 XSS 攻击 + + # 此处的声明只用于生成前端文档 + get.api = { + "arguments": (Argument(name="text", description="输入的文字", required=True),), + "example": "text=hello world", + } + + post = get From e255dea0d1e502104be28e1a8b2ba5b6262d13bd Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Wed, 24 May 2023 14:32:05 +0800 Subject: [PATCH 08/11] =?UTF-8?q?BodyArgument=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=A7=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/handlers/api/__init__.py | 27 ++++++++++++++++++++++++--- web/handlers/api/example.py | 2 ++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index 149cef210bf..495ed36279e 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -141,11 +141,15 @@ class BodyArgument(ArgumentBase): """从 request.body 初始化,比如 POST JSON 情形 初始化函数原型为 init(bytes) -> self.type""" - init: Callable[[bytes], typing.Any] = None # type: ignore + init: Callable[[bytes | str], typing.Any] = None # type: ignore """参数初始化函数,初始化规则如下: 如果用户未提供且 self.type Callable,则使用 self.type; 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 - 该初始化函数原型为 init(bytes) -> self.type""" + 该初始化函数原型为 init(bytes) -> self.type + + request.body 默认是 bytes 类型,框架会尝试根据 Content-Type 进行解码, + 但不保证一定能解码成功,所以建议手动判断一下参数类型。 + """ def __post_init__(self): self.default = "" @@ -153,8 +157,25 @@ def __post_init__(self): return super().__post_init__() + def try_decode(self, api: "ApiBase") -> str | bytes: + type = api.request.headers.get("Content-Type", "").lower() + if "charset=" not in type: + charset = "utf-8" + else: + start = type.index("charset=") + len("charset=") + try: + end = type.index(";", start) + except ValueError: + end = -1 + charset = type[start:end] + + try: + return api.request.body.decode(charset) + except UnicodeDecodeError: + return api.request.body + def get_value(self, api: "ApiBase"): - return self.init(api.request.body) + return self.init(self.try_decode(api)) class ApiMetaclass(type): diff --git a/web/handlers/api/example.py b/web/handlers/api/example.py index f091c605f30..aec4df78c4f 100644 --- a/web/handlers/api/example.py +++ b/web/handlers/api/example.py @@ -19,6 +19,8 @@ # - 建议使用 `raise ApiError(status_code, reason)` 设置异常代码和原因 # - 允许 URL 传参(url?key=value)和 POST form 传参,不允许 /delay/value 形式传参(即不允许在 URL 中使用正则), # - 参数尽量使用简单类型,参数的初始化函数尽量使用内置函数,使用 safe_eval 代替 eval,避免使用 safe_eval +# - 普通参数类型默认为 str,multi 参数类型默认为 list[str], +# Body 参数默认为 bytes|str,框架会尝试根据 Content-Type 进行解码,但不保证一定成功,所以建议在 init 中手动检查类型 # - 所有的 key 都使用 ASCII 字符,而不是中英文混用 From ad3579e1688573eb30085c298bd0740684367baa Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Wed, 7 Jun 2023 16:47:22 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20BodyArgument=20?= =?UTF-8?q?=E7=9A=84=E8=87=AA=E5=8A=A8=E8=A7=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/handlers/api/__init__.py | 59 +++++++++++++++++++++++++++--------- web/handlers/base.py | 5 +++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/web/handlers/api/__init__.py b/web/handlers/api/__init__.py index 495ed36279e..00b4881e087 100644 --- a/web/handlers/api/__init__.py +++ b/web/handlers/api/__init__.py @@ -145,10 +145,13 @@ class BodyArgument(ArgumentBase): """参数初始化函数,初始化规则如下: 如果用户未提供且 self.type Callable,则使用 self.type; 如果用户未提供且 self.type 不是 Callable,则使用 lambda x: x。 - 该初始化函数原型为 init(bytes) -> self.type + 该初始化函数原型为 init(bytes | str) -> self.type - request.body 默认是 bytes 类型,框架会尝试根据 Content-Type 进行解码, - 但不保证一定能解码成功,所以建议手动判断一下参数类型。 + request.body 默认是 bytes 类型,在调用 init 函数前,框架会尝试根据 Content-Type 进行解码: + 有 charset 则使用 charset 解码, + 没有 charset 或 Content-Type 则尝试使用 utf-8 解码, + application/octet-stream 或 charset=raw 则不会进行尝试解码, + 尝试解码失败则返回 bytes 类型,成功则返回 str,所以建议手动判断一下参数类型。 """ def __post_init__(self): @@ -158,19 +161,19 @@ def __post_init__(self): return super().__post_init__() def try_decode(self, api: "ApiBase") -> str | bytes: - type = api.request.headers.get("Content-Type", "").lower() - if "charset=" not in type: - charset = "utf-8" - else: - start = type.index("charset=") + len("charset=") - try: - end = type.index(";", start) - except ValueError: - end = -1 - charset = type[start:end] + encoding = "utf-8" + if header := api.request.headers.get("Content-Type", ""): + content_type, parms = parse_content_type_header(header) + if content_type == "application/octet-stream": + encoding = "raw" + elif "cahrset" in parms: + encoding = parms["charset"] + + if encoding == "raw": + return api.request.body try: - return api.request.body.decode(charset) + return api.request.body.decode(encoding) except UnicodeDecodeError: return api.request.body @@ -327,6 +330,34 @@ def load_all_api(): # 注册 API 的工作交给元类做了,定义即注册 +# requests.utils._parse_content_type_header +# https://github.com/psf/requests/blob/6e5b15d542a4e85945fd72066bb6cecbc3a82191/requests/utils.py#L513-L535 +# 下划线开头的,还是自己复制一份比较靠谱 +def parse_content_type_header(header): + """Returns content type and parameters from given header + + :param header: string + :return: tuple containing content type and dictionary of + parameters + """ + + tokens = header.split(";") + content_type, params = tokens[0].strip(), tokens[1:] + params_dict = {} + items_to_strip = "\"' " + + for param in params: + param = param.strip() + if param: + key, value = param, True + index_of_equals = param.find("=") + if index_of_equals != -1: + key = param[:index_of_equals].strip(items_to_strip) + value = param[index_of_equals + 1 :].strip(items_to_strip) + params_dict[key.lower()] = value + return content_type, params_dict + + # apis 是给 about.py 看的,用于生成前端页面 # handlers 是给 handlers 看的,用于注册路由 apis: list[ApiBase] = [] diff --git a/web/handlers/base.py b/web/handlers/base.py index 7c2c137fef4..fc2f083e3d3 100644 --- a/web/handlers/base.py +++ b/web/handlers/base.py @@ -28,6 +28,11 @@ class BaseHandler(tornado.web.RequestHandler): application_export = set(('db', 'fetcher')) db:DB # db = DB() + + def __init__(self, *args, **kwargs): + super(BaseHandler, self).__init__(*args, **kwargs) + self.settings['serve_traceback'] = config.traceback_print + def __getattr__(self, key): if key in self.application_export: return getattr(self.application, key) From 48d186230e7ec7521ba454018c3383a7690ea8e2 Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Wed, 7 Jun 2023 16:48:25 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E5=9C=A8=20jinja=20=E4=B8=AD=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=20json=20=E7=9B=B8=E5=85=B3=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libs/utils.py b/libs/utils.py index 4c324716e48..8c53c82be58 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -13,6 +13,7 @@ import struct import urllib import uuid +import json import jinja2 from Crypto.Cipher import AES @@ -797,6 +798,13 @@ def _aes_decrypt(word:str, key:str, mode='CBC', iv:str=None, input_format='base6 mode = switch_mode(mode) return aes_decrypt(word.encode("utf-8"), key.encode("utf-8"), mode=mode, iv=iv.encode("utf-8"), input=input_format, padding=padding, padding_style=padding_style, no_packb=no_packb) + +def json_parse(data): + return json.loads(data) + +def json_stringify(data): + return json.dumps(data) + jinja_globals = { # types 'quote_chinese': quote_chinese, @@ -840,6 +848,9 @@ def _aes_decrypt(word:str, key:str, mode='CBC', iv:str=None, input_format='base6 # random stuff 'random': random_fliter, 'shuffle': randomize_list, + # json + 'json_parse': json_parse, + 'json_stringify': json_stringify, # undefined 'mandatory': mandatory, # debug From 0c9738284f760214f08180561cb527493020f865 Mon Sep 17 00:00:00 2001 From: Cirn09 Date: Wed, 7 Jun 2023 17:08:06 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E9=81=97=E7=95=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rename BaseWebSocket -> BaseWebSocketHandler rename BaseHandler -> _BaseHandler remove about.html {% raw %} --- web/handlers/__init__.py | 4 ++-- web/handlers/base.py | 2 +- web/tpl/about.html | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/handlers/__init__.py b/web/handlers/__init__.py index 863e2e3b236..f0ae272d99d 100644 --- a/web/handlers/__init__.py +++ b/web/handlers/__init__.py @@ -15,7 +15,7 @@ def load_modules(): handlers: list[tuple[str, base.BaseHandler]] = [] ui_modules: dict[str, base.BaseUIModule] = {} - ui_methods: dict[str, base.BaseWebSocket] = {} + ui_methods: dict[str, base.BaseWebSocketHandler] = {} path = os.path.join(os.path.dirname(__file__), "") for finder, name, ispkg in pkgutil.iter_modules([path]): @@ -32,6 +32,6 @@ def load_modules(): handlers: list[tuple[str, base.BaseHandler]] ui_modules: dict[str, base.BaseUIModule] -ui_methods: dict[str, base.BaseWebSocket] +ui_methods: dict[str, base.BaseWebSocketHandler] handlers, ui_modules, ui_methods = load_modules() diff --git a/web/handlers/base.py b/web/handlers/base.py index 005c6ddef3e..b07b46f532f 100644 --- a/web/handlers/base.py +++ b/web/handlers/base.py @@ -29,7 +29,7 @@ class _BaseHandler(tornado.web.RequestHandler): db:DB def __init__(self, *args, **kwargs): - super(BaseHandler, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.settings['serve_traceback'] = config.traceback_print def __getattr__(self, key): diff --git a/web/tpl/about.html b/web/tpl/about.html index d961469d908..63043a7bb67 100644 --- a/web/tpl/about.html +++ b/web/tpl/about.html @@ -9,7 +9,6 @@ {% block body %} {{ utils.header(current_user, title="常用API/过滤器", sup=False, about=False) }} -{% raw %}
API名称 描述 类型默认 必须默认 多重 From body
{{ argv[0].name }} {{ argv[0].description | safe }} {{ argv[0].type_display }}{{ argv[0].default_display }} {{ "✅" if argv[0].required else " " }}{{ argv[0].default_display }} {{ "✅" if argv[0].multi else " " }} {{ "✅" if argv[0].from_body else " " }}
{{ api.api_arguments[j].name }}{{ api.api_arguments[j].description | safe }}{{ api.api_arguments[j].type_display }}{{ api.api_arguments[j].default_display }}{{ "✅" if api.api_arguments[j].required else " " }}{{ "✅" if api.api_arguments[j].multi else " " }}{{ "✅" if api.api_arguments[j].from_body else " " }}{{ argv[j].name }}{{ argv[j].description | safe }}{{ argv[j].type_display }}{{ "✅" if argv[j].required else " " }}{{ argv[j].default_display }}{{ "✅" if argv[j].multi else " " }}{{ "✅" if argv[j].from_body else " " }}
{{ argv[0].type_display }} {{ "✅" if argv[0].required else " " }} {{ argv[0].default_display }}{{ "✅" if argv[0].multi else " " }}{{ "✅" if argv[0].from_body else " " }}{{ "✅" if ismulti(argv[0]) else " " }}{{ "✅" if isbody(argv[0]) else " " }}   {{ argv[j].type_display }} {{ "✅" if argv[j].required else " " }} {{ argv[j].default_display }}{{ "✅" if argv[j].multi else " " }}{{ "✅" if argv[j].from_body else " " }}{{ "✅" if ismulti(argv[j]) else " " }}{{ "✅" if isbody(argv[j]) else " " }}
{{ api.api_name }} - api:/{{ api.api_url }} + api:/{{ api.api_url }} {{ api.api_description | safe }} - api:/{{ method['example'] }} + api:/{{ method['example'] }}