使用 Bottle 框架构建 Rest API

已发表: 2022-03-11

REST API 已成为在 Web 后端和前端之间以及不同 Web 服务之间建立接口的常用方法。 这种接口的简单性,以及对 HTTP 和 HTTPS 协议在不同网络和框架中的普遍支持,使其在考虑互操作性问题时成为一个简单的选择。

Bottle 是一个极简的 Python Web 框架。 它轻量、快速且易于使用,非常适合构建 RESTful 服务。 Andriy Kornatskyy 进行的基本比较将其列为响应时间和吞吐量(每秒请求数)的前三名框架。 在我自己对 DigitalOcean 提供的虚拟服务器的测试中,我发现 uWSGI 服务器堆栈和 Bottle 的组合可以实现低至每个请求 140μs 的开销。

在本文中,我将介绍如何使用 Bottle 构建 RESTful API 服务。

Bottle:一个快速、轻量级的 Python Web 框架

安装和配置

Bottle 框架之所以能取得令人印象深刻的性能,部分原因在于其重量轻。 事实上,整个库是作为一个文件模块分发的。 这意味着它不像其他框架那样牵手,但它也更灵活,可以适应许多不同的技术堆栈。 因此,Bottle 最适合那些性能和可定制性非常重要的项目,并且不太需要考虑更多重型框架的省时优势。

Bottle 的灵活性使得对设置平台的深入描述有点徒劳,因为它可能无法反映您自己的堆栈。 但是,这里适合快速浏览这些选项,以及在哪里了解有关如何设置它们的更多信息:

安装

安装 Bottle 就像安装任何其他 Python 包一样简单。 您的选择是:

  • 使用系统的包管理器在您的系统上安装。 Debian Jessie(当前稳定版)将 0.12 版打包为python-bottle
  • 使用 Python 包索引和pip install bottle在您的系统上安装。
  • 在虚拟环境中安装(推荐)。

要在虚拟环境中安装 Bottle,您需要virtualenvpip工具。 要安装它们,请参阅 virtualenv 和 pip 文档,尽管您可能已经在系统上安装了它们。

在 Bash 中,使用 Python 3 创建一个环境:

 $ virtualenv -p `which python3` env

抑制-p `which python3`参数将导致安装系统上存在的默认 Python 解释器——通常是 Python 2.7。 支持 Python 2.7,但本教程假定使用 Python 3.4。

现在激活环境并安装 Bottle:

 $ . env/bin/activate $ pip install bottle

而已。 瓶子已安装并可以使用。 如果您不熟悉virtualenvpip ,他们的文档是一流的。 看一看! 他们非常值得。

服务器

Bottle 符合 Python 的标准 Web 服务器网关接口 (WSGI),这意味着它可以与任何 WSGI 兼容的服务器一起使用。 这包括 uWSGI、Tornado、Gunicorn、Apache、Amazon Beanstalk、Google App Engine 等。

设置它的正确方法因每​​个环境而略有不同。 Bottle 暴露了一个符合 WSGI 接口的对象,服务器必须配置为与这个对象交互。

要了解有关如何设置服务器的更多信息,请参阅服务器的文档和 Bottle 的文档,请点击此处。

数据库

Bottle 与数据库无关,不关心数据来自哪里。 如果您想在应用程序中使用数据库,Python 包索引有几个有趣的选项,例如 SQLAlchemy、PyMongo、MongoEngine、CouchDB 和 Boto for DynamoDB。 您只需要适当的适配器即可使其与您选择的数据库一起使用。

Bottle 框架基础知识

现在,让我们看看如何在 Bottle 中制作一个基本的应用程序。 对于代码示例,我将假设 Python >= 3.4。 但是,我将在这里写的大部分内容也适用于 Python 2.7。

Bottle 中的一个基本应用程序如下所示:

 import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

当我说基本时,我的意思是这个程序甚至没有“Hello World”你。 (您最后一次访问回答“Hello World”的 REST 接口是什么时候?)所有对127.0.0.1:8000的 HTTP 请求都将收到 404 Not Found 响应状态。

瓶中应用

Bottle 可能创建了多个应用程序实例,但为了方便起见,为您创建了第一个实例; 这是默认应用程序。 Bottle 将这些实例保存在模块内部的堆栈中。 每当您使用 Bottle 执行某些操作(例如运行应用程序或附加路由)并且未指定您正在谈论的应用程序时,它指的是默认应用程序。 事实上, app = application = bottle.default_app()行甚至不需要在这个基本应用程序中存在,但它存在以便我们可以轻松地使用 Gunicorn、uWSGI 或一些通用 WSGI 服务器调用默认应用程序。

