跳转至

处理请求参数

API 请求可以使用多种方式携带参数信息,如

  • 路径参数
  • 查询参数
  • 请求体(JSON/表单/文件等)
  • 请求头(包括 Cookie)

我们将会一一介绍如何在 UtilMeta 中处理请求的各类参数

Tip

UtilMeta 中请求参数的声明基于 Python 标准的类型注解(类型提示)语法与 utype 库,如果你对 Python 类型注解语法还不熟悉,可以参考 utype - Python 类型用法 这篇文档

路径参数

在请求 URL 路径中传递数据是一种常见的方式,比如通过 GET /article/3 得到 ID 为 3 的文章数据,ID 的参数就是在 URL 路径中提供的,在 UtilMeta 中声明路径参数的方式如下

from utilmeta.core import api

class RootAPI(api.API):
    @api.get('article/{id}')
    def get_article(self, id: int):
        return {"id": id}

我们在 @api 装饰器的第一个参数传入路径的模板字符串,如例子中的 'article/{id}',使用花括号定义路径参数,并且在函数中声明一个同名的参数用来接收,并且可以声明期望的类型与规则

定义多个路径参数的用法类似

from utilmeta.core import api
import utype
from typing import Literal

class RootAPI(api.API):
    @api.get('doc/{lang}/{page}')
    def get_doc(self, 
               lang: Literal['en', 'zh'], 
               page: int = utype.Param(1, ge=1)):
        return {"lang": lang, "page": page}
在这个例子中我们声明了两个路径参数:

  • lang 只能在 'en''zh' 中取值
  • page 是一个大于等于 1 且默认为 1 的参数

有默认值的参数如果没有在路径中提供则会直接传入默认值,于是当我们请求 GET /doc/en 时我们会得到输出

{"lang": "en", "page": 1}

Tip

如果路径中缺少没有提供默认值的参数,则会返回 404 Notfound 响应,比如当访问 GET /doc

如果请求参数不满足声明的规则或者无法转化到对应的类型,则会得到 400 BadRequest 的错误响应,比如

  • GET /doc/fr/3lang 参数没有在 'en''zh' 中取值
  • GET /doc/en/0page 参数不符合大于等于一的规则

Warning

多个路径参数之间必须有字符串进行分割,'{category}{page}' 就是一个无效的路径参数字符串,因为路径参数之间无法进行区分

路径正则表达式

我们有时需要路径参数满足一定的规则,这时我们可以通过声明正则表达式轻松做到这一点,使用方式如下

from utilmeta.core import api, request

class RootAPI(API):
    @api.get('item/{code}')
    def get_item(self, code: str = request.PathParam(regex='[a-z]{1,9}')):
        return code

在 UtilMeta 的 request 模块中内置了一些常用的路径参数配置

  • request.PathParam:默认的路径参数规则,即 '[^/]+',匹配除了路径下划线外的所有字符
  • request.FilePathParam:匹配所有的字符串,包括路径下划线,常用于当路径参数需要传递文件路径,URL 路径等情况时
  • request.SlugPathParam:匹配类似 how-to-guide 这样由字母数据连接线组成的字符串,尝用于文章的 URL 编码中

以下是一个例子

from utilmeta.core import api, request

class RootAPI(API):
    @api.get('file/{path}')
    def get_file(self, path: str = request.FilePathParam):
        return open(f'/tmp/{path}', 'r')

在这个例子中当我们请求 GET /file/path/to/README.md 时,path 参数可以得到 'path/to/README.md' 这一路径

声明请求路径

上面的这些例子展示在 UtilMeta 中声明使用模板字符串声明请求路径的方式,但这并不是唯一的方式

在 UtilMeta 中,API 接口请求路径的声明规则为

  • @api装饰器中传入路径字符串,作为这个接口请求路径的模板
  • 没有路径字符串时,函数的名称将作为请求的路径
  • 当函数的名称为 HTTP 动词时,其路径将自动设为 '/' 并无法被覆盖

API 接口函数

在 API 类中使用 @api.<METHOD> 装饰器装饰的函数,或名称为 HTTP 动词(get/post/put/patch/delete)的函数,将会被处理为 API 接口函数,对外提供 HTTP 请求访问

下面的例子覆盖了以上的几种情况,可以很清晰地说明路径的声明规则

from utilmeta.core import api

@api.route('/article')
class ArticleAPI(api.API):
    @api.get
    def feed(self): pass
    # GET /article/feed

    @api.get('{slug}')
    def get_article(self, slug: str): pass
    # GET /article/{slug}

    def get(self, id: int): pass
    # GET /article?id=<ID>

路径匹配优先级

类似例子中同时声明了固定路径与可变的路径参数的接口时,需要把固定路径的接口声明放在上方,这样 UtilMeta 在匹配 /article/feed 请求时会先匹配到 feed 函数,而不是作为 slug 参数匹配到 get_article 函数

查询参数

使用查询参数传入键值对是很普遍的传参方式,比如通过 GET /article?id=3 得到 ID 为 3 的文章数据

查询参数声明的方式很简单,直接在函数中定义即可,如

