Spec-Zone.ru › Falcon 1.4

ЧАВО

  • Философия проектирования
    • Почему Falcon не поставляется с батареями?
    • Почему Falcon не создаёт новый экземпляр Resource для каждого запроса?
    • Почему возникновение ошибки внутри ресурса приводит к сбою моего приложения?
    • Как сгенерировать документацию API для моего Falcon API?
  • Производительность
    • Поддерживает ли Falcon HTTP/2?
    • Falcon потокобезопасен?
    • Поддерживает ли Falcon asyncio?
    • Поддерживает ли Falcon WebSocket?
  • Маршрутизация
    • Как реализовать CORS с Falcon?
    • Как реализовать перенаправления внутри Falcon?
    • Как реализовать и POST, и GET для одного ресурса?
    • Какой рекомендуемый способ сопоставления связанных маршрутов с классами ресурсов?
  • Расширяемость
    • Как использовать WSGI-мидлварь с Falcon?
    • Как передать данные от хука к обработчику и между хуками?
    • Как написать пользовательский обработчик для страниц 404 и 500 в Falcon?
  • Обработка запросов
    • Как аутентифицировать запросы?
    • Почему req.stream.read() зависает для определённых запросов?
    • Почему с req.path обрезаются слеши в конце?
    • Почему мой параметр запроса отсутствует в объекте req?
    • Почему символы «+» в моих параметрах заменяются пробелами?
    • Как получить доступ к параметрам POST-формы?
    • Как получить доступ к отправленным файлам?
    • Как обработать строку запроса, содержащую JSON-значение?
    • Как обработать обратные слэши в поле шаблона маршрута?
  • Обработка ответов
    • Как использовать resp.media с типами, такими как datetime?
    • Falcon устанавливает Content-Length или нужно ли это делать явно?
    • Почему возвращается пустое тело ответа, когда я поднимаю экземпляр HTTPError?
    • Я устанавливаю тело ответа, но оно не возвращается. Что происходит?
    • Я устанавливаю cookie, но он не возвращается в последующих запросах.
    • Как обслуживать загружаемый файл с помощью Falcon?
    • Может ли Falcon обслуживать статические файлы?
  • Разное
    • Как управлять подключениями к базе данных?
    • Какой рекомендуемый подход для обеспечения доступности переменных конфигурации для нескольких классов ресурсов?
    • Как протестировать моё приложение Falcon? Могу ли я использовать pytest?

Философия проектирования

Почему Falcon не поставляется с батареями?

Falcon разработан для приложений, требующих высокой степени настройки или тонкой настройки производительности. Минималистский дизайн фреймворка освобождает разработчика для выбора лучших стратегий и сторонних библиотек для решения конкретной задачи.

Экосистема Python предоставляет множество отличных пакетов, которые можно использовать внутри ваших обработчиков, хуков и компонентов мидлвары. В качестве отправной точки сообщество поддерживает список дополнений и дополнительных пакетов Falcon.

Почему Falcon не создаёт новый экземпляр Resource для каждого запроса?

Falcon обычно старается минимизировать количество создаваемых объектов. Это делается по двум причинам: во-первых, для экономии ресурсов при создании объекта, и, во-вторых, для снижения потребления памяти. Поэтому, при добавлении маршрута, Falcon требует экземпляр вашего класса ресурса, а не тип класса. Тот же экземпляр будет использоваться для обработки всех запросов, поступающих по этому маршруту.

Почему возникновение ошибки внутри ресурса приводит к сбою моего приложения?

В общем случае, Falcon предполагает, что обработчики ресурсов (такие как on_get(), on_post(), и т.д.) в большинстве случаев будут действовать правильно. Другими словами, Falcon не пытается сильно защитить код обработчика от самого себя.

Этот подход уменьшает количество проверок, которые Falcon в противном случае должен выполнять, делая фреймворк более эффективным. При этом, разработка качественного API на основе Falcon требует:

  1. Обработчики ресурсов устанавливают переменные ответа в разумные значения.
  2. Ваш код хорошо протестирован с высокой покрытием кода.
  3. Ошибки предвидятся, обнаруживаются и обрабатываются надлежащим образом внутри каждого обработчика и с помощью пользовательских обработчиков ошибок.

Подсказка

Falcon будет повторно поднимать ошибки, которые не наследуются от HTTPError, если вы не зарегистрировали пользовательский обработчик ошибок для этого типа (см. также: falcon.API).