多个应用程序的可能性起初可能看起来令人困惑,但它们增加了 Bottle 的灵活性。 对于应用程序的不同模块,您可以通过实例化其他 Bottle 类并根据需要使用不同的配置对其进行设置来创建专门的 Bottle 应用程序。 通过 Bottle 的 URL 路由器,可以通过不同的 URL 访问这些不同的应用程序。 我们不会在本教程中深入研究,但我们鼓励您在此处和此处查看 Bottle 的文档。

服务器调用

脚本的最后一行使用指定的服务器运行 Bottle。 如果没有指明服务器,就像这里的情况,默认服务器是 Python 内置的 WSGI 参考服务器,只适合开发目的。 可以像这样使用不同的服务器:

 bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)

这是语法糖,让您通过运行此脚本来启动应用程序。 例如,如果此文件名为main.py ,您可以简单地运行python main.py来启动应用程序。 Bottle 提供了相当多的可以以这种方式使用的服务器适配器列表。

一些 WSGI 服务器没有 Bottle 适配器。 这些可以使用服务器自己的运行命令启动。 例如,在 uWSGI 上,您所要做的就是像这样调用uwsgi

 $ uwsgi --http :8000 --wsgi-file main.py

关于文件结构的说明

Bottle 让您的应用程序的文件结构完全由您决定。 我发现我的文件结构策略会随着项目的发展而变化,但往往基于 MVC 理念。

构建您的 REST API

当然,没有人需要只为每个请求的 URI 返回 404 的服务器。 我已经向你保证我们会构建一个 REST API,所以让我们开始吧。

假设您想构建一个操作一组名称的接口。 在一个真实的应用程序中,您可能会为此使用数据库,但对于这个示例,我们将只使用内存中的set数据结构。

我们的 API 的骨架可能看起来像这样。 您可以将此代码放在项目中的任何位置,但我的建议是单独的 API 文件,例如api/names.py

 from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass

路由

正如我们所见,Bottle 中的路由是使用装饰器完成的。 导入的装饰器postgetputdelete为这四个操作注册处理程序。 了解这些工作如何分解如下:

  • 以上所有装饰器都是default_app路由装饰器的快捷方式。 例如, @get()装饰器将bottle.default_app().get()应用于处理程序。
  • default_app上的路由方法都是route()的快捷方式。 所以default_app().get('/')等价于default_app().route(method='GET', '/')

所以@get('/')@route(method='GET', '/')一样,和@bottle.default_app().route(method='GET', '/')一样,并且这些可以互换使用。

@route装饰器的一个有用之处是,例如,如果您想使用相同的处理程序来处理对象更新和删除,您可以传递它处理的方法列表,如下所示:

 @route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass

好吧,让我们实现其中的一些处理程序。

使用 Bottle Framework 打造完美的 REST API。

RESTful API 是现代 Web 开发的主要内容。 使用 Bottle 后端为您的 API 客户提供有效的混合物。
鸣叫

POST:资源创建

我们的 POST 处理程序可能如下所示:

 import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})

嗯,这相当多。 让我们逐步回顾这些步骤。

身体解析

此 API 要求用户在正文中 POST 一个带有名为“name”的属性的 JSON 字符串。

之前从bottle中导入的request对象总是指向当前请求并保存所有请求的数据。 它的body属性包含请求体的字节流,任何能够读取流对象(如读取文件)的函数都可以访问该字节流。

request.json()方法检查“application/json”内容类型的请求标头,如果正确,则解析正文。 如果 Bottle 检测到格式错误的正文(例如:空或内容类型错误),则此方法返回None ,因此我们会引发ValueError 。 如果 JSON 解析器检测到格式错误的 JSON 内容; 它引发了我们捕获并重新引发的异常,再次作为ValueError

对象解析和验证

如果没有错误,我们已将请求的主体转换为data变量引用的 Python 对象。 如果我们收到带有“name”键的字典,我们将能够通过data['name']访问它。 如果我们收到一个没有这个键的字典,尝试访问它会导致一个KeyError异常。 如果我们收到的不是字典,我们会得到一个TypeError异常。 如果出现这些错误中的任何一个,我们再次将其重新引发为ValueError ,表示输入错误。

要检查名称键是否具有正确的格式,我们应该针对正则表达式掩码对其进行测试,例如我们在此处创建的namepattern掩码。 如果键name不是字符串, namepattern.match()将引发TypeError ,如果不匹配,则返回None

使用此示例中的掩码,名称必须是 ASCII 字母数字,不包含 1 到 64 个字符之间的空格。 这是一个简单的验证,例如,它不会测试带有垃圾数据的对象。 更复杂和更完整的验证可以通过使用诸如 FormEncode 之类的工具来实现。

测试存在

