使用 Bottle 框架构建 Rest API
已发表: 2022-03-11REST API 已成为在 Web 后端和前端之间以及不同 Web 服务之间建立接口的常用方法。 这种接口的简单性,以及对 HTTP 和 HTTPS 协议在不同网络和框架中的普遍支持,使其在考虑互操作性问题时成为一个简单的选择。
Bottle 是一个极简的 Python Web 框架。 它轻量、快速且易于使用,非常适合构建 RESTful 服务。 Andriy Kornatskyy 进行的基本比较将其列为响应时间和吞吐量(每秒请求数)的前三名框架。 在我自己对 DigitalOcean 提供的虚拟服务器的测试中,我发现 uWSGI 服务器堆栈和 Bottle 的组合可以实现低至每个请求 140μs 的开销。
在本文中,我将介绍如何使用 Bottle 构建 RESTful API 服务。
安装和配置
Bottle 框架之所以能取得令人印象深刻的性能,部分原因在于其重量轻。 事实上,整个库是作为一个文件模块分发的。 这意味着它不像其他框架那样牵手,但它也更灵活,可以适应许多不同的技术堆栈。 因此,Bottle 最适合那些性能和可定制性非常重要的项目,并且不太需要考虑更多重型框架的省时优势。
Bottle 的灵活性使得对设置平台的深入描述有点徒劳,因为它可能无法反映您自己的堆栈。 但是,这里适合快速浏览这些选项,以及在哪里了解有关如何设置它们的更多信息:
安装
安装 Bottle 就像安装任何其他 Python 包一样简单。 您的选择是:
- 使用系统的包管理器在您的系统上安装。 Debian Jessie(当前稳定版)将 0.12 版打包为python-bottle 。
- 使用 Python 包索引和
pip install bottle
在您的系统上安装。 - 在虚拟环境中安装(推荐)。
要在虚拟环境中安装 Bottle,您需要virtualenv和pip工具。 要安装它们,请参阅 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
而已。 瓶子已安装并可以使用。 如果您不熟悉virtualenv或pip ,他们的文档是一流的。 看一看! 他们非常值得。
服务器
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 中的路由是使用装饰器完成的。 导入的装饰器post
、 get
、 put
和delete
为这四个操作注册处理程序。 了解这些工作如何分解如下:
- 以上所有装饰器都是
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
好吧,让我们实现其中的一些处理程序。
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 修饰了所有方法,因此无需进行任何进一步的设置。 我们的方法已经到位,可以访问。
您可以使用 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 参考文档来加深对这个小而强大的框架的了解。