Создание RESTful API в Google App Engine на основе Flask


    Гомес Хульё Марильё де Серванте — известный международный наркобарон, который беспокоится о качестве предоставляемых его организацией услуг. По этому он, Гомес, решил разработать систему online-заказов для своих партнёров.

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

    Требования по функционалу к данному API совершенно незначительные. Всего-то принимать заказы. По этому было принято решение использовать REST-подход.
    http-протокол, помимо всем известных методов GET и POST, умеет ещё несколько методов, например такие как PUT и DELETE.

    При создании RESTful примем следующая договорённость относительно методов:
    POST — для создания новых записей.
    GET — для получения информации о записи.
    PUT — для внесения изменений в запись.
    DELETE — для удаления записей.

    Гомес Хульё Марильё — очень экономный и технологически продвинутый человек, который знает цену деньгам, не готов платить за услуги больше положенного и знаком с концепцией облачных технологий. По этому выбор сразу же пал на Google App Engine.

    А вот его подчинённые-разработчики (т.е. мы) очень ленивые и не хотят делать лишнюю работу. В то же время понимают, что трафика может быть на столько много, что иcпользовать громоздкие решения попросту не экономно. Они сразу же взяли на вооружение Flask.
    Нам повезло, ведь Flask для работы с WSGI использует Werkzeug, который прекрастно умеет обрабатывать не только GET и POST, но и PUT и DELETE.

    Ваш покорный слуга на основе flask-скелета для app engine создал свой скелет для создания API без блэкджека и женщин.

    Сделав форк проекта приступим к его модификации.
    Для начала нам потребуется разработать модель заказа. Заказ характерезуется своим уникальным номером (будет создан по умолчанию), наименованием товара, весом и ключём доступа для модификации. Откроем файл application/models.py удалим всё его содержимое и запишем:
    from google.appengine.ext import db
    
    class Order(db.Model):
        title = db.StringProperty()
        weight = db.FloatProperty()
        token = db.StringProperty()
    

    Теперь поработаем с urls.py. Нам необходимо добавить 3 обработчика. Для добавления заказа, получения иформации о заказе и уделения заказа. Сразу после строки:
    app.add_url_rule('/', view_func=views.home, methods=['GET',]) # Main page

    Добавим строки:
    app.add_url_rule('/drug/orders/', view_func=views.add_order, methods=['POST',])
    app.add_url_rule('/drug/orders/', view_func=views.get_order, methods=['GET',])
    app.add_url_rule('/drug/orders/', view_func=views.delete_order, methods=['DELETE',])
    


    Описание процесса создания формы не буду приводить. Оно достаточно травиально.
    Осталось создать 3 соответсвующие функции в views.py. Начнём с добавления заказа:
    def add_order():
        form = NewOrderForm()
    
        if form.title.data != "":
            drug_title = form.title.data
        else: return json_response({'error': 'Drug title is empty'})
    
        if form.weight.data != "":
            drug_weight = form.weight.data
        else: return json_response({'error': 'Drug weight is empty'})
    
        token = str(uuid.uuid4())
    
        order = Order(title = drug_title, weight = float(drug_weight), token = token)
        order.put()
    
        return json_response({
            'id': int(order.key().id()),
            'token': token,
            'success': True
        })

    Запускаем сервер разработки из консоли (перейдите в каталог проекта):
    dev_appserver.py project_src/
    Проверим работоспособность данной функции:
    curl -d "title=drug1&weight=10" localhost:8080/drug/orders/
    Результат:
    {"token": "2eac6ef6-198b-45a4-bab5-dd2c56d9fb0a", "id": 171, "success": true}
    Замечательно. Переходим к созданию функции, которая будет сообщаться нам информацию о заказе:
    def get_order():
        token = request.args.get('token')
        if token is None:
            return json_response({'error': 'Token is empty'})
    
        order = Order.all().filter('token = ', token).fetch(1)
        if len(order) is 1:
            return json_response(order[0]._entity)
        else: return json_response({'error': 'Access denied'})

    Проверяем:
    curl -v -X GET localhost:8080/drug/orders/?token=2eac6ef6-198b-45a4-bab5-dd2c56d9fb0a
    Результат:
    {"token": "2eac6ef6-198b-45a4-bab5-dd2c56d9fb0a", "weight": 10.0, "title": "value1"}
    Последний штрих. Удаление заказа:
    def delete_order():
        token = request.args.get('token')
        if token is None:
            return json_response({'error': 'Token is empty'})
    
        order = Order.all().filter('token = ', token).fetch(1)
        if len(order) is 1:
            order = order[0]
            order.delete()
            return json_response({'success': True})
        else: return json_response({'error': 'Access denied'})

    Проверяем:
    curl -v -X DELETE localhost:8080/drug/orders/?token=2eac6ef6-198b-45a4-bab5-dd2c56d9fb0a
    Результат:
    {"success": true}

    На всякий случай заглянем в базу данных:


    Ещё раз ссылка на исходники проекта.

    Замечание.
    Данный набор скриптов хотя и представлен в шуточном виде, может быть расширен до практически любых размеров API-я. Надеюсь, что мне удалось написать не много букв, но при этом дать тебе, %username%, представление о процессе создания RESTful API в облаке.

    P.S.
    Для упрощения изложения и уменьшения количества кода мы немного нарушили REST-принципы. В частности «однозначную идентификацию любого ресура».
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 15
    • НЛО прилетело и опубликовало эту надпись здесь
      • +2
        За Вами выехали парни из банды наркобарона ;)
      • 0
        А почему без декораторов?
        • 0
          Спасибо, хорошая статья.
          • 0
            Сколько денег заплатили Гуглу за наркотрафик?

            На каком тарифе сидите?
            • +2
              Такой скелет приложения сейчас малоактуален т.к. GAE уже поддерживает python2.7 и wsgi
              Да и код можно упростить.
              #вместо
              if 'SERVER_SOFTWARE' in os.environ and os.environ['SERVER_SOFTWARE'].startswith('Dev'):
              if os.getenv('SERVER_SOFTWARE', '').startswith('Dev'):
              # да и вообще настройки можно выделить в модуль settings (development, production, testing)
              
              # вместо
              app.add_url_rule('/drug/orders/', view_func=views.add_order, methods=['POST',])
              app.add_url_rule('/drug/orders/', view_func=views.get_order, methods=['GET',])
              app.add_url_rule('/drug/orders/', view_func=views.delete_order, methods=['DELETE',])
              
              # использовать Blueprint или просто @app.route
              @app.route('/qwerty/', methods = ['GET'])
              def showme():
                return 'show'
              
              @app.route('/qwerty/', methods = ['POST'])
              def addme():
                return 'add'
              

              Ну и в целом можно реализовать пример компактнее
              • +1
                Зачем так:
                order = Order.all().filter('token = ', token).fetch(1)
                if len(order) is 1:
                  order = order[0]
                  order.delete()
                  return json_response({'success': True})
                

                Когда можно так (fetch(n) это n+1 read операции, get — 1 read операция. Billing. Ну и кэширование стоит использовать):
                order = Order.all().filter('token = ', token).get()
                if order:
                  order.delete()
                  return json_response({'success': True})
                

                Или вообще использовать @classmethod чтобы упростить повторное использование.
                Если используется WTForms, то зачем проверять форму руками? Есть же валидаторы.
                • 0
                  > (fetch(n) это n+1 read операции, get — 1 read операция. Billing. Ну и кэширование стоит использовать)
                  Вру. По сути дела тут тоже 2 read операции. 1 read при загрузке по id
                  • 0
                    .get, собака, по параметрам выборку делать не хочет. Требует только ключ на входе.
                    • 0
                      db.get да, а в запросе просто возвращает 1 элемент. Пример и app.yaml к нему
                      #app.yaml
                      application: sample
                      version: 2012041101-dev
                      runtime: python27
                      api_version: 1
                      threadsafe: true
                      
                      ## App handlers
                      handlers:
                      - url: /.*
                        script: runme.app
                      
              • +3
                Для создания апи, наверное, лучше использовать встроенный MethodView — flask.pocoo.org/docs/views/#method-views-for-apis
                • 0
                  Спасибо! Вот что знчит мануал до конца не прочитал ))
                • +1
                  За язык повествования — 5+ :)
                  • 0
                    Насчет регистрации ресурса реализованного с помощью MethodView: я в своё время нарисовал небольшой такой декоратор:

                    def api_resource(bp, endpoint, pk_def):
                        pk = pk_def.keys()[0]
                        pk_type = pk_def[pk] and pk_def[pk].__name__ or None
                        # building url from the endpoint
                        url = "/{}/".format(endpoint)
                    
                        def wrapper(resource_class):
                            resource = resource_class().as_view(endpoint)
                            bp.add_url_rule(url, view_func=resource, methods=['GET', 'POST'])
                            if pk_type is None:
                                url_rule = "%s<%s>" % (url, pk)
                            else:
                                url_rule = "%s<%s:%s>" % (url, pk_type, pk)
                            bp.add_url_rule(url_rule,
                                            view_func=resource,
                                            methods=['GET', 'PUT', 'DELETE'])
                        return wrapper
                    


                    Который позволяет регистрировать ресурсы легко и непринужденно простыми строчками:

                    @api_resource(account, 'sessions', {'id': None})
                    class SessionResource(MethodView):
                        pass
                    


                    Где `account` — инстанс `Blueprint` класса

                    Что заменит вам монотонные вызовы
                    app.add_url_rule('/drug/orders/', view_func=views.add_order, methods=['POST',])
                    app.add_url_rule('/drug/orders/', view_func=views.get_order, methods=['GET',])
                    app.add_url_rule('/drug/orders/', view_func=views.delete_order, methods=['DELETE',])
                    


                    И добавит немного удобства с конвертерами типов идентификаторов ресурса
                    • 0
                      Еще при работе с GAE я бы посоветовал бы вам отказаться от стандартного API DataStore и присмотреться к такой штуке как NDB — там действительно много вкусных вещей включая асинхронные вызовы и возможности по заворачиванию собственного data-зависимого кода в tasklets.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.