完成请求之前的最后一个测试是给定名称是否已存在于集合中。 在一个更结构化的应用程序中,该测试可能应该由一个专用模块完成,并通过一个专门的异常向我们的 API 发出信号,但是由于我们直接操作一个集合,所以我们必须在这里进行。

我们通过引发KeyError来表示名称的存在。

错误响应

正如请求对象保存所有请求数据一样,响应对象对响应数据执行相同的操作。 有两种设置响应状态的方法:

 response.status = 400

和:

 response.status = '400 Bad Request'

对于我们的示例,我们选择了更简单的形式,但第二种形式可用于指定错误的文本描述。 在内部,Bottle 将拆分第二个字符串并适当地设置数字代码。

成功响应

如果所有步骤都成功,我们通过将名称添加到集合_names 、设置Content-Type响应标头并返回响应来满足请求。 该函数返回的任何字符串都将被视为200 Success响应的响应主体,因此我们只需使用json.dumps生成一个。

GET:资源列表

从名称创建开始,我们将实现名称列表处理程序:

 @get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})

列出名称要容易得多,不是吗? 与创建名称相比,这里没什么可做的。 只需设置一些响应标头并返回所有名称的 JSON 表示,我们就完成了。

PUT:资源更新

现在,让我们看看如何实现更新方法。 和create方法差别不大,但是我们用这个例子来介绍URI参数。

 @put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})

更新操作的主体模式与创建操作相同,但现在我们在 URI 中还有一个新的oldname参数,由路由@put('/names/<oldname>')定义。

URI 参数

如您所见,Bottle 的 URI 参数表示法非常简单。 您可以使用任意数量的参数构建 URI。 Bottle 自动从 URI 中提取它们并将它们传递给请求处理程序:

 @get('/<param1>/<param2>') def handler(param1, param2): pass

使用级联路由装饰器,您可以使用可选参数构建 URI:

 @get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass

此外,Bottle 允许在 URI 中使用以下路由过滤器:

  • int

仅匹配可以转换为int的参数,并将转换后的值传递给处理程序:

 @get('/<param:int>') def handler(param): pass
  • float

int相同,但具有浮点值:

 @get('/<param:float>') def handler(param): pass
  • re (正则表达式)

仅匹配与给定正则表达式匹配的参数:

 @get('/<param:re:^[az]+$>') def handler(param): pass
  • path

以灵活的方式匹配 URI 路径的子段:

 @get('/<param:path>/id>') def handler(param): pass

火柴:

  • /x/id ,将x作为param传递。
  • /x/y/id ,将x/y作为param传递。

DELETE:资源删除

与 GET 方法一样,DELETE 方法给我们带来的消息也很少。 请注意,在不设置状态的情况下返回None会返回带有空正文和 200 状态代码的响应。

 @delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return

最后一步:激活 API

假设我们将名称 API 保存为api/names.py ,我们现在可以在主应用程序文件main.py中启用这些路由。

 import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

请注意,我们只导入了names模块。 由于我们已经使用附加到默认应用程序的 URI 修饰了所有方法,因此无需进行任何进一步的设置。 我们的方法已经到位,可以访问。

没有什么比制作精良的 REST API 更能让前端开心了。奇迹般有效!

您可以使用 Curl 或 Postman 等工具来使用 API 并手动对其进行测试。 (如果您使用的是 Curl,则可以使用 JSON 格式化程序来使响应看起来不那么混乱。)

奖励:跨域资源共享 (CORS)

构建 REST API 的一个常见原因是通过 AJAX 与 JavaScript 前端进行通信。 对于某些应用程序,应该允许这些请求来自任何域,而不仅仅是您的 API 的主域。 默认情况下,大多数浏览器都不允许这种行为,所以让我向您展示如何在 Bottle 中设置跨域资源共享 (CORS) 以允许这种行为:

 from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return

hook装饰器允许我们在每个请求之前或之后调用一个函数。 在我们的例子中,要启用 CORS,我们必须为每个响应设置Access-Control-Allow-Origin-Allow-Methods-Allow-Headers标头。 这些向请求者表明我们将提供指定的请求。

此外,客户端可能会向服务器发出 OPTIONS HTTP 请求,以查看它是否真的可以使用其他方法发出请求。 使用这个包罗万象的示例,我们使用 200 状态代码和空正文响应所有 OPTIONS 请求。

要启用此功能,只需将其保存并从主模块导入即可。

包起来

这里的所有都是它的!

在本教程中,我尝试介绍使用 Bottle Web 框架为 Python 应用程序创建 REST API 的基本步骤。

您可以通过访问它的教程和 API 参考文档来加深对这个小而强大的框架的了解。

相关:构建 Node.js/TypeScript REST API,第 1 部分:Express.js