Как сгенерировать документацию API для моего Falcon API?

Когда дело доходит до документации API, некоторые разработчики предпочитают использовать реализацию API в качестве пользовательского контракта или источника истины (подход, основанный на реализации), в то время как другие предпочитают использовать сам API-спецификацию в качестве контракта, реализуя и тестируя API по отношению к этой спецификации (подход, основанный на проектировании).

Рискуя ошибиться в сторону гибкости, Falcon не предоставляет поддержку API-спецификации в стандартной поставке. Тем не менее, существуют несколько проектов сообщества, направленных на это. В нашем каталоге дополнений есть несколько таких проектов, но вы также можете поискать на PyPI дополнительные пакеты.

Если вас интересует подход, основанный на проектировании, упомянутый выше, вы также можете ознакомиться с проектированием API и службами шлюзов, такими как Tyk, Apiary, Amazon API Gateway или Google Cloud Endpoints.

Производительность

Поддерживает ли Falcon HTTP/2?

Falcon — это фреймворк WSGI, и как таковой он напрямую не обрабатывает HTTP-запросы. Однако вы можете получить большинство преимуществ HTTP/2, просто развернув любой совместимый с HTTP/2 веб-сервер или балансировщик нагрузки перед вашим приложением для преобразования между HTTP/2 и HTTP/1.1. В конечном итоге мы ожидаем, что Python веб-серверы (например, uWSGI) будут поддерживать HTTP/2 напрямую, устраняя необходимость уровня преобразования.

Falcon потокобезопасен?

Новые объекты Request и Response создаются для каждого входящего HTTP-запроса. Однако один экземпляр каждого класса ресурса, привязанного к маршруту, используется для всех запросов. Следовательно, пока вы осторожны в способе, которым обработчики доступа к переменным класса, чтобы избежать конфликтов, ваше WSGI-приложение должно быть потокобезопасным.

Тем не менее, IO-ориентированные Falcon API обычно масштабируются с помощью зелёных потоков (благодаря библиотеке gevent или подобным), которые не являются по-настоящему одновременными, поэтому могут существовать некоторые граничные случаи, когда Falcon не является потокобезопасным, которые ещё не обнаружены.

Caveat emptor!

Поддерживает ли Falcon asyncio?

Из-за ограничений WSGI, Falcon в настоящее время не может поддерживать asyncio. Однако мы изучаем альтернативы WSGI (например, ASGI), которые позволят нам в будущем поддерживать asyncio напрямую.

В то же время мы рекомендуем использовать gevent через Gunicorn или uWSGI для масштабирования ресурсоемких сервисов.

Поддерживает ли Falcon WebSocket?

Из-за ограничений WSGI, Falcon не может поддерживать протокол WebSocket, как указано выше.

В то же время вы можете попробовать использовать родную поддержку WebSocket в uWSGI или реализовать автономный сервис с помощью удобной библиотеки websockets от Aymeric Augustin.

Маршрутизация

Как реализовать CORS с Falcon?

Чтобы веб-сайт или SPA мог получить доступ к API, размещённому на другом домене, этот API должен реализовывать Cross-Origin Resource Sharing (CORS). Для публичного API реализация CORS в Falcon может быть такой же простой, как реализация компонента промежуточного ПО, подобного следующему:

class CORSComponent(object):
    def process_response(self, req, resp, resource, req_succeeded):
        resp.set_header('Access-Control-Allow-Origin', '*')

        if (req_succeeded
            and req.method == 'OPTIONS'
            and req.get_header('Access-Control-Request-Method')
        ):
            # NOTE(kgriffs): This is a CORS preflight request. Patch the
            #   response accordingly.

            allow = resp.get_header('Allow')
            resp.delete_header('Allow')

            allow_headers = req.get_header(
                'Access-Control-Request-Headers',
                default='*'
            )

            resp.set_headers((
                ('Access-Control-Allow-Methods', allow),
                ('Access-Control-Allow-Headers', allow_headers),
                ('Access-Control-Max-Age', '86400'),  # 24 hours
            ))

При использовании вышеуказанного подхода запросы OPTIONS также должны быть обработаны особым образом в любом другом промежуточном ПО или хуках, используемых для аутентификации, определения типа контента и т. д. Например, для запросов preflight обычно следует пропустить аутентификацию, так как она просто не нужна; обратите внимание, что такие запросы в любом случае не содержат заголовка Authorization.

