Falcon 是一个简约的 ASGI/WSGI 框架,用于构建关键任务的 REST API 和微服务,重点关注大规模的可靠性、正确性和性能。
在构建 HTTP API 时,其他框架会因为大量依赖项和不必要的抽象而拖垮您。 Falcon 采用简洁的设计,拥抱 HTTP 和 REST 架构风格。
Falcon 应用程序可与任何 WSGI 或 ASGI 服务器配合使用,并且在 CPython 3.8+ 和 PyPy 3.8+ 下运行得像冠军一样。
“猎鹰坚如磐石,而且速度很快。”
“我们一直在使用 Falcon 作为[另一个框架]的替代品,我们只是喜欢它的性能(快三倍)和代码库大小(很容易是我们[原始]代码的一半)。”
“我喜欢#falconframework!超级干净简单,我终于拥有了我需要的速度和灵活性!”
“到目前为止,Falcon 看起来很棒。我对我的小型服务器进行了快速测试,只用了 20 分钟,速度就快了 40%。”
“我觉得我最后只是在谈论 HTTP,中间没有任何内容。Falcon 看起来像是后端的请求。”
“Falcon 的源代码非常好,我几乎更喜欢它而不是文档。它基本上不会错。”
“还有什么其他框架集成了对 786 的支持,现在就尝试一下?”
Falcon 尝试在保持高效的同时尽可能少做事。
asyncio
支持Falcon 是否帮助您制作了出色的应用程序?今天就通过一次性捐款或成为赞助人来表达您的支持。支持者可以获得炫酷的装备、向 Python 开发者推广其品牌的机会以及优先支持。
谢谢!
最终达到完美,不是当不再有任何东西可以添加时,而是当不再有任何东西可以删除时。
——安东尼·德·圣埃克苏佩里
我们设计 Falcon 是为了支持大规模微服务和响应式应用程序后端的苛刻需求。 Falcon 通过在您需要的地方提供裸机性能、可靠性和灵活性来补充更通用的 Python Web 框架。
可靠的。我们竭尽全力避免引入重大更改,当我们这样做时,它们会被完整记录,并且仅在主要版本增量的情况下引入(本着 SemVer 的精神)。该代码经过大量输入的严格测试,我们始终要求 100% 的覆盖率。 Falcon 没有标准库之外的依赖项,有助于最大限度地减少应用程序的攻击面,同时避免传递性错误和重大更改。
可调试。猎鹰避开魔法。很容易判断哪些输入导致哪些输出。未处理的异常永远不会被封装或屏蔽。潜在的令人惊讶的行为,例如自动请求正文解析,都有详细记录并默认禁用。最后,当谈到框架本身时,我们注意保持逻辑路径简单易懂。所有这些都使得在大规模部署中推理代码和调试边缘情况变得更加容易。
快速地。相同的硬件,更多的要求。 Falcon 处理请求的速度明显快于其他流行的 Python 框架(如 Django 和 Flask)。为了获得额外的速度提升,Falcon 在可用时使用 Cython 进行编译,并且也可以与 PyPy 很好地配合。考虑转向另一种编程语言?先用Falcon+PyPy进行基准测试!
灵活的。 Falcon 将许多决策和实施细节留给 API 开发人员。这为您提供了很大的自由来定制和调整您的实现。它还可以帮助您更深入地了解您的应用程序,从长远来看,使它们更容易调整、调试和重构。 Falcon 的极简设计为 Python 社区成员在 Falcon 附加组件和补充包上独立创新提供了空间。
Falcon 在世界各地被越来越多的组织使用,包括:
如果您将 Falcon 框架用于社区或商业项目,请考虑将您的信息添加到我们的 wiki 中的谁在使用 Falcon?
许多 Falcon 附加组件、模板和补充包可供您在项目中使用。我们在 Falcon wiki 上列出了其中的几个作为起点,但您可能还希望搜索 PyPI 以获取其他资源。
Gitter 上的 Falconry 社区是提问和分享想法的好地方。您可以在猎鹰/用户中找到我们。我们还有一个猎鹰/开发室来讨论框架本身的设计和开发。
根据我们的行为准则,我们希望参与社区讨论的每个人都能专业行事,并以身作则,鼓励建设性讨论。社区中的每个人都有责任创造积极、建设性和富有成效的文化。
PyPy 是运行 Falcon 应用程序的最快方式。从 PyPy v7.3.7+ 开始支持 PyPy3.8+。
$ pip install falcon
或者,安装最新的测试版或候选发布版(如果有):
$ pip install --pre falcon
Falcon 还完全支持 CPython 3.8+。
最新稳定版本的 Falcon 可以直接从 PyPI 安装:
$ pip install falcon
或者,安装最新的测试版或候选发布版(如果有):
$ pip install --pre falcon
为了提供额外的速度提升,Falcon 在任何符合 PEP 517 的安装程序下自动使用 Cython 进行编译。
为了您的方便,PyPI 提供了适用于大多数常见平台的包含预编译二进制文件的轮子。即使您选择的平台的二进制版本不可用, pip
也会选择纯 Python 轮。您还可以针对您的环境对 Falcon 进行 cythonize;有关此选项和其他高级选项的更多信息,请参阅我们的安装文档。
Falcon 不需要安装任何其他软件包。
Falcon 使用 WSGI(或 ASGI;另见下文)。为了提供 Falcon 应用程序,您将需要一个 WSGI 服务器。 Gunicorn 和 uWSGI 是其中一些比较流行的应用程序,但任何可以加载 WSGI 应用程序的应用程序都可以。
$ pip install [gunicorn | uwsgi]
为了提供 Falcon ASGI 应用程序,您将需要一个 ASGI 服务器。 Uvicorn 是一个受欢迎的选择:
$ pip install uvicorn
Falcon 位于 GitHub 上,使代码易于浏览、下载、fork 等。随时欢迎 Pull 请求!另外,如果该项目让您满意,请记得为该项目加注星标。 :)
克隆存储库或从 GitHub 下载 tarball 后,您可以像这样安装 Falcon:
$ cd falcon
$ pip install .
或者,如果您想编辑代码,请首先分叉主存储库,将分叉克隆到桌面,然后运行以下命令以使用符号链接安装它,这样当您更改代码时,更改将自动可用您的应用程序无需重新安装软件包:
$ cd falcon
$ FALCON_DISABLE_CYTHON=Y pip install -e .
您可以通过切换到克隆存储库的目录然后运行 pytest 来手动测试对 Falcon 框架的更改:
$ cd falcon
$ pip install -r requirements/tests
$ pytest tests
或者,运行默认的测试集:
$ pip install tox && tox
另请参阅 tox.ini 文件以获取可用环境的完整列表。
Falcon 代码库中的文档字符串相当广泛,我们建议在学习框架时保持 REPL 运行,以便您可以在有问题时查询各种模块和类。
在线文档位于:https://falcon.readthedocs.io
您可以在本地构建相同的文档,如下所示:
$ pip install tox && tox -e docs
文档构建完成后,您可以通过在浏览器中打开以下索引页面来查看它们。在 OS X 上,它很简单:
$ 打开文档/_build/html/index.html
或者在 Linux 上:
$ xdg-open docs/_build/html/index.html
这是一个简单的、人为的示例,展示了如何创建基于 Falcon 的 WSGI 应用程序(ASGI 版本包含在下面):
# examples/things.py
# Let's get this party started!
from wsgiref . simple_server import make_server
import falcon
# Falcon follows the REST architectural style, meaning (among
# other things) that you think in terms of resources and state
# transitions, which map to HTTP verbs.
class ThingsResource :
def on_get ( self , req , resp ):
"""Handles GET requests"""
resp . status = falcon . HTTP_200 # This is the default status
resp . content_type = falcon . MEDIA_TEXT # Default is JSON, so override
resp . text = ( ' n Two things awe me most, the starry sky '
'above me and the moral law within me. n '
' n '
' ~ Immanuel Kant n n ' )
# falcon.App instances are callable WSGI apps...
# in larger applications the app is created in a separate file
app = falcon . App ()
# Resources are represented by long-lived class instances
things = ThingsResource ()
# things will handle all requests to the '/things' URL path
app . add_route ( '/things' , things )
if __name__ == '__main__' :
with make_server ( '' , 8000 , app ) as httpd :
print ( 'Serving on port 8000...' )
# Serve until process is killed
httpd . serve_forever ()
您可以使用附带的 wsgiref 服务器直接运行上面的示例:
$ pip install falcon
$ python things.py
然后,在另一个终端中:
$ curl localhost:8000/things
该示例的 ASGI 版本类似:
# examples/things_asgi.py
import falcon
import falcon . asgi
# Falcon follows the REST architectural style, meaning (among
# other things) that you think in terms of resources and state
# transitions, which map to HTTP verbs.
class ThingsResource :
async def on_get ( self , req , resp ):
"""Handles GET requests"""
resp . status = falcon . HTTP_200 # This is the default status
resp . content_type = falcon . MEDIA_TEXT # Default is JSON, so override
resp . text = ( ' n Two things awe me most, the starry sky '
'above me and the moral law within me. n '
' n '
' ~ Immanuel Kant n n ' )
# falcon.asgi.App instances are callable ASGI apps...
# in larger applications the app is created in a separate file
app = falcon . asgi . App ()
# Resources are represented by long-lived class instances
things = ThingsResource ()
# things will handle all requests to the '/things' URL path
app . add_route ( '/things' , things )
您可以使用 uvicorn 或任何其他 ASGI 服务器运行 ASGI 版本:
$ pip install falcon uvicorn
$ uvicorn things_asgi:app
这是一个更复杂的示例,演示了读取标头和查询参数、处理错误以及使用请求和响应主体。请注意,此示例假设已安装 requests 包。
(有关等效的 ASGI 应用程序,请参阅:更复杂的示例 (ASGI))。
# examples/things_advanced.py
import json
import logging
import uuid
from wsgiref import simple_server
import falcon
import requests
class StorageEngine :
def get_things ( self , marker , limit ):
return [{ 'id' : str ( uuid . uuid4 ()), 'color' : 'green' }]
def add_thing ( self , thing ):
thing [ 'id' ] = str ( uuid . uuid4 ())
return thing
class StorageError ( Exception ):
@ staticmethod
def handle ( ex , req , resp , params ):
# TODO: Log the error, clean up, etc. before raising
raise falcon . HTTPInternalServerError ()
class SinkAdapter :
engines = {
'ddg' : 'https://duckduckgo.com' ,
'y' : 'https://search.yahoo.com/search' ,
}
def __call__ ( self , req , resp , engine ):
url = self . engines [ engine ]
params = { 'q' : req . get_param ( 'q' , True )}
result = requests . get ( url , params = params )
resp . status = str ( result . status_code ) + ' ' + result . reason
resp . content_type = result . headers [ 'content-type' ]
resp . text = result . text
class AuthMiddleware :
def process_request ( self , req , resp ):
token = req . get_header ( 'Authorization' )
account_id = req . get_header ( 'Account-ID' )
challenges = [ 'Token type="Fernet"' ]
if token is None :
description = ( 'Please provide an auth token '
'as part of the request.' )
raise falcon . HTTPUnauthorized ( title = 'Auth token required' ,
description = description ,
challenges = challenges ,
href = 'http://docs.example.com/auth' )
if not self . _token_is_valid ( token , account_id ):
description = ( 'The provided auth token is not valid. '
'Please request a new token and try again.' )
raise falcon . HTTPUnauthorized ( title = 'Authentication required' ,
description = description ,
challenges = challenges ,
href = 'http://docs.example.com/auth' )
def _token_is_valid ( self , token , account_id ):
return True # Suuuuuure it's valid...
class RequireJSON :
def process_request ( self , req , resp ):
if not req . client_accepts_json :
raise falcon . HTTPNotAcceptable (
description = 'This API only supports responses encoded as JSON.' ,
href = 'http://docs.examples.com/api/json' )
if req . method in ( 'POST' , 'PUT' ):
if 'application/json' not in req . content_type :
raise falcon . HTTPUnsupportedMediaType (
title = 'This API only supports requests encoded as JSON.' ,
href = 'http://docs.examples.com/api/json' )
class JSONTranslator :
# NOTE: Normally you would simply use req.media and resp.media for
# this particular use case; this example serves only to illustrate
# what is possible.
def process_request ( self , req , resp ):
# req.stream corresponds to the WSGI wsgi.input environ variable,
# and allows you to read bytes from the request body.
#
# See also: PEP 3333
if req . content_length in ( None , 0 ):
# Nothing to do
return
body = req . stream . read ()
if not body :
raise falcon . HTTPBadRequest ( title = 'Empty request body' ,
description = 'A valid JSON document is required.' )
try :
req . context . doc = json . loads ( body . decode ( 'utf-8' ))
except ( ValueError , UnicodeDecodeError ):
description = ( 'Could not decode the request body. The '
'JSON was incorrect or not encoded as '
'UTF-8.' )
raise falcon . HTTPBadRequest ( title = 'Malformed JSON' ,
description = description )
def process_response ( self , req , resp , resource , req_succeeded ):
if not hasattr ( resp . context , 'result' ):
return
resp . text = json . dumps ( resp . context . result )
def max_body ( limit ):
def hook ( req , resp , resource , params ):
length = req . content_length
if length is not None and length > limit :
msg = ( 'The size of the request is too large. The body must not '
'exceed ' + str ( limit ) + ' bytes in length.' )
raise falcon . HTTPContentTooLarge (
title = 'Request body is too large' , description = msg )
return hook
class ThingsResource :
def __init__ ( self , db ):
self . db = db
self . logger = logging . getLogger ( 'thingsapp.' + __name__ )
def on_get ( self , req , resp , user_id ):
marker = req . get_param ( 'marker' ) or ''
limit = req . get_param_as_int ( 'limit' ) or 50
try :
result = self . db . get_things ( marker , limit )
except Exception as ex :
self . logger . error ( ex )
description = ( 'Aliens have attacked our base! We will '
'be back as soon as we fight them off. '
'We appreciate your patience.' )
raise falcon . HTTPServiceUnavailable (
title = 'Service Outage' ,
description = description ,
retry_after = 30 )
# NOTE: Normally you would use resp.media for this sort of thing;
# this example serves only to demonstrate how the context can be
# used to pass arbitrary values between middleware components,
# hooks, and resources.
resp . context . result = result
resp . set_header ( 'Powered-By' , 'Falcon' )
resp . status = falcon . HTTP_200
@ falcon . before ( max_body ( 64 * 1024 ))
def on_post ( self , req , resp , user_id ):
try :
doc = req . context . doc
except AttributeError :
raise falcon . HTTPBadRequest (
title = 'Missing thing' ,
description = 'A thing must be submitted in the request body.' )
proper_thing = self . db . add_thing ( doc )
resp . status = falcon . HTTP_201
resp . location = '/%s/things/%s' % ( user_id , proper_thing [ 'id' ])
# Configure your WSGI server to load "things.app" (app is a WSGI callable)
app = falcon . App ( middleware = [
AuthMiddleware (),
RequireJSON (),
JSONTranslator (),
])
db = StorageEngine ()
things = ThingsResource ( db )
app . add_route ( '/{user_id}/things' , things )
# If a responder ever raises an instance of StorageError, pass control to
# the given handler.
app . add_error_handler ( StorageError , StorageError . handle )
# Proxy some things to another service; this example shows how you might
# send parts of an API off to a legacy system that hasn't been upgraded
# yet, or perhaps is a single cluster that all data centers have to share.
sink = SinkAdapter ()
app . add_sink ( sink , r'/search/(?P<engine>ddg|y)Z' )
# Useful for debugging problems in your API; works with pdb.set_trace(). You
# can also use Gunicorn to host your app. Gunicorn can be configured to
# auto-restart workers when it detects a code change, and it also works
# with pdb.
if __name__ == '__main__' :
httpd = simple_server . make_server ( '127.0.0.1' , 8000 , app )
httpd . serve_forever ()
此代码再次使用 wsgiref,但您也可以使用任何 WSGI 服务器(例如 uWSGI 或 Gunicorn)运行上面的示例。例如:
$ pip install requests gunicorn
$ gunicorn things:app
在 Windows 上,您可以通过 WSL 运行 Gunicorn 和 uWSGI,或者您可以尝试 Waitress:
$ pip install requests waitress
$ waitress-serve --port=8000 things:app
要测试此示例,请打开另一个终端并运行:
$ http localhost:8000/1/things authorization:custom-token
您还可以通过与框架捆绑的falcon-inspect-app
脚本从 CLI 查看应用程序配置:
falcon-inspect-app things_advanced:app
这是上面应用程序的 ASGI 版本。请注意,它使用 httpx 包代替 requests。
# examples/things_advanced_asgi.py
import json
import logging
import uuid
import falcon
import falcon . asgi
import httpx
class StorageEngine :
async def get_things ( self , marker , limit ):
return [{ 'id' : str ( uuid . uuid4 ()), 'color' : 'green' }]
async def add_thing ( self , thing ):
thing [ 'id' ] = str ( uuid . uuid4 ())
return thing
class StorageError ( Exception ):
@ staticmethod
async def handle ( ex , req , resp , params ):
# TODO: Log the error, clean up, etc. before raising
raise falcon . HTTPInternalServerError ()
class SinkAdapter :
engines = {
'ddg' : 'https://duckduckgo.com' ,
'y' : 'https://search.yahoo.com/search' ,
}
async def __call__ ( self , req , resp , engine ):
url = self . engines [ engine ]
params = { 'q' : req . get_param ( 'q' , True )}
async with httpx . AsyncClient () as client :
result = await client . get ( url , params = params )
resp . status = result . status_code
resp . content_type = result . headers [ 'content-type' ]
resp . text = result . text
class AuthMiddleware :
async def process_request ( self , req , resp ):
token = req . get_header ( 'Authorization' )
account_id = req . get_header ( 'Account-ID' )
challenges = [ 'Token type="Fernet"' ]
if token is None :
description = ( 'Please provide an auth token '
'as part of the request.' )
raise falcon . HTTPUnauthorized ( title = 'Auth token required' ,
description = description ,
challenges = challenges ,
href = 'http://docs.example.com/auth' )
if not self . _token_is_valid ( token , account_id ):
description = ( 'The provided auth token is not valid. '
'Please request a new token and try again.' )
raise falcon . HTTPUnauthorized ( title = 'Authentication required' ,
description = description ,
challenges = challenges ,
href = 'http://docs.example.com/auth' )
def _token_is_valid ( self , token , account_id ):
return True # Suuuuuure it's valid...
class RequireJSON :
async def process_request ( self , req , resp ):
if not req . client_accepts_json :
raise falcon . HTTPNotAcceptable (
description = 'This API only supports responses encoded as JSON.' ,
href = 'http://docs.examples.com/api/json' )
if req . method in ( 'POST' , 'PUT' ):
if 'application/json' not in req . content_type :
raise falcon . HTTPUnsupportedMediaType (
description = 'This API only supports requests encoded as JSON.' ,
href = 'http://docs.examples.com/api/json' )
class JSONTranslator :
# NOTE: Normally you would simply use req.get_media() and resp.media for
# this particular use case; this example serves only to illustrate
# what is possible.
async def process_request ( self , req , resp ):
# NOTE: Test explicitly for 0, since this property could be None in
# the case that the Content-Length header is missing (in which case we
# can't know if there is a body without actually attempting to read
# it from the request stream.)
if req . content_length == 0 :
# Nothing to do
return
body = await req . stream . read ()
if not body :
raise falcon . HTTPBadRequest ( title = 'Empty request body' ,
description = 'A valid JSON document is required.' )
try :
req . context . doc = json . loads ( body . decode ( 'utf-8' ))
except ( ValueError , UnicodeDecodeError ):
description = ( 'Could not decode the request body. The '
'JSON was incorrect or not encoded as '
'UTF-8.' )
raise falcon . HTTPBadRequest ( title = 'Malformed JSON' ,
description = description )
async def process_response ( self , req , resp , resource , req_succeeded ):
if not hasattr ( resp . context , 'result' ):
return
resp . text = json . dumps ( resp . context . result )
def max_body ( limit ):
async def hook ( req , resp , resource , params ):
length = req . content_length
if length is not None and length > limit :
msg = ( 'The size of the request is too large. The body must not '
'exceed ' + str ( limit ) + ' bytes in length.' )
raise falcon . HTTPContentTooLarge (
title = 'Request body is too large' , description = msg )
return hook
class ThingsResource :
def __init__ ( self , db ):
self . db = db
self . logger = logging . getLogger ( 'thingsapp.' + __name__ )
async def on_get ( self , req , resp , user_id ):
marker = req . get_param ( 'marker' ) or ''
limit = req . get_param_as_int ( 'limit' ) or 50
try :
result = await self . db . get_things ( marker , limit )
except Exception as ex :
self . logger . error ( ex )
description = ( 'Aliens have attacked our base! We will '
'be back as soon as we fight them off. '
'We appreciate your patience.' )
raise falcon . HTTPServiceUnavailable (
title = 'Service Outage' ,
description = description ,
retry_after = 30 )
# NOTE: Normally you would use resp.media for this sort of thing;
# this example serves only to demonstrate how the context can be
# used to pass arbitrary values between middleware components,
# hooks, and resources.
resp . context . result = result
resp . set_header ( 'Powered-By' , 'Falcon' )
resp . status = falcon . HTTP_200
@ falcon . before ( max_body ( 64 * 1024 ))
async def on_post ( self , req , resp , user_id ):
try :
doc = req . context . doc
except AttributeError :
raise falcon . HTTPBadRequest (
title = 'Missing thing' ,
description = 'A thing must be submitted in the request body.' )
proper_thing = await self . db . add_thing ( doc )
resp . status = falcon . HTTP_201
resp . location = '/%s/things/%s' % ( user_id , proper_thing [ 'id' ])
# The app instance is an ASGI callable
app = falcon . asgi . App ( middleware = [
# AuthMiddleware(),
RequireJSON (),
JSONTranslator (),
])
db = StorageEngine ()
things = ThingsResource ( db )
app . add_route ( '/{user_id}/things' , things )
# If a responder ever raises an instance of StorageError, pass control to
# the given handler.
app . add_error_handler ( StorageError , StorageError . handle )
# Proxy some things to another service; this example shows how you might
# send parts of an API off to a legacy system that hasn't been upgraded
# yet, or perhaps is a single cluster that all data centers have to share.
sink = SinkAdapter ()
app . add_sink ( sink , r'/search/(?P<engine>ddg|y)Z' )
您可以使用任何 ASGI 服务器运行 ASGI 版本,例如 uvicorn:
$ pip install falcon httpx uvicorn
$ uvicorn things_advanced_asgi:app
感谢您对该项目的兴趣!我们欢迎各种技能水平的开发人员提出拉取请求。首先,只需将 GitHub 上的 master 分支分叉到您的个人帐户,然后将分叉克隆到您的开发环境中。
如果您想做出贡献但尚未有所想法,我们邀请您查看下一个里程碑下列出的问题。如果您看到想要合作的内容,请快速发表评论,以免我们陷入重复的工作。提前致谢!
请注意,该项目的所有贡献者和维护者均须遵守我们的行为准则。
在提交拉取请求之前,请确保您已添加/更新了适当的测试(并且所有现有测试仍然通过您的更改),并且您的编码风格遵循 PEP 8 并且不会导致 pyflakes 抱怨。
提交消息应使用 AngularJS 约定进行格式化。
评论遵循 Google 的风格指南,并附加要求使用您的 GitHub 昵称和适当的前缀为内联评论添加前缀:
Falcon 项目的核心维护者是:
如果您有任何疑问,或者只是需要一些入门帮助,请随时与我们联系。您可以在 Gitter 的 falconry/dev 中找到我们。
另请参阅:CONTRIBUTING.md
版权所有 2013-2024,归个人和企业贡献者所有,如个人源文件中所述。
根据 Apache 许可证 2.0 版(“许可证”)获得许可;除非遵守许可,否则您不得使用 Falcon 框架的任何部分。贡献者同意在同一许可证下许可他们的作品。您可以在 http://www.apache.org/licenses/LICENSE-2.0 获取许可证副本
除非适用法律要求或书面同意,否则根据许可证分发的软件均按“原样”分发,不带任何明示或暗示的保证或条件。请参阅许可证,了解许可证下管理权限和限制的特定语言。