from utilmeta.utils import *

class RootAPI(API):
    @api.get
    def doc(self, 
            lang: Literal['en', 'zh'], 
            page: int = utype.Param(1, ge=1)):
        return {"lang": lang, "page": page}

当我们请求 GET /doc?lang=en 时我们会得到输出

{"lang": "en", "page": 1}

Tip

在 API 函数中,查询参数是 默认 的参数类型,也就是说如果一个参数没有被路径模板定义,也没有指定为其他的参数类型的话,就会被处理成一个查询参数

参数别名

如果参数名称无法表示为一个 Python 变量(如语法关键字或含有特殊符号),则可以使用 utype.Param 组件的 alias 参数指定字段的期望名称,如

from utilmeta.core import api
import utype

class RootAPI(API):
    @api.get
    def doc(self,
            cls_name: str = utype.Param(alias='class'),
            page: int = utype.Param(1, alias='@page')
            ):
        return {cls_name: page}

当访问 GET /api/doc?class=tech&@page=3 时就会得到 {"tech": 3}

使用 Schema 类

除了在函数中声明查询参数外,你也可以将所有查询参数定义为一个 Schema 类,从而更好地组合与复用,用法如下

from utilmeta.core import api, request
import utype
from typing import Literal

class QuerySchema(utype.Schema):
    lang: Literal['en', 'zh']
    page: int = utype.Field(ge=1, default=1)

class RootAPI(API):
    @api.get
    def doc(self, query: QuerySchema = request.Query):
        return {"lang": query.lang, "page": query.page}

在例子中,我们将查询参数 langpage 定义在了 QuerySchema 中,然后使用 query: QuerySchema = request.Query 注入到 API 函数中

使用这种方式,你可以方便地使用类的继承和组合等方式在接口间复用查询参数

Warning

使用 Schema 类定义的查询参数必须指定 request.Query 作为函数参数的默认值,否则这个参数将视为查询参数中的一个字段,你需要请求 GET /api/doc?query={"lang":"en","page":3} 才能映射到对应的参数

请求体数据

请求体数据常用于在 POST / PUT / PATCH 方法传递 JSON 对象,表单或文件等数据

通常在 UtilMeta 中,你可以使用 Schema 类来声明 JSON 或表单格式的请求体数据,用法如下

from utilmeta.core import api, request
import utype

class LoginSchema(utype.Schema):
    username: str
    password: str
    remember: bool = False

class UserAPI(api.API):
    @api.post
    def login(self, data: LoginSchema = request.Body):
        pass
我们声明了一个名为 LoginSchema 的 Schema 类,作为请求体参数的类型提示,再使用 request.Body 作为参数的默认值标记这个参数是请求体参数

当你使用一个 Schema 类声明请求体,这个接口便拥有了处理 JSOM / XML 和表单数据的能力,比如你可以传递这样的 JSON 请求体

{
    "username": "alice",
    "password": "123abc",
    "remember": true
}

你也可以使用 application/x-www-form-urlencode 格式的请求体,语法类似于查询参数,如

username=alice&password=123abc&remember=true

如果你需要限定请求体的类型(Content-Type),你可以使用 request 包提供的更多的请求体参数

  • request.Json:请求体 Content-Type 需要为 application/json
  • request.Form:请求体 Content-Type 需要为 multipart/form-dataapplication/x-www-form-urlencoded

Tip

你也可以使用 request.Body 中的 content_type 字段,例如 request.Body(content_type='application/json')

列表数据

一些如批量创建与更新等场景需要上传列表类型的请求体数据,其声明方式就是在对应的 Schema 类外加上 List[] ,如

from utilmeta.core import api, orm, request
from .models import User

class UserAPI(api.API):
    class UserSchema(orm.Schema[User]):
        username: str = utype.Field(regex='[a-zA-Z0-9]{3,20}')
        password: str = utype.Field(min_length=6, max_length=20)

    @api.post
    def batch_create(self, users: List[UserSchema] = request.Body):
        for user in users:
            user.save()

客户端需要使用 JSON (application/json)类型的请求体传递列表数据,例如

[{
    "username": "alice",
    "password": "123abc"
}, {
    "username": "bob",
    "password": "XYZ789"
}]

Tip

如果客户端只传递了一个 JSON 对象或表单,那么将会被自动转化为只有这一个元素的列表

处理文件上传

如果你需要支持文件上传,只需要将文件字段的类型提示声明为文件即可,用法如下

from utilmeta.core import api, request, file
import utype

class FileAPI(api.API):
    class AvatarData(utype.Schema):
        user_id: int
        avatar: file.Image = utype.Field(max_length=10 * 1024 ** 2)

    @api.post
    def avatar(self, data: AvatarData = request.Body):
        pass