Для более сложных случаев обратите внимание на дополнения к Falcon от сообщества, такие как falcon-cors, или попробуйте одну из общих библиотек CORS для WSGI, доступных на PyPI . Если вы используете шлюз API, вы также можете изучить функциональность CORS, которую он предоставляет на этом уровне.

Как реализовать перенаправления внутри Falcon?

Falcon предоставляет ряд классов исключений, которые можно поднять для перенаправления клиента на другое место (см. также Перенаправление).

Однако учтите, что постоянные перенаправления эффективнее обрабатывать непосредственно на уровне вашего веб-сервера, если это возможно, а не нагружать ваш приложение дополнительной работой для таких запросов.

Как реализовать POST и GET для одного и того же ресурса?

Предположим, у вас есть следующие маршруты:

# Resource Collection
POST /resources
GET /resources{?marker, limit}

# Resource Item
GET /resources/{id}
PATCH /resources/{id}
DELETE /resources/{id}

Вы можете реализовать такой API, просто используя два класса Python: один для представления отдельного ресурса, а другой — для представления коллекции этих ресурсов. Обычно оба класса размещаются в одном модуле.

Было предложено добавить новую функцию маршрутизации, которая позволит сопоставить связанные маршруты с одним классом ресурса, если это необходимо. Подробнее см. #584 на GitHub.

(См. также эту часть руководства.)

Какой рекомендуемый способ сопоставления связанных маршрутов с классами ресурсов?

Предположим, у нас есть следующая схема URL:

GET  /game/ping
GET  /game/{game_id}
POST /game/{game_id}
GET  /game/{game_id}/state
POST /game/{game_id}/state

Мы можем разбить её на три ресурса:

Ping:

    GET  /game/ping

Game:

    GET  /game/{game_id}
    POST /game/{game_id}

GameState:

    GET  /game/{game_id}/state
    POST /game/{game_id}/state

GameState можно рассматривать как подресурс Game. Это отдельная логическая сущность, вложенная в более общий концепт Game.

В Falcon эти ресурсы реализуются с помощью стандартных классов:

class Ping(object):

    def on_get(self, req, resp):
        resp.body = '{"message": "pong"}'


class Game(object):

    def __init__(self, dal):
        self._dal = dal

    def on_get(self, req, resp, game_id):
        pass

    def on_post(self, req, resp, game_id):
        pass


class GameState(object):

    def __init__(self, dal):
        self._dal = dal

    def on_get(self, req, resp, game_id):
        pass

    def on_post(self, req, resp, game_id):
        pass


api = falcon.API()

# Game and GameState are closely related, and so it
# probably makes sense for them to share an object
# in the Data Access Layer. This could just as
# easily use a DB object or ORM layer.
#
# Note how the resources classes provide a layer
# of abstraction or indirection which makes your
# app more flexible since the data layer can
# evolve somewhat independently from the presentation
# layer.
game_dal = myapp.DAL.Game(myconfig)

api.add_route('/game/ping', Ping())
api.add_route('/game/{game_id}', Game(game_dal))
api.add_route('/game/{game_id}/state', GameState(game_dal))

В будущем мы надеемся поддержать альтернативный подход, используя предложение из #584 на GitHub, который позволит объединить все эти ресурсы в один класс следующим образом:

class Ping(object):


class Game(object):

    def __init__(self, dal):
        self._dal = dal

    def on_get(self, req, resp, game_id):
        pass

    def on_post(self, req, resp, game_id):
        pass

    def on_get_ping(self, req, resp):
        resp.body = '{"message": "pong"}'

    def on_get_state(self, req, resp, game_id):
        pass

    def on_post_state(self, req, resp, game_id):
        pass


api = falcon.API()

game = Game(myapp.DAL.Game(myconfig))

api.add_route('/game/ping', game, 'ping')
api.add_route('/game/{game_id}', game)
api.add_route('/game/{game_id}/state', game, 'state')

Расширяемость

Как использовать WSGI-прослойки с Falcon?

Экземпляры falcon.API — это WSGI-приложения первого класса, поэтому вы можете использовать стандартный шаблон, описанный в PEP-3333. В вашем основном файле «app» вы просто обернёте свой экземпляр api приложением промежуточного ПО. Например:

import my_restful_service
import some_middleware

app = some_middleware.DoSomethingFancy(my_restful_service.api)

См. также пример WSGI-прослойки, приведённый в PEP-3333 .

Как передать данные из хука в ответ и между хуками?

Вы можете вставить дополнительные параметры ответа из хука, добавив их в словарь params, переданный в этот хук. Вы также можете добавить пользовательские данные в словарь req.context, чтобы передавать контекстную информацию.

Как написать пользовательский обработчик для страниц 404 и 500 в Falcon?

Когда маршрут для входящего запроса не найден, Falcon использует по умолчанию ответ, который просто поднимает экземпляр falcon.HTTPNotFound. Вы можете использовать falcon.API.add_error_handler() для регистрации пользовательского обработчика ошибок для этого типа исключения. Кроме того, вы можете настроить свой веб-сервер для преобразования ответа (например, используя директиву Nginx’s error_page).

Ошибки 500 обычно возникают в результате необработанного исключения, дошедшего до веб-сервера. Чтобы более элегантно обрабатывать эти ошибки, вы можете добавить пользовательский обработчик ошибок для базового типа Python Exception.

Обработка запросов

Как аутентифицировать запросы?

Хуки и компоненты промежуточного ПО могут использоваться вместе для аутентификации и авторизации запросов. Например, компонент промежуточного ПО может использоваться для анализа входящих учетных данных и размещения результатов в req.context. Далее, компоненты или хуки могут использовать эту информацию для авторизации запроса, учитывая роль пользователя и запрашиваемый ресурс.

Почему req.stream.read() зависает для некоторых запросов?

Это нежелательное явление, связанное с тем, что механизм обработки тела запроса не полностью определён спецификацией WSGI (PEP-3333). Это обсуждается в справочной документации для stream, и обходной путь предоставляется в виде bounded_stream.

Почему из req.path обрезаются хвостовые косые черты?

По умолчанию Falcon нормализует пути входящих URI для упрощения последующей обработки и повышения предсказуемости логики приложения. Это поведение можно отключить с помощью параметра запроса strip_url_path_trailing_slash.

Также обратите внимание, что маршрутизация также нормализуется, поэтому добавление маршрута для «/foo/bar» неявно добавляет маршрут для «/foo/bar/». Запросы, приходящие для любого из этих путей, будут отправлены одному и тому же ресурсу.

Почему мой параметр запроса отсутствует в объекте req?

Если параметр запроса не имеет значения, Falcon по умолчанию игнорирует этот параметр. Например, передача 'foo' или 'foo=' приведёт к игнорированию параметра.

Если вы хотите распознавать такие параметры, вам необходимо установить параметр запроса keep_blank_qs_values в значение True Параметры запроса устанавливаются глобально для каждого экземпляра falcon.API через свойство req_options. Например:

api.req_options.keep_blank_qs_values = True

Почему символы в моих параметрах преобразуются в пробелы?

Символ + часто используется вместо %20 для представления пробелов в параметрах строки запроса из-за исторического объединения кодирования параметров формы (application/x-www-form-urlencoded) и кодирования URI с использованием процентов. Поэтому Falcon преобразует + в пробел при декодировании строк.

Для решения этой проблемы RFC 3986 указывает, что + является зарезервированным символом и рекомендует кодировать такие символы с использованием процентов, если требуется их буквальное значение (%2B в случае с +).

Как получить доступ к параметрам формы, отправленным POST-запросом?

По умолчанию Falcon не обрабатывает тела запросов. Однако установка параметра auto_parse_form_urlencoded в значение True для экземпляра falcon.API заставит фреймворк обрабатывать тело запроса, если тип контента application/x-www-form-urlencoded, делая параметры формы доступными через params, get_param() и т. д.

api.req_options.auto_parse_form_urlencoded = True

В качестве альтернативы параметры формы, отправленные POST-запросом, могут быть прочитаны непосредственно из stream и обработаны с помощью falcon.uri.parse_query_string() или urllib.parse.parse_qs().

Как получить доступ к файлам, отправленным POST-запросом?

Falcon в настоящее время не поддерживает разбор файлов, отправленных с помощью HTTP-формы (multipart/form-data), хотя мы планируем добавить эту функцию в будущих версиях. Тем временем вы можете использовать стандартный класс cgi.FieldStorage для разбора запроса:

# TODO: Either validate that content type is multipart/form-data
# here, or in another hook before allowing execution to proceed.

# This must be done to avoid a bug in cgi.FieldStorage
env = req.env
env.setdefault('QUERY_STRING', '')

# TODO: Add error handling, when the request is not formatted
# correctly or does not contain the desired field...

# TODO: Consider overriding make_file, so that you can
# stream directly to the destination rather than
# buffering using TemporaryFile (see http://goo.gl/Yo8h3P)
form = cgi.FieldStorage(fp=req.stream, environ=env)

file_item = form[name]
if file_item.file:
    # TODO: It's an uploaded file... read it in
else:
    # TODO: Raise an error

Вы также можете попробовать пакет streaming_form_data от Siddhant Goel или поискать на PyPI дополнительные варианты от сообщества.

Как получить доступ к строке запроса, содержащей значение JSON?

Falcon по умолчанию обрабатывает запятые в строке запроса как литеральные символы, разделяющие список, разделенный запятыми. Например, для строки запроса ?c=1,2,3, Falcon по умолчанию добавит её в ваш request.params словарь как {'c': ['1', '2', '3']}. Если вы попытаетесь использовать JSON в значении строки запроса, например, ?c={'a':1,'b':2}, значение будет добавлено в ваш request.params не так, как вы ожидаете: {'c': ["{'a':1", "'b':2}"]}.

Запятая — зарезервированный символ, который может быть экранирован в соответствии с RFC 3986 - 2.2. Зарезервированные символы, поэтому одним из возможных решений является кодирование процентов всех запятых, появляющихся в вашей строке запроса JSON. Другой вариант — изменить способ обработки запятых Falcon в строке запроса, установив auto_parse_qs_csv в значение False в экземпляре falcon.API:

api.auto_parse_qs_csv = False

Когда auto_parse_qs_csv установлено в значение False, значение строки запроса ?c={'a':1,'b':2} будет добавлено в req.params словарь как {'c': "{'a':1,'b':2}"}. Это позволит вам обрабатывать JSON независимо от того, выбрал ли клиент экранировать запятые в запросе.

Как обработать обратные слэши в поле шаблона маршрута?

В Falcon 1.3 мы добавили начальную поддержку конвертеров полей. Мы обсуждали расширение этой функции для поддержки обработки нескольких сегментов пути, как в Flask. Эта работа запланирована на версию 2.0.

Временное решение — кодирование процентов обратного слэша. Если вы не контролируете клиентов и не можете этого потребовать, вы можете реализовать компонент промежуточного ПО Falcon для переписывания пути перед маршрутизацией.

Обработка ответов

Как использовать resp.media с типами, такими как datetime?

Обработчик JSON по умолчанию для resp.media поддерживает только объекты и типы, перечисленные в таблице, описанной в json.JSONEncoder. Для обработки дополнительных типов вы можете либо предварительно сериализовать их, либо создать пользовательский обработчик JSON-средств, устанавливающий параметр default для json.dumps(). При десериализации входящего тела запроса вы также можете реализовать object_hook для json.loads(). Однако следует помнить, что установка параметров default или object_hook может негативно сказаться на производительности (де)сериализации.

Falcon устанавливает Content-Length или мне нужно делать это явно?

Falcon попытается сделать это за вас, основываясь на значении resp.body, resp.data, или resp.stream_len (которое установлено в ответе, проверяется в этом порядке).

Для динамически генерируемого контента вы можете не устанавливать stream_len, в этом случае Falcon опустит заголовок Content-Length, и, надеюсь, ваш сервер WSGI сделает «правильную» вещь (при условии, что вы сказали ему включить keep-alive).

Примечание

PEP-3333 запрещает приложениям устанавливать заголовки, используемые в нескольких этапах, такие как Transfer-Encoding.

Почему возвращается пустое тело ответа, когда я поднимаю экземпляр HTTPError?

Falcon пытается сериализовать экземпляр HTTPError с помощью его методов to_json() или to_xml() в соответствии с заголовком Accept в запросе. Если ни JSON, ни XML не допустимы, тело ответа не будет сгенерировано. Вы можете переопределить это поведение при необходимости с помощью set_error_serializer().

Я устанавливаю тело ответа, но оно не возвращается. Что происходит?

