Falcon — это минималистичная среда ASGI/WSGI для создания критически важных REST API и микросервисов с упором на надежность, корректность и производительность в масштабе.
Когда дело доходит до создания HTTP API, другие платформы отягощают вас множеством зависимостей и ненужных абстракций. Falcon переходит к делу благодаря чистому дизайну, сочетающему HTTP и архитектурный стиль REST.
Приложения Falcon работают с любым сервером WSGI или ASGI и отлично работают под управлением CPython 3.8+ и PyPy 3.8+.
«Falcon надежен и быстр».
«Мы использовали Falcon в качестве замены [другой платформы], и нам просто нравится производительность (в три раза быстрее) и размер базы кода (около половины нашего [исходного] кода)».
«Мне нравится #falconframework! Очень чисто и просто. Наконец-то у меня есть скорость и гибкость, которые мне нужны!»
«Пока Falcon выглядит великолепно. Я провел быстрый тест для своего крошечного сервера и работал примерно на 40% быстрее всего за 20 минут работы».
«Я чувствую, что наконец-то говорю только об HTTP, без ничего посередине. Falcon похож на запросы серверной части».
«Исходный код Falcon настолько хорош, что я почти предпочитаю его документации. В принципе, он не может быть неправильным».
«Какая еще платформа имеет встроенную поддержку 786 TRY IT NOW?»
Falcon старается делать как можно меньше, сохраняя при этом высокую эффективность.
asyncio
Falcon помог вам создать потрясающее приложение? Покажите свою поддержку сегодня, сделав единоразовое пожертвование или став покровителем. Сторонники получают классное оборудование, возможность продвигать свой бренд среди разработчиков Python и приоритетную поддержку.
Спасибо!
Совершенство достигается, наконец, не тогда, когда уже нечего добавить, а когда уже нечего отнять.
- Антуан де Сент-Экзюпери
Мы разработали Falcon для поддержки растущих потребностей крупномасштабных микросервисов и адаптивных серверных частей приложений. Falcon дополняет более общие веб-фреймворки Python, обеспечивая производительность, надежность и гибкость «голого железа» там, где вам это необходимо.
Надежный. Мы прилагаем все усилия, чтобы избежать внесения критических изменений, и когда мы это делаем, они полностью документируются и вводятся только (в духе SemVer) с увеличением основной версии. Код тщательно тестируется с использованием многочисленных входных данных, и нам всегда требуется 100%-ное покрытие. У Falcon нет зависимостей за пределами стандартной библиотеки, что помогает минимизировать поверхность атаки вашего приложения, избегая при этом транзитивных ошибок и критических изменений.
Возможность отладки. Сокол избегает магии. Легко определить, какие входы ведут к каким выходам. Необработанные исключения никогда не инкапсулируются и не маскируются. Потенциально неожиданное поведение, такое как автоматический анализ тела запроса, хорошо документировано и по умолчанию отключено. Наконец, когда дело доходит до самой структуры, мы заботимся о том, чтобы логические пути были простыми и понятными. Все это упрощает анализ кода и отладку крайних случаев в крупномасштабных развертываниях.
Быстрый. То же оборудование, больше запросов. Falcon обрабатывает запросы значительно быстрее, чем другие популярные платформы Python, такие как Django и Flask. Для дополнительного повышения скорости Falcon компилируется с Cython, если он доступен, а также хорошо работает с PyPy. Подумываете о переходе на другой язык программирования? Сначала протестируйте с помощью Falcon+PyPy!
Гибкий. Falcon оставляет множество решений и деталей реализации вам, разработчику API. Это дает вам большую свободу в настройке и настройке вашей реализации. Это также помогает вам понять ваши приложения на более глубоком уровне, что упрощает их настройку, отладку и рефакторинг в долгосрочной перспективе. Минималистичный дизайн Falcon предоставляет членам сообщества Python возможность самостоятельно внедрять инновации в надстройках и дополнительных пакетах Falcon.
Falcon используется по всему миру все большим числом организаций, в том числе:
Если вы используете платформу Falcon для общественного или коммерческого проекта, рассмотрите возможность добавления вашей информации в нашу вики в разделе «Кто использует Falcon?»
Для использования в ваших проектах доступен ряд надстроек, шаблонов и дополнительных пакетов Falcon. Мы перечислили некоторые из них в вики Falcon в качестве отправной точки, но вы также можете поискать дополнительные ресурсы по PyPI.
Сообщество Falconry на Gitter — отличное место, где можно задавать вопросы и делиться своими идеями. Вы можете найти нас в соколиной охоте/user. У нас также есть комната для соколиной охоты и разработки, где можно обсудить проектирование и разработку самой платформы.
В соответствии с нашим Кодексом поведения мы ожидаем, что все, кто участвует в общественных дискуссиях, будут действовать профессионально и подавать пример, поощряя конструктивные дискуссии. Каждый человек в сообществе несет ответственность за создание позитивной, конструктивной и продуктивной культуры.
PyPy — это самый быстрый способ запустить приложение Falcon. PyPy3.8+ поддерживается начиная с PyPy v7.3.7+.
$ pip install falcon
Или, чтобы установить последнюю бета-версию или версию-кандидат, если таковая имеется:
$ pip install --pre falcon
Falcon также полностью поддерживает CPython 3.8+.
Последнюю стабильную версию Falcon можно установить непосредственно из PyPI:
$ pip install falcon
Или, чтобы установить последнюю бета-версию или версию-кандидат, если таковая имеется:
$ pip install --pre falcon
Чтобы обеспечить дополнительный прирост скорости, Falcon автоматически компилируется с Cython при любом установщике, совместимом с PEP 517.
Для вашего удобства в PyPI доступны колеса, содержащие предварительно скомпилированные двоичные файлы, для большинства распространенных платформ. Даже если бинарная сборка для выбранной вами платформы недоступна, pip
выберет колесо на чистом Python. Вы также можете использовать Cythonia Falcon для своей среды; дополнительную информацию об этом и других дополнительных параметрах см. в нашей документации по установке.
Falcon не требует установки каких-либо других пакетов.
Falcon говорит на WSGI (или ASGI; см. также ниже). Для обслуживания приложения Falcon вам понадобится сервер WSGI. Gunicorn и uWSGI — одни из наиболее популярных, но подойдет все, что может загружать приложение WSGI.
$ pip install [gunicorn | uwsgi]
Для обслуживания приложения Falcon ASGI вам понадобится сервер ASGI. Ювикорн – популярный выбор:
$ pip install uvicorn
Falcon живет на GitHub, что упрощает просмотр, загрузку, разветвление и т. д. код. Запросы на включение всегда приветствуются! Также не забудьте поставить звездочку на проекте, если он вас порадует. :)
После клонирования репозитория или загрузки архива с GitHub вы можете установить Falcon следующим образом:
$ cd falcon
$ pip install .
Или, если вы хотите отредактировать код, сначала создайте основной репозиторий, клонируйте его на рабочий стол, а затем запустите следующую команду, чтобы установить его с помощью символической ссылки, чтобы при изменении кода изменения были автоматически доступны для ваше приложение без необходимости переустанавливать пакет:
$ cd falcon
$ FALCON_DISABLE_CYTHON=Y pip install -e .
Вы можете вручную протестировать изменения в платформе Falcon, перейдя в каталог клонированного репозитория и запустив pytest:
$ 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
Вот простой, надуманный пример, показывающий, как создать приложение WSGI на базе Falcon (версия 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 )
Вы можете запустить версию ASGI с помощью uvicorn или любого другого сервера 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 вы можете запустить Gunicorn и uWSGI через WSL или попробовать Waitress:
$ pip install requests waitress
$ waitress-serve --port=8000 things:app
Чтобы протестировать этот пример, откройте другой терминал и запустите:
$ http localhost:8000/1/things authorization:custom-token
Вы также можете просмотреть конфигурацию приложения из CLI с помощью сценария falcon-inspect-app
, который входит в состав платформы:
falcon-inspect-app things_advanced:app
Вот ASGI-версия приложения сверху. Обратите внимание, что вместо запросов он использует пакет httpx.
# 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
Спасибо за интерес к проекту! Мы приветствуем запросы на включение от разработчиков всех уровней квалификации. Для начала просто создайте ветку master на GitHub в своей личной учетной записи, а затем клонируйте ее в свою среду разработки.
Если вы хотели бы внести свой вклад, но еще ничего не задумали, мы приглашаем вас взглянуть на проблемы, перечисленные в рамках нашей следующей вехи. Если вы видите тот, над которым хотели бы поработать, оставьте быстрый комментарий, чтобы нам не пришлось дублировать усилия. Заранее спасибо!
Обратите внимание, что все участники и сопровождающие этого проекта подчиняются нашему Кодексу поведения.
Прежде чем отправлять запрос на включение, убедитесь, что вы добавили/обновили соответствующие тесты (и что все существующие тесты по-прежнему проходят с вашими изменениями), и что ваш стиль кодирования соответствует PEP 8 и не вызывает жалоб pyflakes.
Сообщения о фиксации должны быть отформатированы с использованием соглашений AngularJS.
Комментарии соответствуют руководству по стилю Google, с дополнительным требованием добавлять к встроенным комментариям свой ник GitHub и соответствующий префикс:
Основными сопровождающими проекта Falcon являются:
Пожалуйста, не стесняйтесь обращаться к нам, если у вас есть какие-либо вопросы или вам просто нужна небольшая помощь для начала работы. Вы можете найти нас в falconry/dev на Gitter.
См. также: CONTRIBUTING.md
Авторские права принадлежат индивидуальным и корпоративным участникам, 2013–2024 гг., как указано в отдельных исходных файлах.
Лицензируется по лицензии Apache версии 2.0 («Лицензия»); вы не имеете права использовать какую-либо часть платформы Falcon, кроме как в соответствии с Лицензией. Участники соглашаются лицензировать свою работу по той же Лицензии. Вы можете получить копию Лицензии по адресу http://www.apache.org/licenses/LICENSE-2.0.
Если это не требуется действующим законодательством или не согласовано в письменной форме, программное обеспечение, распространяемое по Лицензии, распространяется на условиях «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ ИЛИ УСЛОВИЙ, явных или подразумеваемых. См. Лицензию для определения конкретного языка, регулирующего разрешения и ограничения в рамках Лицензии.