utilmeta.core.file 文件中提供了几种常用的文件类型,你可以用它们声明文件参数

  • File:接收任意类型的文件
  • Image:接收图片文件(image/*
  • Audio:接收音频文件(audio/*
  • Video:接收视频文件(video/*

另外,你可以使用规则参数中的 max_length 对文件的大小进行限制,例子中我们限制 avatar 只接收 10M 以下的文件

Tip

对于含有文件的表单,客户端需要传递 multipart/form-data 类型的数据

如果你需要支持上传多个文件,只需要为文件参数的类型声明外加上 List[] 即可,如

from utilmeta.core import api, request, file
import utype
from typing import List

class FileAPI(api.API):
    class FilesData(utype.Schema):
        name: str
        files: List[file.File] = utype.Field(max_length=10)

    @api.post
    def upload(self, data: FilesData = request.Body):
        for i, f in enumerate(data.files):
            f.save(f'/data/{data.name}-{i}')

单独上传文件

如果你希望客户端直接将整个二进制文件作为请求体,而不是使用嵌套在表单中的形式的话,只需要将文件参数指定为 request.Body 即可,如

from utilmeta.core import api, request, file
import utype

class FileAPI(api.API):
    @api.post
    def image(self, image: file.Image = request.Body) -> str:
        name = str(int(self.request.time.timestamp() * 1000)) + '.png'
        image.save(path='/data/image', name=name)

请求体参数

除了支持声明完整的请求体 Schema 外,你还可以使用 request.BodyParam 单独声明请求体中的字段

from utilmeta.core import api, request, file

class FileAPI(api.API):
    @api.post
    def upload(self, name: str = request.BodyParam,
               file: file.File = request.BodyParam):
        file.save(path='/data/files', name=name)

字符串与其他类型数据

如果你希望接口接受字符串等形式的请求体,也只需要为请求体指定对应的类型和 content_type 即可,如

from utilmeta.core import api, request

class ArticleAPI(api.API): 
    @api.post
    def content(self, html: str = request.Body(
        max_length=10000,
        content_type='text/html'
    )):
        pass
在这个函数中,我们使用 html 来指定和接收 'text/html' 类型的请求体,并且限定了请求体上传文本的最大长度为 10000

请求头参数

API 请求通常会携带请求头(HTTP Headers)来传递请求的元信息,如权限凭据,缓存协商,会话 Cookie 等,除了默认的请求头外,你也使用以下两种类自定义请求头

  • request.HeaderParam:声明单个请求头参数
  • request.Headers:声明完整的请求头 Schema
from utilmeta.core import api, request
import utype

class RootAPI(api.API):
    class HeaderSchema(utype.Schema):
        auth_token: str = utype.Field(length=12, alias='X-Auth-Token')
        meta: dict = utype.Field(alias='X-Meta-Data')

    @api.post
    def operation(self, headers: HeaderSchema = request.Headers):
        return [headers.auth_token, headers.meta]

一般来说自定义的请求头都以 X- 开头,并且使用连字符 - 连接单词,我们使用 alias 参数来指定请求头参数的名称,如示例中声明的请求头为

  • auth_token:目标请求头名称为 X-Auth-Token,长度为 12 的字符串
  • meta:目标请求头的名称为 X-Meta-Data,一个能被解析为字典的对象

Tip

请求头是 大小写不敏感 的,所以你使用 alias='X-Auth-Token'alias='x-auth-token' 声明的是一样的请求头

当请求

POST /api/operation HTTP/1.1

X-Auth-Token: OZ3tPOl6
X-Meta-Data: {"version":1.2}
就会得到 ["OZ3tPOl6", {"version": 1.2}] 的响应

Note

一般浏览器在发送含有自定义的请求头的请求之前,还会发送一个 OPTIONS 请求来检查自定义的请求头是否位于响应中的 Access-Control-Allow-Headers 所允许的范围内,不过不用担心,UtilMeta 会自动将你声明的请求头放入 OPTIONS 响应中

通用参数

常见的情况是,某个请求头需要在多个接口间复用,比如鉴权凭据,此时 UtilMeta 的 API 类提供了一种更简洁的方式:在 API 类中声明通用参数,用法如下

from utilmeta.core import api, request
import utype

class RootAPI(api.API):
    auth_token: str = request.HeaderParam(alias='X-Auth-Token')

    @api.post
    def operation(self):
        return self.auth_token

这样在 API 类中定义的所有接口都需要提供 X-Auth-Token 这一请求头参数,你还可以直接通过 self.auth_token 直接获取到对应的值

请求头的 cookie 字段可以携带一系列的键值参数用于和服务端保持会话或者携带凭据信息等,Cookie 参数有两种声明方式

  • request.CookieParam:声明单个 Cookie 参数
  • request.Cookies:声明完整的 Cookie 对象
from utilmeta.core import api, request

class RootAPI(api.API):
    sessionid: str = request.CookieParam(required=True)
    csrftoken: str = request.CookieParam(default=None)

    @api.post
    def operation(self):
        return [self.sessionid, self.csrftoken]

在这个例子中,我们在 RootAPI 类中声明了两个通用的 Cookie 参数,请求可按照如下方式传递 cookie 参数

POST /api/operation HTTP/1.1

Cookie: csrftoken=xxxx; sessionid=xxxx;

自定义请求参数

class QueryParam(RequestParam):
    __in__ = Query

    @classmethod
    def get_mapping(cls, request: Request):
        return request.query