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 等。另外,如果該項目讓您滿意,請記得為該項目加註星標。 :)
克隆儲存庫或從 GitHub 下載 tarball 後,您可以像這樣安裝 Falcon:
$ cd falcon
$ pip install .
或者,如果您想編輯程式碼,請先分叉主儲存庫,將分叉克隆到桌面,然後執行以下命令以使用符號連結安裝它,這樣當您更改程式碼時,更改將自動可用您的應用程式無需重新安裝軟體包:
$ cd falcon
$ FALCON_DISABLE_CYTHON=Y pip install -e .
您可以透過切換到複製儲存庫的目錄然後執行 Python 來手動測試 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 取得授權副本
除非適用法律要求或書面同意,否則根據許可證分發的軟體均以「原樣」分發,不帶任何明示或暗示的保證或條件。請參閱許可證,了解許可證下管理權限和限制的特定語言。