Falcon пропускает обработку тела ответа, когда, согласно спецификации HTTP, тело возвращать не следует. Если клиент отправляет запрос HEAD, фреймворк всегда вернет пустое тело. Falcon также вернет пустое тело, когда статус ответа — любой из следующих:

falcon.HTTP_100
falcon.HTTP_204
falcon.HTTP_416
falcon.HTTP_304

Если у вас есть другой случай, когда тело не возвращается, это, вероятно, ошибка! Сообщите нам, чтобы мы могли помочь.

Я устанавливаю cookie, но он не возвращается в последующих запросах.

По умолчанию Falcon включает атрибут cookie secure. Поэтому, если вы тестируете свое приложение через HTTP (вместо HTTPS), клиент не отправит cookie в последующих запросах.

(См. также документацию по cookie.)

Как можно предоставить скачиваемый файл с помощью falcon?

В методе on_get() ответчика для ресурса вы можете сказать пользовательскому агенту загрузить файл, установив заголовок Content-Disposition. Falcon включает свойство downloadable_as для упрощения этой задачи:

resp.downloadable_as = 'report.pdf'

Может ли Falcon предоставлять статические файлы?

Falcon упрощает эффективное предоставление статических файлов, просто назначив открытый файл resp.stream как показано в руководстве. Вы также можете предоставить весь каталог файлов с помощью falcon.API.add_static_route(). Однако, если это возможно, лучше всего предоставлять статические файлы напрямую с веб-сервера, такого как Nginx, или с CDN.

Разное

Как управлять подключениями к базе данных?

Предполагая, что ваша библиотека базы данных управляет собственным пулом подключений, вам нужно только инициализировать клиент и передать его экземпляр в классы ресурсов. Например, с использованием SQLAlchemy Core:

engine = create_engine('sqlite:///:memory:')
resource = SomeResource(engine)

Затем внутри SomeResource:

# Read from the DB
result = self._engine.execute(some_table.select())
for row in result:
    # ....
result.close()

# ...

# Write to the DB within a transaction
with self._engine.begin() as connection:
    r1 = connection.execute(some_table.select())
    # ...
    connection.execute(
        some_table.insert(),
        col1=7,
        col2='this is some data'
    )

При использовании слоя доступа к данным просто передайте движок в ваши объекты доступа к данным. Смотрите также этот пример проекта Falcon, который демонстрирует использование ORM с Falcon.

Вы также можете создать компонент промежуточного ПО, который автоматически будет получать подключения к базе данных для каждого запроса, но это может затруднить отслеживание ошибок или настройку для потребностей отдельных запросов.

Если вам нужно прозрачно обрабатывать повторное подключение после ошибки или для других случаев использования, которые могут не поддерживаться вашей библиотекой клиента, просто инкапсулируйте библиотеку клиента в управляющий класс, который обрабатывает все сложные моменты, и передавайте его вместо этого.

Какой рекомендуемый подход для предоставления переменных конфигурации для нескольких классов ресурсов?

Люди обычно делятся на два лагеря в отношении этого вопроса. Первый лагерь предпочитает создавать объект конфигурации и передавать его в инициализаторы классов ресурсов, чтобы совместное использование данных было явным. Второй лагерь предпочитает создавать модуль конфигурации и импортировать его, где это необходимо.

При использовании последнего подхода, чтобы управлять временем фактической загрузки конфигурации, лучше не создавать его в пространстве имен основного модуля конфигурации. Это позволит избежать проблемных побочных эффектов, которые могут возникнуть из-за загрузки конфигурации при обработке первого импорта модуля конфигурации Python. Вместо этого рассмотрите реализацию функции в модуле, которая возвращает новый или кэшированный объект конфигурации по запросу.

Кроме этого, вы можете использовать стандартную библиотечную библиотеку конфигурации или что-то вроде aumbry, как показано в этом примере приложения Falcon

(См. также раздел Конфигурация в нашей странице вики-документации для дополнительных пакетов. Вы также можете поискать на PyPI другие варианты).

Как протестировать приложение Falcon? Могу ли я использовать pytest?

Фреймворк Falcon для тестирования поддерживает как unittest, так и pytest. На самом деле, руководство в документации предоставляет отличное введение в тестирование приложений Falcon с помощью pytest.

(См. также: Тестирование)

© 2012–2017 by Rackspace Hosting, Inc. and other contributors
Licensed under the Apache License, Version 2.0.
https://falcon.readthedocs.io/en/1.4.1/user/faq.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API